first commit

This commit is contained in:
Matt Kane
2026-04-01 10:44:22 +01:00
commit 43fcb9a131
1789 changed files with 395041 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
---
import Base from "../layouts/Base.astro";
---
<Base title="Page not found">
<div class="not-found">
<div class="not-found-code">404</div>
<h1>Page not found</h1>
<p>The page you're looking for doesn't exist or has been moved.</p>
<div class="not-found-actions">
<a href="/" class="btn btn-primary btn-lg">Go home</a>
<a href="/contact" class="btn btn-secondary btn-lg">Contact us</a>
</div>
</div>
</Base>
<style>
.not-found {
text-align: center;
padding: var(--spacing-5xl) var(--spacing-lg);
max-width: var(--max-width);
margin: 0 auto;
}
.not-found-code {
font-size: 10rem;
font-weight: 800;
line-height: 1;
letter-spacing: -0.05em;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
opacity: 0.3;
margin-bottom: var(--spacing-lg);
}
.not-found h1 {
font-size: var(--font-size-3xl);
margin-bottom: var(--spacing-md);
}
.not-found p {
font-size: var(--font-size-lg);
color: var(--color-muted);
margin-bottom: var(--spacing-2xl);
}
.not-found-actions {
display: flex;
gap: var(--spacing-md);
justify-content: center;
flex-wrap: wrap;
}
@media (max-width: 480px) {
.not-found-code {
font-size: 6rem;
}
.not-found-actions {
flex-direction: column;
}
.not-found-actions .btn {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,357 @@
---
import { getEmDashEntry } from "emdash";
import Base from "../layouts/Base.astro";
import MarketingBlocks from "../components/MarketingBlocks.astro";
const { entry: page, cacheHint } = await getEmDashEntry("pages", "contact");
try {
Astro.cache.set(cacheHint);
} catch {}
// Handle form submission
// NOTE: This is demo code. For production, add:
// - CSRF token validation
// - Rate limiting (e.g., via Cloudflare or middleware)
// - Actual email sending or webhook integration
let formStatus: "idle" | "success" | "error" = "idle";
let formMessage = "";
if (Astro.request.method === "POST") {
try {
const formData = await Astro.request.formData();
const name = formData.get("name")?.toString() || "";
const email = formData.get("email")?.toString() || "";
const company = formData.get("company")?.toString() || "";
const message = formData.get("message")?.toString() || "";
if (!name || !email || !message) {
formStatus = "error";
formMessage = "Please fill in all required fields.";
} else if (!email.includes("@")) {
formStatus = "error";
formMessage = "Please enter a valid email address.";
} else {
// TODO: Replace with actual email/webhook integration
console.log("Contact form submission:", {
name,
email,
company,
message,
});
formStatus = "success";
formMessage =
"Thanks for reaching out! We'll get back to you within 24 hours.";
}
} catch {
formStatus = "error";
formMessage = "Something went wrong. Please try again.";
}
}
const pageContent = page?.data.content;
---
<Base
title="Contact"
description="Have questions? Want a demo? We'd love to hear from you."
>
{pageContent && <MarketingBlocks value={pageContent} />}
<section class="contact-form-section section">
<div class="container">
<div class="contact-grid">
<div class="contact-info">
<h2>Talk to our team</h2>
<p>Fill out the form and we'll be in touch within 24 hours.</p>
<div class="contact-methods">
<div class="contact-method">
<div class="contact-icon">
<i class="ph ph-envelope" aria-hidden="true"></i>
</div>
<div class="contact-method-content">
<h4>Email</h4>
<a href="mailto:hello@acme.example">hello@acme.example</a>
</div>
</div>
<div class="contact-method">
<div class="contact-icon">
<i class="ph ph-lifebuoy" aria-hidden="true"></i>
</div>
<div class="contact-method-content">
<h4>Support</h4>
<a href="mailto:support@acme.example">support@acme.example</a>
</div>
</div>
<div class="contact-method">
<div class="contact-icon">
<i class="ph ph-currency-dollar" aria-hidden="true"></i>
</div>
<div class="contact-method-content">
<h4>Sales</h4>
<a href="mailto:sales@acme.example">sales@acme.example</a>
</div>
</div>
</div>
</div>
<div class="contact-form-wrapper">
{
formStatus === "success" ? (
<div class="form-success">
<div class="success-icon">✓</div>
<h3>Message Sent!</h3>
<p>{formMessage}</p>
<a href="/contact" class="btn btn-secondary">
Send another message
</a>
</div>
) : (
<form method="POST" class="contact-form">
{formStatus === "error" && (
<div class="form-error">
<p>{formMessage}</p>
</div>
)}
<div class="form-row">
<div class="form-field">
<label for="name">Name *</label>
<input
type="text"
id="name"
name="name"
required
placeholder="Your name"
autocomplete="name"
/>
</div>
<div class="form-field">
<label for="email">Email *</label>
<input
type="email"
id="email"
name="email"
required
placeholder="you@company.com"
autocomplete="email"
/>
</div>
</div>
<div class="form-field">
<label for="company">Company</label>
<input
type="text"
id="company"
name="company"
placeholder="Your company"
autocomplete="organization"
/>
</div>
<div class="form-field">
<label for="message">Message *</label>
<textarea
id="message"
name="message"
required
rows="5"
placeholder="Tell us about your project or question..."
/>
</div>
<button type="submit" class="btn btn-primary btn-lg">
Send Message
</button>
</form>
)
}
</div>
</div>
</div>
</section>
</Base>
<style>
.contact-form-section {
padding-bottom: var(--spacing-5xl);
}
.contact-grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: var(--spacing-4xl);
align-items: start;
}
.contact-info h2 {
font-size: var(--font-size-2xl);
margin-bottom: var(--spacing-md);
}
.contact-info > p {
color: var(--color-muted);
margin-bottom: var(--spacing-2xl);
}
.contact-methods {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.contact-method {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.contact-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: white;
background: linear-gradient(
135deg,
var(--color-primary),
var(--color-accent)
);
border-radius: var(--radius);
flex-shrink: 0;
}
.contact-method-content h4 {
font-size: var(--font-size-sm);
font-weight: 600;
margin-bottom: var(--spacing-xs);
}
.contact-method-content a {
color: var(--color-primary);
font-size: var(--font-size-lg);
}
.contact-method-content a:hover {
text-decoration: underline;
}
.contact-form-wrapper {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--spacing-2xl);
}
.contact-form {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
}
.form-field {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.form-field label {
font-size: var(--font-size-sm);
font-weight: 500;
}
.form-field input,
.form-field textarea {
padding: var(--spacing-md);
font-family: inherit;
font-size: var(--font-size-base);
color: var(--color-text);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast);
}
.form-field input:focus,
.form-field textarea:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.form-field input::placeholder,
.form-field textarea::placeholder {
color: var(--color-muted);
}
.form-field textarea {
resize: vertical;
min-height: 120px;
}
.form-error {
padding: var(--spacing-md);
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: var(--radius-sm);
color: #dc2626;
font-size: var(--font-size-sm);
}
@media (prefers-color-scheme: dark) {
.form-error {
color: #f87171;
}
}
.form-success {
text-align: center;
padding: var(--spacing-2xl);
}
.success-icon {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
color: white;
background: var(--color-success);
border-radius: var(--radius-full);
margin: 0 auto var(--spacing-lg);
}
.form-success h3 {
font-size: var(--font-size-xl);
margin-bottom: var(--spacing-sm);
}
.form-success p {
color: var(--color-muted);
margin-bottom: var(--spacing-xl);
}
@media (max-width: 768px) {
.contact-grid {
grid-template-columns: 1fr;
gap: var(--spacing-2xl);
}
.form-row {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,51 @@
---
import { getEmDashEntry } from "emdash";
import Base from "../layouts/Base.astro";
import MarketingBlocks from "../components/MarketingBlocks.astro";
const { entry: page, cacheHint } = await getEmDashEntry("pages", "home");
try {
Astro.cache.set(cacheHint);
} catch {}
const pageTitle = page?.data.title;
const pageContent = page?.data.content;
---
<Base
title={pageTitle !== "Home" ? pageTitle : undefined}
description="Build products people actually want. The all-in-one platform for modern teams."
>
{
pageContent ? (
<MarketingBlocks value={pageContent} />
) : (
<div class="empty-state">
<h1>Welcome to Acme</h1>
<p>Edit the home page content in the admin to get started.</p>
<a href="/_emdash/admin" class="btn btn-primary">
Open Admin
</a>
</div>
)
}
</Base>
<style>
.empty-state {
text-align: center;
padding: var(--spacing-5xl) var(--spacing-lg);
max-width: var(--max-width);
margin: 0 auto;
}
.empty-state h1 {
margin-bottom: var(--spacing-md);
}
.empty-state p {
color: var(--color-muted);
margin-bottom: var(--spacing-xl);
}
</style>

View File

@@ -0,0 +1,50 @@
---
import { getEmDashEntry } from "emdash";
import Base from "../layouts/Base.astro";
import MarketingBlocks from "../components/MarketingBlocks.astro";
const { entry: page, cacheHint } = await getEmDashEntry("pages", "pricing");
try {
Astro.cache.set(cacheHint);
} catch {}
const pageContent = page?.data.content;
---
<Base
title="Pricing"
description="Simple, transparent pricing. No hidden fees. No surprises."
>
{
pageContent ? (
<MarketingBlocks value={pageContent} />
) : (
<div class="empty-state">
<h1>Pricing</h1>
<p>Add pricing content in the admin.</p>
<a href="/_emdash/admin" class="btn btn-primary">
Open Admin
</a>
</div>
)
}
</Base>
<style>
.empty-state {
text-align: center;
padding: var(--spacing-5xl) var(--spacing-lg);
max-width: var(--max-width);
margin: 0 auto;
}
.empty-state h1 {
margin-bottom: var(--spacing-md);
}
.empty-state p {
color: var(--color-muted);
margin-bottom: var(--spacing-xl);
}
</style>