summaryrefslogtreecommitdiff
path: root/app/routes/artist-edit.tsx
blob: f2e5c186935eaf96f2f9861820332652161f84d4 (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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { useState } from "react";
import { data, Form, Link, redirect, useActionData, useLoaderData } from "react-router";
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
import { getArtistById, getArtistLinks, getIpAddress, updateArtist } from "~/lib/db.server";

export async function loader({ params }: LoaderFunctionArgs) {
  const artist = getArtistById(params.uuid!);
  if (!artist) throw data("Not found", { status: 404 });
  const links = getArtistLinks(artist.id);
  return { artist, links };
}

export async function action({ params, request }: ActionFunctionArgs) {
  const artist = getArtistById(params.uuid!);
  if (!artist) throw data("Not found", { status: 404 });

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

  try {
    updateArtist(artist.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/${artist.id}`);
}

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

export default function ArtistEdit() {
  const { artist, links: initLinks } = useLoaderData<typeof loader>();
  const actionData = useActionData<typeof action>();
  const errors = actionData?.errors ?? {};

  const [name, setName] = useState(artist.name);
  const [slug, setSlug] = useState(artist.slug);
  const [slugManual, setSlugManual] = useState(true);
  const [links, setLinks] = useState(initLinks.map((l) => ({ label: l.label, url: l.url })));

  return (
    <main className="max-w-3xl mx-auto px-4 py-8">
      <div className="flex items-center gap-3 mb-6">
        <Link
          to={`/artists/of/${artist.id}`}
          className="text-gray-400 hover:text-gray-200 transition-colors"
        >
          ←
        </Link>
        <h1 className="text-xl font-semibold">Edit Artist</h1>
      </div>

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

        <div>
          <label className="block text-sm font-medium text-gray-300 mb-1">
            名前 <span className="text-red-400">*</span>
          </label>
          <input
            name="name"
            value={name}
            onChange={(e) => {
              setName(e.target.value);
              if (!slugManual) setSlug(toSlug(e.target.value));
            }}
            className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
          />
          {errors.name && <p className="text-red-400 text-sm mt-1">{errors.name}</p>}
        </div>

        <div>
          <label className="block text-sm font-medium text-gray-300 mb-1">
            Slug <span className="text-red-400">*</span>
          </label>
          <input
            name="slug"
            value={slug}
            onChange={(e) => { setSlugManual(true); setSlug(e.target.value); }}
            className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 font-mono text-sm"
          />
          {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
        </div>

        <div>
          <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
          <div className="space-y-2">
            {links.map((link, i) => (
              <div key={i} className="flex gap-2">
                <input
                  value={link.label}
                  onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))}
                  placeholder="ラベル (例: X)"
                  className="w-28 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
                />
                <input
                  value={link.url}
                  onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, url: e.target.value } : l))}
                  placeholder="https://..."
                  className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
                />
                <button
                  type="button"
                  onClick={() => setLinks(links.filter((_, idx) => idx !== i))}
                  className="text-gray-500 hover:text-red-400 px-2 transition-colors"
                >
                  ×
                </button>
              </div>
            ))}
          </div>
          <button
            type="button"
            onClick={() => setLinks([...links, { label: "", url: "" }])}
            className="mt-2 text-blue-400 hover:text-blue-300 text-sm transition-colors"
          >
            + リンクを追加
          </button>
        </div>

        <div>
          <label className="block text-sm font-medium text-gray-300 mb-1">
            更新メッセージ <span className="text-red-400">*</span>
          </label>
          <input
            name="message"
            placeholder="例: SNSリンク追加"
            className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
          />
          {errors.message && <p className="text-red-400 text-sm mt-1">{errors.message}</p>}
        </div>

        <div className="flex gap-3 pt-2">
          <button
            type="submit"
            className="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded font-medium transition-colors"
          >
            保存
          </button>
          <Link
            to={`/artists/of/${artist.id}`}
            className="bg-gray-800 hover:bg-gray-700 text-gray-300 px-4 py-2 rounded transition-colors"
          >
            キャンセル
          </Link>
        </div>
      </Form>
    </main>
  );
}