|
1 | | -'use client' |
| 1 | +import type { Metadata } from 'next' |
| 2 | +import UserPageContent from './UserPageContent' |
2 | 3 |
|
3 | | -import { useEffect, useState } from 'react' |
4 | | -import { useParams, useRouter } from 'next/navigation' |
5 | | -import type { UserRepoSummary } from '@/lib/types' |
6 | | -import UserDashboard from '@/components/UserDashboard' |
7 | | -import LoadingState from '@/components/LoadingState' |
8 | | -import SubpageShell from '@/components/SubpageShell' |
| 4 | +const VALID_USERNAME = /^[a-zA-Z0-9_.-]+$/ |
9 | 5 |
|
10 | | -const MONO = `var(--font-courier), system-ui, sans-serif` |
11 | | - |
12 | | -export default function UserPage() { |
13 | | - const params = useParams() |
14 | | - const router = useRouter() |
15 | | - const username = typeof params.username === 'string' ? params.username : '' |
16 | | - |
17 | | - const [repos, setRepos] = useState<UserRepoSummary[] | null>(null) |
18 | | - const [error, setError] = useState<string | null>(null) |
19 | | - const [loading, setLoading] = useState(true) |
20 | | - |
21 | | - useEffect(() => { |
22 | | - if (!username) return |
23 | | - setLoading(true) |
24 | | - setError(null) |
25 | | - fetch(`/api/user?username=${encodeURIComponent(username)}`) |
26 | | - .then(r => r.json()) |
27 | | - .then((d: { repos?: UserRepoSummary[]; error?: string }) => { |
28 | | - if (d.error) { setError(d.error); return } |
29 | | - setRepos(d.repos ?? []) |
30 | | - }) |
31 | | - .catch(() => setError('Something went wrong. Try again.')) |
32 | | - .finally(() => setLoading(false)) |
33 | | - }, [username]) |
34 | | - |
35 | | - return ( |
36 | | - <SubpageShell |
37 | | - title={undefined} |
38 | | - subtitle={null} |
39 | | - microcopy={null} |
40 | | - hideHero |
41 | | - > |
42 | | - {loading && <LoadingState />} |
| 6 | +function sanitizeUsername(raw: string): string { |
| 7 | + const trimmed = raw.slice(0, 39) |
| 8 | + return VALID_USERNAME.test(trimmed) ? trimmed : '' |
| 9 | +} |
43 | 10 |
|
44 | | - {error && ( |
45 | | - <div style={{ textAlign: 'center', padding: '40px 0' }}> |
46 | | - <p style={{ fontFamily: MONO, fontSize: '13px', color: '#8B0000', marginBottom: '20px' }}>{error}</p> |
47 | | - <button |
48 | | - onClick={() => router.push('/')} |
49 | | - style={{ fontFamily: MONO, fontSize: '13px', fontWeight: 700, background: 'none', border: 'none', textDecoration: 'underline', textUnderlineOffset: '3px', color: '#160A06', cursor: 'pointer', minHeight: '44px', padding: '10px 0' }} |
50 | | - > |
51 | | - ← examine another subject |
52 | | - </button> |
53 | | - </div> |
54 | | - )} |
| 11 | +export async function generateMetadata( |
| 12 | + { params }: { params: Promise<{ username: string }> } |
| 13 | +): Promise<Metadata> { |
| 14 | + const { username } = await params |
| 15 | + const safe = sanitizeUsername(username) |
| 16 | + |
| 17 | + if (!safe) { |
| 18 | + return { |
| 19 | + title: 'Graveyard · Commitment Issues', |
| 20 | + description: 'Browse the GitHub graveyard for any developer on Commitment Issues.', |
| 21 | + robots: { index: false, follow: true }, |
| 22 | + } |
| 23 | + } |
| 24 | + |
| 25 | + const title = `@${safe}'s GitHub Graveyard · Commitment Issues` |
| 26 | + const description = `See how many of @${safe}'s GitHub repositories are alive, on life support, or deceased. Free abandonment analysis with death certificates for any developer's projects.` |
| 27 | + const canonical = `https://commitmentissues.dev/user/${safe}` |
| 28 | + |
| 29 | + return { |
| 30 | + title, |
| 31 | + description, |
| 32 | + alternates: { canonical }, |
| 33 | + openGraph: { |
| 34 | + title, |
| 35 | + description, |
| 36 | + url: canonical, |
| 37 | + type: 'profile', |
| 38 | + images: [{ url: '/opengraph-image', width: 1200, height: 630 }], |
| 39 | + }, |
| 40 | + twitter: { |
| 41 | + card: 'summary_large_image', |
| 42 | + title, |
| 43 | + description, |
| 44 | + images: ['/opengraph-image'], |
| 45 | + }, |
| 46 | + } |
| 47 | +} |
55 | 48 |
|
56 | | - {repos && !loading && <UserDashboard repos={repos} username={username} />} |
57 | | - </SubpageShell> |
58 | | - ) |
| 49 | +export default function UserPage() { |
| 50 | + return <UserPageContent /> |
59 | 51 | } |
0 commit comments