Project chronological date: YMD admin, month/year public, sort/recent

- Serialize and save project.date as date-only YYYY-MM-DD (no TZ drift)
- Admin label + hint; projects list shows Date column
- Home Recent and portfolio date sort use project.date (nulls last)
- Public display stays month+year / year-only via safe formatters
This commit is contained in:
2026-08-01 11:50:17 -04:00
parent ee4412d911
commit bf2732c936
10 changed files with 126 additions and 18 deletions
+2 -1
View File
@@ -1,11 +1,12 @@
import { Link } from "react-router-dom";
import type { Project } from "../lib/api";
import { thumbOf } from "../lib/api";
import { formatProjectYear } from "../lib/dates";
import "./ProjectCard.scss";
export function ProjectCard({ project, index = 0 }: { project: Project; index?: number }) {
const thumb = thumbOf(project);
const year = project.date ? new Date(project.date).getFullYear() : null;
const year = formatProjectYear(project.date);
return (
<article
+48
View File
@@ -0,0 +1,48 @@
/**
* Project chronological dates are YYYY-MM-DD from the API.
* Parse without Date timezone shifts so public month/year stays correct.
*/
const MONTHS_LONG = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
/** Extract YYYY-MM-DD prefix if present. */
export function dateOnly(value: string | null | undefined): string | null {
if (!value) return null;
const m = String(value).match(/^(\d{4})-(\d{2})-(\d{2})/);
return m ? `${m[1]}-${m[2]}-${m[3]}` : null;
}
/** Public project detail: "March 2024" (month + year only). */
export function formatProjectMonthYear(value: string | null | undefined): string | null {
const ymd = dateOnly(value);
if (!ymd) return null;
const [y, m] = ymd.split("-").map(Number);
if (!y || !m || m < 1 || m > 12) return null;
return `${MONTHS_LONG[m - 1]} ${y}`;
}
/** Public cards: year only. */
export function formatProjectYear(value: string | null | undefined): number | null {
const ymd = dateOnly(value);
if (!ymd) return null;
const y = Number(ymd.slice(0, 4));
return Number.isFinite(y) ? y : null;
}
/** Admin list: YYYY-MM-DD as stored. */
export function formatProjectYmd(value: string | null | undefined): string {
return dateOnly(value) || "—";
}
+2 -1
View File
@@ -3,6 +3,7 @@ import { Link, useParams } from "react-router-dom";
import Lightbox from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css";
import { api, type Project, mediaSrc } from "../lib/api";
import { formatProjectMonthYear } from "../lib/dates";
import { usePageTitle } from "../lib/pageTitle";
import { VimeoEmbed } from "../components/VimeoEmbed";
import "./ProjectPage.scss";
@@ -83,7 +84,7 @@ export function ProjectPage() {
<h1>{project.title}</h1>
<div className="project-page__meta">
{project.date && (
<span>{new Date(project.date).toLocaleDateString(undefined, { year: "numeric", month: "long" })}</span>
<span>{formatProjectMonthYear(project.date)}</span>
)}
{project.software.length > 0 && (
<span>{project.software.join(" · ")}</span>
+6 -2
View File
@@ -47,7 +47,7 @@ export function AdminProjectEdit() {
title: p.title,
description: p.description,
shortDescription: p.shortDescription || "",
date: p.date ? p.date.slice(0, 10) : "",
date: p.date ? p.date.slice(0, 10) : "", // API sends YYYY-MM-DD
software: (p.software || []).join(", "),
externalLinksText: (p.externalLinks || [])
.map((l) => `${l.label}|${l.url}`)
@@ -191,13 +191,17 @@ export function AdminProjectEdit() {
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "1rem" }}>
<div className="field">
<label htmlFor="date">Date</label>
<label htmlFor="date">Project date (YYYY-MM-DD)</label>
<input
id="date"
type="date"
value={form.date}
onChange={(e) => setForm({ ...form, date: e.target.value })}
/>
<p className="field__hint">
Chronological creation date. Used for home Recent and portfolio date sort.
Public site shows month and year only.
</p>
</div>
<div className="field">
<label htmlFor="pri">Display priority</label>
+5
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api, type Project, thumbOf, mediaSrc } from "../../lib/api";
import { formatProjectYmd } from "../../lib/dates";
export function AdminProjects() {
const [projects, setProjects] = useState<Project[]>([]);
@@ -75,6 +76,7 @@ export function AdminProjects() {
<tr>
<th></th>
<th>Title</th>
<th>Date</th>
<th>Status</th>
<th>Priority</th>
<th>Categories</th>
@@ -99,6 +101,9 @@ export function AdminProjects() {
</span>
)}
</td>
<td style={{ fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap" }}>
{formatProjectYmd(p.date)}
</td>
<td>
<span className={`vis-badge vis-badge--${p.visibility}`}>{p.visibility}</span>
</td>
+9
View File
@@ -181,6 +181,15 @@ h4 {
min-height: 120px;
resize: vertical;
}
&__hint {
margin: 0;
font-size: 0.78rem;
color: var(--text-dim);
text-transform: none;
letter-spacing: 0;
line-height: 1.35;
}
}
.badge {