|
1 | 1 | 'use client' |
2 | 2 |
|
3 | | -import Link from 'next/link' |
| 3 | +import { FormEvent, useState } from 'react' |
| 4 | +import { useRouter } from 'next/navigation' |
| 5 | +import { track } from '@vercel/analytics' |
4 | 6 | import SubpageShell from '@/components/SubpageShell' |
5 | 7 |
|
| 8 | +const MONO = `var(--font-courier), system-ui, sans-serif` |
| 9 | +const VALID_USERNAME = /^[a-zA-Z0-9_.-]+$/ |
| 10 | + |
| 11 | +type ParsedInput = |
| 12 | + | { kind: 'repo'; slug: string } |
| 13 | + | { kind: 'user'; username: string } |
| 14 | + | null |
| 15 | + |
| 16 | +function parsePricingInput(value: string): ParsedInput { |
| 17 | + const trimmed = value.trim() |
| 18 | + if (!trimmed) return null |
| 19 | + |
| 20 | + const githubRepoUrl = trimmed.match(/(?:https?:\/\/)?(?:www\.)?github\.com\/([^/\s]+)\/([^/\s#?]+)(?:[/?#]|$)/i) |
| 21 | + if (githubRepoUrl) { |
| 22 | + const owner = githubRepoUrl[1] |
| 23 | + const repo = githubRepoUrl[2].replace(/\.git$/i, '') |
| 24 | + return { kind: 'repo', slug: `${owner}/${repo}` } |
| 25 | + } |
| 26 | + |
| 27 | + const githubUserUrl = trimmed.match(/(?:https?:\/\/)?(?:www\.)?github\.com\/([^/\s?#]+)\/?$/i) |
| 28 | + if (githubUserUrl && VALID_USERNAME.test(githubUserUrl[1])) { |
| 29 | + return { kind: 'user', username: githubUserUrl[1] } |
| 30 | + } |
| 31 | + |
| 32 | + const slugMatch = trimmed.match(/^([^/\s]+)\/([^/\s]+)$/) |
| 33 | + if (slugMatch) { |
| 34 | + const owner = slugMatch[1] |
| 35 | + const repo = slugMatch[2].replace(/\.git$/i, '') |
| 36 | + return { kind: 'repo', slug: `${owner}/${repo}` } |
| 37 | + } |
| 38 | + |
| 39 | + if (VALID_USERNAME.test(trimmed)) { |
| 40 | + return { kind: 'user', username: trimmed } |
| 41 | + } |
| 42 | + |
| 43 | + return null |
| 44 | +} |
| 45 | + |
6 | 46 | const SECTIONS = [ |
7 | 47 | { |
8 | 48 | id: 'no-tiers', |
@@ -33,6 +73,25 @@ const SECTIONS = [ |
33 | 73 | ] |
34 | 74 |
|
35 | 75 | export default function PricingContent() { |
| 76 | + const router = useRouter() |
| 77 | + const [value, setValue] = useState('') |
| 78 | + const [invalid, setInvalid] = useState(false) |
| 79 | + |
| 80 | + function handleSubmit(e: FormEvent) { |
| 81 | + e.preventDefault() |
| 82 | + const parsed = parsePricingInput(value) |
| 83 | + if (!parsed) { |
| 84 | + setInvalid(true) |
| 85 | + return |
| 86 | + } |
| 87 | + track('pricing_repo_submitted', { kind: parsed.kind }) |
| 88 | + if (parsed.kind === 'repo') { |
| 89 | + router.push(`/?repo=${encodeURIComponent(parsed.slug)}`) |
| 90 | + } else { |
| 91 | + router.push(`/user/${encodeURIComponent(parsed.username)}`) |
| 92 | + } |
| 93 | + } |
| 94 | + |
36 | 95 | return ( |
37 | 96 | <SubpageShell |
38 | 97 | title="Death Is Free." |
@@ -78,19 +137,61 @@ export default function PricingContent() { |
78 | 137 | style={{ |
79 | 138 | border: '2px solid var(--c-border)', |
80 | 139 | background: 'var(--c-panel-2, transparent)', |
81 | | - textAlign: 'center', |
82 | 140 | padding: '28px 20px', |
83 | 141 | marginTop: '8px', |
84 | 142 | }} |
85 | 143 | > |
86 | | - <p className="record-label" style={{ marginBottom: '14px' }}>Begin the Examination</p> |
87 | | - <Link |
88 | | - href="/" |
89 | | - className="subpage-faq-cta" |
90 | | - style={{ display: 'inline-flex', justifyContent: 'center' }} |
| 144 | + <p className="record-label" style={{ marginBottom: '14px', textAlign: 'center' }}>Begin the Examination</p> |
| 145 | + <form |
| 146 | + onSubmit={handleSubmit} |
| 147 | + style={{ display: 'flex', flexDirection: 'column', gap: '10px' }} |
91 | 148 | > |
92 | | - ⚰ Bury a repo → |
93 | | - </Link> |
| 149 | + <input |
| 150 | + type="text" |
| 151 | + value={value} |
| 152 | + onChange={e => { setValue(e.target.value); if (invalid) setInvalid(false) }} |
| 153 | + placeholder="username or owner/repo" |
| 154 | + aria-label="GitHub username or repo" |
| 155 | + autoComplete="off" |
| 156 | + spellCheck={false} |
| 157 | + style={{ |
| 158 | + fontFamily: MONO, |
| 159 | + fontSize: '14px', |
| 160 | + padding: '12px 14px', |
| 161 | + background: 'var(--c-bg)', |
| 162 | + color: 'var(--c-ink)', |
| 163 | + border: `2px solid ${invalid ? 'var(--c-red, #8B0000)' : 'var(--c-ink)'}`, |
| 164 | + outline: 'none', |
| 165 | + width: '100%', |
| 166 | + minHeight: '44px', |
| 167 | + }} |
| 168 | + /> |
| 169 | + <button |
| 170 | + type="submit" |
| 171 | + className="subpage-faq-cta" |
| 172 | + style={{ |
| 173 | + fontFamily: MONO, |
| 174 | + fontSize: '13px', |
| 175 | + fontWeight: 700, |
| 176 | + letterSpacing: '0.06em', |
| 177 | + background: 'var(--c-ink)', |
| 178 | + color: 'var(--c-bg)', |
| 179 | + border: '2px solid var(--c-ink)', |
| 180 | + cursor: 'pointer', |
| 181 | + minHeight: '44px', |
| 182 | + display: 'inline-flex', |
| 183 | + alignItems: 'center', |
| 184 | + justifyContent: 'center', |
| 185 | + }} |
| 186 | + > |
| 187 | + ⚰ Issue death certificate → |
| 188 | + </button> |
| 189 | + {invalid && ( |
| 190 | + <p style={{ fontFamily: MONO, fontSize: '12px', color: 'var(--c-red, #8B0000)', margin: 0, textAlign: 'center' }}> |
| 191 | + Could not parse. Try a github URL or owner/repo. |
| 192 | + </p> |
| 193 | + )} |
| 194 | + </form> |
94 | 195 | </div> |
95 | 196 | </div> |
96 | 197 | </SubpageShell> |
|
0 commit comments