Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import ContractSection from "./components/ContractSection.vue";
import EducationSection from "./components/EducationSection.vue";
import ExperienceSection from "./components/ExperienceSection.vue";
import HeroSection from "./components/HeroSection.vue";
import LegalView from "./components/LegalView.vue";
import SiteFooter from "./components/SiteFooter.vue";
import StackSection from "./components/StackSection.vue";
import ThemeToggle from "./components/ThemeToggle.vue";
import { legalDocs } from "./legal";

// Path-based rendering without a router. The worker serves index.html for
// every path (single-page-application fallback), so /privacy, /terms and
// /refunds resolve here
const path = window.location.pathname.replace(/\/+$/, "");
const legalDoc = legalDocs.find((doc) => `/${doc.slug}` === path);
</script>

<template>
Expand All @@ -19,8 +27,8 @@ import ThemeToggle from "./components/ThemeToggle.vue";

<header class="border-line/60 bg-bg/80 sticky top-0 z-10 border-b backdrop-blur-md">
<div class="mx-auto flex h-16 max-w-5xl items-center justify-between px-6">
<a href="#" class="font-mono text-sm font-semibold"> <span class="text-accent">~/</span>gmiles.dev </a>
<nav class="text-soft hidden items-center gap-6 font-mono text-xs md:flex">
<a href="/" class="font-mono text-sm font-semibold"> <span class="text-accent">~/</span>gmiles.dev </a>
<nav v-if="!legalDoc" class="text-soft hidden items-center gap-6 font-mono text-xs md:flex">
<a href="#stack" class="hover:text-accent transition-colors">stack</a>
<a href="#experience" class="hover:text-accent transition-colors">experience</a>
<a href="#education" class="hover:text-accent transition-colors">education</a>
Expand Down Expand Up @@ -51,7 +59,10 @@ import ThemeToggle from "./components/ThemeToggle.vue";
</div>
</header>

<main class="mx-auto max-w-5xl space-y-32 px-6 pb-8">
<main v-if="legalDoc" class="mx-auto max-w-5xl px-6 pb-8">
<LegalView :doc="legalDoc" />
</main>
<main v-else class="mx-auto max-w-5xl space-y-32 px-6 pb-8">
<HeroSection />
<StackSection />
<ExperienceSection />
Expand Down
7 changes: 6 additions & 1 deletion src/components/ContractSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ import SectionLabel from "./SectionLabel.vue";
</li>
<li class="flex gap-3">
<span class="text-accent font-mono" aria-hidden="true">02</span>
{{ contracting.consultation }}
<span>
{{ contracting.consultation }}
<a href="/refunds" class="text-accent underline underline-offset-4 transition-opacity hover:opacity-80">
Refund policy
</a>
</span>
</li>
<li class="flex gap-3">
<span class="text-accent font-mono" aria-hidden="true">03</span>
Expand Down
48 changes: 48 additions & 0 deletions src/components/LegalView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { onMounted } from "vue";
import type { LegalDoc } from "@/legal";

const props = defineProps<{ doc: LegalDoc }>();

onMounted(() => {
document.title = `${props.doc.title} · George Miles`;
});
</script>

<template>
<article class="mx-auto max-w-3xl">
<a
href="/"
class="text-soft hover:text-accent flex w-fit items-center gap-2 pt-12 font-mono text-sm transition-colors"
>
<svg
class="size-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M19 12H5m7 7-7-7 7-7" />
</svg>
back to gmiles.dev
</a>

<p class="text-accent mt-10 font-mono text-sm">
~/gmiles.dev <span class="text-soft">$ cat {{ doc.slug }}.md</span>
</p>
<h1 class="font-display mt-3 text-4xl font-extrabold tracking-tight sm:text-5xl">{{ doc.title }}</h1>
<p class="text-soft mt-3 font-mono text-xs">last updated: {{ doc.updated }}</p>

<div class="mt-12 space-y-10">
<section v-for="section in doc.sections" :key="section.heading">
<h2 class="font-display text-xl font-bold">{{ section.heading }}</h2>
<p v-for="paragraph in section.paragraphs" :key="paragraph" class="text-soft mt-3 leading-relaxed">
{{ paragraph }}
</p>
</section>
</div>
</article>
</template>
5 changes: 5 additions & 0 deletions src/components/SiteFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import { profile } from "@/data";
class="text-soft mt-10 flex flex-col gap-3 font-mono text-xs sm:flex-row sm:items-center sm:justify-between"
>
<p>© {{ new Date().getFullYear() }} George Miles</p>
<nav class="flex items-center gap-4">
<a href="/privacy" class="hover:text-accent transition-colors">privacy</a>
<a href="/terms" class="hover:text-accent transition-colors">terms</a>
<a href="/refunds" class="hover:text-accent transition-colors">refunds</a>
</nav>
<a :href="profile.linkedin" target="_blank" rel="noopener" class="hover:text-accent transition-colors">
linkedin.com/in/gtmmiles
</a>
Expand Down
159 changes: 159 additions & 0 deletions src/legal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
export interface LegalSection {
heading: string;
paragraphs: string[];
}

export interface LegalDoc {
slug: string;
title: string;
updated: string;
sections: LegalSection[];
}

export const legalDocs: LegalDoc[] = [
{
slug: "privacy",
title: "Privacy Policy",
updated: "14 July 2026",
sections: [
{
heading: "Overview",
paragraphs: [
"gmiles.dev is the personal portfolio and contracting site of George Miles, a sole trader based in England. This site is built to collect as little about you as possible.",
],
},
{
heading: "What this site collects",
paragraphs: [
"No accounts, no cookies, no advertising and no tracking. Your theme preference (light or dark) is stored in your browser's local storage and never leaves your device.",
"The site is served by Cloudflare, which may process technical data such as IP addresses transiently to deliver the site and protect it from abuse.",
],
},
{
heading: "What you send me",
paragraphs: [
"If you email me, I receive your email address and whatever you include in the message.",
"If you book a consultation, the booking is handled by Morgen and I receive the details you provide, such as your name, email address and project notes.",
"If you pay a consultation fee or invoice, payment is processed by Stripe. Your card details go to Stripe directly and never reach me. I receive confirmation of payment and basic billing details.",
],
},
{
heading: "How I use it",
paragraphs: [
"To respond to you, to scope and deliver work, and to keep the records a business is required to keep, such as contracts and invoices. I do not sell your data or add you to marketing lists.",
],
},
{
heading: "Third-party services",
paragraphs: [
"Cloudflare hosts the site, Morgen handles bookings and Stripe handles payments. Each processes your data under its own privacy policy.",
],
},
{
heading: "Retention",
paragraphs: [
"Business records such as contracts and invoices are kept for as long as UK law requires, which is typically six years. Everything else is deleted when it is no longer needed.",
],
},
{
heading: "Your rights",
paragraphs: [
"Under UK GDPR you can ask what I hold about you, ask for corrections, or ask for deletion where the law allows. Email hi@gmiles.dev and I will sort it. You can also complain to the ICO at ico.org.uk.",
],
},
{
heading: "Changes",
paragraphs: ["Any changes to this policy will be posted on this page with an updated date."],
},
],
},
{
slug: "terms",
title: "Terms of Service",
updated: "14 July 2026",
sections: [
{
heading: "About",
paragraphs: [
"This site and the services described on it are provided by George Miles, a sole trader based in England. By using the site or booking a consultation you agree to these terms.",
],
},
{
heading: "Use of the site",
paragraphs: [
"The content on this site is provided for general information. It is provided as is, without warranties of accuracy or availability, and may change at any time.",
],
},
{
heading: "Consultations",
paragraphs: [
"Consultations are booked through the booking calendar and carry a fee of £150, which confirms your slot. The fee is credited in full toward your project if we proceed, and refunded if we do not. Full details are in the refund policy.",
"You can reschedule free of charge with at least 24 hours notice.",
],
},
{
heading: "Engagements",
paragraphs: [
"All project work is subject to contract. Work is fully scoped before it starts, quoted at a fixed price for the agreed scope, and booked in with a 50% deposit. The remaining balance is due when the work is signed off.",
"These are headline terms. The signed agreement for each engagement is the authoritative document and prevails over this page.",
],
},
{
heading: "Intellectual property",
paragraphs: [
"The content and design of this site belong to George Miles. Ownership of project deliverables is set out in each engagement's contract, and typically transfers to the client on final payment.",
],
},
{
heading: "Liability",
paragraphs: [
"To the extent permitted by law, I accept no liability arising from use of this site. Liability relating to contracted work is governed by the signed agreement for that engagement.",
],
},
{
heading: "Governing law",
paragraphs: ["These terms are governed by the laws of England and Wales."],
},
{
heading: "Contact",
paragraphs: ["Questions about these terms: hi@gmiles.dev"],
},
],
},
{
slug: "refunds",
title: "Refund Policy",
updated: "14 July 2026",
sections: [
{
heading: "Consultation fees",
paragraphs: [
"The £150 consultation fee confirms your booking and is never lost to a genuine enquiry. If we go ahead with your project, the full £150 is credited against your first invoice. If either of us decides not to proceed, it is refunded in full.",
"You can reschedule free of charge with at least 24 hours notice. If you cancel with less than 24 hours notice or do not attend, the fee may be forfeited.",
],
},
{
heading: "Project deposits",
paragraphs: [
"The 50% deposit books your project in. If you cancel before work has started, the deposit is refunded in full. Once work has started, refunds are governed by the signed agreement and typically account for work already completed.",
],
},
{
heading: "Final balance",
paragraphs: [
"The remaining balance is due when the work is signed off against the acceptance criteria agreed in the contract.",
],
},
{
heading: "How refunds are processed",
paragraphs: [
"Refunds are issued through Stripe to the original payment method, and typically arrive within 5 to 10 business days.",
],
},
{
heading: "Questions",
paragraphs: ["If anything here is unclear, email hi@gmiles.dev before booking."],
},
],
},
];