@@ -21,7 +21,7 @@ | |||||
"markdown-it": "^14.1.0", | "markdown-it": "^14.1.0", | ||||
"react": "^19.1.0", | "react": "^19.1.0", | ||||
"react-dom": "^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": "^10.1.0", | ||||
"react-markdown-editor-lite": "^1.3.4", | "react-markdown-editor-lite": "^1.3.4", | ||||
"react-router-dom": "^6.30.0", | "react-router-dom": "^6.30.0", | ||||
@@ -3841,6 +3841,15 @@ | |||||
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", | "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", | ||||
"license": "MIT" | "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": { | "node_modules/is-alphabetical": { | ||||
"version": "2.0.1", | "version": "2.0.1", | ||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", | "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", | ||||
@@ -4964,6 +4973,7 @@ | |||||
"version": "4.1.1", | "version": "4.1.1", | ||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", | ||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", | ||||
"dev": true, | |||||
"license": "MIT", | "license": "MIT", | ||||
"engines": { | "engines": { | ||||
"node": ">=0.10.0" | "node": ">=0.10.0" | ||||
@@ -5338,17 +5348,6 @@ | |||||
"node": ">= 0.8.0" | "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": { | "node_modules/property-information": { | ||||
"version": "7.1.0", | "version": "7.1.0", | ||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", | "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", | ||||
@@ -5444,27 +5443,20 @@ | |||||
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", | "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", | ||||
"license": "MIT" | "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": { | "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": { | "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": { | "node_modules/react-markdown": { | ||||
"version": "10.1.0", | "version": "10.1.0", | ||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", | "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", | ||||
@@ -5596,15 +5588,6 @@ | |||||
"react-dom": ">=16.8" | "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": { | "node_modules/react-style-singleton": { | ||||
"version": "2.2.3", | "version": "2.2.3", | ||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", | "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", | ||||
@@ -5805,6 +5788,12 @@ | |||||
"semver": "bin/semver.js" | "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": { | "node_modules/shebang-command": { | ||||
"version": "2.0.0", | "version": "2.0.0", | ||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", | ||||
@@ -22,7 +22,7 @@ | |||||
"markdown-it": "^14.1.0", | "markdown-it": "^14.1.0", | ||||
"react": "^19.1.0", | "react": "^19.1.0", | ||||
"react-dom": "^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": "^10.1.0", | ||||
"react-markdown-editor-lite": "^1.3.4", | "react-markdown-editor-lite": "^1.3.4", | ||||
"react-router-dom": "^6.30.0", | "react-router-dom": "^6.30.0", | ||||
@@ -27,57 +27,54 @@ export default () => { | |||||
const [user, setUser] = useState<User | null> (null) | const [user, setUser] = useState<User | null> (null) | ||||
useEffect (() => { | 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') | const code = localStorage.getItem ('user_code') | ||||
if (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 | else | ||||
createUser () | createUser () | ||||
alert ('このサイトはまだ作りかけです!!!!\n出てけ!!!!!!!!!!!!!!!!!!!!') | |||||
}, []) | }, []) | ||||
return ( | 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> | <SidebarComponent> | ||||
<TagSearch /> | <TagSearch /> | ||||
{CATEGORIES.map ((cat: Category) => cat in tags && ( | {CATEGORIES.map ((cat: Category) => cat in tags && ( | ||||
<div className="my-3"> | |||||
<div className="my-3" key={cat}> | |||||
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> | <SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> | ||||
<ul> | <ul> | ||||
{tags[cat].map (tag => ( | {tags[cat].map (tag => ( | ||||
@@ -32,18 +32,18 @@ export default ({ posts }: Props) => { | |||||
loop: | loop: | ||||
for (const post of posts) | 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)) | for (const cat of Object.keys (tagsTmp)) | ||||
tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1) | tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1) | ||||
@@ -52,40 +52,38 @@ export default ({ posts }: Props) => { | |||||
return ( | return ( | ||||
<SidebarComponent> | <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>) | </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 | 'meta'] as const | ||||
export const USER_ROLES = ['admin', 'member', 'guest'] 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 { StrictMode } from 'react' | ||||
import { createRoot } from 'react-dom/client' | 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 MainArea from '@/components/layout/MainArea' | ||||
import { SITE_TITLE } from '@/config' | import { SITE_TITLE } from '@/config' | ||||
@@ -1,7 +1,7 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import toCamel from 'camelcase-keys' | import toCamel from 'camelcase-keys' | ||||
import React, { useEffect, useState } from 'react' | 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 { Link, useLocation, useParams } from 'react-router-dom' | ||||
import TagDetailSidebar from '@/components/TagDetailSidebar' | import TagDetailSidebar from '@/components/TagDetailSidebar' | ||||
@@ -1,7 +1,7 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import toCamel from 'camelcase-keys' | import toCamel from 'camelcase-keys' | ||||
import React, { useEffect, useRef, useState } from 'react' | 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 { Link, useLocation } from 'react-router-dom' | ||||
import TagSidebar from '@/components/TagSidebar' | import TagSidebar from '@/components/TagSidebar' | ||||
@@ -14,7 +14,7 @@ import type { Post, Tag, WikiPage } from '@/types' | |||||
export default () => { | export default () => { | ||||
const [posts, setPosts] = useState<Post[] | null> (null) | |||||
const [posts, setPosts] = useState<Post[]> ([]) | |||||
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null) | const [wikiPage, setWikiPage] = useState<WikiPage | null> (null) | ||||
const [cursor, setCursor] = useState ('') | const [cursor, setCursor] = useState ('') | ||||
const [loading, setLoading] = useState (false) | const [loading, setLoading] = useState (false) | ||||
@@ -24,11 +24,11 @@ export default () => { | |||||
const loadMore = async withCursor => { | const loadMore = async withCursor => { | ||||
setLoading (true) | setLoading (true) | ||||
const res = await axios.get (`${ API_BASE_URL }/posts`, { | 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 }) | const data = toCamel (res.data, { deep: true }) | ||||
setPosts (posts => [...(posts || []), ...data.posts]) | |||||
setPosts (posts => [...(withCursor ? posts : []), ...data.posts]) | |||||
setCursor (data.nextCursor) | setCursor (data.nextCursor) | ||||
setLoading (false) | setLoading (false) | ||||
} | } | ||||
@@ -42,7 +42,7 @@ export default () => { | |||||
useEffect(() => { | useEffect(() => { | ||||
const observer = new IntersectionObserver (entries => { | const observer = new IntersectionObserver (entries => { | ||||
if (entries[0].isIntersecting && !(loading) && cursor) | if (entries[0].isIntersecting && !(loading) && cursor) | ||||
loadMore (true) | |||||
loadMore (true) | |||||
}, { threshold: 1 }) | }, { threshold: 1 }) | ||||
const target = loaderRef.current | const target = loaderRef.current | ||||
@@ -52,58 +52,65 @@ export default () => { | |||||
}, [loaderRef, loading]) | }, [loaderRef, loading]) | ||||
useEffect (() => { | useEffect (() => { | ||||
setPosts (null) | |||||
setCursor ('') | |||||
setPosts ([]) | |||||
loadMore (false) | loadMore (false) | ||||
setWikiPage (null) | setWikiPage (null) | ||||
if (tags.length === 1) | 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]) | }, [location.search]) | ||||
return ( | 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 axios from 'axios' | ||||
import React, { useEffect, useState, useRef } from 'react' | 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 { Link, useLocation, useParams, useNavigate } from 'react-router-dom' | ||||
import NicoViewer from '@/components/NicoViewer' | import NicoViewer from '@/components/NicoViewer' | ||||
@@ -49,8 +49,8 @@ export default () => { | |||||
toast ({ title: '投稿成功!' }) | toast ({ title: '投稿成功!' }) | ||||
navigate ('/posts') | navigate ('/posts') | ||||
}) | }) | ||||
.catch (e => toast ({ title: '投稿失敗', | |||||
description: '入力を確認してください。' }))) | |||||
.catch (() => toast ({ title: '投稿失敗', | |||||
description: '入力を確認してください。' }))) | |||||
} | } | ||||
useEffect (() => { | useEffect (() => { | ||||
@@ -1,50 +1,103 @@ | |||||
import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||
import { Helmet } from 'react-helmet' | |||||
import { Helmet } from 'react-helmet-async' | |||||
import Form from '@/components/common/Form' | import Form from '@/components/common/Form' | ||||
import Label from '@/components/common/Label' | import Label from '@/components/common/Label' | ||||
import PageTitle from '@/components/common/PageTitle' | import PageTitle from '@/components/common/PageTitle' | ||||
import MainArea from '@/components/layout/MainArea' | 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' | 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) => { | export default ({ user, setUser }: Props) => { | ||||
const [name, setName] = useState ('') | 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 (() => { | useEffect (() => { | ||||
if (!user) | if (!user) | ||||
return | return | ||||
setName (user?.name) | |||||
setName (user.name) | |||||
}, [user]) | }, [user]) | ||||
return ( | return ( | ||||
<MainArea> | <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>) | </MainArea>) | ||||
} | } |
@@ -1,7 +1,7 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import toCamel from 'camelcase-keys' | import toCamel from 'camelcase-keys' | ||||
import { useEffect, useState } from 'react' | 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 { Link, useLocation, useNavigate, useParams } from 'react-router-dom' | ||||
import WikiBody from '@/components/WikiBody' | import WikiBody from '@/components/WikiBody' | ||||
@@ -1,7 +1,7 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import toCamel from 'camelcase-keys' | import toCamel from 'camelcase-keys' | ||||
import { useEffect, useState } from 'react' | 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 { Link, useLocation, useParams } from 'react-router-dom' | ||||
import PageTitle from '@/components/common/PageTitle' | import PageTitle from '@/components/common/PageTitle' | ||||
@@ -1,7 +1,7 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import MarkdownIt from 'markdown-it' | import MarkdownIt from 'markdown-it' | ||||
import React, { useEffect, useState, useRef } from 'react' | 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 MdEditor from 'react-markdown-editor-lite' | ||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom' | import { Link, useLocation, useParams, useNavigate } from 'react-router-dom' | ||||
@@ -1,7 +1,7 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import toCamel from 'camelcase-keys' | import toCamel from 'camelcase-keys' | ||||
import { useEffect, useState } from 'react' | 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 { Link, useLocation, useParams } from 'react-router-dom' | ||||
import MainArea from '@/components/layout/MainArea' | import MainArea from '@/components/layout/MainArea' | ||||
@@ -1,7 +1,7 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import MarkdownIt from 'markdown-it' | import MarkdownIt from 'markdown-it' | ||||
import React, { useEffect, useState, useRef } from 'react' | 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 MdEditor from 'react-markdown-editor-lite' | ||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom' | import { Link, useLocation, useParams, useNavigate } from 'react-router-dom' | ||||
@@ -1,7 +1,7 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import toCamel from 'camelcase-keys' | import toCamel from 'camelcase-keys' | ||||
import React, { useEffect, useState } from 'react' | import React, { useEffect, useState } from 'react' | ||||
import { Helmet } from 'react-helmet' | |||||
import { Helmet } from 'react-helmet-async' | |||||
import { Link } from 'react-router-dom' | import { Link } from 'react-router-dom' | ||||
import SectionTitle from '@/components/common/SectionTitle' | import SectionTitle from '@/components/common/SectionTitle' | ||||
import MainArea from '@/components/layout/MainArea' | 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 react from '@vitejs/plugin-react' | ||||
import path from 'path' | import path from 'path' | ||||
// https://vite.dev/config/ | // 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 } } }) |