diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..97db373
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.next/
+.DS_Store
+.env*
diff --git a/Idea.tsx b/Idea.tsx
deleted file mode 100644
index 0875b25..0000000
--- a/Idea.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import ShareButtons from "./ShareButtons";
-
-export default function Idea({ idea, affiliateTag }: { idea: string; affiliateTag: string; }) {
- const q = encodeURIComponent(idea.replace(/[^a-zA-Z0-9\s]/g, ' ').trim());
- const url = `https://www.amazon.co.uk/s?k=${q}&tag=${affiliateTag||'yourtag-21'}`;
- return (
-
- );
-}
diff --git a/ShareButtons.tsx b/ShareButtons.tsx
deleted file mode 100644
index d678205..0000000
--- a/ShareButtons.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-'use client';
-export default function ShareButtons({ text, url }: { text: string; url: string }) {
- const shareText = encodeURIComponent(text);
- const shareUrl = encodeURIComponent(url);
- return (
-
- );
-}
diff --git a/aigiftgen_v101 2/README.md b/aigiftgen_v101 2/README.md
deleted file mode 100644
index 726f4dc..0000000
--- a/aigiftgen_v101 2/README.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# 🎅 AI Gift Generator (Next.js + Serverless)
-
-Deterministic £1 machine → scalable affiliate engine.
-
-## ✨ What it does
-- One-page app where users describe the gift recipient.
-- Serverless API asks **AI Santa** for 3–5 witty, specific gift ideas.
-- Each idea links to **Amazon UK** search with your **affiliate tag**.
-- Footer invites visitors to **clone the site** → virality + compounding.
-
-## 🧱 Stack
-- Next.js (App Router) + TailwindCSS
-- Vercel Serverless (API)
-- OpenAI (your key)
-- Optional: Vercel KV for analytics (not required)
-
-## 🚀 Deploy (Vercel, 5 minutes)
-1. Create a new GitHub repo and upload this folder.
-2. Go to **Vercel → New Project → Import** your repo.
-3. Add **Environment Variables**:
- - `OPENAI_API_KEY` = your OpenAI key
- - `NEXT_PUBLIC_AFFILIATE_TAG` = e.g. `yourtag-21`
-4. Deploy. Then **Domains → Add** `aigiftgen.co.uk` (or use the free `.vercel.app` domain).
-
-> If using the custom domain, point your registrar to Vercel’s DNS or add CNAME per Vercel instructions.
-
-## 🧪 Local dev
-```bash
-npm i
-npm run dev
-# open http://localhost:3000
-```
-
-## 🪄 Customise
-- Adjust the default input text in `app/page.tsx`.
-- Tweak prompt / model in `app/api/gifts/route.ts`.
-- Change styling in `app/globals.css`.
-
-## 🔗 Affiliate link format
-We link to Amazon UK search with your tag:
-```
-https://www.amazon.co.uk/s?k={QUERY}&tag=YOUR_TAG
-```
-
-## 🧬 Clone Network (optional)
-Keep the `/clone` page live so others can fork and deploy.
-When they do, your site gains distribution via social shares.
-
-## 🛡️ Safety / Compliance
-- Mark links as affiliate where appropriate.
-- Respect Amazon Associates Program Policies.
-- Add a simple privacy notice if you enable analytics.
-
----
-
-### Why this can guarantee £1 by Christmas
-- You control at least one user (you); a single qualifying purchase or bounty will surpass £1.
-- Minimal cost-to-deploy → near-zero risk.
-- Tiny push (share once) → high chance of 10+ outbound clicks.
-
-**Build fast. Iterate. Then seasonal clones = £££.**
-
-## 📜 Legal
-- See `/legal` for affiliate disclosure and privacy notice.
diff --git a/aigiftgen_v101 2/app/page.tsx b/aigiftgen_v101 2/app/page.tsx
deleted file mode 100644
index 6059797..0000000
--- a/aigiftgen_v101 2/app/page.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-'use client';
-import { useState } from "react";
-import Idea from "@/components/Idea";
-
-export default function Home() {
- const [who, setWho] = useState('my brother who loves hiking and bad puns');
- const [ideas, setIdeas] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const affiliateTag = process.env.NEXT_PUBLIC_AFFILIATE_TAG || 'yourtag-21';
-
- async function generate() {
- setLoading(true); setError(null);
- try {
- const res = await fetch('/api/gifts', {
- method: 'POST',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({ who })
- });
- if(!res.ok) throw new Error('Failed to fetch gift ideas');
- const data = await res.json();
- setIdeas(data.ideas || []);
- } catch (e:any) {
- setError(e.message);
- } finally {
- setLoading(false);
- }
- }
-
- return (
-
-
-
-
-
Who is the gift for?
-
setWho(e.target.value)}
- placeholder="e.g., my dad who grills in winter and collects jazz vinyl" />
-
-
-
- {error && Error: {error}
}
-
-
- {ideas.map((idea, i)=>(
-
- ))}
-
-
-
-
- );
-}
diff --git a/aigiftgen_v101 2/components/Idea.tsx b/aigiftgen_v101 2/components/Idea.tsx
deleted file mode 100644
index 0875b25..0000000
--- a/aigiftgen_v101 2/components/Idea.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import ShareButtons from "./ShareButtons";
-
-export default function Idea({ idea, affiliateTag }: { idea: string; affiliateTag: string; }) {
- const q = encodeURIComponent(idea.replace(/[^a-zA-Z0-9\s]/g, ' ').trim());
- const url = `https://www.amazon.co.uk/s?k=${q}&tag=${affiliateTag||'yourtag-21'}`;
- return (
-
- );
-}
diff --git a/aigiftgen_v101 2/components/ShareButtons.tsx b/aigiftgen_v101 2/components/ShareButtons.tsx
deleted file mode 100644
index d678205..0000000
--- a/aigiftgen_v101 2/components/ShareButtons.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-'use client';
-export default function ShareButtons({ text, url }: { text: string; url: string }) {
- const shareText = encodeURIComponent(text);
- const shareUrl = encodeURIComponent(url);
- return (
-
- );
-}
diff --git a/aigiftgen_v101 2/next-env.d.ts b/aigiftgen_v101 2/next-env.d.ts
deleted file mode 100644
index 4f11a03..0000000
--- a/aigiftgen_v101 2/next-env.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/aigiftgen_v101 2/next.config.js b/aigiftgen_v101 2/next.config.js
deleted file mode 100644
index 658404a..0000000
--- a/aigiftgen_v101 2/next.config.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/** @type {import('next').NextConfig} */
-const nextConfig = {};
-
-module.exports = nextConfig;
diff --git a/aigiftgen_v101 2/package.json b/aigiftgen_v101 2/package.json
deleted file mode 100644
index bfd027b..0000000
--- a/aigiftgen_v101 2/package.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "name": "ai-gift-generator",
- "version": "1.0.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint"
- },
- "dependencies": {
- "next": "14.2.5",
- "react": "18.2.0",
- "react-dom": "18.2.0",
- "openai": "^4.58.1",
- "zod": "^3.23.8"
- },
- "devDependencies": {
- "@types/node": "20.11.30",
- "@types/react": "18.2.66",
- "@types/react-dom": "18.2.22",
- "autoprefixer": "10.4.19",
- "eslint": "8.57.0",
- "eslint-config-next": "14.2.5",
- "postcss": "8.4.38",
- "tailwindcss": "3.4.10",
- "typescript": "5.4.5"
- }
-}
diff --git a/aigiftgen_v101 2/postcss.config.js b/aigiftgen_v101 2/postcss.config.js
deleted file mode 100644
index 33ad091..0000000
--- a/aigiftgen_v101 2/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-}
diff --git a/aigiftgen_v101 2/public/og.png b/aigiftgen_v101 2/public/og.png
deleted file mode 100644
index e69de29..0000000
diff --git a/aigiftgen_v101 2/tailwind.config.ts b/aigiftgen_v101 2/tailwind.config.ts
deleted file mode 100644
index 5c88f51..0000000
--- a/aigiftgen_v101 2/tailwind.config.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { Config } from "tailwindcss";
-
-const config: Config = {
- content: [
- "./app/**/*.{ts,tsx}",
- "./components/**/*.{ts,tsx}"
- ],
- theme: {
- extend: {},
- },
- plugins: [],
-};
-export default config;
diff --git a/aigiftgen_v101 2/tsconfig.json b/aigiftgen_v101 2/tsconfig.json
deleted file mode 100644
index 6ca3d3e..0000000
--- a/aigiftgen_v101 2/tsconfig.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "compilerOptions": {
- "target": "es2020",
- "lib": [
- "dom",
- "dom.iterable",
- "es2020"
- ],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "baseUrl": "."
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts"
- ],
- "exclude": [
- "node_modules"
- ]
-}
\ No newline at end of file
diff --git a/aigiftgen_v101 2/app/api/gifts/route.ts b/app/api/gifts/route.ts
similarity index 100%
rename from aigiftgen_v101 2/app/api/gifts/route.ts
rename to app/api/gifts/route.ts
diff --git a/aigiftgen_v101 2/app/clone/page.tsx b/app/clone/page.tsx
similarity index 100%
rename from aigiftgen_v101 2/app/clone/page.tsx
rename to app/clone/page.tsx
diff --git a/aigiftgen_v101 2/app/globals.css b/app/globals.css
similarity index 100%
rename from aigiftgen_v101 2/app/globals.css
rename to app/globals.css
diff --git a/aigiftgen_v101 2/app/layout.tsx b/app/layout.tsx
similarity index 100%
rename from aigiftgen_v101 2/app/layout.tsx
rename to app/layout.tsx
diff --git a/aigiftgen_v101 2/app/legal/page.tsx b/app/legal/page.tsx
similarity index 100%
rename from aigiftgen_v101 2/app/legal/page.tsx
rename to app/legal/page.tsx
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..b7c325d
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,87 @@
+'use client';
+
+import { useState } from "react";
+
+import Idea from "@/components/Idea";
+
+export default function Home() {
+ const [who, setWho] = useState("my brother who loves hiking and bad puns");
+ const [ideas, setIdeas] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const affiliateTag = process.env.NEXT_PUBLIC_AFFILIATE_TAG || "yourtag-21";
+
+ async function generate() {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const res = await fetch("/api/gifts", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ who }),
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to fetch gift ideas");
+ }
+
+ const data = await res.json();
+ setIdeas(data.ideas || []);
+ } catch (err: any) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+
Who is the gift for?
+
setWho(event.target.value)}
+ placeholder="e.g., my dad who grills in winter and collects jazz vinyl"
+ />
+
+
+
+ {error && Error: {error}
}
+
+
+ {ideas.map((idea, index) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/components/Idea.tsx b/components/Idea.tsx
new file mode 100644
index 0000000..07f47b3
--- /dev/null
+++ b/components/Idea.tsx
@@ -0,0 +1,33 @@
+import ShareButtons from "./ShareButtons";
+
+type IdeaProps = {
+ idea: string;
+ affiliateTag: string;
+};
+
+export default function Idea({ idea, affiliateTag }: IdeaProps) {
+ const sanitized = idea.replace(/[^a-zA-Z0-9\s]/g, " ").trim();
+ const query = encodeURIComponent(sanitized);
+ const tag = affiliateTag || "yourtag-21";
+ const url = `https://www.amazon.co.uk/s?k=${query}&tag=${tag}`;
+
+ return (
+
+ );
+}
diff --git a/components/ShareButtons.tsx b/components/ShareButtons.tsx
new file mode 100644
index 0000000..1514662
--- /dev/null
+++ b/components/ShareButtons.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+type ShareButtonsProps = {
+ text: string;
+ url: string;
+};
+
+export default function ShareButtons({ text, url }: ShareButtonsProps) {
+ const shareText = encodeURIComponent(text);
+ const shareUrl = encodeURIComponent(url);
+
+ return (
+
+ );
+}
diff --git a/globals.css b/globals.css
deleted file mode 100644
index 472ebe5..0000000
--- a/globals.css
+++ /dev/null
@@ -1,42 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- --bg: #0b1220;
- --card: #121a2b;
- --text: #e8eefc;
- --muted: #9bb0d3;
-}
-
-html, body, #__next {
- height: 100%;
-}
-
-body {
- background: radial-gradient(1200px 600px at 20% -10%, #14213d 0%, transparent 60%),
- radial-gradient(800px 400px at 80% 0%, #0b3b5e 0%, transparent 60%),
- var(--bg);
- color: var(--text);
-}
-.btn {
- @apply px-4 py-2 rounded-2xl shadow-md hover:shadow-lg transition font-medium;
-}
-.btn-primary {
- @apply bg-blue-600 hover:bg-blue-500;
-}
-.btn-ghost {
- @apply bg-white/5 hover:bg-white/10;
-}
-.card {
- @apply bg-[var(--card)]/80 backdrop-blur-md border border-white/10 rounded-2xl shadow-lg;
-}
-.input {
- @apply w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 outline-none focus:border-blue-500;
-}
-.link {
- @apply underline decoration-dotted underline-offset-4 hover:text-blue-300;
-}
-.small {
- @apply text-sm text-[var(--muted)];
-}
diff --git a/layout.tsx b/layout.tsx
deleted file mode 100644
index 903ee64..0000000
--- a/layout.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import "./globals.css";
-import type { Metadata } from "next";
-
-export const metadata: Metadata = {
- title: "AI Gift Generator – Find the perfect gift (with AI Santa)",
- description: "Type who you're shopping for. AI Santa suggests witty, spot-on gifts with direct links.",
- openGraph: {
- title: "AI Gift Generator – Find the perfect gift (with AI Santa)",
- description: "Type who you're shopping for. AI Santa suggests witty, spot-on gifts with direct links.",
- url: "https://aigiftgen.co.uk",
- siteName: "AI Gift Generator",
- images: [{ url: "/og.png", width: 1200, height: 630 }],
- locale: "en_GB",
- type: "website",
- },
- twitter: {
- card: "summary_large_image",
- title: "AI Gift Generator – Find the perfect gift (with AI Santa)",
- description: "Type who you're shopping for. AI Santa suggests witty, spot-on gifts with direct links.",
- images: ["/og.png"]
- }
-};
-
-export default function RootLayout({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/page.tsx b/page.tsx
deleted file mode 100644
index 7f962ae..0000000
--- a/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-export default function Legal() {
- return (
-
- Legal & Privacy
-
- Affiliate Disclosure
- Some links are affiliate links. As an Amazon Associate, this site earns from qualifying purchases.
-
-
- Privacy
- We do not collect personal data. If analytics are enabled in the future, we will update this page.
-
-
- );
-}
diff --git a/route.ts b/route.ts
deleted file mode 100644
index 6bc1b66..0000000
--- a/route.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-import { z } from "zod";
-import OpenAI from "openai";
-
-const schema = z.object({
- who: z.string().min(3).max(300)
-});
-
-// Use the official OpenAI SDK; expect OPENAI_API_KEY to be provided in Vercel env
-const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
-function extractJsonArray(text: string): string[] {
- // Try direct parse
- try {
- const maybe = JSON.parse(text);
- if (Array.isArray(maybe)) return maybe;
- } catch {}
- // Try to find the first [...] block
- const m = text.match(/\[[\s\S]*\]/);
- if (m) {
- try {
- const maybe = JSON.parse(m[0]);
- if (Array.isArray(maybe)) return maybe;
- } catch {}
- }
- // Fallback: line split
- return text.split(/\n|\r/).map(s => s.trim()).filter(Boolean).slice(0,5);
-}
-
-
-export async function POST(req: NextRequest) {
- try {
- const body = await req.json();
- const { who } = schema.parse(body);
-
- // Prompt engineering: humorous + useful hybrid
- const sys = `You are 'AI Santa'—a witty but helpful gift recommender.
-Return ONLY a JSON array of 3-5 concise gift ideas. Each idea should be 8-20 words,
-specific enough to search on Amazon UK. Avoid brand names unless iconic. No extra commentary.`;
-
- const user = `Who: ${who}
-Constraints:
-- Mix fun + practicality
-- Vary price points
-- Include at least one small stocking stuffer
-Output: JSON array of strings only.`;
-
- // Use Responses API (compatible with gpt-4.1/gpt-4o/gpt-5 when available)
- const resp = await client.chat.completions.create({
- model: "gpt-4o-mini",
- messages: [
- { role: "system", content: sys },
- { role: "user", content: user }
- ],
- temperature: 0.8,
- max_tokens: 300
- });
-
- const text = resp.choices?.[0]?.message?.content || "[]";
- // Best-effort JSON parse
- let ideas: string[] = [];
- try {
- ideas = JSON.parse(text);
- if (!Array.isArray(ideas)) ideas = [];
- } catch {
- // fallback: split lines if LLM didn't return JSON
- ideas = text.split(/\n|\r/).filter(Boolean).slice(0,5);
- }
-
- // Basic sanitization & cap
- ideas = ideas.map(s => String(s).trim()).filter(Boolean).slice(0,5);
- if (ideas.length < 3) {
- // Force a minimal list if the model misbehaves
- ideas = [
- "Insulated hiking mug with clip carabiner",
- "Waterproof phone pouch for trail photos",
- "Compact headlamp with rechargeable battery"
- ];
- }
-
- // TODO: optional: log to KV (requires Vercel KV or Upstash Redis)
- // Skipped by default for zero-config deploy
-
- return NextResponse.json({ ideas });
- } catch (err: any) {
- return NextResponse.json({ error: err.message || "Bad request" }, { status: 400 });
- }
-}
diff --git a/tsconfig.json b/tsconfig.json
index 6ca3d3e..acad97f 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -22,7 +22,10 @@
"name": "next"
}
],
- "baseUrl": "."
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./*"]
+ }
},
"include": [
"next-env.d.ts",