summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/lib/db.server.ts25
-rw-r--r--app/routes/band-by-uuid.tsx14
-rw-r--r--app/routes/band-edit.tsx27
-rw-r--r--app/routes/band-new.tsx26
-rw-r--r--app/routes/home.tsx48
5 files changed, 112 insertions, 28 deletions
diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts
index dd9184d..bf63e70 100644
--- a/app/lib/db.server.ts
+++ b/app/lib/db.server.ts
@@ -74,6 +74,13 @@ function initSchema(db: Database.Database) {
);
CREATE INDEX IF NOT EXISTS idx_band_links_band_id ON band_links(band_id);
+ `);
+
+ // migrations
+ try { db.exec("ALTER TABLE bands ADD COLUMN description TEXT"); } catch { /* already exists */ }
+ try { db.exec("ALTER TABLE bands ADD COLUMN status TEXT NOT NULL DEFAULT 'active'"); } catch { /* already exists */ }
+
+ db.exec(`
CREATE INDEX IF NOT EXISTS idx_artist_links_artist_id ON artist_links(artist_id);
CREATE INDEX IF NOT EXISTS idx_band_artists_band_id ON band_artists(band_id);
CREATE INDEX IF NOT EXISTS idx_band_artists_artist_id ON band_artists(artist_id);
@@ -87,6 +94,8 @@ export interface Band {
slug: string;
name: string;
area: string | null;
+ description: string | null;
+ status: string;
created_at: string;
}
@@ -208,6 +217,8 @@ export interface CreateBandInput {
slug: string;
name: string;
area: string | null;
+ description: string | null;
+ status: string;
links: { label: string; url: string }[];
artists: { id: string; role: string | null }[];
message: string;
@@ -217,8 +228,8 @@ export interface CreateBandInput {
export function createBand(input: CreateBandInput): Band {
const db = getDb();
return db.transaction(() => {
- db.prepare("INSERT INTO bands (id, slug, name, area) VALUES (?, ?, ?, ?)").run(
- input.id, input.slug, input.name, input.area
+ db.prepare("INSERT INTO bands (id, slug, name, area, description, status) VALUES (?, ?, ?, ?, ?, ?)").run(
+ input.id, input.slug, input.name, input.area, input.description, input.status
);
input.links.forEach((link, i) => {
db.prepare(
@@ -236,6 +247,8 @@ export function createBand(input: CreateBandInput): Band {
const snapshot = JSON.stringify({
name: band.name,
area: band.area,
+ description: band.description,
+ status: band.status,
links: links.map((l) => ({ label: l.label, url: l.url })),
artists: artists.map((a) => ({ id: a.artist_id, name: a.artist_name, role: a.role })),
});
@@ -250,6 +263,8 @@ export interface UpdateBandInput {
slug: string;
name: string;
area: string | null;
+ description: string | null;
+ status: string;
links: { label: string; url: string }[];
artists: { id: string; role: string | null }[];
message: string;
@@ -259,8 +274,8 @@ export interface UpdateBandInput {
export function updateBand(id: string, input: UpdateBandInput): void {
const db = getDb();
db.transaction(() => {
- db.prepare("UPDATE bands SET slug = ?, name = ?, area = ? WHERE id = ?").run(
- input.slug, input.name, input.area, id
+ db.prepare("UPDATE bands SET slug = ?, name = ?, area = ?, description = ?, status = ? WHERE id = ?").run(
+ input.slug, input.name, input.area, input.description, input.status, id
);
db.prepare("DELETE FROM band_links WHERE band_id = ?").run(id);
input.links.forEach((link, i) => {
@@ -280,6 +295,8 @@ export function updateBand(id: string, input: UpdateBandInput): void {
const snapshot = JSON.stringify({
name: band.name,
area: band.area,
+ description: band.description,
+ status: band.status,
links: links.map((l) => ({ label: l.label, url: l.url })),
artists: artists.map((a) => ({ id: a.artist_id, name: a.artist_name, role: a.role })),
});
diff --git a/app/routes/band-by-uuid.tsx b/app/routes/band-by-uuid.tsx
index c55472e..1945a57 100644
--- a/app/routes/band-by-uuid.tsx
+++ b/app/routes/band-by-uuid.tsx
@@ -16,6 +16,12 @@ export async function loader({ params }: LoaderFunctionArgs) {
return { band, links, artists, latest: revisions[0] ?? null };
}
+const STATUS_LABEL: Record<string, string> = {
+ active: "活動中",
+ hiatus: "活動休止",
+ disbanded: "解散",
+};
+
export default function BandDetail() {
const { band, links, artists, latest } = useLoaderData<typeof loader>();
return (
@@ -23,7 +29,13 @@ export default function BandDetail() {
<div className="flex items-start justify-between mb-6">
<div>
<h1 className="text-2xl font-bold">{band.name}</h1>
- {band.area && <p className="text-gray-400 mt-1 text-sm">{band.area}</p>}
+ <div className="flex items-center gap-3 mt-1 text-sm text-gray-400">
+ {band.area && <span>{band.area}</span>}
+ <span>{STATUS_LABEL[band.status] ?? band.status}</span>
+ </div>
+ {band.description && (
+ <p className="mt-3 text-gray-300 text-sm leading-relaxed">{band.description}</p>
+ )}
</div>
<div className="flex items-center gap-3 text-sm shrink-0 ml-4">
<Link
diff --git a/app/routes/band-edit.tsx b/app/routes/band-edit.tsx
index 70e1d60..99d4ee7 100644
--- a/app/routes/band-edit.tsx
+++ b/app/routes/band-edit.tsx
@@ -27,6 +27,8 @@ export async function action({ params, request }: ActionFunctionArgs) {
const name = (fd.get("name") as string).trim();
const slug = (fd.get("slug") as string).trim();
const area = (fd.get("area") as string).trim() || null;
+ const description = (fd.get("description") as string).trim() || null;
+ const status = (fd.get("status") as string) || "active";
const message = (fd.get("message") as string).trim();
const links: { label: string; url: string }[] = JSON.parse(
(fd.get("links") as string) || "[]"
@@ -42,7 +44,7 @@ export async function action({ params, request }: ActionFunctionArgs) {
if (Object.keys(errors).length > 0) return { errors };
try {
- updateBand(band.id, { slug, name, area, links, artists, message, ip_address: getIpAddress(request) });
+ updateBand(band.id, { slug, name, area, description, status, links, artists, message, ip_address: getIpAddress(request) });
} catch (e) {
if (e instanceof Error && e.message.includes("UNIQUE constraint failed: bands.slug")) {
return { errors: { slug: "このslugは既に使用されています" } };
@@ -130,6 +132,29 @@ export default function BandEdit() {
</div>
<div>
+ <label className="block text-sm font-medium text-gray-300 mb-1">ステータス</label>
+ <select
+ name="status"
+ defaultValue={band.status}
+ className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
+ >
+ <option value="active">活動中</option>
+ <option value="hiatus">活動休止</option>
+ <option value="disbanded">解散</option>
+ </select>
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium text-gray-300 mb-1">説明</label>
+ <textarea
+ name="description"
+ rows={3}
+ defaultValue={band.description ?? ""}
+ 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 resize-none"
+ />
+ </div>
+
+ <div>
<label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
<div className="space-y-2">
{links.map((link, i) => (
diff --git a/app/routes/band-new.tsx b/app/routes/band-new.tsx
index b0325a3..b4d49e2 100644
--- a/app/routes/band-new.tsx
+++ b/app/routes/band-new.tsx
@@ -12,6 +12,8 @@ export async function action({ request }: ActionFunctionArgs) {
const name = (fd.get("name") as string).trim();
const slug = (fd.get("slug") as string).trim();
const area = (fd.get("area") as string).trim() || null;
+ const description = (fd.get("description") as string).trim() || null;
+ const status = (fd.get("status") as string) || "active";
const message = (fd.get("message") as string).trim();
const links: { label: string; url: string }[] = JSON.parse(
(fd.get("links") as string) || "[]"
@@ -28,7 +30,7 @@ export async function action({ request }: ActionFunctionArgs) {
const id = crypto.randomUUID();
try {
- createBand({ id, slug, name, area, links, artists, message, ip_address: getIpAddress(request) });
+ createBand({ id, slug, name, area, description, status, links, artists, message, ip_address: getIpAddress(request) });
} catch (e) {
if (e instanceof Error && e.message.includes("UNIQUE constraint failed: bands.slug")) {
return { errors: { slug: "このslugは既に使用されています" } };
@@ -108,6 +110,28 @@ export default function BandNew() {
</div>
<div>
+ <label className="block text-sm font-medium text-gray-300 mb-1">ステータス</label>
+ <select
+ name="status"
+ defaultValue="active"
+ className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500"
+ >
+ <option value="active">活動中</option>
+ <option value="hiatus">活動休止</option>
+ <option value="disbanded">解散</option>
+ </select>
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium text-gray-300 mb-1">説明</label>
+ <textarea
+ name="description"
+ rows={3}
+ 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 resize-none"
+ />
+ </div>
+
+ <div>
<label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
<div className="space-y-2">
{links.map((link, i) => (
diff --git a/app/routes/home.tsx b/app/routes/home.tsx
index 3df4ab5..5600645 100644
--- a/app/routes/home.tsx
+++ b/app/routes/home.tsx
@@ -5,38 +5,44 @@ export function loader() {
return { bands: listBands() };
}
+const STATUS_LABEL: Record<string, string> = {
+ active: "活動中",
+ hiatus: "活動休止",
+ disbanded: "解散",
+};
+
export default function Home() {
const { bands } = useLoaderData<typeof loader>();
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center justify-between mb-6">
- <h1 className="text-xl font-semibold">Bands</h1>
- <Link to="/bands/new" className="text-sm text-blue-400 hover:text-blue-300">
- + New Band
- </Link>
- </div>
+ <main className="max-w-2xl mx-auto px-6 py-12">
{bands.length === 0 ? (
- <p className="text-gray-400">
- まだバンドがありません。{" "}
- <Link to="/bands/new" className="text-blue-400 hover:text-blue-300">
+ <p className="text-gray-600 text-sm">
+ バンドがまだありません。{" "}
+ <Link to="/bands/new" className="underline">
追加する
</Link>
</p>
) : (
- <ul className="divide-y divide-gray-800">
+ <ul className="divide-y divide-gray-900">
{bands.map((band) => (
- <li key={band.id} className="py-3">
- <Link
- to={`/bands/of/${band.id}`}
- className="flex items-baseline gap-3 group"
- >
- <span className="font-medium group-hover:text-blue-300 transition-colors">
+ <li key={band.id} className="py-4 flex items-start justify-between gap-6">
+ <div className="min-w-0">
+ <Link
+ to={`/bands/of/${band.id}`}
+ className="text-gray-100 hover:text-white font-medium"
+ >
{band.name}
- </span>
- {band.area && (
- <span className="text-gray-400 text-sm">{band.area}</span>
+ </Link>
+ {band.description && (
+ <p className="text-gray-500 text-sm mt-0.5 line-clamp-1">
+ {band.description}
+ </p>
)}
- </Link>
+ </div>
+ <div className="flex items-center gap-4 text-sm text-gray-500 shrink-0 pt-0.5">
+ {band.area && <span>{band.area}</span>}
+ <span>{STATUS_LABEL[band.status] ?? band.status}</span>
+ </div>
</li>
))}
</ul>