summaryrefslogtreecommitdiff
path: root/app/routes/artist-new.tsx
blob: ce91dab0a1fa4da1d7f81e42b32a323e7059391d (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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { useState } from "react";
import { Form, Link, redirect, useActionData } from "react-router";
import type { ActionFunctionArgs } from "react-router";
import { createArtist, getIpAddress } from "~/lib/db.server";

export async function action({ request }: ActionFunctionArgs) {
  const fd = await request.formData();
  const name = (fd.get("name") as string).trim();
  const slug = (fd.get("slug") as string).trim();
  const message = (fd.get("message") as string).trim();
  const links: { label: string; url: string }[] = JSON.parse(
    (fd.get("links") as string) || "[]"
  );

  const errors: Record<string, string> = {};
  if (!name) errors.name = "必須です";
  if (!slug) errors.slug = "必須です";
  if (!message) errors.message = "必須です";
  if (Object.keys(errors).length > 0) return { errors };

  const id = crypto.randomUUID();
  try {
    createArtist({ id, slug, name, links, message, ip_address: getIpAddress(request) });
  } catch (e) {
    if (e instanceof Error && e.message.includes("UNIQUE constraint failed: artists.slug")) {
      return { errors: { slug: "このslugは既に使用されています" } };
    }
    throw e;
  }
  return redirect(`/artists/of/${id}`);
}

function toSlug(s: string) {
  return s.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^\w぀-ヿ一-鿿＀-￯-]/g, "").replace(/^-+|-+$/g, "");
}

export default function ArtistNew() {
  const actionData = useActionData<typeof action>();
  const errors = actionData?.errors ?? {};

  const [name, setName] = useState("");
  const [slug, setSlug] = useState("");
  const [slugManual, setSlugManual] = useState(false);
  const [links, setLinks] = useState<{ label: string; url: string }[]>([]);

  return (
    <main>
      <div className="page-header">
        <Link to="/" className="back">←</Link>
        <h1>New Artist</h1>
      </div>

      <Form method="post">
        <input type="hidden" name="links" value={JSON.stringify(links)} />

        <div>
          <label>名前 <span className="req">*</span></label>
          <input
            name="name"
            value={name}
            onChange={(e) => {
              setName(e.target.value);
              if (!slugManual) setSlug(toSlug(e.target.value));
            }}
          />
          {errors.name && <p className="error">{errors.name}</p>}
        </div>

        <div>
          <label>Slug <span className="req">*</span></label>
          <input
            name="slug"
            value={slug}
            onChange={(e) => { setSlugManual(true); setSlug(e.target.value); }}
            className="mono"
          />
          {errors.slug && <p className="error">{errors.slug}</p>}
        </div>

        <div>
          <label>リンク</label>
          <div className="links-form">
            {links.map((link, i) => (
              <div key={i} className="link-row">
                <input
                  className="label-input"
                  value={link.label}
                  onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))}
                  placeholder="ラベル (例: X)"
                />
                <input
                  value={link.url}
                  onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, url: e.target.value } : l))}
                  placeholder="https://..."
                />
                <button
                  type="button"
                  className="btn-icon"
                  onClick={() => setLinks(links.filter((_, idx) => idx !== i))}
                >
                  ×
                </button>
              </div>
            ))}
          </div>
          <button
            type="button"
            className="btn-text"
            onClick={() => setLinks([...links, { label: "", url: "" }])}
          >
            + リンクを追加
          </button>
        </div>

        <div>
          <label>更新メッセージ <span className="req">*</span></label>
          <input name="message" placeholder="例: 初回登録" />
          {errors.message && <p className="error">{errors.message}</p>}
        </div>

        <div className="actions">
          <button type="submit">作成</button>
          <Link to="/" className="btn">キャンセル</Link>
        </div>
      </Form>
    </main>
  );
}