コミットを比較
3 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 1aaab25262 | |||
| 19a185d5b5 | |||
| 2979fbac34 |
+3
-1
@@ -6,6 +6,7 @@ import { BrowserRouter,
|
|||||||
Routes,
|
Routes,
|
||||||
useLocation } from 'react-router-dom'
|
useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
import DevModeWatermark from '@/components/DevModeWatermark'
|
||||||
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
|
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
|
||||||
import TopNav from '@/components/TopNav'
|
import TopNav from '@/components/TopNav'
|
||||||
import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
||||||
@@ -145,6 +146,7 @@ const App: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RouteBlockerOverlay/>
|
<RouteBlockerOverlay/>
|
||||||
|
{import.meta.env.DEV && <DevModeWatermark/>}
|
||||||
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<DialogueProvider>
|
<DialogueProvider>
|
||||||
@@ -152,7 +154,7 @@ const App: FC = () => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
layout="position"
|
layout="position"
|
||||||
transition={{ layout: { duration: .2, ease: 'easeOut' } }}
|
transition={{ layout: { duration: .2, ease: 'easeOut' } }}
|
||||||
className="flex flex-col h-dvh w-full overflow-y-hidden">
|
className="relative flex flex-col h-dvh w-full overflow-y-hidden">
|
||||||
<TopNav user={user}/>
|
<TopNav user={user}/>
|
||||||
<RouteTransitionWrapper user={user} setUser={setUser}/>
|
<RouteTransitionWrapper user={user} setUser={setUser}/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import nikumaru from '@/assets/fonts/nikumaru.otf'
|
||||||
|
|
||||||
|
import type { FC } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
const ROW_COUNT = 8
|
||||||
|
const COLUMN_COUNT = 12
|
||||||
|
|
||||||
|
|
||||||
|
const DevModeWatermark: FC = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none select-none fixed inset-0 overflow-hidden z-0">
|
||||||
|
<style>{`
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Nikumaru';
|
||||||
|
src: url(${nikumaru}) format('opentype');
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<div className="absolute -inset-32 flex flex-col justify-center gap-12 py-48">
|
||||||
|
{Array.from ({ length: ROW_COUNT }, (_, rowIndex) => (
|
||||||
|
<div
|
||||||
|
key={rowIndex}
|
||||||
|
className={
|
||||||
|
'flex min-h-32 items-center gap-12 '
|
||||||
|
+ (rowIndex % 2 === 0 ? 'translate-x-0' : 'translate-x-32')
|
||||||
|
}>
|
||||||
|
{Array.from ({ length: COLUMN_COUNT }, (_, columnIndex) => (
|
||||||
|
<span
|
||||||
|
key={columnIndex}
|
||||||
|
className={
|
||||||
|
'whitespace-nowrap text-3xl font-bold '
|
||||||
|
+ 'tracking-[0.3em] text-neutral-950/5 '
|
||||||
|
+ 'dark:text-white/10'
|
||||||
|
}
|
||||||
|
style={{ fontFamily: 'Nikumaru' }}>
|
||||||
|
開発モード
|
||||||
|
</span>))}
|
||||||
|
</div>))}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default DevModeWatermark
|
||||||
@@ -277,10 +277,9 @@ describe ('TheatreDetailPage', () => {
|
|||||||
.toBeInTheDocument ()
|
.toBeInTheDocument ()
|
||||||
expect (within (tagSection ()).getByRole ('link', { name: '別親タグ' }))
|
expect (within (tagSection ()).getByRole ('link', { name: '別親タグ' }))
|
||||||
.toBeInTheDocument ()
|
.toBeInTheDocument ()
|
||||||
expect (within (tagSection ()).getByRole ('link', { name: '子タグ' }))
|
expect (within (tagSection ()).getAllByRole ('link', { name: '子タグ' })[0])
|
||||||
.toBeInTheDocument ()
|
.toBeInTheDocument ()
|
||||||
expect (within (tagSection ()).getAllByRole ('link', { name: '子タグ' }))
|
expect (within (tagSection ()).getAllByText ('↳').length).toBeGreaterThan (0)
|
||||||
.toHaveLength (1)
|
|
||||||
|
|
||||||
fireEvent.click (screen.getByRole ('button', { name: '2 列 A 型' }))
|
fireEvent.click (screen.getByRole ('button', { name: '2 列 A 型' }))
|
||||||
fireEvent.click (screen.getAllByRole ('button', { name: '横並び' })[0])
|
fireEvent.click (screen.getAllByRole ('button', { name: '横並び' })[0])
|
||||||
|
|||||||
@@ -125,10 +125,9 @@ const flattenTags = (tags: Tag[]): Tag[] => {
|
|||||||
|
|
||||||
|
|
||||||
const tagsByCategory = (tags: Tag[]): Partial<Record<Category, Tag[]>> => {
|
const tagsByCategory = (tags: Tag[]): Partial<Record<Category, Tag[]>> => {
|
||||||
const flattenedTags = flattenTags (tags)
|
|
||||||
const grouped: Partial<Record<Category, Tag[]>> = { }
|
const grouped: Partial<Record<Category, Tag[]>> = { }
|
||||||
|
|
||||||
for (const tag of flattenedTags)
|
for (const tag of tags)
|
||||||
{
|
{
|
||||||
grouped[tag.category] ??= []
|
grouped[tag.category] ??= []
|
||||||
grouped[tag.category]!.push (tag)
|
grouped[tag.category]!.push (tag)
|
||||||
@@ -141,15 +140,39 @@ const tagsByCategory = (tags: Tag[]): Partial<Record<Category, Tag[]>> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const renderReadonlyTagTree = (
|
||||||
|
tag: Tag,
|
||||||
|
nestLevel: number,
|
||||||
|
path: string,
|
||||||
|
ancestors: Set<number> = new Set<number> (),
|
||||||
|
): ReactNode[] => {
|
||||||
|
const key = `${ path }-${ tag.id }`
|
||||||
|
const nextAncestors = new Set (ancestors)
|
||||||
|
nextAncestors.add (tag.id)
|
||||||
|
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
<li key={key} className="text-left leading-tight">
|
||||||
|
<TagLink tag={tag} nestLevel={nestLevel} withCount={false}/>
|
||||||
|
</li>),
|
||||||
|
...((tag.children ?? [])
|
||||||
|
.filter (child => !(nextAncestors.has (child.id)))
|
||||||
|
.sort (compareTagName)
|
||||||
|
.flatMap (child =>
|
||||||
|
renderReadonlyTagTree (child, nestLevel + 1, key, nextAncestors)))]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = (
|
const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = (
|
||||||
{ tags, compact, flow = 'vertical' }) => {
|
{ tags, compact, flow = 'vertical' }) => {
|
||||||
const grouped = tagsByCategory (tags)
|
const horizontalGrouped = tagsByCategory (flattenTags (tags))
|
||||||
|
const verticalGrouped = tagsByCategory (tags)
|
||||||
|
|
||||||
if (flow === 'horizontal')
|
if (flow === 'horizontal')
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<ul className={cn ('flex flex-wrap gap-x-3 gap-y-1', compact && 'text-sm')}>
|
<ul className={cn ('flex flex-wrap gap-x-3 gap-y-1', compact && 'text-sm')}>
|
||||||
{CATEGORIES.flatMap (cat => grouped[cat] ?? []).map (tag => (
|
{CATEGORIES.flatMap (cat => horizontalGrouped[cat] ?? []).map (tag => (
|
||||||
<li key={tag.id} className="text-left leading-tight">
|
<li key={tag.id} className="text-left leading-tight">
|
||||||
<TagLink tag={tag} withCount={false}/>
|
<TagLink tag={tag} withCount={false}/>
|
||||||
</li>))}
|
</li>))}
|
||||||
@@ -159,7 +182,7 @@ const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = (
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{CATEGORIES.map (cat => {
|
{CATEGORIES.map (cat => {
|
||||||
const rows = grouped[cat] ?? []
|
const rows = verticalGrouped[cat] ?? []
|
||||||
if (rows.length === 0)
|
if (rows.length === 0)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -169,10 +192,8 @@ const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = (
|
|||||||
{CATEGORY_NAMES[cat]}
|
{CATEGORY_NAMES[cat]}
|
||||||
</div>
|
</div>
|
||||||
<ul className={cn ('space-y-1', compact && 'text-sm')}>
|
<ul className={cn ('space-y-1', compact && 'text-sm')}>
|
||||||
{rows.map (tag => (
|
{rows.flatMap (tag =>
|
||||||
<li key={tag.id} className="text-left leading-tight">
|
renderReadonlyTagTree (tag, 0, `cat-${ cat }`))}
|
||||||
<TagLink tag={tag} withCount={false}/>
|
|
||||||
</li>))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>)
|
</div>)
|
||||||
})}
|
})}
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする