diff options
Diffstat (limited to 'app/routes/home.tsx')
| -rw-r--r-- | app/routes/home.tsx | 121 |
1 files changed, 86 insertions, 35 deletions
diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 58004c9..7732326 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -1,31 +1,51 @@ import { useEffect, useRef } from "react"; -import { Form, redirect, useNavigation } from "react-router"; +import { Form, Link, redirect, useNavigation } from "react-router"; import type { Route } from "./+types/home"; -import { createPost, listPosts } from "~/lib/db.server"; +import { createPost, deletePost, listPosts, listCredentials } from "~/lib/db.server"; import { renderMarkdown } from "~/lib/md.server"; +import { getSession, commitSession } from "~/lib/session.server"; -export async function loader() { +export async function loader({ request }: Route.LoaderArgs) { + const session = await getSession(request.headers.get("Cookie")); + const isAuthenticated = session.get("authenticated") === true; + const hasCredentials = listCredentials().length > 0; const posts = listPosts(); - return { posts: posts.map((p) => ({ ...p, html: renderMarkdown(p.content) })) }; + return { + posts: posts.map((p) => ({ ...p, html: renderMarkdown(p.content) })), + isAuthenticated, + hasCredentials, + }; } export async function action({ request }: Route.ActionArgs) { + const session = await getSession(request.headers.get("Cookie")); + if (session.get("authenticated") !== true) { + throw new Response("Unauthorized", { status: 401 }); + } + const form = await request.formData(); - const content = String(form.get("content") ?? "").trim(); + const intent = String(form.get("intent") ?? "create"); - if (!content) { - return { error: "内容を入力してください", content }; - } - if (content.length > 10000) { - return { error: "投稿が長すぎます(最大10,000文字)", content }; + if (intent === "delete") { + const id = String(form.get("id") ?? ""); + if (id) deletePost(id); + return redirect("/", { + headers: { "Set-Cookie": await commitSession(session) }, + }); } + const content = String(form.get("content") ?? "").trim(); + if (!content) return { error: "内容を入力してください", content }; + if (content.length > 10000) return { error: "投稿が長すぎます(最大10,000文字)", content }; + createPost({ id: crypto.randomUUID(), content }); - return redirect("/"); + return redirect("/", { + headers: { "Set-Cookie": await commitSession(session) }, + }); } export default function Home({ loaderData, actionData }: Route.ComponentProps) { - const { posts } = loaderData; + const { posts, isAuthenticated, hasCredentials } = loaderData; const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; const formRef = useRef<HTMLFormElement>(null); @@ -43,28 +63,42 @@ export default function Home({ loaderData, actionData }: Route.ComponentProps) { <div className="wrap"> <header className="site-header"> <h1>log</h1> + <nav className="header-nav"> + {isAuthenticated ? ( + <Form method="post" action="/auth/logout"> + <button type="submit" className="logout-btn">ログアウト</button> + </Form> + ) : hasCredentials ? ( + <Link to="/auth/login" className="auth-link">ログイン</Link> + ) : ( + <Link to="/auth/register" className="auth-link">登録</Link> + )} + </nav> </header> - <div className="compose"> - <Form method="post" ref={formRef}> - <textarea - name="content" - placeholder="Markdown で投稿..." - rows={4} - defaultValue={actionData?.content ?? ""} - autoFocus - /> - {actionData?.error && ( - <p className="error-msg">{actionData.error}</p> - )} - <div className="compose-row"> - <span className="hint">markdown</span> - <button type="submit" disabled={isSubmitting}> - {isSubmitting ? "投稿中…" : "投稿"} - </button> - </div> - </Form> - </div> + {isAuthenticated && ( + <div className="compose"> + <Form method="post" ref={formRef}> + <input type="hidden" name="intent" value="create" /> + <textarea + name="content" + placeholder="Markdown で投稿..." + rows={4} + defaultValue={actionData?.content ?? ""} + autoFocus + /> + {actionData?.error && ( + <p className="error-msg">{actionData.error}</p> + )} + <div className="compose-row"> + <span className="hint">markdown</span> + <button type="submit" disabled={isSubmitting}> + {isSubmitting ? "投稿中…" : "投稿"} + </button> + </div> + </Form> + </div> + )} <div className="feed"> {posts.length === 0 && ( @@ -72,9 +106,26 @@ export default function Home({ loaderData, actionData }: Route.ComponentProps) { )} {posts.map((post) => ( <article key={post.id} className="post"> - <time className="post-meta" dateTime={post.created_at + "Z"}> - {formatDate(post.created_at)} - </time> + <div className="post-header"> + <time className="post-meta" dateTime={post.created_at + "Z"}> + {formatDate(post.created_at)} + </time> + {isAuthenticated && ( + <Form method="post"> + <input type="hidden" name="intent" value="delete" /> + <input type="hidden" name="id" value={post.id} /> + <button + type="submit" + className="delete-btn" + onClick={(e) => { + if (!confirm("この投稿を削除しますか?")) e.preventDefault(); + }} + > + 削除 + </button> + </Form> + )} + </div> <div className="prose" dangerouslySetInnerHTML={{ __html: post.html }} |
