summaryrefslogtreecommitdiff
path: root/app/routes
diff options
context:
space:
mode:
Diffstat (limited to 'app/routes')
-rw-r--r--app/routes/artist-by-uuid.tsx75
-rw-r--r--app/routes/artist-edit.tsx71
-rw-r--r--app/routes/artist-history.tsx35
-rw-r--r--app/routes/artist-new.tsx66
-rw-r--r--app/routes/band-by-uuid.tsx77
-rw-r--r--app/routes/band-edit.tsx112
-rw-r--r--app/routes/band-history.tsx40
-rw-r--r--app/routes/band-new.tsx114
-rw-r--r--app/routes/home.tsx27
9 files changed, 203 insertions, 414 deletions
diff --git a/app/routes/artist-by-uuid.tsx b/app/routes/artist-by-uuid.tsx
index 9b8a4b1..6eb06a7 100644
--- a/app/routes/artist-by-uuid.tsx
+++ b/app/routes/artist-by-uuid.tsx
@@ -14,42 +14,23 @@ export async function loader({ params }: LoaderFunctionArgs) {
export default function ArtistDetail() {
const { artist, links, bands, latest } = useLoaderData<typeof loader>();
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-start justify-between mb-6">
- <h1 className="text-2xl font-bold">{artist.name}</h1>
- <div className="flex items-center gap-3 text-sm shrink-0 ml-4">
- <Link
- to={`/artists/of/${artist.id}/history`}
- className="text-gray-400 hover:text-gray-200 transition-colors"
- >
- 履歴
- </Link>
- <Link
- to={`/artists/of/${artist.id}/edit`}
- className="bg-gray-800 hover:bg-gray-700 text-gray-200 px-3 py-1.5 rounded transition-colors"
- >
- 編集
- </Link>
+ <main>
+ <div className="detail-header">
+ <h1>{artist.name}</h1>
+ <div className="detail-actions">
+ <Link to={`/artists/of/${artist.id}/history`} className="history">履歴</Link>
+ <Link to={`/artists/of/${artist.id}/edit`} className="edit">編集</Link>
</div>
</div>
{bands.length > 0 && (
- <section className="mb-6">
- <h2 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
- バンド
- </h2>
- <ul className="space-y-2">
+ <section>
+ <h2>バンド</h2>
+ <ul className="member-list">
{bands.map((b) => (
- <li key={b.band_id} className="flex items-center gap-3">
- <Link
- to={`/bands/of/${b.band_id}`}
- className="text-blue-400 hover:text-blue-300 transition-colors font-medium"
- >
- {b.band_name}
- </Link>
- {b.role && (
- <span className="text-gray-400 text-sm">{b.role}</span>
- )}
+ <li key={b.band_id}>
+ <Link to={`/bands/of/${b.band_id}`}>{b.band_name}</Link>
+ {b.role && <span className="muted">{b.role}</span>}
</li>
))}
</ul>
@@ -57,42 +38,26 @@ export default function ArtistDetail() {
)}
{links.length > 0 && (
- <section className="mb-6">
- <h2 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
- リンク
- </h2>
- <ul className="space-y-1.5">
+ <section>
+ <h2>リンク</h2>
+ <ul className="link-list">
{links.map((l) => (
<li key={l.id}>
- <a
- href={l.url}
- target="_blank"
- rel="noopener noreferrer"
- className="text-blue-400 hover:text-blue-300 transition-colors text-sm"
- >
- {l.label}
- </a>
+ <a href={l.url} target="_blank" rel="noopener noreferrer">{l.label}</a>
</li>
))}
</ul>
</section>
)}
- <hr className="border-gray-800 my-6" />
- <div className="text-xs text-gray-600 space-y-1 font-mono">
+ <hr />
+ <div className="meta">
<p>/artists/of/{artist.id}</p>
<p>
- <Link
- to={`/artists/named/${artist.slug}`}
- className="hover:text-gray-400 transition-colors"
- >
- /artists/named/{artist.slug}
- </Link>
+ <Link to={`/artists/named/${artist.slug}`}>/artists/named/{artist.slug}</Link>
</p>
{latest && (
- <p className="font-sans text-gray-500 mt-2">
- 最終更新: {latest.created_at} — {latest.message}
- </p>
+ <p className="updated">最終更新: {latest.created_at} — {latest.message}</p>
)}
</div>
</main>
diff --git a/app/routes/artist-edit.tsx b/app/routes/artist-edit.tsx
index f2e5c18..e3a49af 100644
--- a/app/routes/artist-edit.tsx
+++ b/app/routes/artist-edit.tsx
@@ -54,24 +54,17 @@ export default function ArtistEdit() {
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>
+ <main>
+ <div className="page-header">
+ <Link to={`/artists/of/${artist.id}`} className="back">←</Link>
+ <h1>Edit Artist</h1>
</div>
- <Form method="post" className="space-y-5">
+ <Form method="post">
<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>
+ <label>名前 <span className="req">*</span></label>
<input
name="name"
value={name}
@@ -79,45 +72,41 @@ export default function ArtistEdit() {
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>}
+ {errors.name && <p className="error">{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>
+ <label>Slug <span className="req">*</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"
+ className="mono"
/>
- {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
+ {errors.slug && <p className="error">{errors.slug}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
- <div className="space-y-2">
+ <label>リンク</label>
+ <div className="links-form">
{links.map((link, i) => (
- <div key={i} className="flex gap-2">
+ <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)"
- 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"
+ className="btn-icon"
onClick={() => setLinks(links.filter((_, idx) => idx !== i))}
- className="text-gray-500 hover:text-red-400 px-2 transition-colors"
>
×
</button>
@@ -126,38 +115,22 @@ export default function ArtistEdit() {
</div>
<button
type="button"
+ className="btn-text"
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>}
+ <label>更新メッセージ <span className="req">*</span></label>
+ <input name="message" placeholder="例: SNSリンク追加" />
+ {errors.message && <p className="error">{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 className="actions">
+ <button type="submit">保存</button>
+ <Link to={`/artists/of/${artist.id}`} className="btn">キャンセル</Link>
</div>
</Form>
</main>
diff --git a/app/routes/artist-history.tsx b/app/routes/artist-history.tsx
index c2fb4cb..2808927 100644
--- a/app/routes/artist-history.tsx
+++ b/app/routes/artist-history.tsx
@@ -12,38 +12,29 @@ export async function loader({ params }: LoaderFunctionArgs) {
export default function ArtistHistory() {
const { artist, revisions } = useLoaderData<typeof loader>();
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">{artist.name} — 編集履歴</h1>
+ <main>
+ <div className="page-header">
+ <Link to={`/artists/of/${artist.id}`} className="back">←</Link>
+ <h1>{artist.name} — 編集履歴</h1>
</div>
{revisions.length === 0 ? (
- <p className="text-gray-400">履歴がありません。</p>
+ <p className="muted">履歴がありません。</p>
) : (
- <ol className="space-y-4">
+ <ol className="rev-list">
{revisions.map((rev, i) => {
let snap: { name?: string; links?: unknown[] } = {};
try { snap = JSON.parse(rev.snapshot); } catch { /* ignore */ }
return (
- <li key={rev.id} className="bg-gray-900 rounded-lg p-4">
- <div className="flex items-start justify-between gap-4">
- <div className="flex-1 min-w-0">
- <p className="font-medium text-gray-100 truncate">{rev.message}</p>
- <p className="text-xs text-gray-500 mt-1">
- {rev.created_at} · {rev.ip_address}
- </p>
+ <li key={rev.id} className="rev">
+ <div className="rev-header">
+ <div className="rev-main">
+ <p className="rev-message">{rev.message}</p>
+ <p className="rev-time">{rev.created_at} · {rev.ip_address}</p>
</div>
- {i === 0 && (
- <span className="text-xs text-blue-400 shrink-0">最新</span>
- )}
+ {i === 0 && <span className="rev-latest">最新</span>}
</div>
- <div className="mt-3 text-xs text-gray-400 space-y-0.5">
+ <div className="rev-snap">
<p>名前: {snap.name ?? "—"}</p>
<p>リンク: {snap.links?.length ?? 0}件</p>
</div>
diff --git a/app/routes/artist-new.tsx b/app/routes/artist-new.tsx
index 168a7cc..ce91dab 100644
--- a/app/routes/artist-new.tsx
+++ b/app/routes/artist-new.tsx
@@ -44,19 +44,17 @@ export default function ArtistNew() {
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link to="/" className="text-gray-400 hover:text-gray-200 transition-colors">←</Link>
- <h1 className="text-xl font-semibold">New Artist</h1>
+ <main>
+ <div className="page-header">
+ <Link to="/" className="back">←</Link>
+ <h1>New Artist</h1>
</div>
- <Form method="post" className="space-y-5">
+ <Form method="post">
<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>
+ <label>名前 <span className="req">*</span></label>
<input
name="name"
value={name}
@@ -64,45 +62,41 @@ export default function ArtistNew() {
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>}
+ {errors.name && <p className="error">{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>
+ <label>Slug <span className="req">*</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"
+ className="mono"
/>
- {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
+ {errors.slug && <p className="error">{errors.slug}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
- <div className="space-y-2">
+ <label>リンク</label>
+ <div className="links-form">
{links.map((link, i) => (
- <div key={i} className="flex gap-2">
+ <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)"
- 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"
+ className="btn-icon"
onClick={() => setLinks(links.filter((_, idx) => idx !== i))}
- className="text-gray-500 hover:text-red-400 px-2 transition-colors"
>
×
</button>
@@ -111,38 +105,22 @@ export default function ArtistNew() {
</div>
<button
type="button"
+ className="btn-text"
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="例: 初回登録"
- 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>}
+ <label>更新メッセージ <span className="req">*</span></label>
+ <input name="message" placeholder="例: 初回登録" />
+ {errors.message && <p className="error">{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="/"
- className="bg-gray-800 hover:bg-gray-700 text-gray-300 px-4 py-2 rounded transition-colors"
- >
- キャンセル
- </Link>
+ <div className="actions">
+ <button type="submit">作成</button>
+ <Link to="/" className="btn">キャンセル</Link>
</div>
</Form>
</main>
diff --git a/app/routes/band-by-uuid.tsx b/app/routes/band-by-uuid.tsx
index 9cb4d33..99a8b5c 100644
--- a/app/routes/band-by-uuid.tsx
+++ b/app/routes/band-by-uuid.tsx
@@ -26,50 +26,33 @@ const STATUS_LABEL: Record<string, string> = {
export default function BandDetail() {
const { band, links, artists, latest } = useLoaderData<typeof loader>();
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-start justify-between mb-6">
- <div>
- <h1 className="text-2xl font-bold">{band.name}</h1>
- <div className="flex items-center gap-3 mt-1 text-sm text-gray-400">
+ <main>
+ <div className="detail-header">
+ <div className="detail-info">
+ <h1>{band.name}</h1>
+ <div className="detail-subtitle">
{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>
+ <p className="detail-desc">{band.description}</p>
)}
</div>
- <div className="flex items-center gap-3 text-sm shrink-0 ml-4">
- <Link
- to={`/bands/of/${band.id}/history`}
- className="text-gray-400 hover:text-gray-200 transition-colors"
- >
- 履歴
- </Link>
- <Link
- to={`/bands/of/${band.id}/edit`}
- className="bg-gray-800 hover:bg-gray-700 text-gray-200 px-3 py-1.5 rounded transition-colors"
- >
- 編集
- </Link>
+ <div className="detail-actions">
+ <Link to={`/bands/of/${band.id}/history`} className="history">履歴</Link>
+ <Link to={`/bands/of/${band.id}/edit`} className="edit">編集</Link>
</div>
</div>
{artists.length > 0 && (
- <section className="mb-6">
- <h2 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
- メンバー
- </h2>
- <ul className="space-y-2">
+ <section>
+ <h2>メンバー</h2>
+ <ul className="member-list">
{artists.map((a) => (
- <li key={a.artist_id} className="flex items-center gap-3 flex-wrap">
- <Link
- to={`/artists/of/${a.artist_id}`}
- className="text-blue-400 hover:text-blue-300 transition-colors font-medium"
- >
- {a.artist_name}
- </Link>
+ <li key={a.artist_id}>
+ <Link to={`/artists/of/${a.artist_id}`}>{a.artist_name}</Link>
{a.role && a.role.split(", ").filter(Boolean).map((r, i) => (
- <span key={i} className="text-gray-500 text-xs bg-gray-900 px-1.5 py-0.5 rounded">{r}</span>
+ <span key={i} className="badge">{r}</span>
))}
</li>
))}
@@ -78,19 +61,12 @@ export default function BandDetail() {
)}
{links.length > 0 && (
- <section className="mb-6">
- <h2 className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">
- リンク
- </h2>
- <ul className="space-y-1.5">
+ <section>
+ <h2>リンク</h2>
+ <ul className="link-list">
{links.map((l) => (
<li key={l.id}>
- <a
- href={l.url}
- target="_blank"
- rel="noopener noreferrer"
- className="text-blue-400 hover:text-blue-300 transition-colors text-sm"
- >
+ <a href={l.url} target="_blank" rel="noopener noreferrer">
{LINK_TYPE_LABEL[l.label] ?? l.label}
</a>
</li>
@@ -99,21 +75,14 @@ export default function BandDetail() {
</section>
)}
- <hr className="border-gray-800 my-6" />
- <div className="text-xs text-gray-600 space-y-1 font-mono">
+ <hr />
+ <div className="meta">
<p>/bands/of/{band.id}</p>
<p>
- <Link
- to={`/bands/named/${band.slug}`}
- className="hover:text-gray-400 transition-colors"
- >
- /bands/named/{band.slug}
- </Link>
+ <Link to={`/bands/named/${band.slug}`}>/bands/named/{band.slug}</Link>
</p>
{latest && (
- <p className="font-sans text-gray-500 mt-2">
- 最終更新: {latest.created_at} — {latest.message}
- </p>
+ <p className="updated">最終更新: {latest.created_at} — {latest.message}</p>
)}
</div>
</main>
diff --git a/app/routes/band-edit.tsx b/app/routes/band-edit.tsx
index 0a98bd6..2e29277 100644
--- a/app/routes/band-edit.tsx
+++ b/app/routes/band-edit.tsx
@@ -97,13 +97,13 @@ export default function BandEdit() {
}
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link to={`/bands/of/${band.id}`} className="text-gray-400 hover:text-gray-200 transition-colors">←</Link>
- <h1 className="text-xl font-semibold">Edit Band</h1>
+ <main>
+ <div className="page-header">
+ <Link to={`/bands/of/${band.id}`} className="back">←</Link>
+ <h1>Edit Band</h1>
</div>
- <Form method="post" className="space-y-5">
+ <Form method="post">
<input type="hidden" name="links" value={JSON.stringify(links)} />
<input
type="hidden"
@@ -112,47 +112,34 @@ export default function BandEdit() {
/>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- バンド名 <span className="text-red-400">*</span>
- </label>
+ <label>バンド名 <span className="req">*</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>}
+ {errors.name && <p className="error">{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>
+ <label>Slug <span className="req">*</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"
+ className="mono"
/>
- {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
+ {errors.slug && <p className="error">{errors.slug}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">活動拠点</label>
- <input
- name="area"
- defaultValue={band.area ?? ""}
- 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"
- />
+ <label>活動拠点</label>
+ <input name="area" defaultValue={band.area ?? ""} />
</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"
- >
+ <label>ステータス</label>
+ <select name="status" defaultValue={band.status}>
<option value="active">活動中</option>
<option value="hiatus">活動休止</option>
<option value="disbanded">解散</option>
@@ -160,24 +147,18 @@ export default function BandEdit() {
</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"
- />
+ <label>説明</label>
+ <textarea name="description" rows={3} defaultValue={band.description ?? ""} />
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
- <div className="space-y-2">
+ <label>リンク</label>
+ <div className="links-form">
{links.map((link, i) => (
- <div key={i} className="flex gap-2">
+ <div key={i} className="link-row">
<select
value={link.label}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))}
- className="w-36 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
>
{LINK_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
@@ -185,73 +166,65 @@ export default function BandEdit() {
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>
+ <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: LINK_TYPES[0].value, 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-2">メンバー</label>
+ <label>メンバー</label>
{picked.length > 0 && (
- <div className="space-y-2 mb-3">
+ <div className="members-form">
{picked.map((p) => {
const pend = pending[p.id] ?? DEFAULT_PENDING;
return (
- <div key={p.id} className="bg-gray-900 rounded-lg p-3">
- <div className="flex items-center justify-between mb-2">
- <span className="text-gray-200 text-sm font-medium">{p.name}</span>
+ <div key={p.id} className="member-card">
+ <div className="card-header">
+ <span className="card-name">{p.name}</span>
<button
type="button"
+ className="btn-icon"
onClick={() => setPicked(picked.filter((a) => a.id !== p.id))}
- className="text-gray-500 hover:text-red-400 text-xs transition-colors"
>
削除
</button>
</div>
{p.roles.length > 0 && (
- <div className="flex flex-wrap gap-1.5 mb-2">
+ <div className="badges">
{p.roles.map((r, ri) => (
- <span key={ri} className="inline-flex items-center gap-1 bg-gray-800 text-gray-300 text-xs px-2 py-0.5 rounded">
+ <span key={ri} className="badge">
{r}
- <button type="button" onClick={() => removeRole(p.id, ri)} className="text-gray-500 hover:text-red-400 leading-none">×</button>
+ <button type="button" onClick={() => removeRole(p.id, ri)}>×</button>
</span>
))}
</div>
)}
- <div className="flex gap-2 items-center">
+ <div className="role-row">
<select
value={pend.type}
onChange={(e) => setPending({ ...pending, [p.id]: { ...pend, type: e.target.value } })}
- className="bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-gray-300 text-sm focus:outline-none focus:border-blue-500"
>
{ARTIST_ROLES.map((r) => <option key={r} value={r}>{r}</option>)}
<option value="other">その他...</option>
</select>
{pend.type === "other" && (
<input
+ className="custom-input"
value={pend.custom}
onChange={(e) => setPending({ ...pending, [p.id]: { ...pend, custom: e.target.value } })}
placeholder="ロール名"
- className="bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-gray-300 text-sm focus:outline-none focus:border-blue-500 w-32"
/>
)}
- <button
- type="button"
- onClick={() => addRole(p.id)}
- className="text-blue-400 hover:text-blue-300 text-sm transition-colors"
- >
- + 追加
- </button>
+ <button type="button" className="btn-text" onClick={() => addRole(p.id)}>+ 追加</button>
</div>
</div>
);
@@ -269,7 +242,6 @@ export default function BandEdit() {
}
}}
defaultValue=""
- className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-400 focus:outline-none focus:border-blue-500 text-sm"
>
<option value="">+ アーティストを追加...</option>
{available.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
@@ -278,20 +250,14 @@ export default function BandEdit() {
</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="例: メンバー追加"
- 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>}
+ <label>更新メッセージ <span className="req">*</span></label>
+ <input name="message" placeholder="例: メンバー追加" />
+ {errors.message && <p className="error">{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={`/bands/of/${band.id}`} className="bg-gray-800 hover:bg-gray-700 text-gray-300 px-4 py-2 rounded transition-colors">キャンセル</Link>
+ <div className="actions">
+ <button type="submit">保存</button>
+ <Link to={`/bands/of/${band.id}`} className="btn">キャンセル</Link>
</div>
</Form>
</main>
diff --git a/app/routes/band-history.tsx b/app/routes/band-history.tsx
index 11e2f2f..954fd52 100644
--- a/app/routes/band-history.tsx
+++ b/app/routes/band-history.tsx
@@ -12,44 +12,32 @@ export async function loader({ params }: LoaderFunctionArgs) {
export default function BandHistory() {
const { band, revisions } = useLoaderData<typeof loader>();
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link
- to={`/bands/of/${band.id}`}
- className="text-gray-400 hover:text-gray-200 transition-colors"
- >
- ←
- </Link>
- <h1 className="text-xl font-semibold">{band.name} — 編集履歴</h1>
+ <main>
+ <div className="page-header">
+ <Link to={`/bands/of/${band.id}`} className="back">←</Link>
+ <h1>{band.name} — 編集履歴</h1>
</div>
{revisions.length === 0 ? (
- <p className="text-gray-400">履歴がありません。</p>
+ <p className="muted">履歴がありません。</p>
) : (
- <ol className="space-y-4">
+ <ol className="rev-list">
{revisions.map((rev, i) => {
let snap: { name?: string; area?: string; links?: unknown[]; artists?: unknown[] } = {};
try { snap = JSON.parse(rev.snapshot); } catch { /* ignore */ }
return (
- <li key={rev.id} className="bg-gray-900 rounded-lg p-4">
- <div className="flex items-start justify-between gap-4">
- <div className="flex-1 min-w-0">
- <p className="font-medium text-gray-100 truncate">{rev.message}</p>
- <p className="text-xs text-gray-500 mt-1">
- {rev.created_at} · {rev.ip_address}
- </p>
+ <li key={rev.id} className="rev">
+ <div className="rev-header">
+ <div className="rev-main">
+ <p className="rev-message">{rev.message}</p>
+ <p className="rev-time">{rev.created_at} · {rev.ip_address}</p>
</div>
- {i === 0 && (
- <span className="text-xs text-blue-400 shrink-0">最新</span>
- )}
+ {i === 0 && <span className="rev-latest">最新</span>}
</div>
- <div className="mt-3 text-xs text-gray-400 space-y-0.5">
+ <div className="rev-snap">
<p>名前: {snap.name ?? "—"}</p>
{snap.area && <p>拠点: {snap.area}</p>}
- <p>
- リンク: {snap.links?.length ?? 0}件 / メンバー:{" "}
- {snap.artists?.length ?? 0}人
- </p>
+ <p>リンク: {snap.links?.length ?? 0}件 / メンバー: {snap.artists?.length ?? 0}人</p>
</div>
</li>
);
diff --git a/app/routes/band-new.tsx b/app/routes/band-new.tsx
index a15ce24..62be1d4 100644
--- a/app/routes/band-new.tsx
+++ b/app/routes/band-new.tsx
@@ -75,13 +75,13 @@ export default function BandNew() {
}
return (
- <main className="max-w-3xl mx-auto px-4 py-8">
- <div className="flex items-center gap-3 mb-6">
- <Link to="/" className="text-gray-400 hover:text-gray-200 transition-colors">←</Link>
- <h1 className="text-xl font-semibold">New Band</h1>
+ <main>
+ <div className="page-header">
+ <Link to="/" className="back">←</Link>
+ <h1>New Band</h1>
</div>
- <Form method="post" className="space-y-5">
+ <Form method="post">
<input type="hidden" name="links" value={JSON.stringify(links)} />
<input
type="hidden"
@@ -90,46 +90,34 @@ export default function BandNew() {
/>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">
- バンド名 <span className="text-red-400">*</span>
- </label>
+ <label>バンド名 <span className="req">*</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>}
+ {errors.name && <p className="error">{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>
+ <label>Slug <span className="req">*</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"
+ className="mono"
/>
- {errors.slug && <p className="text-red-400 text-sm mt-1">{errors.slug}</p>}
+ {errors.slug && <p className="error">{errors.slug}</p>}
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-1">活動拠点</label>
- <input
- name="area"
- 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"
- />
+ <label>活動拠点</label>
+ <input name="area" />
</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"
- >
+ <label>ステータス</label>
+ <select name="status" defaultValue="active">
<option value="active">活動中</option>
<option value="hiatus">活動休止</option>
<option value="disbanded">解散</option>
@@ -137,23 +125,18 @@ export default function BandNew() {
</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"
- />
+ <label>説明</label>
+ <textarea name="description" rows={3} />
</div>
<div>
- <label className="block text-sm font-medium text-gray-300 mb-2">リンク</label>
- <div className="space-y-2">
+ <label>リンク</label>
+ <div className="links-form">
{links.map((link, i) => (
- <div key={i} className="flex gap-2">
+ <div key={i} className="link-row">
<select
value={link.label}
onChange={(e) => setLinks(links.map((l, idx) => idx === i ? { ...l, label: e.target.value } : l))}
- className="w-36 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 text-sm"
>
{LINK_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
@@ -161,73 +144,65 @@ export default function BandNew() {
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>
+ <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: LINK_TYPES[0].value, 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-2">メンバー</label>
+ <label>メンバー</label>
{picked.length > 0 && (
- <div className="space-y-2 mb-3">
+ <div className="members-form">
{picked.map((p) => {
const pend = pending[p.id] ?? DEFAULT_PENDING;
return (
- <div key={p.id} className="bg-gray-900 rounded-lg p-3">
- <div className="flex items-center justify-between mb-2">
- <span className="text-gray-200 text-sm font-medium">{p.name}</span>
+ <div key={p.id} className="member-card">
+ <div className="card-header">
+ <span className="card-name">{p.name}</span>
<button
type="button"
+ className="btn-icon"
onClick={() => setPicked(picked.filter((a) => a.id !== p.id))}
- className="text-gray-500 hover:text-red-400 text-xs transition-colors"
>
削除
</button>
</div>
{p.roles.length > 0 && (
- <div className="flex flex-wrap gap-1.5 mb-2">
+ <div className="badges">
{p.roles.map((r, ri) => (
- <span key={ri} className="inline-flex items-center gap-1 bg-gray-800 text-gray-300 text-xs px-2 py-0.5 rounded">
+ <span key={ri} className="badge">
{r}
- <button type="button" onClick={() => removeRole(p.id, ri)} className="text-gray-500 hover:text-red-400 leading-none">×</button>
+ <button type="button" onClick={() => removeRole(p.id, ri)}>×</button>
</span>
))}
</div>
)}
- <div className="flex gap-2 items-center">
+ <div className="role-row">
<select
value={pend.type}
onChange={(e) => setPending({ ...pending, [p.id]: { ...pend, type: e.target.value } })}
- className="bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-gray-300 text-sm focus:outline-none focus:border-blue-500"
>
{ARTIST_ROLES.map((r) => <option key={r} value={r}>{r}</option>)}
<option value="other">その他...</option>
</select>
{pend.type === "other" && (
<input
+ className="custom-input"
value={pend.custom}
onChange={(e) => setPending({ ...pending, [p.id]: { ...pend, custom: e.target.value } })}
placeholder="ロール名"
- className="bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-gray-300 text-sm focus:outline-none focus:border-blue-500 w-32"
/>
)}
- <button
- type="button"
- onClick={() => addRole(p.id)}
- className="text-blue-400 hover:text-blue-300 text-sm transition-colors"
- >
- + 追加
- </button>
+ <button type="button" className="btn-text" onClick={() => addRole(p.id)}>+ 追加</button>
</div>
</div>
);
@@ -245,34 +220,27 @@ export default function BandNew() {
}
}}
defaultValue=""
- className="bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-400 focus:outline-none focus:border-blue-500 text-sm"
>
<option value="">+ アーティストを追加...</option>
{available.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
</select>
) : artists.length === 0 ? (
- <p className="text-gray-500 text-sm">
+ <p className="muted">
アーティストがいません。{" "}
- <Link to="/artists/new" className="text-blue-400 hover:text-blue-300">先に作成</Link>
+ <Link to="/artists/new">先に作成</Link>
</p>
) : null}
</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="例: 初回登録"
- 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>}
+ <label>更新メッセージ <span className="req">*</span></label>
+ <input name="message" placeholder="例: 初回登録" />
+ {errors.message && <p className="error">{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="/" className="bg-gray-800 hover:bg-gray-700 text-gray-300 px-4 py-2 rounded transition-colors">キャンセル</Link>
+ <div className="actions">
+ <button type="submit">作成</button>
+ <Link to="/" className="btn">キャンセル</Link>
</div>
</Form>
</main>
diff --git a/app/routes/home.tsx b/app/routes/home.tsx
index 5600645..5aa7de1 100644
--- a/app/routes/home.tsx
+++ b/app/routes/home.tsx
@@ -14,32 +14,23 @@ const STATUS_LABEL: Record<string, string> = {
export default function Home() {
const { bands } = useLoaderData<typeof loader>();
return (
- <main className="max-w-2xl mx-auto px-6 py-12">
+ <main>
{bands.length === 0 ? (
- <p className="text-gray-600 text-sm">
+ <p className="muted">
バンドがまだありません。{" "}
- <Link to="/bands/new" className="underline">
- 追加する
- </Link>
+ <Link to="/bands/new">追加する</Link>
</p>
) : (
- <ul className="divide-y divide-gray-900">
+ <ul className="band-list">
{bands.map((band) => (
- <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}
- </Link>
+ <li key={band.id}>
+ <div className="name-col">
+ <Link to={`/bands/of/${band.id}`}>{band.name}</Link>
{band.description && (
- <p className="text-gray-500 text-sm mt-0.5 line-clamp-1">
- {band.description}
- </p>
+ <p className="desc">{band.description}</p>
)}
</div>
- <div className="flex items-center gap-4 text-sm text-gray-500 shrink-0 pt-0.5">
+ <div className="info">
{band.area && <span>{band.area}</span>}
<span>{STATUS_LABEL[band.status] ?? band.status}</span>
</div>