summaryrefslogtreecommitdiff
path: root/app/routes/home.tsx
blob: 51d6f042fcd938df5c1374ccd536aac79ebde99a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { Form, redirect, useNavigation } from "react-router";
import type { Route } from "./+types/home";
import { createPost, listPosts } from "~/lib/db.server";
import { renderMarkdown } from "~/lib/md.server";

export async function loader() {
  const posts = listPosts();
  return { posts: posts.map((p) => ({ ...p, html: renderMarkdown(p.content) })) };
}

export async function action({ request }: Route.ActionArgs) {
  const form = await request.formData();
  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("/");
}

export default function Home({ loaderData, actionData }: Route.ComponentProps) {
  const { posts } = loaderData;
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <div className="wrap">
      <header className="site-header">
        <h1>log</h1>
      </header>

      <div className="compose">
        <Form method="post">
          <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 && (
          <p className="empty">まだ投稿がありません</p>
        )}
        {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="prose"
              dangerouslySetInnerHTML={{ __html: post.html }}
            />
          </article>
        ))}
      </div>
    </div>
  );
}

function formatDate(isoUtc: string): string {
  return new Date(isoUtc + "Z").toLocaleString("ja-JP", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    timeZone: "Asia/Tokyo",
  });
}