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