This commit is contained in:
2025-07-06 19:57:20 +09:00
parent da31fe93c4
commit 191e5d3a76
22 changed files with 344 additions and 231 deletions
+38 -41
View File
@@ -27,57 +27,54 @@ export default () => {
const [user, setUser] = useState<User | null> (null)
useEffect (() => {
const createUser = () => (
axios.post (`${ API_BASE_URL }/users`)
.then (res => {
if (res.data.code)
{
localStorage.setItem ('user_code', res.data.code)
setUser (toCamel (res.data.user, { deep: true }))
}
}))
const createUser = async () => {
const { data } = await axios.post (`${ API_BASE_URL }/users`)
if (data.code)
{
localStorage.setItem ('user_code', data.code)
setUser (toCamel (data.user, { deep: true }))
}
}
const code = localStorage.getItem ('user_code')
if (code)
{
void (axios.post (`${ API_BASE_URL }/users/verify`, { code })
.then (res => {
if (res.data.valid)
setUser (toCamel (res.data.user, { deep: true }))
else
createUser ()
}))
void (async () => {
const { data } = await axios.post (`${ API_BASE_URL }/users/verify`, { code })
if (data.valid)
setUser (toCamel (data.user, { deep: true }))
else
createUser ()
}) ()
}
else
createUser ()
alert ('このサイトはまだ作りかけです!!!!\n出てけ!!!!!!!!!!!!!!!!!!!!')
}, [])
return (
<>
<Router>
<div className="flex flex-col h-screen w-screen">
<TopNav user={user} setUser={setUser} />
<div className="flex flex-1">
<Routes>
<Route path="/" element={<Navigate to="/posts" replace />} />
<Route path="/posts" element={<PostListPage />} />
<Route path="/posts/new" element={<PostNewPage />} />
<Route path="/posts/:id" element={<PostDetailPage user={user} />} />
<Route path="/wiki" element={<WikiSearchPage />} />
<Route path="/wiki/:title" element={<WikiDetailPage />} />
<Route path="/wiki/new" element={<WikiNewPage />} />
<Route path="/wiki/:id/edit" element={<WikiEditPage />} />
<Route path="/wiki/:id/diff" element={<WikiDiffPage />} />
<Route path="/wiki/changes" element={<WikiHistoryPage />} />
<Route path="/users/settings" element={<SettingPage user={user} setUser={setUser} />} />
<Route path="/settings" element={<Navigate to="/users/settings" replace />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
</Router>
<Toaster />
<Router>
<div className="flex flex-col h-screen w-screen">
<TopNav user={user} setUser={setUser} />
<div className="flex flex-1">
<Routes>
<Route path="/" element={<Navigate to="/posts" replace />} />
<Route path="/posts" element={<PostListPage />} />
<Route path="/posts/new" element={<PostNewPage />} />
<Route path="/posts/:id" element={<PostDetailPage user={user} />} />
<Route path="/wiki" element={<WikiSearchPage />} />
<Route path="/wiki/:title" element={<WikiDetailPage />} />
<Route path="/wiki/new" element={<WikiNewPage />} />
<Route path="/wiki/:id/edit" element={<WikiEditPage />} />
<Route path="/wiki/:id/diff" element={<WikiDiffPage />} />
<Route path="/wiki/changes" element={<WikiHistoryPage />} />
<Route path="/users/settings" element={<SettingPage user={user} setUser={setUser} />} />
<Route path="/settings" element={<Navigate to="/users/settings" replace />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</div>
</Router>
<Toaster />
</>)
}
+1 -1
View File
@@ -47,7 +47,7 @@ export default ({ post }: Props) => {
<SidebarComponent>
<TagSearch />
{CATEGORIES.map ((cat: Category) => cat in tags && (
<div className="my-3">
<div className="my-3" key={cat}>
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
<ul>
{tags[cat].map (tag => (
+45 -47
View File
@@ -32,18 +32,18 @@ export default ({ posts }: Props) => {
loop:
for (const post of posts)
{
for (const tag of post.tags)
{
if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = []
if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id)))
{
tagsTmp[tag.category].push (tag)
++cnt
if (cnt >= 25)
break loop
}
}
for (const tag of post.tags)
{
if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = []
if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id)))
{
tagsTmp[tag.category].push (tag)
++cnt
if (cnt >= 25)
break loop
}
}
}
for (const cat of Object.keys (tagsTmp))
tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1)
@@ -52,40 +52,38 @@ export default ({ posts }: Props) => {
return (
<SidebarComponent>
<TagSearch />
<SectionTitle></SectionTitle>
<ul>
{CATEGORIES.map (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>
</li>))}
</>))}
</ul>
<SectionTitle></SectionTitle>
{posts.length && (
<a href="#"
onClick={ev => {
ev.preventDefault ()
void ((async () => {
try
{
const { data } = await axios.get (`${ API_BASE_URL }/posts/random`,
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (','),
match: (anyFlg ? 'any' : 'all') } })
navigate (`/posts/${ (data as Post).id }`)
}
catch
{
;
}
}) ())
}}>
</a>)}
<TagSearch />
<SectionTitle></SectionTitle>
<ul>
{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>
</li>))) : [])}
</ul>
<SectionTitle></SectionTitle>
{posts.length && (
<a href="#"
onClick={ev => {
ev.preventDefault ()
void ((async () => {
try
{
const { data } = await axios.get (`${ API_BASE_URL }/posts/random`,
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (','),
match: (anyFlg ? 'any' : 'all') } })
navigate (`/posts/${ (data as Post).id }`)
}
catch
{
;
}
}) ())
}}>
</a>)}
</SidebarComponent>)
}
@@ -0,0 +1,21 @@
import { Button } from '@/components/ui/button'
import { Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger } from '@/components/ui/dialog'
type Props = { visible: boolean
onVisibleChange: (visible: boolean) => void
user: User | null,
setUser: (user: User) => void }
export default ({ visible, onVisibleChange, user, setUser }: Props) => {
return (
<Dialog open={visible} onOpenChange={onVisibleChange}>
<DialogContent className="space-y-6">
<DialogTitle></DialogTitle>
</DialogContent>
</Dialog>)
}
@@ -0,0 +1,32 @@
import { Button } from '@/components/ui/button'
import { Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger } from '@/components/ui/dialog'
type Props = { visible: boolean
onVisibleChange: (visible: boolean) => void
user: User | null }
export default ({ visible, onVisibleChange, user }: Props) => {
return (
<Dialog open={visible} onOpenChange={onVisibleChange}>
<DialogContent className="space-y-6">
<DialogTitle></DialogTitle>
<div>
<p></p>
<div className="m-2">{user?.inheritanceCode}</div>
<p className="mt-1 text-sm text-red-500">
!
</p>
<div className="my-4">
<Button className="px-4 py-2 bg-red-600 text-white rounded disabled:bg-gray-400">
</Button>
</div>
</div>
</DialogContent>
</Dialog>)
}
+4
View File
@@ -7,3 +7,7 @@ export const CATEGORIES = ['general',
'meta'] as const
export const USER_ROLES = ['admin', 'member', 'guest'] as const
export const enum ViewFlagBehavior { OnShowedDetail = 1,
OnClickedLink = 2,
NotAuto = 3 }
+12 -7
View File
@@ -1,10 +1,15 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { HelmetProvider } from 'react-helmet-async'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
import '@/index.css'
import App from '@/App'
const helmetContext = { }
createRoot (document.getElementById ('root')!).render (
<StrictMode>
<HelmetProvider context={helmetContext}>
<App />
</HelmetProvider>
</StrictMode>)
+1 -1
View File
@@ -1,4 +1,4 @@
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import React, { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation, useParams } from 'react-router-dom'
import TagDetailSidebar from '@/components/TagDetailSidebar'
+57 -50
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import React, { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation } from 'react-router-dom'
import TagSidebar from '@/components/TagSidebar'
@@ -14,7 +14,7 @@ import type { Post, Tag, WikiPage } from '@/types'
export default () => {
const [posts, setPosts] = useState<Post[] | null> (null)
const [posts, setPosts] = useState<Post[]> ([])
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)
const [cursor, setCursor] = useState ('')
const [loading, setLoading] = useState (false)
@@ -24,11 +24,11 @@ export default () => {
const loadMore = async withCursor => {
setLoading (true)
const res = await axios.get (`${ API_BASE_URL }/posts`, {
params: { tags: tags.join (' '),
match: (anyFlg ? 'any' : 'all'),
...(withCursor ? { cursor } : { }) } })
params: { tags: tags.join (' '),
match: (anyFlg ? 'any' : 'all'),
...(withCursor ? { cursor } : { }) } })
const data = toCamel (res.data, { deep: true })
setPosts (posts => [...(posts || []), ...data.posts])
setPosts (posts => [...(withCursor ? posts : []), ...data.posts])
setCursor (data.nextCursor)
setLoading (false)
}
@@ -42,7 +42,7 @@ export default () => {
useEffect(() => {
const observer = new IntersectionObserver (entries => {
if (entries[0].isIntersecting && !(loading) && cursor)
loadMore (true)
loadMore (true)
}, { threshold: 1 })
const target = loaderRef.current
@@ -52,58 +52,65 @@ export default () => {
}, [loaderRef, loading])
useEffect (() => {
setPosts (null)
setCursor ('')
setPosts ([])
loadMore (false)
setWikiPage (null)
if (tags.length === 1)
{
void (axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (tags[0]) }`)
.then (res => setWikiPage (toCamel (res.data, { deep: true })))
.catch (() => {
;
}))
void (async () => {
try
{
const tagName = tags[0]
const { data } = await axios.get (`${ API_BASE_URL }/wiki/title/${ tagName }`)
setWikiPage (toCamel (data, { deep: true }))
}
catch
{
;
}
}) ()
}
}, [location.search])
return (
<>
<Helmet>
<title>
{tags.length
? `${ tags.join (anyFlg ? ' or ' : ' and ') } | ${ SITE_TITLE }`
: `${ SITE_TITLE } 〜 ぼざクリ関聯綜合リンク集サイト`}
</title>
</Helmet>
<TagSidebar posts={posts?.slice (0, 20) || []} />
<MainArea>
<TabGroup key={wikiPage}>
<Tab name="広場">
{posts == null ? 'Loading...' : (
posts.length
? (
<div className="flex flex-wrap gap-4 p-4">
{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}
alt={post.title || post.url}
className="object-none w-full h-full" />
</Link>))}
</div>)
: '広場には何もありませんよ.')}
<div ref={loaderRef} className="h-12"></div>
</Tab>
{(wikiPage && wikiPage.body) && (
<Tab name="Wiki" init={posts && !(posts.length)}>
<WikiBody body={wikiPage.body} />
<div className="my-2">
<Link to={`/wiki/${ wikiPage.title }`}>Wiki </Link>
</div>
</Tab>)}
</TabGroup>
</MainArea>
<Helmet>
<title>
{tags.length
? `${ tags.join (anyFlg ? ' or ' : ' and ') } | ${ SITE_TITLE }`
: `${ SITE_TITLE } 〜 ぼざクリ関聯綜合リンク集サイト`}
</title>
</Helmet>
<TagSidebar posts={posts.slice (0, 20)} />
<MainArea>
<TabGroup key={wikiPage}>
<Tab name="広場">
{posts.length
? (
<div className="flex flex-wrap gap-4 p-4">
{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 || null}
alt={post.title || post.url}
title={post.title || post.url || null}
className="object-none w-full h-full" />
</Link>))}
</div>)
: !(loading) && '広場には何もありませんよ.'}
{loading && 'Loading...'}
<div ref={loaderRef} className="h-12"></div>
</Tab>
{(wikiPage && wikiPage.body) && (
<Tab name="Wiki" init={!(posts.length)}>
<WikiBody body={wikiPage.body} />
<div className="my-2">
<Link to={`/wiki/${ wikiPage.title }`}>Wiki </Link>
</div>
</Tab>)}
</TabGroup>
</MainArea>
</>)
}
+3 -3
View File
@@ -1,6 +1,6 @@
import axios from 'axios'
import React, { useEffect, useState, useRef } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom'
import NicoViewer from '@/components/NicoViewer'
@@ -49,8 +49,8 @@ export default () => {
toast ({ title: '投稿成功!' })
navigate ('/posts')
})
.catch (e => toast ({ title: '投稿失敗',
description: '入力を確認してください。' })))
.catch (() => toast ({ title: '投稿失敗',
description: '入力を確認してください。' })))
}
useEffect (() => {
+78 -25
View File
@@ -1,50 +1,103 @@
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import Form from '@/components/common/Form'
import Label from '@/components/common/Label'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import InheritDialogue from '@/components/users/InheritDialogue'
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
import { API_BASE_URL, SITE_TITLE } from '@/config'
import { Button } from '@/components/ui/button'
import type { User } from '@/types'
type Props = { user: User
setUser: (user: User) => void }
type Props = { user: User | null
setUser: (user: User) => void }
export default ({ user, setUser }: Props) => {
const [name, setName] = useState ('')
const [userCodeVsbl, setUserCodeVsbl] = useState (false)
const [inheritVsbl, setInheritVsbl] = useState (false)
const handleSubmit = async () => {
const formData = new FormData ()
formData.append ('name', name)
try
{
await axios.post (`${ API_BASE_URL }/users`, formData, { headers: {
'Content-Type': 'multipart/form-data',
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
toast ({ title: '設定を更新しました.' })
}
catch
{
toast ({ title: 'しっぱい……' })
}
}
useEffect (() => {
if (!user)
return
setName (user?.name)
setName (user.name)
}, [user])
return (
<MainArea>
<Helmet>
<meta name="robots" content="noindex" />
<title> | {SITE_TITLE}</title>
</Helmet>
<Form>
<PageTitle></PageTitle>
<Helmet>
<meta name="robots" content="noindex" />
<title> | {SITE_TITLE}</title>
</Helmet>
{/* 名前 */}
<div>
<Label></Label>
<input type="text"
className="w-full border rounded p-2"
value={name}
placeholder="名もなきニジラー"
onChange={ev => setName (ev.target.value)} />
{(user && !(user.name)) && (
<p class="mt-1 text-sm text-red-500">
30 !!!!
</p>)}
</div>
</Form>
<Form>
<PageTitle></PageTitle>
{/* 名前 */}
<div>
<Label></Label>
<input type="text"
className="w-full border rounded p-2"
value={name}
placeholder="名もなきニジラー"
onChange={ev => setName (ev.target.value)} />
{(user && !(user.name)) && (
<p className="mt-1 text-sm text-red-500">
30 !!!!
</p>)}
</div>
{/* 送信 */}
<Button onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
</Button>
{/* 引継ぎ */}
<div>
<Label></Label>
<Button onClick={() => setUserCodeVsbl (true)}
className="px-4 py-2 bg-gray-600 text-white rounded disabled:bg-gray-400"
disabled={!(user)}>
</Button>
<Button onClick={() => setInheritVsbl (true)}
className="ml-2 px-4 py-2 bg-red-600 text-white rounded disabled:bg-gray-400"
disabled={!(user)}>
</Button>
</div>
</Form>
<UserCodeDialogue visible={userCodeVsbl}
onVisibleChange={setUserCodeVsbl}
user={user} />
<InheritDialogue visible={inheritVsbl}
onVisibleChange={setInheritVsbl}
user={user}
setUser={setUser} />
</MainArea>)
}
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
import WikiBody from '@/components/WikiBody'
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation, useParams } from 'react-router-dom'
import PageTitle from '@/components/common/PageTitle'
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import MarkdownIt from 'markdown-it'
import React, { useEffect, useState, useRef } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import MdEditor from 'react-markdown-editor-lite'
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom'
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import { Link, useLocation, useParams } from 'react-router-dom'
import MainArea from '@/components/layout/MainArea'
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import MarkdownIt from 'markdown-it'
import React, { useEffect, useState, useRef } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import MdEditor from 'react-markdown-editor-lite'
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom'
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import React, { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'
import { Helmet } from 'react-helmet-async'
import { Link } from 'react-router-dom'
import SectionTitle from '@/components/common/SectionTitle'
import MainArea from '@/components/layout/MainArea'
+5
View File
@@ -0,0 +1,5 @@
import { Route,
createBrowserRouter,
createRoutesFromElements } from 'react-router-dom'
import App from '@/App'