| @@ -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 } } }) | |||