@@ -21,7 +21,7 @@ | |||
"markdown-it": "^14.1.0", | |||
"react": "^19.1.0", | |||
"react-dom": "^19.1.0", | |||
"react-helmet": "^6.1.0", | |||
"react-helmet-async": "^2.0.5", | |||
"react-markdown": "^10.1.0", | |||
"react-markdown-editor-lite": "^1.3.4", | |||
"react-router-dom": "^6.30.0", | |||
@@ -3841,6 +3841,15 @@ | |||
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", | |||
"license": "MIT" | |||
}, | |||
"node_modules/invariant": { | |||
"version": "2.2.4", | |||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", | |||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", | |||
"license": "MIT", | |||
"dependencies": { | |||
"loose-envify": "^1.0.0" | |||
} | |||
}, | |||
"node_modules/is-alphabetical": { | |||
"version": "2.0.1", | |||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", | |||
@@ -4964,6 +4973,7 @@ | |||
"version": "4.1.1", | |||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", | |||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", | |||
"dev": true, | |||
"license": "MIT", | |||
"engines": { | |||
"node": ">=0.10.0" | |||
@@ -5338,17 +5348,6 @@ | |||
"node": ">= 0.8.0" | |||
} | |||
}, | |||
"node_modules/prop-types": { | |||
"version": "15.8.1", | |||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", | |||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", | |||
"license": "MIT", | |||
"dependencies": { | |||
"loose-envify": "^1.4.0", | |||
"object-assign": "^4.1.1", | |||
"react-is": "^16.13.1" | |||
} | |||
}, | |||
"node_modules/property-information": { | |||
"version": "7.1.0", | |||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", | |||
@@ -5444,27 +5443,20 @@ | |||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", | |||
"license": "MIT" | |||
}, | |||
"node_modules/react-helmet": { | |||
"version": "6.1.0", | |||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", | |||
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", | |||
"license": "MIT", | |||
"node_modules/react-helmet-async": { | |||
"version": "2.0.5", | |||
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", | |||
"integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", | |||
"license": "Apache-2.0", | |||
"dependencies": { | |||
"object-assign": "^4.1.1", | |||
"prop-types": "^15.7.2", | |||
"react-fast-compare": "^3.1.1", | |||
"react-side-effect": "^2.1.0" | |||
"invariant": "^2.2.4", | |||
"react-fast-compare": "^3.2.2", | |||
"shallowequal": "^1.1.0" | |||
}, | |||
"peerDependencies": { | |||
"react": ">=16.3.0" | |||
"react": "^16.6.0 || ^17.0.0 || ^18.0.0" | |||
} | |||
}, | |||
"node_modules/react-is": { | |||
"version": "16.13.1", | |||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | |||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", | |||
"license": "MIT" | |||
}, | |||
"node_modules/react-markdown": { | |||
"version": "10.1.0", | |||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", | |||
@@ -5596,15 +5588,6 @@ | |||
"react-dom": ">=16.8" | |||
} | |||
}, | |||
"node_modules/react-side-effect": { | |||
"version": "2.1.2", | |||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", | |||
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", | |||
"license": "MIT", | |||
"peerDependencies": { | |||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0" | |||
} | |||
}, | |||
"node_modules/react-style-singleton": { | |||
"version": "2.2.3", | |||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", | |||
@@ -5805,6 +5788,12 @@ | |||
"semver": "bin/semver.js" | |||
} | |||
}, | |||
"node_modules/shallowequal": { | |||
"version": "1.1.0", | |||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", | |||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", | |||
"license": "MIT" | |||
}, | |||
"node_modules/shebang-command": { | |||
"version": "2.0.0", | |||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", | |||
@@ -22,7 +22,7 @@ | |||
"markdown-it": "^14.1.0", | |||
"react": "^19.1.0", | |||
"react-dom": "^19.1.0", | |||
"react-helmet": "^6.1.0", | |||
"react-helmet-async": "^2.0.5", | |||
"react-markdown": "^10.1.0", | |||
"react-markdown-editor-lite": "^1.3.4", | |||
"react-router-dom": "^6.30.0", | |||
@@ -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 /> | |||
</>) | |||
} |
@@ -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 => ( | |||
@@ -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>) | |||
} |
@@ -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 } |
@@ -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,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,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' | |||
@@ -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> | |||
</>) | |||
} |
@@ -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 (() => { | |||
@@ -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> | |||
{/* 名前 */} | |||
<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> | |||
<Helmet> | |||
<meta name="robots" content="noindex" /> | |||
<title>設定 | {SITE_TITLE}</title> | |||
</Helmet> | |||
<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,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,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,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,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,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,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' | |||
@@ -0,0 +1,5 @@ | |||
import { Route, | |||
createBrowserRouter, | |||
createRoutesFromElements } from 'react-router-dom' | |||
import App from '@/App' |
@@ -2,15 +2,17 @@ import { defineConfig } from 'vite' | |||
import react from '@vitejs/plugin-react' | |||
import path from 'path' | |||
// https://vite.dev/config/ | |||
export default defineConfig({ | |||
plugins: [react()], | |||
resolve: { alias: { '@': path.resolve (__dirname, './src') } }, | |||
server: { host: true, | |||
port: 5173, | |||
strictPort: true, | |||
allowedHosts: ['hub.nizika.monster', 'localhost'], | |||
proxy: { '/api': { target: 'http://localhost:3002', | |||
changeOrigin: true, | |||
secure: false } } } | |||
}) | |||
export default defineConfig ({ | |||
plugins: [react()], | |||
resolve: { alias: { '@': path.resolve (__dirname, './src') } }, | |||
server: { host: true, | |||
port: 5173, | |||
strictPort: true, | |||
allowedHosts: ['hub.nizika.monster', 'localhost'], | |||
proxy: { '/api': { target: 'http://localhost:3002', | |||
changeOrigin: true, | |||
secure: false } }, | |||
watch: { usePolling: true, | |||
interval: 100 } } }) |