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:
@@ -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
|
||||
|
||||
@@ -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) || "—";
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user