このコミットが含まれているのは:
@@ -76,7 +76,7 @@ export default (({ post }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<SidebarComponent>
|
<SidebarComponent>
|
||||||
<TagSearch/>
|
<TagSearch/>
|
||||||
<motion.div layout>
|
<motion.div key={post?.id ?? 0} layout>
|
||||||
{CATEGORIES.map ((cat: Category) => cat in tags && (
|
{CATEGORIES.map ((cat: Category) => cat in tags && (
|
||||||
<motion.div layout className="my-3" key={cat}>
|
<motion.div layout className="my-3" key={cat}>
|
||||||
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
|
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import toCamel from 'camelcase-keys'
|
import toCamel from 'camelcase-keys'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { Fragment, useState, useEffect } from 'react'
|
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import Separator from '@/components/MenuSeparator'
|
import Separator from '@/components/MenuSeparator'
|
||||||
@@ -20,9 +20,31 @@ type Props = { user: User | null }
|
|||||||
export default (({ user }: Props) => {
|
export default (({ user }: Props) => {
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
|
|
||||||
|
const itemRefs = useRef<(HTMLAnchorElement | null)[]> ([])
|
||||||
|
const navRef = useRef<HTMLDivElement | null> (null)
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
const nav = navRef.current
|
||||||
|
const el = itemRefs.current[activeIdx]
|
||||||
|
if (!(nav) || !(el) || activeIdx < 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
const navRect = nav.getBoundingClientRect ()
|
||||||
|
const elRect = el.getBoundingClientRect ()
|
||||||
|
|
||||||
|
setHl ({ left: elRect.left - navRect.left,
|
||||||
|
width: elRect.width,
|
||||||
|
visible: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const [hl, setHl] = useState<{ left: number; width: number; visible: boolean }> ({
|
||||||
|
left: 0,
|
||||||
|
width: 0,
|
||||||
|
visible: false })
|
||||||
const [menuOpen, setMenuOpen] = useState (false)
|
const [menuOpen, setMenuOpen] = useState (false)
|
||||||
const [openItemIdx, setOpenItemIdx] = useState (-1)
|
const [openItemIdx, setOpenItemIdx] = useState (-1)
|
||||||
const [postCount, setPostCount] = useState<number | null> (null)
|
const [postCount, setPostCount] = useState<number | null> (null)
|
||||||
|
const [subDir, setSubDir] = useState<(-1) | 1> (1)
|
||||||
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
||||||
|
|
||||||
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
|
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
|
||||||
@@ -54,11 +76,38 @@ export default (({ user }: Props) => {
|
|||||||
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
|
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
|
||||||
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] }]
|
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] }]
|
||||||
|
|
||||||
|
const activeIdx = menu.findIndex (item => location.pathname.startsWith (item.base || item.to))
|
||||||
|
|
||||||
|
const prevActiveIdxRef = useRef<number> (activeIdx)
|
||||||
|
|
||||||
|
useLayoutEffect (() => {
|
||||||
|
if (activeIdx < 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
const raf = requestAnimationFrame (measure)
|
||||||
|
const onResize = () => requestAnimationFrame (measure)
|
||||||
|
|
||||||
|
addEventListener ('resize', onResize)
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame (raf)
|
||||||
|
removeEventListener ('resize', onResize)
|
||||||
|
}
|
||||||
|
}, [activeIdx])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
const unsubscribe = WikiIdBus.subscribe (setWikiId)
|
const unsubscribe = WikiIdBus.subscribe (setWikiId)
|
||||||
return () => unsubscribe ()
|
return () => unsubscribe ()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
const prev = prevActiveIdxRef.current
|
||||||
|
if (prev !== activeIdx)
|
||||||
|
{
|
||||||
|
setSubDir (activeIdx > prev ? 1 : -1)
|
||||||
|
prevActiveIdxRef.current = activeIdx
|
||||||
|
}
|
||||||
|
}, [activeIdx])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
setMenuOpen (false)
|
setMenuOpen (false)
|
||||||
setOpenItemIdx (menu.findIndex (item => (
|
setOpenItemIdx (menu.findIndex (item => (
|
||||||
@@ -99,16 +148,28 @@ export default (({ user }: Props) => {
|
|||||||
ぼざクリ タグ広場
|
ぼざクリ タグ広場
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{menu.map ((item, i) => (
|
<div ref={navRef} className="relative hidden md:flex h-full items-center">
|
||||||
<Link key={i}
|
<div aria-hidden
|
||||||
to={item.to}
|
className={cn ('absolute top-1/2 -translate-y-1/2 h-full rounded-md',
|
||||||
className={cn ('hidden md:flex h-full items-center',
|
'bg-yellow-200 dark:bg-red-950',
|
||||||
(location.pathname.startsWith (item.base || item.to)
|
'transition-[transform,width,opacity] duration-200 ease-out')}
|
||||||
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold'
|
style={{ width: hl.width,
|
||||||
: 'px-2'))}>
|
transform: `translate(${ hl.left }px, -50%)`,
|
||||||
{item.name}
|
opacity: hl.visible ? 1 : 0 }}/>
|
||||||
</Link>
|
|
||||||
))}
|
{menu.map ((item, i) => (
|
||||||
|
<Link key={i}
|
||||||
|
to={item.to}
|
||||||
|
ref={el => {
|
||||||
|
itemRefs.current[i] = el
|
||||||
|
}}
|
||||||
|
className={cn ('relative z-10 flex h-full items-center px-5',
|
||||||
|
((i === openItemIdx)
|
||||||
|
? 'font-bold'
|
||||||
|
: 'opacity-90 hover:opacity-100'))}>
|
||||||
|
{item.name}
|
||||||
|
</Link>))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TopNavUser user={user}/>
|
<TopNavUser user={user}/>
|
||||||
@@ -126,15 +187,28 @@ export default (({ user }: Props) => {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="hidden md:flex bg-yellow-200 dark:bg-red-950
|
<div className="hidden md:flex bg-yellow-200 dark:bg-red-950
|
||||||
items-center w-full min-h-[40px] px-3">
|
items-center w-full min-h-[40px] px-3 overflow-hidden">
|
||||||
{menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
.filter (item => item.visible ?? true)
|
<motion.div
|
||||||
.map ((item, i) => 'component' in item ? item.component : (
|
key={activeIdx}
|
||||||
<Link key={i}
|
className="flex items-center"
|
||||||
to={item.to}
|
initial={{ y: subDir * 24, opacity: 0 }}
|
||||||
className="h-full flex items-center px-3">
|
animate={{ y: 0, opacity: 1 }}
|
||||||
{item.name}
|
exit={{ y: subDir * 24, opacity: 0 }}
|
||||||
</Link>))}
|
transition={{ duration: .1, ease: 'easeOut' }}>
|
||||||
|
{(menu[activeIdx]?.subMenu ?? [])
|
||||||
|
.filter (item => item.visible ?? true)
|
||||||
|
.map ((item, i) => (
|
||||||
|
'component' in item
|
||||||
|
? <Fragment key={`c-${ i }`}>{item.component}</Fragment>
|
||||||
|
: (
|
||||||
|
<Link key={`l-${ i }`}
|
||||||
|
to={item.to}
|
||||||
|
className="h-full flex items-center px-3">
|
||||||
|
{item.name}
|
||||||
|
</Link>)))}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
@@ -167,16 +241,38 @@ export default (({ user }: Props) => {
|
|||||||
}}>
|
}}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
{i === openItemIdx && (
|
|
||||||
item.subMenu
|
<AnimatePresence initial={false}>
|
||||||
.filter (subItem => subItem.visible ?? true)
|
{i === openItemIdx && (
|
||||||
.map ((subItem, j) => 'component' in subItem ? subItem.component : (
|
<motion.div
|
||||||
<Link key={j}
|
key={`sp-sub-${ i }`}
|
||||||
to={subItem.to}
|
className="w-full bg-yellow-50 dark:bg-red-950"
|
||||||
className="w-full min-h-[36px] flex items-center pl-12
|
variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
|
||||||
bg-yellow-50 dark:bg-red-950">
|
height: 0,
|
||||||
{subItem.name}
|
opacity: 0 },
|
||||||
</Link>)))}
|
open: { clipPath: 'inset(0 0 0% 0)',
|
||||||
|
height: 'auto',
|
||||||
|
opacity: 1 } }}
|
||||||
|
initial="closed"
|
||||||
|
animate="open"
|
||||||
|
exit="closed"
|
||||||
|
transition={{ duration: .2, ease: 'easeOut' }}>
|
||||||
|
{item.subMenu
|
||||||
|
.filter (subItem => subItem.visible ?? true)
|
||||||
|
.map ((subItem, j) => (
|
||||||
|
'component' in subItem
|
||||||
|
? (
|
||||||
|
<Fragment key={`sp-c-${ i }-${ j }`}>
|
||||||
|
{subItem.component}
|
||||||
|
</Fragment>)
|
||||||
|
: (
|
||||||
|
<Link key={`sp-l-${ i }-${ j }`}
|
||||||
|
to={subItem.to}
|
||||||
|
className="w-full min-h-[36px] flex items-center pl-12">
|
||||||
|
{subItem.name}
|
||||||
|
</Link>)))}
|
||||||
|
</motion.div>)}
|
||||||
|
</AnimatePresence>
|
||||||
</Fragment>))}
|
</Fragment>))}
|
||||||
<TopNavUser user={user} sp/>
|
<TopNavUser user={user} sp/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする