← back to Goodquestion Ai
src/pages/projects/[slug].astro
532 lines
---
import { projects } from '../../data/projects';
import type { Project } from '../../data/projects';
export function getStaticPaths() {
return projects.map((project) => ({
params: { slug: project.slug },
props: { project },
}));
}
interface Props {
project: Project;
}
const { project } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{project.name} — Case Study | goodquestion.ai</title>
<meta name="description" content={`${project.name}: ${project.tagline}`} />
<meta name="author" content="Agent Abrams" />
<meta property="og:title" content={`${project.name} — Case Study`} />
<meta property="og:description" content={project.tagline} />
<meta property="og:type" content="article" />
<meta property="og:url" content={`https://goodquestion.ai/projects/${project.slug}`} />
<meta property="og:site_name" content="goodquestion.ai" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@agentabrams" />
<meta name="twitter:title" content={`${project.name} — Case Study`} />
<meta name="twitter:description" content={project.tagline} />
<link rel="canonical" href={`https://goodquestion.ai/projects/${project.slug}`} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@700;800;900&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0A0A0A;
--text: #FFFFFF;
--text-secondary: #AAAAAA;
--brass: #E85D26;
--brass-hover: #FF7040;
--card-bg: #111111;
--card-border: #1A1A1A;
--display: 'Playfair Display', serif;
--body: 'Inter', sans-serif;
--mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; }
body {
font-family: var(--body);
font-weight: 400;
color: var(--text);
background-color: var(--bg);
font-size: 18px;
line-height: 1.75;
overflow-x: hidden;
}
a { text-decoration: none; color: inherit; }
img { max-width: 100%; display: block; }
/* ── NAV ── */
.nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 1000;
background: rgba(10, 10, 10, 0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--card-border);
}
.nav-inner {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
padding: 0 24px;
}
.nav-logo {
display: flex;
align-items: center;
gap: 12px;
}
.nav-logo-circle {
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--brass);
display: flex;
align-items: center;
justify-content: center;
color: #FFFFFF;
font-family: var(--display);
font-weight: 900;
font-size: 1.15rem;
}
.nav-logo-text {
font-family: var(--body);
font-weight: 600;
font-size: 1rem;
letter-spacing: 0.12em;
color: var(--text);
}
.nav-links {
display: flex;
align-items: center;
gap: 32px;
}
.nav-links a {
font-family: var(--body);
font-size: 0.95rem;
font-weight: 500;
color: var(--text-secondary);
transition: color 0.2s;
}
.nav-links a:hover,
.nav-links a.active {
color: var(--brass);
}
/* ── HERO ── */
.hero {
padding: 100px 24px 0;
max-width: 1200px;
margin: 0 auto;
}
.hero-back {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 32px;
transition: color 0.2s;
}
.hero-back:hover { color: var(--brass); }
.hero-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 32px;
}
.hero-header h1 {
font-family: var(--display);
font-weight: 900;
font-size: 3.5rem;
line-height: 1.1;
}
.hero-url {
font-family: var(--mono);
font-size: 0.85rem;
color: var(--brass);
padding: 8px 16px;
border: 1px solid var(--card-border);
border-radius: 8px;
background: var(--card-bg);
white-space: nowrap;
transition: border-color 0.2s;
}
.hero-url:hover { border-color: var(--brass); }
.hero-tagline {
font-size: 1.15rem;
color: var(--text-secondary);
max-width: 700px;
margin-bottom: 40px;
line-height: 1.6;
}
.hero-screenshot {
width: 100%;
border-radius: 12px;
border: 1px solid var(--card-border);
overflow: hidden;
background: var(--card-bg);
}
.hero-screenshot img {
width: 100%;
display: block;
}
.hero-screenshot.no-image {
aspect-ratio: 21 / 9;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #111 0%, #1A1A1A 100%);
}
.hero-screenshot.no-image::after {
content: 'Screenshot coming soon';
color: #555;
font-size: 1rem;
}
/* ── CONTENT ── */
.content {
max-width: 800px;
margin: 0 auto;
padding: 60px 24px 80px;
}
.content-section {
margin-bottom: 56px;
}
.content-section:last-child {
margin-bottom: 0;
}
.section-label {
font-family: var(--body);
font-weight: 600;
font-size: 0.85rem;
letter-spacing: 0.15em;
color: var(--brass);
text-transform: uppercase;
margin-bottom: 12px;
}
.section-title {
font-family: var(--display);
font-weight: 900;
font-size: 2rem;
margin-bottom: 20px;
line-height: 1.2;
}
/* ── OVERVIEW ── */
.overview-text {
font-size: 1.1rem;
color: var(--text-secondary);
line-height: 1.8;
}
/* ── CHALLENGE ── */
.challenge-card {
background: var(--card-bg);
border-left: 4px solid var(--brass);
border-radius: 0 12px 12px 0;
padding: 28px 32px;
}
.challenge-card p {
color: var(--text-secondary);
font-size: 1.05rem;
line-height: 1.8;
}
/* ── SOLUTIONS ── */
.solutions-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 20px;
}
.solution-item {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px 24px;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
transition: border-color 0.2s;
}
.solution-item:hover {
border-color: rgba(232, 93, 38, 0.3);
}
.solution-check {
flex-shrink: 0;
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(232, 93, 38, 0.15);
display: flex;
align-items: center;
justify-content: center;
margin-top: 2px;
}
.solution-check svg {
width: 14px;
height: 14px;
stroke: var(--brass);
fill: none;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
.solution-item p {
color: var(--text-secondary);
font-size: 1rem;
line-height: 1.6;
}
/* ── TECH STACK ── */
.tech-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.tech-pill {
font-family: var(--mono);
font-size: 0.85rem;
padding: 8px 18px;
border-radius: 20px;
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text);
transition: border-color 0.2s, color 0.2s;
}
.tech-pill:hover {
border-color: var(--brass);
color: var(--brass);
}
/* ── FOOTER NAV ── */
.footer-nav {
max-width: 800px;
margin: 0 auto;
padding: 0 24px 80px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--card-border);
padding-top: 40px;
}
.footer-link {
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 1rem;
color: var(--text-secondary);
transition: color 0.2s;
}
.footer-link:hover { color: var(--brass); }
.footer-link--primary { color: var(--brass); }
.footer-link--primary:hover { color: var(--brass-hover); }
/* ── FOOTER ── */
footer {
border-top: 1px solid var(--card-border);
padding: 24px;
text-align: center;
}
footer p {
font-size: 0.85rem;
color: #555;
}
footer a {
color: var(--brass);
transition: color 0.2s;
}
footer a:hover { color: var(--brass-hover); }
/* ── RESPONSIVE ── */
@media (max-width: 640px) {
.hero-header {
flex-direction: column;
gap: 12px;
}
.hero-header h1 { font-size: 2.5rem; }
.hero-url { align-self: flex-start; }
.section-title { font-size: 1.6rem; }
.challenge-card { padding: 20px 24px; }
.solution-item { padding: 16px 20px; }
.nav-links { gap: 20px; }
.nav-links a { font-size: 0.85rem; }
.footer-nav { flex-direction: column; gap: 16px; }
}
</style>
</head>
<body>
<!-- Navigation -->
<nav class="nav">
<div class="nav-inner">
<a href="/" class="nav-logo">
<div class="nav-logo-circle">A</div>
<span class="nav-logo-text">AGENT ABRAMS</span>
</a>
<div class="nav-links">
<a href="/">Posts</a>
<a href="/about">About</a>
<a href="/projects" class="active">Projects</a>
<a href="/hire">Hire</a>
<a href="https://bsky.app/profile/agentabrams.bsky.social" target="_blank" rel="noopener">Bluesky</a>
</div>
</div>
</nav>
<!-- Hero -->
<section class="hero">
<a href="/projects" class="hero-back">← All Projects</a>
<div class="hero-header">
<h1>{project.name}</h1>
<a href={project.url} class="hero-url" target="_blank" rel="noopener">{project.url.replace('https://', '')}</a>
</div>
<p class="hero-tagline">{project.tagline}</p>
<div class="hero-screenshot" id="screenshot-container">
<img
src={project.screenshot}
alt={`${project.name} website`}
loading="eager"
onerror="document.getElementById('screenshot-container').classList.add('no-image'); this.remove();"
/>
</div>
</section>
<!-- Content -->
<div class="content">
<!-- Overview -->
<section class="content-section">
<p class="section-label">Overview</p>
<h2 class="section-title">About the Project</h2>
<p class="overview-text">{project.description}</p>
</section>
<!-- Challenge -->
<section class="content-section">
<p class="section-label">The Challenge</p>
<h2 class="section-title">What Made This Hard</h2>
<div class="challenge-card">
<p>{project.challenge}</p>
</div>
</section>
<!-- Solutions -->
<section class="content-section">
<p class="section-label">The Solution</p>
<h2 class="section-title">How I Solved It</h2>
<ul class="solutions-list">
{project.solutions.map((solution) => (
<li class="solution-item">
<div class="solution-check">
<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
<p>{solution}</p>
</li>
))}
</ul>
</section>
<!-- Tech Stack -->
<section class="content-section">
<p class="section-label">Tech Stack</p>
<h2 class="section-title">Built With</h2>
<div class="tech-list">
{project.techStack.map((tech) => (
<span class="tech-pill">{tech}</span>
))}
</div>
</section>
</div>
<!-- Footer Nav -->
<div class="footer-nav">
<a href="/projects" class="footer-link">← All Projects</a>
<a href={project.url} class="footer-link footer-link--primary" target="_blank" rel="noopener">Visit Live Site →</a>
</div>
<!-- Footer -->
<footer>
<p>Agent Abrams © {new Date().getFullYear()} — Built with <a href="https://astro.build">Astro</a> and too much coffee.</p>
</footer>
</body>
</html>