みてるぞ 10 hours ago
parent
commit
3f42eb6915
12 changed files with 211 additions and 144 deletions
  1. +0
    -42
      frontend/src/App.css
  2. +32
    -31
      frontend/src/components/TagDetailSidebar.tsx
  3. +48
    -0
      frontend/src/components/TagLink.tsx
  4. +7
    -5
      frontend/src/components/TagSidebar.tsx
  5. +20
    -16
      frontend/src/components/TopNav.tsx
  6. +17
    -3
      frontend/src/consts.ts
  7. +35
    -17
      frontend/src/index.css
  8. +3
    -2
      frontend/src/pages/posts/PostDetailPage.tsx
  9. +11
    -11
      frontend/src/pages/posts/PostListPage.tsx
  10. +16
    -9
      frontend/src/pages/tags/NicoTagListPage.tsx
  11. +2
    -3
      frontend/src/types.ts
  12. +20
    -5
      frontend/tailwind.config.js

+ 0
- 42
frontend/src/App.css View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}

.card {
padding: 2em;
}

.read-the-docs {
color: #888;
}

+ 32
- 31
frontend/src/components/TagDetailSidebar.tsx View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'

import TagLink from '@/components/TagLink'
import TagSearch from '@/components/TagSearch'
import SubsectionTitle from '@/components/common/SubsectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent'
@@ -14,47 +14,48 @@ type Props = { post: Post | null }


export default ({ post }: Props) => {
const [tags, setTags] = useState({ } as TagByCategory)
const [tags, setTags] = useState ({ } as TagByCategory)

const categoryNames: Partial<{ [key in Category]: string }> = {
general: '一般',
const categoryNames: Record<Category, string> = {
deerjikist: 'ニジラー',
meme: '原作・ネタ元・ミーム等',
character: 'キャラクター',
general: '一般',
material: '素材',
meta: 'メタタグ',
nico: 'ニコニコタグ' }

useEffect (() => {
if (!(post))
return

const fetchTags = () => {
const tagsTmp = { } as TagByCategory
for (const tag of post.tags)
{
if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = []
tagsTmp[tag.category].push (tag)
}
for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[])
tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1)
setTags (tagsTmp)
}

fetchTags ()
const tagsTmp = { } as TagByCategory

for (const tag of post.tags)
{
if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = []
tagsTmp[tag.category].push (tag)
}

for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[])
tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1)

setTags (tagsTmp)
}, [post])

return (
<SidebarComponent>
<TagSearch />
{CATEGORIES.map ((cat: Category) => cat in tags && (
<div className="my-3" key={cat}>
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
<ul>
{tags[cat].map (tag => (
<li key={tag.id} className="mb-1">
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}>
{tag.name}
</Link>
</li>))}
</ul>
</div>))}
<TagSearch />
{CATEGORIES.map ((cat: Category) => cat in tags && (
<div className="my-3" key={cat}>
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
<ul>
{tags[cat].map ((tag, i) => (
<li key={i} className="mb-1">
<TagLink tag={tag} />
</li>))}
</ul>
</div>))}
</SidebarComponent>)
}

+ 48
- 0
frontend/src/components/TagLink.tsx View File

@@ -0,0 +1,48 @@
import { Link } from 'react-router-dom'

import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts'
import { cn } from '@/lib/utils'

import type { Tag } from '@/types'

type Props = { tag: Tag
linkFlg?: boolean
withWiki?: boolean
withCount?: boolean }


export default ({ tag,
linkFlg = true,
withWiki = true,
withCount = true }: Props) => {
const spanClass = cn (
`text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`,
`dark:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE }`)
const linkClass = cn (
spanClass,
`hover:text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE - 200 }`,
`dark:hover:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE - 200 }`)

return (
<>
{(linkFlg && withWiki) && (
<span className="mr-1">
<Link to={`/wiki/${ encodeURIComponent (tag.name) }`}
className={linkClass}>
?
</Link>
</span>)}
{linkFlg
? (
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
className={linkClass}>
{tag.name}
</Link>)
: (
<span className={spanClass}>
{tag.name}
</span>)}
{withCount && (
<span className="ml-1">{tag.postCount}</span>)}
</>)
}

+ 7
- 5
frontend/src/components/TagSidebar.tsx View File

@@ -1,7 +1,8 @@
import axios from 'axios'
import { useEffect, useState } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'

import TagLink from '@/components/TagLink'
import TagSearch from '@/components/TagSearch'
import SectionTitle from '@/components/common/SectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent'
@@ -28,6 +29,7 @@ export default ({ posts }: Props) => {
useEffect (() => {
const tagsTmp: TagByCategory = { }
let cnt = 0

loop:
for (const post of posts)
{
@@ -35,6 +37,7 @@ export default ({ posts }: Props) => {
{
if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = []

if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id)))
{
tagsTmp[tag.category].push (tag)
@@ -44,8 +47,10 @@ export default ({ posts }: Props) => {
}
}
}

for (const cat of Object.keys (tagsTmp))
tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1)

setTags (tagsTmp)
}, [posts])

@@ -57,10 +62,7 @@ export default ({ posts }: Props) => {
{CATEGORIES.flatMap (cat => cat in tags ? (
tags[cat].map (tag => (
<li key={tag.id} className="mb-1">
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}>
{tag.name}
</Link>
<span className="ml-1">{tag.postCount}</span>
<TagLink tag={tag} />
</li>))) : [])}
</ul>
<SectionTitle>関聯</SectionTitle>


+ 20
- 16
frontend/src/components/TopNav.tsx View File

@@ -17,7 +17,6 @@ const Menu = { None: 'None',
User: 'User',
Tag: 'Tag',
Wiki: 'Wiki' } as const

type Menu = typeof Menu[keyof typeof Menu]


@@ -38,9 +37,9 @@ export default ({ user }: Props) => {
const MyLink = ({ to, title, base }: { to: string
title: string
base?: string }) => (
<Link to={to} className={cn ('hover:text-orange-500 h-full flex items-center',
<Link to={to} className={cn ('h-full flex items-center',
(location.pathname.startsWith (base ?? to)
? 'bg-gray-700 px-4 font-bold'
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold'
: 'px-2'))}>
{title}
</Link>)
@@ -121,11 +120,9 @@ export default ({ user }: Props) => {
try
{
const pageRes = await axios.get (`${ API_BASE_URL }/wiki/${ wikiId }`)
const pageData: any = pageRes.data
const wikiPage = toCamel (pageData, { deep: true }) as WikiPage
const wikiPage = toCamel (pageRes.data as any, { deep: true }) as WikiPage

const tagRes = await axios.get (`${ API_BASE_URL }/tags/name/${ wikiPage.title }`)
const tagData: any = tagRes.data
const tag = toCamel (tagData, { deep: true }) as Tag

setPostCount (tag.postCount)
@@ -139,25 +136,32 @@ export default ({ user }: Props) => {

return (
<>
<nav className="bg-gray-800 text-white px-3 flex justify-between items-center w-full min-h-[48px]">
<nav className="px-3 flex justify-between items-center w-full min-h-[48px]
bg-yellow-50 dark:bg-red-975">
<div className="flex items-center gap-2 h-full">
<Link to="/" className="mx-4 text-xl font-bold text-orange-500">ぼざクリ タグ広場</Link>
<Link to="/" className="mx-4 text-xl font-bold
text-pink-600 hover:text-pink-400
dark:text-pink-300 dark:hover:text-pink-100">
ぼざクリ タグ広場
</Link>
<MyLink to="/posts" title="広場" />
<MyLink to="/tags" title="タグ" />
<MyLink to="/wiki/ヘルプ:ホーム" base="/wiki" title="Wiki" />
<MyLink to="/users/settings" base="/users" title="ニジラー" />
</div>
<div className="ml-auto pr-4">
{user && (
<Button onClick={() => navigate ('/users/settings')}
className="bg-gray-600">
{user &&
<div className="ml-auto pr-4">
<Link to="/users/settings"
className="font-bold text-red-600 hover:text-red-400
dark:text-yellow-400 dark:hover:text-yellow-200">
{user.name || '名もなきニジラー'}
</Button>)}
</div>
</Link>
</div>}
</nav>
{(() => {
const className = 'bg-gray-700 text-white px-3 flex items-center w-full min-h-[40px]'
const subClass = 'hover:text-orange-500 h-full flex items-center px-3'
const className = cn ('bg-yellow-200 dark:bg-red-950',
'text-white px-3 flex items-center w-full min-h-[40px]')
const subClass = 'h-full flex items-center px-3'
// const inputBox = 'flex items-center px-3 mx-2'
const Separator = () => <span className="flex items-center px-2">|</span>
switch (selectedMenu)


+ 17
- 3
frontend/src/consts.ts View File

@@ -1,11 +1,25 @@
export const CATEGORIES = ['general',
'character',
'deerjikist',
import type { Category } from 'types'

export const LIGHT_COLOUR_SHADE = 800
export const DARK_COLOUR_SHADE = 300

export const CATEGORIES = ['deerjikist',
'meme',
'character',
'general',
'material',
'meta',
'nico'] as const

export const TAG_COLOUR = {
deerjikist: 'rose',
meme: 'purple',
character: 'lime',
general: 'cyan',
material: 'orange',
meta: 'yellow',
nico: 'gray' } as const satisfies Record<Category, string>

export const USER_ROLES = ['admin', 'member', 'guest'] as const

export const ViewFlagBehavior = { OnShowedDetail: 1,


+ 35
- 17
frontend/src/index.css View File

@@ -2,13 +2,25 @@
@tailwind components;
@tailwind utilities;

@layer base {
body {
@layer base
{
body
{
@apply overflow-x-clip;
}

a
{
@apply text-blue-600 dark:text-blue-300;
}
a:hover
{
@apply text-blue-400 dark:text-blue-100;
}
}

:root {
:root
{
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
@@ -23,16 +35,14 @@
-moz-osx-font-smoothing: grayscale;
}

a {
a
{
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}

body {
body
{
margin: 0;
display: flex;
place-items: center;
@@ -40,12 +50,14 @@ body {
min-height: 100vh;
}

h1 {
h1
{
font-size: 3.2em;
line-height: 1.1;
}

button {
button
{
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
@@ -56,23 +68,29 @@ button {
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
button:hover
{
border-color: #646cff;
}
button:focus,
button:focus-visible {
button:focus-visible
{
outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
:root {
@media (prefers-color-scheme: light)
{
:root
{
color: #213547;
background-color: #ffffff;
}
a:hover {
a:hover
{
color: #747bff;
}
button {
button
{
background-color: #f9f9f9;
}
}

+ 3
- 2
frontend/src/pages/posts/PostDetailPage.tsx View File

@@ -51,7 +51,7 @@ export default ({ user }: Props) => {
if (!(id))
return

void (async () => {
const fetchPost = async () => {
try
{
const res = await axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: {
@@ -63,7 +63,8 @@ export default ({ user }: Props) => {
if (axios.isAxiosError (err))
setStatus (err.status ?? 200)
}
}) ()
}
fetchPost ()
}, [id])

useEffect (() => {


+ 11
- 11
frontend/src/pages/posts/PostListPage.tsx View File

@@ -93,16 +93,16 @@ export default () => {
<Tab name="広場">
{posts.length
? (
<div className="flex flex-wrap gap-4 gap-y-8 p-4 justify-between">
{posts.map (post => (
<Link to={`/posts/${ post.id }`}
key={post.id}
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg">
<img src={post.thumbnail || post.thumbnailBase || undefined}
alt={post.title || post.url}
title={post.title || post.url || undefined}
className="object-none w-full h-full" />
</Link>))}
<div className="flex flex-wrap gap-6 p-4">
{posts.map ((post, i) => (
<Link to={`/posts/${ post.id }`}
key={i}
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg">
<img src={post.thumbnail || post.thumbnailBase || undefined}
alt={post.title || post.url}
title={post.title || post.url || undefined}
className="object-none w-full h-full" />
</Link>))}
</div>)
: !(loading) && '広場には何もありませんよ.'}
{loading && 'Loading...'}
@@ -112,7 +112,7 @@ export default () => {
<Tab name="Wiki" init={!(posts.length)}>
<WikiBody body={wikiPage.body} />
<div className="my-2">
<Link to={`/wiki/${ wikiPage.title }`}>Wiki を見る</Link>
<Link to={`/wiki/${ encodeURIComponent (wikiPage.title) }`}>Wiki を見る</Link>
</div>
</Tab>)}
</TabGroup>


+ 16
- 9
frontend/src/pages/tags/NicoTagListPage.tsx View File

@@ -3,6 +3,7 @@ import toCamel from 'camelcase-keys'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'

import TagLink from '@/components/TagLink'
import SectionTitle from '@/components/common/SectionTitle'
import TextArea from '@/components/common/TextArea'
import MainArea from '@/components/layout/MainArea'
@@ -15,10 +16,10 @@ type Props = { user: User | null }


export default ({ user }: Props) => {
const [nicoTags, setNicoTags] = useState<NicoTag[]> ([])
const [cursor, setCursor] = useState ('')
const [loading, setLoading] = useState (false)
const [editing, setEditing] = useState<{ [key: number]: boolean }> ({ })
const [loading, setLoading] = useState (false)
const [nicoTags, setNicoTags] = useState<NicoTag[]> ([])
const [rawTags, setRawTags] = useState<{ [key: number]: string }> ({ })

const loaderRef = useRef<HTMLDivElement | null> (null)
@@ -56,6 +57,10 @@ export default ({ user }: Props) => {
'Content-Type': 'multipart/form-data',
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
const data = toCamel (res.data as any, { deep: true }) as Tag[]
setNicoTags (nicoTags => {
nicoTags.find (t => t.id === id).linkedTags = data
return [...nicoTags]
})
setRawTags (rawTags => ({ ...rawTags, [id]: data.map (t => t.name).join (' ') }))

toast ({ title: '更新しました.' })
@@ -106,13 +111,10 @@ export default ({ user }: Props) => {
</tr>
</thead>
<tbody>
{nicoTags.map (tag => (
<tr key={tag.id}>
{nicoTags.map ((tag, i) => (
<tr key={i}>
<td className="p-2">
<a href={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
target="_blank">
{tag.name}
</a>
<TagLink tag={tag} withWiki={false} withCount={false} />
</td>
<td className="p-2">
{editing[tag.id]
@@ -120,7 +122,12 @@ export default ({ user }: Props) => {
<TextArea value={rawTags[tag.id]} onChange={ev => {
setRawTags (rawTags => ({ ...rawTags, [tag.id]: ev.target.value }))
}} />)
: rawTags[tag.id]}
: tag.linkedTags.map((lt, j) => (
<span key={j} className="mr-2">
<TagLink tag={lt}
linkFlg={false}
withCount={false} />
</span>))}
</td>
{memberFlg && (
<td>


+ 2
- 3
frontend/src/types.ts View File

@@ -2,9 +2,8 @@ import { CATEGORIES, USER_ROLES, ViewFlagBehavior } from '@/consts'

export type Category = typeof CATEGORIES[number]

export type NicoTag = {
id: number
name: string
export type NicoTag = Tag & {
category: 'nico'
linkedTags: Tag[] }

export type Post = {


+ 20
- 5
frontend/tailwind.config.js View File

@@ -1,20 +1,35 @@
/** @type {import('tailwindcss').Config} */
import type { Config } from 'tailwindcss'

import { DARK_COLOUR_SHADE,
LIGHT_COLOUR_SHADE,
TAG_COLOUR } from './src/consts'

const colours = Object.values (TAG_COLOUR)

export default {
content: ['./index.html',
'./src/**/*.{js,ts,jsx,tsx}'],
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
safelist: [...colours.map (c => `text-${ c }-${ LIGHT_COLOUR_SHADE }`),
...colours.map (c => `hover:text-${ c }-${ LIGHT_COLOUR_SHADE - 200 }`),
...colours.map (c => `dark:text-${ c }-${ DARK_COLOUR_SHADE }`),
...colours.map (c => `dark:hover:text-${ c }-${ DARK_COLOUR_SHADE - 200 }`)],
theme: {
extend: {
animation: {
'rainbow-scroll': 'rainbow-scroll .25s linear infinite',
},
colors: {
red: {
925: '#5f1414',
975: '#230505',
}
},
keyframes: {
'rainbow-scroll': {
'0%': { backgroundPosition: '0% 50%' },
'100%': { backgroundPosition: '200% 50%' },
},
},
animation: {
'rainbow-scroll': 'rainbow-scroll .25s linear infinite',
},
}
},
plugins: [],


Loading…
Cancel
Save