Add shared building-block components: header, footer, CTAs, topographic background
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
47
src/components/CTAButtons.astro
Normal file
47
src/components/CTAButtons.astro
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import { getAuthConfig } from '../config/auth';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
variant?: 'primary' | 'inverted';
|
||||||
|
size?: 'md' | 'lg';
|
||||||
|
showSecondary?: boolean;
|
||||||
|
primaryLabel?: string;
|
||||||
|
secondaryLabel?: string;
|
||||||
|
secondaryHref?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = getAuthConfig();
|
||||||
|
|
||||||
|
const {
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
showSecondary = true,
|
||||||
|
primaryLabel = 'Start free trial',
|
||||||
|
secondaryLabel = 'Sign in',
|
||||||
|
secondaryHref = auth.signInUrl,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const padY = size === 'lg' ? 'py-3' : 'py-2';
|
||||||
|
const padX = size === 'lg' ? 'px-6' : 'px-5';
|
||||||
|
const fontSize = size === 'lg' ? 'text-base' : 'text-sm';
|
||||||
|
---
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<a
|
||||||
|
href={auth.signUpUrl}
|
||||||
|
class={`inline-flex items-center justify-center rounded ${padX} ${padY} ${fontSize} font-semibold transition-colors
|
||||||
|
${variant === 'primary'
|
||||||
|
? 'bg-accent text-bg hover:bg-accent-muted focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg'
|
||||||
|
: 'bg-bg text-accent border border-accent hover:bg-accent hover:text-bg'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{primaryLabel}
|
||||||
|
</a>
|
||||||
|
{showSecondary && (
|
||||||
|
<a
|
||||||
|
href={secondaryHref}
|
||||||
|
class={`inline-flex items-center justify-center rounded ${padX} ${padY} ${fontSize} font-medium text-text border border-border-strong hover:border-accent hover:text-accent transition-colors`}
|
||||||
|
>
|
||||||
|
{secondaryLabel}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
23
src/components/SiteFooter.astro
Normal file
23
src/components/SiteFooter.astro
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
---
|
||||||
|
<footer class="border-t border-border mt-24">
|
||||||
|
<div class="max-w-content mx-auto px-6 py-12 flex flex-col md:flex-row md:items-center md:justify-between gap-8">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 32 32" aria-hidden="true">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#0c111a"/>
|
||||||
|
<g fill="none" stroke="#f0b429" stroke-width="1.6" stroke-linecap="round">
|
||||||
|
<path d="M4 10 Q10 6 16 12 T28 10"/>
|
||||||
|
<path d="M4 16 Q10 12 16 18 T28 16"/>
|
||||||
|
<path d="M4 22 Q10 18 16 24 T28 22"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span class="text-text-muted text-sm">© {year} Cameleer</span>
|
||||||
|
</div>
|
||||||
|
<nav class="flex items-center gap-8 text-sm text-text-muted">
|
||||||
|
<a href="/pricing" class="hover:text-text transition-colors">Pricing</a>
|
||||||
|
<a href="/imprint" class="hover:text-text transition-colors">Imprint</a>
|
||||||
|
<a href="/privacy" class="hover:text-text transition-colors">Privacy</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
22
src/components/SiteHeader.astro
Normal file
22
src/components/SiteHeader.astro
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
import CTAButtons from './CTAButtons.astro';
|
||||||
|
---
|
||||||
|
<header class="sticky top-0 z-40 backdrop-blur-md bg-bg/80 border-b border-border">
|
||||||
|
<div class="max-w-content mx-auto px-6 h-16 flex items-center justify-between gap-6">
|
||||||
|
<a href="/" class="flex items-center gap-2 group" aria-label="Cameleer home">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 32 32" aria-hidden="true">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#0c111a"/>
|
||||||
|
<g fill="none" stroke="#f0b429" stroke-width="1.6" stroke-linecap="round">
|
||||||
|
<path d="M4 10 Q10 6 16 12 T28 10"/>
|
||||||
|
<path d="M4 16 Q10 12 16 18 T28 16"/>
|
||||||
|
<path d="M4 22 Q10 18 16 24 T28 22"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span class="font-sans font-bold text-lg tracking-tight text-text group-hover:text-accent transition-colors">Cameleer</span>
|
||||||
|
</a>
|
||||||
|
<nav class="flex items-center gap-8 text-sm">
|
||||||
|
<a href="/pricing" class="text-text-muted hover:text-text transition-colors">Pricing</a>
|
||||||
|
</nav>
|
||||||
|
<CTAButtons size="md" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
26
src/components/TopographicBg.astro
Normal file
26
src/components/TopographicBg.astro
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
opacity?: number;
|
||||||
|
lines?: number;
|
||||||
|
}
|
||||||
|
const { opacity = 0.12, lines = 8 } = Astro.props;
|
||||||
|
|
||||||
|
const paths: string[] = [];
|
||||||
|
const stepY = 100 / (lines + 1);
|
||||||
|
for (let i = 1; i <= lines; i++) {
|
||||||
|
const y = i * stepY;
|
||||||
|
const amp = 4 + (i % 3) * 2;
|
||||||
|
paths.push(`M0,${y} Q25,${y - amp} 50,${y + amp * 0.6} T100,${y}`);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
<svg
|
||||||
|
class="absolute inset-0 w-full h-full pointer-events-none"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={`opacity:${opacity}`}
|
||||||
|
>
|
||||||
|
<g fill="none" stroke="#f0b429" stroke-width="0.15" vector-effect="non-scaling-stroke">
|
||||||
|
{paths.map((d) => <path d={d} />)}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
Reference in New Issue
Block a user