Skip to content

Commit de7a253

Browse files
committed
modules/projects: ページ作成日を編集可能にし一覧順を安定化
旧 utcode.net からの移行データはバルクインサートで createdAt が 近い値になり、一覧順が非決定的になっていた。createdAt を CMS 側 で編集可能にし、id をタイブレーカーに追加して安定化。公開カード には最終更新日を併記。
1 parent 57276f1 commit de7a253

8 files changed

Lines changed: 64 additions & 3 deletions

File tree

src/lib/components/ProjectForm.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
repoUrl: "",
1919
demoUrl: "",
2020
category: "active",
21+
createdAt: new Date(),
2122
leadMemberId: null,
2223
},
2324
members = [],
@@ -99,6 +100,7 @@
99100
bind:category={formData.category}
100101
bind:repoUrl={formData.repoUrl}
101102
bind:demoUrl={formData.demoUrl}
103+
bind:createdAt={formData.createdAt}
102104
{errors}
103105
onNameChange={handleNameChange}
104106
/>

src/lib/components/project-form/ProjectEditor.svelte

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
category = $bindable<ProjectCategory>("active"),
1818
repoUrl = $bindable(""),
1919
demoUrl = $bindable(""),
20+
createdAt = $bindable<Date>(new Date()),
2021
errors,
2122
onNameChange,
2223
}: {
@@ -28,11 +29,31 @@
2829
category?: ProjectCategory;
2930
repoUrl?: string;
3031
demoUrl?: string;
32+
createdAt?: Date;
3133
errors: Record<string, string>;
3234
onNameChange: () => void;
3335
} = $props();
3436
3537
let showPreview = $state(false);
38+
39+
function toDateInputValue(d: Date): string {
40+
const y = d.getFullYear();
41+
const m = String(d.getMonth() + 1).padStart(2, "0");
42+
const day = String(d.getDate()).padStart(2, "0");
43+
return `${y}-${m}-${day}`;
44+
}
45+
46+
function handleDateChange(event: Event & { currentTarget: HTMLInputElement }) {
47+
const raw = event.currentTarget.value;
48+
if (!raw) return;
49+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
50+
if (!match) return;
51+
const y = Number(match[1]);
52+
const m = Number(match[2]);
53+
const d = Number(match[3]);
54+
// Local midnight matches what the picker shows; sort precision is day-level.
55+
createdAt = new Date(y, m - 1, d);
56+
}
3657
</script>
3758

3859
<main class="flex-1 bg-white">
@@ -93,6 +114,21 @@
93114
</div>
94115
</div>
95116

117+
<!-- Page Creation Date -->
118+
<div class="mb-6 space-y-2">
119+
<label for="createdAt" class="text-sm font-medium text-zinc-700">
120+
ページ作成日
121+
<span class="ml-1 font-normal text-zinc-400">(一覧の並び順に使われます)</span>
122+
</label>
123+
<input
124+
type="date"
125+
id="createdAt"
126+
value={toDateInputValue(createdAt)}
127+
onchange={handleDateChange}
128+
class="rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none"
129+
/>
130+
</div>
131+
96132
<!-- Name Input -->
97133
<input
98134
type="text"

src/lib/data/private/projects.remote.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const saveProject = command(
5555
repoUrl: v.nullable(v.string()),
5656
demoUrl: v.nullable(v.string()),
5757
category: categorySchema,
58+
createdAt: v.optional(v.date()),
5859
}),
5960
leadMemberId: v.string(),
6061
}),
@@ -78,6 +79,7 @@ export const editProject = command(
7879
repoUrl: v.optional(v.nullable(v.string())),
7980
demoUrl: v.optional(v.nullable(v.string())),
8081
category: v.optional(categorySchema),
82+
createdAt: v.optional(v.date()),
8183
}),
8284
}),
8385
async ({ id, data }) => {

src/lib/server/database/projects.server.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export type NewProject = typeof project.$inferInsert;
88

99
export async function listProjects(limit: number) {
1010
return db.query.project.findMany({
11-
orderBy: (t, { desc }) => desc(t.createdAt),
11+
// id is a deterministic tiebreaker so rows sharing createdAt (e.g. bulk-migrated
12+
// records) keep a stable order between requests.
13+
orderBy: (t, { desc }) => [desc(t.createdAt), desc(t.id)],
1214
limit,
1315
with: {
1416
projectMembers: { with: { member: true } },
@@ -116,7 +118,7 @@ export async function searchProjects(query: string, limit: number) {
116118
like(project.description, searchPattern),
117119
like(project.content, searchPattern),
118120
),
119-
orderBy: (t, { desc }) => desc(t.createdAt),
121+
orderBy: (t, { desc }) => [desc(t.createdAt), desc(t.id)],
120122
limit,
121123
with: {
122124
projectMembers: { with: { member: true } },
@@ -138,7 +140,7 @@ export async function getRecentProjects(limit: number) {
138140

139141
export async function listRecentProjects(limit: number) {
140142
return db.query.project.findMany({
141-
orderBy: (t, { desc }) => desc(t.createdAt),
143+
orderBy: (t, { desc }) => [desc(t.createdAt), desc(t.id)],
142144
limit,
143145
with: {
144146
projectMembers: { with: { member: true } },

src/lib/shared/models/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type ProjectData = {
2222
repoUrl: string;
2323
demoUrl: string;
2424
category: ProjectCategory;
25+
createdAt: Date;
2526
leadMemberId: string | null;
2627
};
2728

src/routes/(admin)/admin/projects/edit/[id]/+page.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
repoUrl: string;
4242
demoUrl: string;
4343
category: ProjectCategory;
44+
createdAt: Date;
4445
leadMemberId: string | null;
4546
}) {
4647
if (!project) return;
@@ -67,6 +68,7 @@
6768
repoUrl: data.repoUrl || null,
6869
demoUrl: data.demoUrl || null,
6970
category: data.category,
71+
createdAt: data.createdAt,
7072
},
7173
});
7274
toast.show("Saved", "success");
@@ -219,6 +221,7 @@
219221
repoUrl: project.repoUrl ?? "",
220222
demoUrl: project.demoUrl ?? "",
221223
category: project.category,
224+
createdAt: project.createdAt,
222225
leadMemberId: null,
223226
}}
224227
{members}

src/routes/(admin)/admin/projects/new/+page.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
repoUrl: string;
2121
demoUrl: string;
2222
category: ProjectCategory;
23+
createdAt: Date;
2324
leadMemberId: string | null;
2425
}) {
2526
if (!data.leadMemberId) return;
@@ -35,6 +36,7 @@
3536
repoUrl: data.repoUrl || null,
3637
demoUrl: data.demoUrl || null,
3738
category: data.category,
39+
createdAt: data.createdAt,
3840
},
3941
leadMemberId: data.leadMemberId,
4042
});

src/routes/(site)/projects/+page.svelte

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@
3838
const base = data.kindParam ? `/projects?category=${data.kindParam}` : "/projects";
3939
return pageNum === 1 ? base : `${base}${data.kindParam ? "&" : "?"}page=${pageNum}`;
4040
}
41+
42+
const updatedAtFormatter = new Intl.DateTimeFormat("ja-JP", {
43+
year: "numeric",
44+
month: "long",
45+
day: "numeric",
46+
});
47+
48+
function formatUpdatedAt(d: Date): string {
49+
return updatedAtFormatter.format(d);
50+
}
4151
</script>
4252

4353
<svelte:head>
@@ -141,6 +151,9 @@
141151
</span>
142152
{/if}
143153
</div>
154+
<p class="mt-4 font-[JetBrains_Mono,monospace] text-[11px] text-zinc-400">
155+
最終更新: {formatUpdatedAt(project.updatedAt)}
156+
</p>
144157
</a>
145158
{/each}
146159
</div>

0 commit comments

Comments
 (0)