Restore the app files (prior commit mis-scoped its tar)
build-and-publish / build (push) Failing after 8s

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
2026-07-22 20:50:58 -05:00
co-authored by Claude Opus 4.8
parent eaad7b916e
commit 2a0dd1ae6b
85 changed files with 6012 additions and 0 deletions
@@ -0,0 +1,78 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import { format } from 'date-fns';
import { getComments, postComment } from '../api';
import { useAsync } from '../lib/useAsync';
export default function CommentSection({ postId }: { postId: string }) {
const [reloadKey, setReloadKey] = useState(0);
const { data: comments, loading } = useAsync(() => getComments(postId), [postId, reloadKey]);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [comment, setComment] = useState('');
const [state, setState] = useState<'idle' | 'busy' | 'error'>('idle');
async function onSubmit(e: FormEvent) {
e.preventDefault();
if (!name.trim() || !email.trim() || !comment.trim()) return;
setState('busy');
try {
await postComment({ postId, name: name.trim(), email: email.trim(), comment: comment.trim() });
setName(''); setEmail(''); setComment('');
setState('idle');
setReloadKey((k) => k + 1);
} catch {
setState('error');
}
}
const list = comments ?? [];
return (
<section className="mt-12">
<h2 className="mb-6 border-b border-primary-200 pb-2 text-2xl font-bold">
{list.length === 1 ? '1 Comment' : `${list.length} Comments`}
</h2>
{loading && <p className="text-primary-500">Loading comments</p>}
<ul className="mb-10 space-y-4">
{list.map((c) => (
<li key={c.id} className="card">
<div className="mb-1 flex items-center text-sm text-primary-500">
<span className="font-semibold text-primary-700">{c.name}</span>
{c.createdAt && (
<>
<span className="mx-2"></span>
<time dateTime={c.createdAt}>{format(new Date(c.createdAt), 'MMMM d, yyyy')}</time>
</>
)}
</div>
<p className="whitespace-pre-line text-primary-700">{c.body}</p>
</li>
))}
{!loading && list.length === 0 && (
<li className="text-primary-500">No comments yet be the first to share a thought.</li>
)}
</ul>
<div className="card">
<h3 className="mb-4 text-xl font-bold">Leave a comment</h3>
<form onSubmit={onSubmit} className="space-y-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<input className="field" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} required />
<input className="field" type="email" placeholder="Email (not published)" value={email}
onChange={(e) => setEmail(e.target.value)} required />
</div>
<textarea className="field min-h-32" placeholder="Your comment" value={comment}
onChange={(e) => setComment(e.target.value)} required />
<button type="submit" className="button" disabled={state === 'busy'}>
{state === 'busy' ? 'Posting…' : 'Post Comment'}
</button>
{state === 'error' && <p className="text-sm text-red-700">Something went wrong. Please try again.</p>}
</form>
</div>
</section>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { Link } from 'react-router-dom';
import SubscribeForm from './SubscribeForm';
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="mt-16 bg-primary-800 text-white">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<div>
<h3 className="mb-4 text-xl font-bold text-white">Confessions of Grace</h3>
<p className="text-primary-300">
A blog dedicated to exploring the doctrines of grace and Reformed theology.
</p>
</div>
<div>
<h3 className="mb-4 text-xl font-bold text-white">Navigation</h3>
<ul className="space-y-2">
<li><Link to="/" className="text-primary-300 hover:text-white">Home</Link></li>
<li><Link to="/about" className="text-primary-300 hover:text-white">About</Link></li>
<li><Link to="/posts" className="text-primary-300 hover:text-white">Archive</Link></li>
<li><Link to="/confession" className="text-primary-300 hover:text-white">1689 Confession</Link></li>
<li><Link to="/resources" className="text-primary-300 hover:text-white">Resources</Link></li>
<li>
<a
href="https://www.etsy.com/shop/ConfessionsOfGrace"
target="_blank"
rel="noreferrer"
className="text-primary-300 hover:text-white"
>
Shop
</a>
</li>
</ul>
</div>
<div>
<h3 className="mb-4 text-xl font-bold text-white">Subscribe</h3>
<p className="mb-4 text-primary-300">Stay updated with the latest posts.</p>
<SubscribeForm placeholder="Your email" buttonLabel="Subscribe" />
</div>
</div>
<div className="mt-8 border-t border-primary-700 pt-8 text-center text-primary-400">
<p>&copy; {currentYear} Confessions of Grace. All rights reserved.</p>
<p className="mt-2 text-sm">
&ldquo;For by grace you have been saved through faith. And this is not your own doing; it is the
gift of God.&rdquo; Ephesians 2:8
</p>
</div>
</div>
</footer>
);
}
+48
View File
@@ -0,0 +1,48 @@
import { Link } from 'react-router-dom';
import { useAuth } from '../lib/auth';
import { logout } from '../api';
export default function Header() {
const { me } = useAuth();
return (
<header className="border-b border-primary-200 bg-white shadow-sm">
<div className="container mx-auto px-4 py-6">
<div className="flex flex-col items-center justify-between md:flex-row">
<Link to="/" className="no-underline">
<div className="mb-4 flex items-center space-x-4 md:mb-0">
<img src="/assets/logo.svg" alt="Confessions of Grace" className="h-auto max-h-12 w-auto" />
<div>
<h1 className="mb-0 text-3xl font-bold text-primary-900">Confessions of Grace</h1>
<p className="text-sm italic text-primary-500">Confessing Christ. Rejoicing in Grace.</p>
</div>
</div>
</Link>
<nav className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
<Link to="/" className="text-primary-700 hover:text-accent-dark">Home</Link>
<Link to="/about" className="text-primary-700 hover:text-accent-dark">About</Link>
<Link to="/posts" className="text-primary-700 hover:text-accent-dark">Archive</Link>
<Link to="/confession" className="text-primary-700 hover:text-accent-dark">1689 Confession</Link>
<Link to="/resources" className="text-primary-700 hover:text-accent-dark">Resources</Link>
<a
href="https://www.etsy.com/shop/ConfessionsOfGrace"
target="_blank"
rel="noreferrer"
className="text-primary-700 hover:text-accent-dark"
>
Shop
</a>
{me?.admin && (
<>
<Link to="/admin" className="font-semibold text-accent-dark hover:text-accent">Admin</Link>
<button onClick={() => logout()} className="text-primary-500 hover:text-accent-dark">
Sign out
</button>
</>
)}
</nav>
</div>
</div>
</header>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { Link } from 'react-router-dom';
import { format } from 'date-fns';
import type { PostSummary } from '../types';
export default function PostCard({ post }: { post: PostSummary }) {
return (
<article className="card transition-shadow duration-200 hover:shadow-md">
{post.coverImage && (
<div className="relative mb-4 h-48 w-full overflow-hidden rounded-md">
<img src={post.coverImage} alt={post.title} className="h-full w-full object-cover" />
</div>
)}
<h2 className="mb-2 text-xl font-bold">
<Link to={`/posts/${post.slug}`} className="hover:text-accent-dark">{post.title}</Link>
</h2>
<div className="mb-2 flex items-center text-sm text-primary-500">
<span>{post.author}</span>
<span className="mx-2"></span>
<time dateTime={post.date}>{format(new Date(post.date), 'MMMM d, yyyy')}</time>
</div>
<p className="mb-4 text-primary-600">{post.excerpt}</p>
<div className="mb-4 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<Link
key={tag}
to={`/tags/${encodeURIComponent(tag)}`}
className="rounded-md bg-primary-100 px-2 py-1 text-xs text-primary-600 no-underline hover:bg-primary-200"
>
{tag}
</Link>
))}
</div>
<Link to={`/posts/${post.slug}`} className="inline-flex items-center text-accent-dark hover:text-accent">
Read more
<svg xmlns="http://www.w3.org/2000/svg" className="ml-1 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</Link>
</article>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { Link } from 'react-router-dom';
import { format } from 'date-fns';
import type { PostSummary, TagCount } from '../types';
import SubscribeForm from './SubscribeForm';
interface Props {
recentPosts: PostSummary[];
tags: TagCount[];
}
export default function Sidebar({ recentPosts, tags }: Props) {
return (
<aside className="flex flex-col gap-8">
<div className="card">
<h3 className="mb-4 border-b border-primary-200 pb-2 text-xl font-bold">Recent Posts</h3>
<ul className="space-y-3">
{recentPosts.map((post) => (
<li key={post.slug}>
<Link to={`/posts/${post.slug}`} className="hover:text-accent">{post.title}</Link>
<div className="text-sm text-primary-500">
{format(new Date(post.date), 'MMMM d, yyyy')}
</div>
</li>
))}
{recentPosts.length === 0 && <li className="text-primary-500">No posts yet.</li>}
</ul>
</div>
<div className="card">
<h3 className="mb-4 border-b border-primary-200 pb-2 text-xl font-bold">Tags</h3>
<div className="flex flex-wrap gap-2">
{tags.map((t) => (
<Link
key={t.tag}
to={`/tags/${encodeURIComponent(t.tag)}`}
className="rounded-md bg-primary-100 px-2 py-1 text-sm text-primary-600 no-underline hover:bg-primary-200"
>
{t.tag} <span className="text-primary-400">({t.count})</span>
</Link>
))}
{tags.length === 0 && <span className="text-primary-500">No tags yet.</span>}
</div>
</div>
<div className="card">
<h3 className="mb-4 border-b border-primary-200 pb-2 text-xl font-bold">Subscribe</h3>
<p className="mb-4 text-primary-600">Stay updated with the latest posts.</p>
<SubscribeForm />
</div>
</aside>
);
}
+54
View File
@@ -0,0 +1,54 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import { subscribe } from '../api';
import { cn } from '../lib/utils';
interface Props {
placeholder?: string;
buttonLabel?: string;
className?: string;
}
export default function SubscribeForm({
placeholder = 'Your email',
buttonLabel = 'Subscribe',
className,
}: Props) {
const [email, setEmail] = useState('');
const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>('idle');
async function onSubmit(e: FormEvent) {
e.preventDefault();
if (!email.trim()) return;
setState('busy');
try {
await subscribe(email.trim());
setState('done');
setEmail('');
} catch {
setState('error');
}
}
if (state === 'done') {
return <p className="text-sm text-accent-light">Thank you you&apos;re subscribed.</p>;
}
return (
<form onSubmit={onSubmit} className={cn('flex flex-col space-y-2', className)}>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={placeholder}
aria-label="Email address"
className="field"
/>
<button type="submit" className="button" disabled={state === 'busy'}>
{state === 'busy' ? 'Subscribing…' : buttonLabel}
</button>
{state === 'error' && <p className="text-sm text-red-300">Something went wrong. Please try again.</p>}
</form>
);
}