diff --git a/.gitignore b/.gitignore index 5ef6a52..941bbfe 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* +# admin data storage +src/data/storage/ + # env files (can opt-in for committing if needed) .env* diff --git a/UI.md b/UI.md new file mode 100644 index 0000000..a57dbd6 --- /dev/null +++ b/UI.md @@ -0,0 +1,183 @@ +## Design System — sui_blog + +水墨纸质杂志风格。暖色纸张底色、近黑墨色文字、赭石色点缀,整体营造宋式文人审美。 + +### 色彩 + +| Token | Hex | 用途 | +|---|---|---| +| `parchment` | `#FDFCFA` | 页面背景 | +| `parchment-deep` | `#F5F2EE` | 深底(代码块、时间轴圆点、标签底色) | +| `ink` | `#050404` | 主文字、标题 | +| `ink-light` | `#121010` | 略浅的墨色(about 正文) | +| `ink-muted` | `#2A2624` | 辅助文字、meta、标签 | +| `terracotta` | `#A63D2F` | 强调色(链接、CTA、active 状态) | +| `terracotta-light` | `#C46B5E` | 链接下划线装饰 | +| `sage` | `#6E8264` | 语义色——已发布/成功状态 | +| `sage-light` | `#A3B59B` | 预留 | +| `warm-gray` | `#C5BDB4` | 边框、分隔线、meta 点 | +| `cream` | `#FAF9F7` | 卡片背景、侧栏、深色按钮文字 | + +辅助色:`red-600`(删除/错误)、`red-50`(删除 hover 背景)、`white`(内联编辑输入框)。 + +透明度层级:`/80`、`/50`、`/30`、`/20`、`/15`、`/10`、`/5`,用于边框和文字色微调。 + +选中文字:`bg-terracotta` + `text-cream`。 + +### 字体 + +| Class | 字体栈 | 场景 | +|---|---|---| +| `font-display` | Noto Serif SC → Cormorant Garamond → Source Han Serif SC → Songti SC, serif | 标题、logo、页面名、时间轴年份、分类名 | +| `font-body` | Noto Serif SC → Source Serif 4 → Source Han Serif SC → Songti SC, serif | 正文段落、摘要、prose 内容 | +| `font-sans` | Noto Sans SC → DM Sans → system-ui, sans-serif | 标签、meta、导航、按钮、表单、badge | +| `font-mono` | JetBrains Mono → Fira Code, monospace | prose 中的代码、admin HTML textarea | + +正文基础样式:`line-height: 1.9`、`letter-spacing: 0.02em`、开启 antialiased。 + +### 圆角 + +| Class | 用途 | +|---|---| +| `rounded-full` | 标签 pill、status badge、CTA 按钮、导航圆点、时间轴圆点 | +| `rounded-2xl` | 精选卡片、分类卡片、about 信息框 | +| `rounded-xl` | 标准卡片、表单输入框、admin 列表项、登录框 | +| `rounded-lg` | 侧栏导航项、操作按钮(保存/取消)、hover 背景 | + +不使用 `rounded-sm`、`rounded-md`、`rounded-3xl`。层级关系:full(pill)> 2xl(大卡片)> xl(标准卡片/输入框)> lg(小元素)。 + +### 间距 + +页面水平内边距:`px-page`(`clamp(1.5rem, 5vw, 6rem)`)。 + +内容宽度层级:`max-w-5xl`(页面)→ `max-w-3xl`(标签云)→ `max-w-2xl`(文章正文)→ `max-w-lg`(描述文字)→ `max-w-xs`(页脚/短输入)。 + +页面垂直间距:`pt-16 pb-24`(标准区块)、`pt-20 pb-24`(hero 区)、`mt-20`(大段落分隔)。 + +常用 gap:`gap-2`(标签 pill)、`gap-4`(网格卡片)、`gap-5`(精选/分类网格)。 + +卡片内边距:`p-7 md:p-10`(精选大卡片)、`p-7`(分类卡片)、`p-5`(列表项)、`p-4`(统计卡片)。 + +### 卡片 + +所有卡片共享基础样式: + +``` +bg-cream border border-warm-gray/10 +``` + +Hover 提升: + +``` +hover:border-terracotta/20 +``` + +大卡片额外加阴影: + +``` +hover:shadow-lg hover:shadow-terracotta/5 +``` + +精选卡片 / 分类卡片完整样式: + +``` +relative p-7 md:p-10 rounded-2xl bg-cream border border-warm-gray/10 +hover:border-terracotta/20 hover:shadow-lg hover:shadow-terracotta/5 +transition-all duration-500 +``` + +不使用静态阴影。阴影仅出现在 hover 状态,且仅用于最大的两种卡片。 + +### 按钮 + +**主要 CTA(圆角胶囊):** + +``` +px-6 py-3 rounded-full bg-ink text-cream font-sans text-sm tracking-wide +hover:bg-terracotta transition-colors duration-300 +``` + +**次要 CTA(描边胶囊):** + +``` +px-6 py-3 rounded-full border border-warm-gray/30 text-ink-muted font-sans text-sm tracking-wide +hover:border-terracotta/40 hover:text-terracotta transition-colors duration-300 +``` + +**Admin 主按钮(直角):** + +``` +px-4 py-2 rounded-lg bg-ink text-cream font-sans text-sm hover:bg-terracotta transition-colors +``` + +**Admin 次要按钮:** + +``` +px-6 py-2.5 rounded-lg border border-warm-gray/20 font-sans text-sm text-ink-muted +hover:text-ink transition-colors +``` + +**联系链接(描边胶囊):** + +``` +inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-warm-gray/20 +font-sans text-sm text-ink-muted +hover:border-terracotta hover:text-terracotta transition-all duration-300 +``` + +### 过渡与动画 + +**CSS 过渡(微交互):** +- `transition-colors duration-300` — 最常用,所有链接和按钮 +- `transition-all duration-300` — 标签、联系按钮、列表项 +- `transition-all duration-500` — 精选/分类大卡片(更缓的动效) +- `transition-opacity duration-300` — 箭头 hover 渐显 + +**自定义缓动:** `--ease-literary: cubic-bezier(0.25, 0.1, 0.25, 1)` + +**CSS 关键帧动画:** +- `animate-fade-up` — translateY(20px) → 0,0.7s +- `animate-fade-in` — opacity 0 → 1,0.5s + +**GSAP(主要动画引擎):** +- 缓动函数:`power3.out`(标准)、`power2.inOut`(分隔线)、`back.out(2)`(弹跳缩放) +- 逐字动画:拆分 `` + stagger `y`/`opacity`/`blur`,stagger 0.035s +- ScrollTrigger:`start: "top 85%"` ~ `"top 92%"` +- GsapReveal 组件变体:`fade-up`(y:40)、`fade-in`(opacity:0)、`slide-left`(x:-40)、`slide-right`(x:40)、`scale`(0.92) +- Stagger 值:0.035(逐字)、0.04~0.15(卡片)、0.08~0.12(列表) + +### 悬停效果 + +**文字:** `hover:text-terracotta`(主要)、`hover:text-ink`(次要)、`hover:text-red-600`(删除) + +**背景:** `hover:bg-terracotta`(按钮)、`hover:bg-cream`(列表行高亮)、`hover:bg-warm-gray/10`(侧栏导航) + +**边框:** `hover:border-terracotta/20`(卡片)、`hover:border-terracotta`(联系按钮)、`hover:border-terracotta/40`(次要 CTA) + +**Group hover:** `group-hover:text-terracotta`(卡片标题)、`group-hover:opacity-100`(箭头/删除按钮渐显)、`group-hover:translate-x-0`(箭头滑入) + +**表单聚焦:** `focus:border-terracotta/40 focus:outline-none`,复选框/单选框 `accent-terracotta` + +### 布局 + +**根布局:** `min-h-full flex flex-col` + `
`(sticky footer) + +**页面容器:** `px-page max-w-5xl mx-auto` + +**Admin 布局:** `min-h-screen bg-parchment flex` → `aside w-56 shrink-0` + `flex-1 overflow-auto p-8 max-w-5xl` + +**网格:** +- `grid md:grid-cols-2 gap-5` — 精选文章 +- `grid sm:grid-cols-2 lg:grid-cols-3 gap-5` — 分类 +- `grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4` — Admin 统计 +- `grid grid-cols-2 gap-4` — Admin 表单双列行 + +### Prose 排版 + +文章正文使用 `.prose-literary` 类:字号 1.0625rem、行高 2、字距 0.04em。段落首行缩进 2em(首段不缩进),段间距 1.5em。h2 使用 `font-display` 1.75rem,h3 为 1.35rem。引用块左侧 3px terracotta 色竖线。代码使用 `font-mono` + `bg-parchment-deep`。链接使用 terracotta 色 + 下划线,hover 时下划线加深。 + +### 全局细节 + +- 滚动条:6px 宽,track 为 `parchment-deep`,thumb 为 `warm-gray`,hover 变 `ink-muted` +- 纸质纹理:`body::before` 叠加 SVG fractalNoise,opacity 0.03 +- 分隔线装饰:`.divider-ornament` 使用两端渐变线 + 中间符号 diff --git a/package.json b/package.json index 174f82f..5bc4243 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "gsap": "^3.15.0", "next": "16.2.9", "react": "19.2.4", "react-dom": "19.2.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d85c1a5..9b578e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + gsap: + specifier: ^3.15.0 + version: 3.15.0 next: specifier: 16.2.9 version: 16.2.9(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1221,6 +1224,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gsap@3.15.0: + resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3269,6 +3275,8 @@ snapshots: graceful-fs@4.2.11: {} + gsap@3.15.0: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..5d0e399 --- /dev/null +++ b/src/app/about/page.tsx @@ -0,0 +1,136 @@ +import Link from "next/link"; +import GsapReveal from "@/components/GsapReveal"; + +export const metadata = { + title: "关于", + description: "关于胡旭和这个博客", +}; + +const timeline = [ + { year: "2026", title: "开始写博客", desc: "用 Next.js + Halo CMS 搭建个人博客,记录技术与生活" }, + { year: "2025", title: "sui_lightbox 项目", desc: "开发3D灯箱模拟平台,探索标识行业的数字化可能" }, + { year: "2024", title: "AI 图像生成", desc: "深入研究 Stable Diffusion,在 Apple Silicon 上部署本地 AI 环境" }, + { year: "2023", title: "Web 3D 可视化", desc: "从 Three.js 到 React Three Fiber,进入数字孪生领域" }, + { year: "2022", title: "前端开发", desc: "从后端转向全栈,React + TypeScript 成为主力技术栈" }, +]; + +export default function AboutPage() { + return ( +
+
+ {/* Header */} + +

+ 关于 +

+

+ About me & this blog +

+
+ + {/* Intro */} + +

+ 你好,我是胡旭,一个来自安徽六安的前端开发者。 +

+

+ 我对 AI 图像生成、Web 3D 可视化、以及将技术落地到实际产品中充满兴趣。目前我正在探索标识灯箱行业的数字化可能,希望用 3D 预览技术帮助标识制作商更高效地展示他们的产品。 +

+

+ 这个博客是我记录技术笔记、旅途见闻和生活感悟的地方。写字对我来说是一种思考的方式 — 当你试图把一个想法写下来的时候,你会发现自己对它的理解远比想象中要浅。 +

+

+ 如果你有任何想法或合作意向,欢迎通过以下方式联系我。 +

+
+ + {/* Contact */} + + + + + + GitHub + + + + + + hi@asui.xyz + + + + + + asui.xyz + + + + {/* Timeline */} +
+ +

时间线

+
+ + {timeline.map((item, i) => ( +
+ {i < timeline.length - 1 && ( +
+ )} +
+
+
+
+ {item.year} +

{item.title}

+

{item.desc}

+
+
+ ))} + +
+ + {/* Tech stack */} +
+ +

技术栈

+
+ + {[ + "React", "TypeScript", "Next.js", "Tailwind CSS", "Three.js", + "React Three Fiber", "Python", "Stable Diffusion", "Node.js", + "Vite", "Halo CMS", "Docker" + ].map((tech) => ( + + {tech} + + ))} + +
+ + {/* Colophon */} + +
+

关于这个博客

+

+ 这个博客使用 Next.js 16 构建,样式使用 Tailwind CSS 4,内容通过 Halo CMS 管理。字体使用了 Noto Serif SC(宋体)和 Cormorant Garamond 的组合,追求一种接近纸质杂志的阅读体验。 +

+
+
+
+
+ ); +} diff --git a/src/app/admin/categories/page.tsx b/src/app/admin/categories/page.tsx new file mode 100644 index 0000000..b8bbffe --- /dev/null +++ b/src/app/admin/categories/page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState, useEffect } from "react"; +import type { Category } from "@/lib/store"; + +export default function CategoriesPage() { + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [newName, setNewName] = useState(""); + const [newDesc, setNewDesc] = useState(""); + const [editingId, setEditingId] = useState(null); + const [editName, setEditName] = useState(""); + + async function load() { + const res = await fetch("/api/categories"); + setCategories(await res.json()); + setLoading(false); + } + + useEffect(() => { load(); }, []); + + async function handleAdd(e: React.FormEvent) { + e.preventDefault(); + if (!newName.trim()) return; + await fetch("/api/categories", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: newName.trim(), description: newDesc.trim() }), + }); + setNewName(""); + setNewDesc(""); + load(); + } + + async function handleDelete(id: string, name: string) { + if (!confirm(`确定删除分类「${name}」?`)) return; + await fetch(`/api/categories?id=${id}`, { method: "DELETE" }); + load(); + } + + async function handleSave(id: string) { + await fetch(`/api/categories?id=${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: editName }), + }); + setEditingId(null); + load(); + } + + if (loading) return
加载中...
; + + return ( +
+

分类管理

+ + {/* Add form */} +
+ setNewName(e.target.value)} + placeholder="分类名称" + className="flex-1 max-w-xs px-4 py-2.5 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink placeholder:text-ink-muted/50 focus:outline-none focus:border-terracotta/40 transition-colors" + /> + setNewDesc(e.target.value)} + placeholder="描述(可选)" + className="flex-1 max-w-sm px-4 py-2.5 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink placeholder:text-ink-muted/50 focus:outline-none focus:border-terracotta/40 transition-colors" + /> + +
+ + {/* List */} +
+ {categories.map((cat) => ( +
+ {editingId === cat.id ? ( +
+ setEditName(e.target.value)} + className="px-3 py-1.5 rounded-lg border border-warm-gray/20 bg-white font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40" + autoFocus + /> + + +
+ ) : ( + <> +
+ {cat.name} + {cat.description && {cat.description}} +
+
+ + +
+ + )} +
+ ))} + {categories.length === 0 && ( +
暂无分类
+ )} +
+
+ ); +} diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 0000000..2377baa --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,99 @@ +"use client"; + +import Link from "next/link"; +import { usePathname, useRouter } from "next/navigation"; +import { useState, useEffect } from "react"; + +const navItems = [ + { label: "仪表盘", href: "/admin", icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0h4" }, + { label: "文章", href: "/admin/posts", icon: "M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2" }, + { label: "分类", href: "/admin/categories", icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }, + { label: "标签", href: "/admin/tags", icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" }, +]; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const router = useRouter(); + const [authed, setAuthed] = useState(null); + + // Login page — render bare children without sidebar or auth check + const isLoginPage = pathname === "/admin/login"; + + useEffect(() => { + if (isLoginPage) return; + fetch("/api/auth") + .then((r) => r.json()) + .then((data) => { + if (!data.authenticated) router.push("/admin/login"); + else setAuthed(true); + }); + }, [router, isLoginPage]); + + if (isLoginPage) { + return <>{children}; + } + + if (authed === null || !authed) { + return
加载中...
; + } + + async function handleLogout() { + await fetch("/api/auth", { method: "DELETE" }); + router.push("/admin/login"); + } + + return ( +
+ {/* Sidebar */} + + + {/* Main */} +
+
+ {children} +
+
+
+ ); +} diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx new file mode 100644 index 0000000..fc7b301 --- /dev/null +++ b/src/app/admin/login/page.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function LoginPage() { + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + setError(""); + const res = await fetch("/api/auth", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + }); + if (res.ok) { + router.push("/admin"); + } else { + const data = await res.json(); + setError(data.error || "登录失败"); + } + setLoading(false); + } + + return ( +
+
+
+

后台管理

+

asui.xyz

+
+
+
+ setPassword(e.target.value)} + className="w-full px-4 py-3 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink placeholder:text-ink-muted/50 focus:outline-none focus:border-terracotta/40 transition-colors" + autoFocus + /> +
+ {error &&

{error}

} + +
+
+
+ ); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..17aff71 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import type { Post, Category, Tag } from "@/lib/store"; + +export default function DashboardPage() { + const [posts, setPosts] = useState([]); + const [categories, setCategories] = useState([]); + const [tags, setTags] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + Promise.all([ + fetch("/api/posts").then((r) => r.json()), + fetch("/api/categories").then((r) => r.json()), + fetch("/api/tags").then((r) => r.json()), + ]).then(([p, c, t]) => { + setPosts(p); + setCategories(c); + setTags(t); + setLoading(false); + }); + }, []); + + if (loading) { + return
加载中...
; + } + + const published = posts.filter((p) => p.status === "published").length; + const drafts = posts.filter((p) => p.status === "draft").length; + const featured = posts.filter((p) => p.featured).length; + + const stats = [ + { label: "文章总数", value: posts.length, color: "text-ink" }, + { label: "已发布", value: published, color: "text-sage" }, + { label: "草稿", value: drafts, color: "text-terracotta" }, + { label: "精选", value: featured, color: "text-terracotta" }, + { label: "分类", value: categories.length, color: "text-ink" }, + { label: "标签", value: tags.length, color: "text-ink" }, + ]; + + return ( +
+
+

仪表盘

+ + + 新文章 + +
+ + {/* Stats */} +
+ {stats.map((s) => ( +
+
{s.value}
+
{s.label}
+
+ ))} +
+ + {/* Recent posts */} +

最近文章

+
+ {posts.slice(0, 5).map((post) => ( + +
+
{post.title}
+
{post.category} · {post.date}
+
+ + {post.status === "published" ? "已发布" : "草稿"} + + + ))} +
+
+ ); +} diff --git a/src/app/admin/posts/[id]/page.tsx b/src/app/admin/posts/[id]/page.tsx new file mode 100644 index 0000000..9a1946b --- /dev/null +++ b/src/app/admin/posts/[id]/page.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter, useParams } from "next/navigation"; +import type { Post, Category, Tag } from "@/lib/store"; + +export default function EditPostPage() { + const router = useRouter(); + const params = useParams(); + const id = params.id as string; + + const [post, setPost] = useState(null); + const [categories, setCategories] = useState([]); + const [allTags, setAllTags] = useState([]); + const [loading, setLoading] = useState(true); + + const [form, setForm] = useState({ + title: "", slug: "", excerpt: "", content: "", + category: "", tags: [] as string[], readingTime: 5, + featured: false, status: "draft" as "draft" | "published", + date: "", + }); + + useEffect(() => { + Promise.all([ + fetch(`/api/posts/${id}`).then((r) => r.json()), + fetch("/api/categories").then((r) => r.json()), + fetch("/api/tags").then((r) => r.json()), + ]).then(([p, c, t]) => { + setPost(p); + setCategories(c); + setAllTags(t); + setForm({ + title: p.title, slug: p.slug, excerpt: p.excerpt, + content: p.content, category: p.category, tags: p.tags, + readingTime: p.readingTime, featured: p.featured, + status: p.status, date: p.date, + }); + setLoading(false); + }); + }, [id]); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + const res = await fetch(`/api/posts/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(form), + }); + if (res.ok) router.push("/admin/posts"); + } + + function toggleTag(tagName: string) { + setForm((prev) => ({ + ...prev, + tags: prev.tags.includes(tagName) + ? prev.tags.filter((t) => t !== tagName) + : [...prev.tags, tagName], + })); + } + + if (loading) return
加载中...
; + if (!post) return
文章未找到
; + + return ( +
+
+ +

编辑文章

+
+ +
+
+ + setForm({ ...form, title: e.target.value })} + className="w-full px-4 py-3 rounded-xl border border-warm-gray/20 bg-cream font-display text-lg text-ink focus:outline-none focus:border-terracotta/40 transition-colors" + required + /> +
+ +
+
+ + setForm({ ...form, slug: e.target.value })} + className="w-full px-4 py-2.5 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40 transition-colors" + /> +
+
+ + setForm({ ...form, date: e.target.value })} + className="w-full px-4 py-2.5 rounded-xl border border-warm-gray/20 bg-cream font-sans text-sm text-ink focus:outline-none focus:border-terracotta/40 transition-colors" + /> +
+
+ +
+ +