diff --git a/README.md b/README.md
index e40afdbb..bf307ff2 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,12 @@ Here are some useful links for getting started with Wix Headless and the availab
Wix Rentals
+
+ Code Before AI (Wix Events)
+ A Windows XP desktop as an events site — lists Wix Events and takes RSVPs through an elevated backend endpoint.
+ Live demo
+ Wix Events
+
Next.js
diff --git a/astro/before-ai-v2/.gitignore b/astro/before-ai-v2/.gitignore
new file mode 100644
index 00000000..dd0f070e
--- /dev/null
+++ b/astro/before-ai-v2/.gitignore
@@ -0,0 +1,28 @@
+# wix integration
+.wix/
+
+# build output
+dist/
+
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# environment variables
+.env
+.env.production
+.env.local
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/astro/before-ai-v2/README.md b/astro/before-ai-v2/README.md
new file mode 100644
index 00000000..7ce460b1
--- /dev/null
+++ b/astro/before-ai-v2/README.md
@@ -0,0 +1,50 @@
+# Code Before AI — Wix Events template
+
+A Windows XP desktop that happens to be an events site: old programmers tell
+stories about writing code before AI, and you RSVP through an XP-style wizard.
+
+**[Live demo](https://before-ai-v-2-tuvitk-0d06.wix-site-host.com)**
+
+It demonstrates:
+
+- A full desktop metaphor — draggable-feeling windows, a Start menu, a taskbar
+ and balloon tips — as **one** React island (`client:load`), server-rendered
+ and then hydrated, so the event listing is in the HTML for crawlers
+- Listing upcoming events as a plain **visitor-scoped** read
+ (`wixEventsV2.queryEvents`) — no client, no API key: the `@wix/astro`
+ integration authenticates module-level SDK imports for you
+- Two Astro endpoints, `GET /api/events` and `POST /api/rsvp`, whose handler
+ bodies are portable Web `Request`/`Response` code
+- A **privileged write** behind an endpoint: `auth.elevate(rsvpV2.createRsvp)`,
+ which only ever runs server-side
+
+The Wix integration lives entirely in `src/lib/wix-events.ts`; the UI is
+`src/components/XpDesktop.tsx`.
+
+## Wix apps required
+
+| App | Used for |
+|---|---|
+| Wix Events | the RSVP events and the RSVPs themselves |
+
+## Notes
+
+- **Events must be `RSVP`, not `TICKETING`.** `registration.initialType` is
+ immutable after create, and this template's write path is `createRsvp`.
+- **Event dates must be in the future** or the listing filters them out
+ (`status` is queried as `UPCOMING`/`STARTED`).
+- A site with no upcoming events renders an empty state. The template never
+ substitutes sample events — what you see is what Wix returns.
+- Each card's icon comes from a fixed decorative emoji list; Wix Events has no
+ per-event icon, and an event's `mainImage` is not rendered by this design.
+
+## Getting started
+
+Use this template with the [Wix CLI for Headless quick start](https://dev.wix.com/docs/go-headless/get-started/quick-starts/wix-managed-headless/quick-start-with-the-wix-cli), then:
+
+```bash
+npm install
+npm run dev
+```
+
+`npm run build` produces the Wix-hosted server bundle.
diff --git a/astro/before-ai-v2/astro.config.mjs b/astro/before-ai-v2/astro.config.mjs
new file mode 100644
index 00000000..e41d8605
--- /dev/null
+++ b/astro/before-ai-v2/astro.config.mjs
@@ -0,0 +1,23 @@
+// @ts-check
+import { defineConfig } from "astro/config";
+import wix from "@wix/astro";
+import wixPages from "@wix/astro-pages";
+import wixHostingAdapter from "@wix/astro-wix-hosting-adapter";
+import react from "@astrojs/react";
+import tailwindcss from "@tailwindcss/vite";
+
+// https://astro.build/config
+export default defineConfig({
+ // Every route renders per request: the event listing is a live read, so
+ // there is nothing to prerender.
+ output: "server",
+ adapter: wixHostingAdapter(),
+ integrations: [wix(), wixPages(), react()],
+ security: { checkOrigin: false },
+
+ vite: { plugins: [tailwindcss()] },
+
+ image: {
+ domains: ["static.wixstatic.com"],
+ },
+});
diff --git a/astro/before-ai-v2/package.json b/astro/before-ai-v2/package.json
new file mode 100644
index 00000000..0fe70dd2
--- /dev/null
+++ b/astro/before-ai-v2/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "wix-astro-before-ai-v2",
+ "type": "module",
+ "version": "0.0.1",
+ "scripts": {
+ "astro": "astro",
+ "dev": "astro dev",
+ "build": "astro build"
+ },
+ "dependencies": {
+ "@astrojs/react": "^4.3.0",
+ "@tailwindcss/vite": "^4.2.1",
+ "@wix/astro": "^2.39.0",
+ "@wix/astro-pages": "^2.0.4",
+ "@wix/astro-wix-hosting-adapter": "^2.0.0",
+ "@wix/essentials": "^1.0.6",
+ "@wix/events": "^1.0.803",
+ "@wix/sdk": "^1.21.13",
+ "astro": "^5.8.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "tailwindcss": "^4.2.1",
+ "typescript": "^5.9.3"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.1",
+ "@types/react-dom": "^18.3.1"
+ }
+}
diff --git a/astro/before-ai-v2/public/favicon.svg b/astro/before-ai-v2/public/favicon.svg
new file mode 100644
index 00000000..ca747cb6
--- /dev/null
+++ b/astro/before-ai-v2/public/favicon.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/astro/before-ai-v2/src/components/XpDesktop.tsx b/astro/before-ai-v2/src/components/XpDesktop.tsx
new file mode 100644
index 00000000..eef5e154
--- /dev/null
+++ b/astro/before-ai-v2/src/components/XpDesktop.tsx
@@ -0,0 +1,388 @@
+import { type FormEvent, useEffect, useState } from "react";
+import type { StoryEvent } from "../lib/types";
+
+type XpDesktopProps = {
+ events: StoryEvent[];
+};
+
+type RsvpStatus = {
+ tone: "idle" | "pending" | "success" | "error";
+ message: string;
+};
+
+export function XpDesktop({ events }: XpDesktopProps) {
+ const [selectedEvent, setSelectedEvent] = useState(
+ events[0] ?? null,
+ );
+ const [isStartOpen, setIsStartOpen] = useState(false);
+ const [isDetailsOpen, setIsDetailsOpen] = useState(events.length > 0);
+ const [isRsvpOpen, setIsRsvpOpen] = useState(false);
+ const [isHelpOpen, setIsHelpOpen] = useState(true);
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [email, setEmail] = useState("");
+ const [rsvpStatus, setRsvpStatus] = useState({
+ tone: "idle",
+ message: "",
+ });
+
+ const openEvent = (event: StoryEvent) => {
+ setSelectedEvent(event);
+ setIsDetailsOpen(true);
+ setIsRsvpOpen(false);
+ };
+
+ const openRsvp = (event: StoryEvent | null = selectedEvent) => {
+ if (!event) {
+ return;
+ }
+
+ setSelectedEvent(event);
+ setRsvpStatus({ tone: "idle", message: "" });
+ setIsRsvpOpen(true);
+ };
+
+ const submitRsvp = async (event: FormEvent) => {
+ event.preventDefault();
+
+ if (!selectedEvent) {
+ return;
+ }
+
+ if (!firstName.trim() || !lastName.trim() || !email.trim()) {
+ setRsvpStatus({
+ tone: "error",
+ message: "Type your first name, last name, and email before finishing.",
+ });
+ return;
+ }
+
+ setRsvpStatus({ tone: "pending", message: "Dialing Wix Events..." });
+
+ try {
+ const response = await fetch("/api/rsvp", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ eventId: selectedEvent.id,
+ firstName,
+ lastName,
+ email,
+ }),
+ });
+ const result = (await response.json()) as {
+ message?: string;
+ error?: string;
+ };
+
+ if (!response.ok) {
+ throw new Error(result.error || "RSVP failed.");
+ }
+
+ setRsvpStatus({
+ tone: "success",
+ message: result.message || "RSVP saved.",
+ });
+ } catch (error) {
+ console.error("RSVP request failed", error);
+ setRsvpStatus({
+ tone: "error",
+ message:
+ error instanceof Error
+ ? error.message
+ : "We could not record that RSVP. Please try again.",
+ });
+ }
+ };
+
+ return (
+
+
+ {events.slice(0, 3).map((event) => (
+ openEvent(event)}
+ >
+ {event.icon}
+ {event.title}
+
+ ))}
+ setIsHelpOpen(true)}>
+ ❔
+ ReadMe.txt
+
+
+
+
+
+
Code Before AI - Event Explorer
+
+ _
+ □
+ ×
+
+
+
+ File
+ Events
+ Stories
+ Help
+
+
+ setIsHelpOpen(true)}>About
+ openRsvp()} disabled={!selectedEvent}>
+ RSVP Wizard
+
+ setIsStartOpen((value) => !value)}>
+ Start Menu
+
+
+
+
+
+ {events.length === 0 ? (
+
+ No upcoming events are published on this site yet. Add an RSVP
+ event in the Wix dashboard and it will show up here.
+
+ ) : (
+ events.map((event) => (
+
+ openEvent(event)}
+ >
+ {event.icon}
+
+
+
{event.status}
+
{event.title}
+ {event.summary ?
{event.summary}
: null}
+
+
+
Date
+ {event.date}
+
+
+
Venue
+ {event.venue}
+
+
+
+ openEvent(event)}>Open
+ openRsvp(event)}>RSVP
+
+
+
+ ))
+ )}
+
+
+
+
+ {isDetailsOpen && selectedEvent ? (
+
+
+
{selectedEvent.title}.exe
+
+ setIsDetailsOpen(false)}
+ >
+ ×
+
+
+
+
+
{selectedEvent.icon}
+
+
Opening story archive
+
{selectedEvent.title}
+ {selectedEvent.summary ?
{selectedEvent.summary}
: null}
+
+ {selectedEvent.date}
+ {selectedEvent.time}
+ {selectedEvent.venue}
+
+
+
openRsvp()}>Run RSVP Wizard
+ {selectedEvent.eventPageUrl ? (
+
+ Wix Event Page
+
+ ) : null}
+
setIsDetailsOpen(false)}>OK
+
+
+
+
+ ) : null}
+
+ {isRsvpOpen && selectedEvent ? (
+
+
+
RSVP Wizard
+
+ setIsRsvpOpen(false)}
+ >
+ ×
+
+
+
+
+
+ ) : null}
+
+ {isHelpOpen ? (
+
+
+ Windows XP vibe enabled
+ setIsHelpOpen(false)}>
+ ×
+
+
+
+ Double-click the memories. Hear programmers explain how they coded
+ before AI autocomplete, instant answers, and deploy buttons with
+ manners.
+
+
+ ) : null}
+
+ {isStartOpen ? (
+
+ 👴 Code Before AI
+ selectedEvent && openEvent(selectedEvent)}
+ disabled={!selectedEvent}
+ >
+ Recent Story
+
+ openRsvp()} disabled={!selectedEvent}>
+ RSVP Wizard
+
+ setIsHelpOpen(true)}>Help and Support
+ setIsStartOpen(false)}>Log Off
+
+ ) : null}
+
+
+ setIsStartOpen((value) => !value)}
+ >
+ start
+
+ Code Before AI
+ {isDetailsOpen && selectedEvent ? (
+ setIsDetailsOpen(true)}>
+ {selectedEvent.title}
+
+ ) : null}
+ {isRsvpOpen ? (
+ setIsRsvpOpen(true)}>
+ RSVP Wizard
+
+ ) : null}
+
+ 🔊
+ LAN
+
+
+
+
+ );
+}
+
+/**
+ * The taskbar clock. Rendered empty on the server and filled in after
+ * hydration — a server-rendered time would be the pod's clock, not the
+ * visitor's, and would mismatch on hydration.
+ */
+function TrayClock() {
+ const [now, setNow] = useState("");
+
+ useEffect(() => {
+ const tick = () =>
+ setNow(
+ new Intl.DateTimeFormat(undefined, {
+ hour: "numeric",
+ minute: "2-digit",
+ }).format(new Date()),
+ );
+
+ tick();
+ const timer = window.setInterval(tick, 30_000);
+
+ return () => window.clearInterval(timer);
+ }, []);
+
+ return {now} ;
+}
diff --git a/astro/before-ai-v2/src/layouts/Layout.astro b/astro/before-ai-v2/src/layouts/Layout.astro
new file mode 100644
index 00000000..491fdfcd
--- /dev/null
+++ b/astro/before-ai-v2/src/layouts/Layout.astro
@@ -0,0 +1,29 @@
+---
+import "../styles/global.css";
+
+interface Props {
+ title?: string;
+ description?: string;
+}
+
+const {
+ title = "Code Before AI | Windows XP Story Events",
+ description = "A Windows XP-inspired event site where old programmers tell stories about writing code before AI.",
+} = Astro.props;
+---
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
diff --git a/astro/before-ai-v2/src/lib/types.ts b/astro/before-ai-v2/src/lib/types.ts
new file mode 100644
index 00000000..5977170d
--- /dev/null
+++ b/astro/before-ai-v2/src/lib/types.ts
@@ -0,0 +1,13 @@
+/** One upcoming event, flattened from the Wix Events API for the desktop UI. */
+export type StoryEvent = {
+ id: string;
+ title: string;
+ date: string;
+ time: string;
+ venue: string;
+ icon: string;
+ imageUrl?: string;
+ status: string;
+ summary: string;
+ eventPageUrl?: string;
+};
diff --git a/astro/before-ai-v2/src/lib/wix-events.ts b/astro/before-ai-v2/src/lib/wix-events.ts
new file mode 100644
index 00000000..6a236e1c
--- /dev/null
+++ b/astro/before-ai-v2/src/lib/wix-events.ts
@@ -0,0 +1,149 @@
+import { auth } from "@wix/essentials";
+import { rsvpV2, wixEventsV2 } from "@wix/events";
+import type { StoryEvent } from "./types";
+
+type WixEvent = wixEventsV2.Event;
+
+type RsvpInput = {
+ eventId: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+};
+
+// Purely decorative — Wix Events has no per-event icon, so the desktop
+// metaphor assigns one from this list by position.
+const eventIcons = ["💾", "🖥", "📘", "💿", "☎", "🧰", "🪟", "⌨"];
+
+/**
+ * Upcoming events, newest first. Published events are public, so this is a
+ * plain visitor-scoped read — `@wix/astro` authenticates it automatically
+ * (no client, no API key). An empty list is a legitimate result: a site with
+ * no upcoming events renders the empty state rather than invented ones.
+ */
+export async function loadStoryEvents(): Promise {
+ try {
+ const result = await wixEventsV2
+ .queryEvents({ fields: ["DETAILS", "REGISTRATION", "URLS"] })
+ .in("status", ["UPCOMING", "STARTED"])
+ .ascending("dateAndTimeSettings.startDate")
+ .limit(12)
+ .find();
+
+ return result.items
+ .map(mapWixEvent)
+ .filter((event): event is StoryEvent => event !== null);
+ } catch (error) {
+ console.error("Failed to load Wix Events", error);
+ return [];
+ }
+}
+
+/**
+ * Creating an RSVP is a privileged write, so it runs elevated. This only ever
+ * executes inside the /api/rsvp endpoint (server-side) — never in the browser.
+ */
+export async function createWixRsvp({
+ eventId,
+ firstName,
+ lastName,
+ email,
+}: RsvpInput) {
+ // The generated type marks additionalGuestDetails as required, but Wix rejects
+ // those fields when the event form does not include additional guests.
+ const rsvp = {
+ eventId,
+ email,
+ firstName,
+ lastName,
+ status: "YES",
+ } as unknown as Parameters[0];
+
+ await auth.elevate(rsvpV2.createRsvp)(rsvp);
+}
+
+function mapWixEvent(event: WixEvent, index: number): StoryEvent | null {
+ if (!event._id || !event.title) {
+ return null;
+ }
+
+ const formatted = event.dateAndTimeSettings?.formatted;
+
+ return {
+ id: event._id,
+ title: event.title,
+ date:
+ formatted?.startDate || formatDate(event.dateAndTimeSettings?.startDate),
+ time:
+ formatted?.startTime || formatTime(event.dateAndTimeSettings?.startDate),
+ venue: formatVenue(event),
+ icon: eventIcons[index % eventIcons.length],
+ status: formatStatus(event),
+ summary: event.shortDescription?.trim() || "",
+ eventPageUrl: event.eventPageUrl ?? undefined,
+ };
+}
+
+function formatVenue(event: WixEvent) {
+ const location = event.location;
+
+ if (!location) {
+ return "Venue TBA";
+ }
+
+ if (location.name) {
+ return location.name;
+ }
+
+ if (location.locationTbd) {
+ return "Venue TBA";
+ }
+
+ if (location.type === "ONLINE") {
+ return "Online";
+ }
+
+ const address = location.address;
+ const parts = [address?.addressLine1, address?.city, address?.country].filter(
+ Boolean,
+ );
+
+ return parts.join(", ") || "Venue TBA";
+}
+
+function formatStatus(event: WixEvent) {
+ const rsvps = event.summaries?.rsvps;
+
+ if (rsvps?.yesCount !== undefined) {
+ return `${rsvps.yesCount} RSVP${rsvps.yesCount === 1 ? "" : "s"}`;
+ }
+
+ if (event.registration?.status) {
+ return event.registration.status.replaceAll("_", " ").toLowerCase();
+ }
+
+ return event.status?.toLowerCase() || "open";
+}
+
+// Wix returns pre-formatted date strings in the site's locale; these only run
+// when a site has formatting disabled, and follow the visitor's own locale.
+function formatDate(date?: Date | string | null) {
+ if (!date) {
+ return "Date TBA";
+ }
+
+ return new Intl.DateTimeFormat(undefined, { dateStyle: "long" }).format(
+ new Date(date),
+ );
+}
+
+function formatTime(date?: Date | string | null) {
+ if (!date) {
+ return "Time TBA";
+ }
+
+ return new Intl.DateTimeFormat(undefined, {
+ hour: "2-digit",
+ minute: "2-digit",
+ }).format(new Date(date));
+}
diff --git a/astro/before-ai-v2/src/pages/api/events.ts b/astro/before-ai-v2/src/pages/api/events.ts
new file mode 100644
index 00000000..13a6ad0b
--- /dev/null
+++ b/astro/before-ai-v2/src/pages/api/events.ts
@@ -0,0 +1,10 @@
+import type { APIRoute } from "astro";
+import { loadStoryEvents } from "../../lib/wix-events";
+
+export const prerender = false;
+
+export const GET: APIRoute = async () => {
+ const events = await loadStoryEvents();
+
+ return Response.json({ events });
+};
diff --git a/astro/before-ai-v2/src/pages/api/rsvp.ts b/astro/before-ai-v2/src/pages/api/rsvp.ts
new file mode 100644
index 00000000..91b31859
--- /dev/null
+++ b/astro/before-ai-v2/src/pages/api/rsvp.ts
@@ -0,0 +1,37 @@
+import type { APIRoute } from "astro";
+import { createWixRsvp } from "../../lib/wix-events";
+
+export const prerender = false;
+
+export const POST: APIRoute = async ({ request }) => {
+ const body = await request.json().catch(() => null);
+ const eventId = getString(body?.eventId);
+ const firstName = getString(body?.firstName);
+ const lastName = getString(body?.lastName);
+ const email = getString(body?.email);
+
+ if (!eventId || !firstName || !lastName || !email) {
+ return Response.json(
+ { error: "Event, first name, last name, and email are required." },
+ { status: 400 },
+ );
+ }
+
+ try {
+ await createWixRsvp({ eventId, firstName, lastName, email });
+
+ return Response.json({ message: "RSVP sent to Wix Events." });
+ } catch (error) {
+ // Log the real error server-side; the visitor gets a generic message.
+ console.error("Failed to create Wix RSVP", error);
+
+ return Response.json(
+ { error: "We could not record that RSVP. Please try again." },
+ { status: 502 },
+ );
+ }
+};
+
+function getString(value: unknown) {
+ return typeof value === "string" ? value.trim() : "";
+}
diff --git a/astro/before-ai-v2/src/pages/index.astro b/astro/before-ai-v2/src/pages/index.astro
new file mode 100644
index 00000000..6a7fa782
--- /dev/null
+++ b/astro/before-ai-v2/src/pages/index.astro
@@ -0,0 +1,14 @@
+---
+import Layout from "../layouts/Layout.astro";
+import { XpDesktop } from "../components/XpDesktop";
+import { loadStoryEvents } from "../lib/wix-events";
+
+// Every request renders fresh, so the events load stays request-time.
+export const prerender = false;
+
+const events = await loadStoryEvents();
+---
+
+
+
+
diff --git a/astro/before-ai-v2/src/styles/global.css b/astro/before-ai-v2/src/styles/global.css
new file mode 100644
index 00000000..0b9baf51
--- /dev/null
+++ b/astro/before-ai-v2/src/styles/global.css
@@ -0,0 +1,665 @@
+@import "tailwindcss";
+
+:root {
+ --xp-blue: #245edb;
+ --xp-blue-dark: #1746a2;
+ --xp-blue-light: #4f8cff;
+ --xp-green: #39a939;
+ --xp-green-dark: #177a17;
+ --xp-cream: #ece9d8;
+ --xp-cream-dark: #d6d2bf;
+ --xp-border: #003c9e;
+ --xp-text: #111111;
+ --xp-window: #f3f1e8;
+ --xp-highlight: #fff8c6;
+ --font-ui: Tahoma, Verdana, Arial, sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ min-height: 100%;
+}
+
+body {
+ color: var(--xp-text);
+ font-family: var(--font-ui);
+ font-size: 13px;
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+:focus-visible {
+ outline: 2px dotted #000000;
+ outline-offset: 2px;
+}
+
+.xp-screen {
+ background:
+ radial-gradient(circle at 78% 18%, rgba(255, 255, 255, 0.45), transparent 18%),
+ linear-gradient(140deg, #2368cf 0%, #2b8eea 38%, #78bd38 39%, #3b941f 100%);
+ min-height: 100svh;
+ overflow-x: hidden;
+ padding: 22px 22px 74px 112px;
+ position: relative;
+}
+
+.xp-screen::before {
+ background:
+ radial-gradient(ellipse at center, rgba(255, 255, 255, 0.55), transparent 62%);
+ border-radius: 50%;
+ content: "";
+ height: 360px;
+ left: 48%;
+ opacity: 0.45;
+ position: fixed;
+ top: 10%;
+ transform: rotate(-10deg);
+ width: 520px;
+}
+
+.desktop-icons {
+ display: grid;
+ gap: 18px;
+ left: 18px;
+ position: fixed;
+ top: 22px;
+ width: 72px;
+ z-index: 4;
+}
+
+.desktop-icon {
+ background: transparent;
+ border: 0;
+ color: #ffffff;
+ display: grid;
+ gap: 5px;
+ justify-items: center;
+ min-height: 72px;
+ padding: 2px;
+ text-shadow: 1px 1px 2px #12346e;
+}
+
+.desktop-icon:hover,
+.desktop-icon:focus-visible {
+ background: rgba(49, 106, 197, 0.55);
+ outline: 1px dotted #ffffff;
+}
+
+.desktop-icon span {
+ align-items: center;
+ background: linear-gradient(135deg, #ffffff, #cfdcff);
+ border: 1px solid #163f8f;
+ border-radius: 3px;
+ box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.25);
+ display: flex;
+ font-size: 27px;
+ height: 38px;
+ justify-content: center;
+ text-shadow: none;
+ width: 38px;
+}
+
+.desktop-icon small {
+ line-height: 1.12;
+ overflow-wrap: anywhere;
+}
+
+.xp-window {
+ background: var(--xp-window);
+ border: 2px solid var(--xp-border);
+ border-radius: 8px 8px 3px 3px;
+ box-shadow:
+ inset 1px 1px 0 rgba(255, 255, 255, 0.9),
+ 8px 12px 22px rgba(0, 0, 0, 0.28);
+ overflow: hidden;
+ position: relative;
+ z-index: 2;
+}
+
+.main-window {
+ margin: 0 auto;
+ max-width: 1180px;
+}
+
+.xp-titlebar {
+ align-items: center;
+ background:
+ linear-gradient(180deg, #66a7ff 0%, var(--xp-blue) 18%, var(--xp-blue-dark) 100%);
+ color: #ffffff;
+ display: flex;
+ font-weight: 700;
+ justify-content: space-between;
+ min-height: 32px;
+ padding: 5px 6px 5px 10px;
+ text-shadow: 1px 1px 0 #08276f;
+}
+
+.window-actions {
+ display: flex;
+ gap: 4px;
+}
+
+.window-actions span,
+.window-actions button {
+ align-items: center;
+ background: linear-gradient(180deg, #ffffff, #8cb9ff 48%, #245edb);
+ border: 1px solid #ffffff;
+ border-radius: 3px;
+ box-shadow: inset -1px -1px 0 #0d3b9b;
+ color: #ffffff;
+ display: flex;
+ font-weight: 900;
+ height: 21px;
+ justify-content: center;
+ padding: 0;
+ text-shadow: 1px 1px 0 #08276f;
+ width: 23px;
+}
+
+.window-actions button:last-child {
+ background: linear-gradient(180deg, #ffb4a4, #e04720 48%, #a51b0b);
+}
+
+.xp-menubar {
+ background: var(--xp-cream);
+ border-bottom: 1px solid #aca899;
+ display: flex;
+ gap: 18px;
+ min-height: 26px;
+ padding: 6px 10px;
+}
+
+.xp-toolbar {
+ align-items: center;
+ background: linear-gradient(180deg, #fbfaf2, var(--xp-cream));
+ border-bottom: 1px solid #aca899;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 7px;
+ padding: 8px;
+}
+
+
+.xp-toolbar button,
+.button-row button,
+.xp-link-button,
+.xp-form button,
+.start-menu button {
+ background: linear-gradient(180deg, #ffffff, #ece9d8);
+ border: 1px solid #003c74;
+ border-radius: 3px;
+ box-shadow:
+ inset 1px 1px 0 #ffffff,
+ inset -1px -1px 0 #aca899;
+ color: #000000;
+ min-height: 28px;
+ min-width: 78px;
+ padding: 4px 12px;
+ text-decoration: none;
+}
+
+.xp-toolbar button:active,
+.button-row button:active,
+.xp-link-button:active,
+.xp-form button:active,
+.start-menu button:active {
+ box-shadow:
+ inset 1px 1px 0 #aca899,
+ inset -1px -1px 0 #ffffff;
+ transform: translateY(1px);
+}
+
+.xp-window-body {
+ padding: 12px;
+}
+
+.explorer-layout {
+ display: grid;
+ gap: 12px;
+ grid-template-columns: 230px minmax(0, 1fr);
+}
+
+.folder-pane {
+ background: linear-gradient(180deg, #7ba2e7, #d7e5ff);
+ border: 1px solid #6d8dce;
+ border-radius: 4px;
+ display: grid;
+ gap: 8px;
+ align-content: start;
+ padding: 12px;
+}
+
+.folder-pane h2 {
+ color: #163f8f;
+ font-size: 17px;
+ margin: 0 0 4px;
+}
+
+.folder {
+ margin: 0;
+ background: rgba(255, 255, 255, 0.74);
+ border: 1px solid #9db7ea;
+ border-radius: 4px;
+ min-height: 34px;
+ padding: 7px 9px;
+ text-align: left;
+}
+
+.folder.active {
+ background: #fff8c6;
+ border-color: #d6a600;
+}
+
+.empty-state {
+ background: #ffffff;
+ border: 1px inset #aca899;
+ color: #444444;
+ grid-column: 1 / -1;
+ margin: 0;
+ padding: 24px;
+ text-align: center;
+}
+
+.event-grid {
+ display: grid;
+ gap: 12px;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr));
+}
+
+.xp-card {
+ background: #ffffff;
+ border: 1px solid #aca899;
+ box-shadow: inset 1px 1px 0 #ffffff;
+ display: grid;
+ grid-template-rows: 138px minmax(0, 1fr);
+ min-height: 486px;
+}
+
+.event-image {
+ background:
+ linear-gradient(135deg, rgba(255, 255, 255, 0.72), transparent 38%),
+ repeating-linear-gradient(90deg, #b7d5ff 0 16px, #99c0ff 16px 32px);
+ border: 0;
+ border-bottom: 1px solid #aca899;
+ display: grid;
+ place-items: center;
+}
+
+.event-image span {
+ align-items: center;
+ background: linear-gradient(180deg, #fffdf2, #d6e4ff);
+ border: 2px solid #245edb;
+ border-radius: 5px;
+ box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.22);
+ display: flex;
+ font-size: 54px;
+ height: 86px;
+ justify-content: center;
+ width: 86px;
+}
+
+.event-copy {
+ display: grid;
+ gap: 9px;
+ padding: 12px;
+}
+
+.file-label {
+ background: var(--xp-highlight);
+ border: 1px solid #d6a600;
+ color: #5c4a00;
+ display: inline-flex;
+ font-size: 12px;
+ font-weight: 700;
+ justify-self: start;
+ margin: 0;
+ padding: 3px 7px;
+}
+
+.event-copy h2,
+.details-window h2,
+.xp-form h2 {
+ color: #0b3d91;
+ font-size: 21px;
+ line-height: 1.1;
+ margin: 0;
+}
+
+.event-copy p,
+.details-window p,
+.xp-form p,
+.xp-popup p {
+ line-height: 1.45;
+ margin: 0;
+}
+
+.event-copy dl {
+ display: grid;
+ gap: 7px;
+ margin: 0;
+}
+
+.event-copy dt {
+ color: #565656;
+ font-weight: 700;
+}
+
+.event-copy dd {
+ margin: 0;
+}
+
+.button-row {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: auto;
+}
+
+.button-row.right {
+ justify-content: flex-end;
+}
+
+.floating-window {
+ position: fixed;
+ z-index: 8;
+}
+
+.details-window {
+ max-width: min(560px, calc(100vw - 28px));
+ right: 28px;
+ top: 96px;
+}
+
+.details-window .xp-window-body {
+ display: grid;
+ gap: 14px;
+ grid-template-columns: 70px minmax(0, 1fr);
+}
+
+.dialog-icon {
+ align-items: center;
+ background: linear-gradient(180deg, #ffffff, #d7e5ff);
+ border: 2px solid #245edb;
+ border-radius: 5px;
+ display: flex;
+ font-size: 42px;
+ height: 62px;
+ justify-content: center;
+ width: 62px;
+}
+
+.details-list {
+ background: #ffffff;
+ border: 1px inset #aca899;
+ display: grid;
+ gap: 6px;
+ margin: 12px 0;
+ padding: 9px;
+}
+
+.rsvp-window {
+ left: 50%;
+ max-width: min(620px, calc(100vw - 28px));
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 620px;
+}
+
+.wizard-body {
+ display: grid;
+ gap: 16px;
+ grid-template-columns: 160px minmax(0, 1fr);
+ min-height: 310px;
+ padding: 0;
+}
+
+.wizard-sidebar {
+ background:
+ radial-gradient(circle at 50% 20%, rgba(255, 255, 255, 0.78), transparent 26%),
+ linear-gradient(180deg, #245edb, #163f8f);
+ color: #ffffff;
+ display: grid;
+ gap: 10px;
+ place-content: center;
+ text-align: center;
+}
+
+.wizard-sidebar span {
+ font-size: 58px;
+}
+
+.xp-form {
+ display: grid;
+ gap: 13px;
+ padding: 18px;
+}
+
+.xp-form label {
+ display: grid;
+ font-weight: 700;
+ gap: 5px;
+}
+
+.xp-form input,
+.xp-form select {
+ background: #ffffff;
+ border: 2px inset #d4d0c8;
+ min-height: 30px;
+ padding: 4px 6px;
+}
+
+.xp-form button:disabled {
+ color: #777777;
+ cursor: wait;
+}
+
+.selected-rsvp-event,
+.rsvp-status {
+ background: #ffffff;
+ border: 1px inset #aca899;
+ padding: 8px;
+}
+
+.selected-rsvp-event {
+ color: #0b3d91;
+ font-weight: 700;
+}
+
+.rsvp-status.pending {
+ color: #1746a2;
+}
+
+.rsvp-status.success {
+ background: #e4ffd6;
+ color: #176b17;
+}
+
+.rsvp-status.error {
+ background: #ffe1d6;
+ color: #9b1d0d;
+}
+
+.xp-popup {
+ background: #fff7bd;
+ border: 1px solid #6f5b00;
+ border-radius: 0 8px 8px;
+ bottom: 62px;
+ box-shadow: 4px 6px 16px rgba(0, 0, 0, 0.24);
+ max-width: min(360px, calc(100vw - 28px));
+ padding: 11px;
+ position: fixed;
+ right: 18px;
+ z-index: 10;
+}
+
+.popup-title {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.popup-title button {
+ background: transparent;
+ border: 0;
+ font-weight: 900;
+}
+
+.start-menu {
+ background: #ffffff;
+ border: 2px solid var(--xp-blue);
+ border-radius: 8px 8px 0 0;
+ bottom: 38px;
+ box-shadow: 8px 8px 22px rgba(0, 0, 0, 0.35);
+ display: grid;
+ gap: 6px;
+ left: 0;
+ padding: 8px;
+ position: fixed;
+ width: min(320px, 92vw);
+ z-index: 20;
+}
+
+.start-user {
+ background: linear-gradient(180deg, #66a7ff, #245edb);
+ border-radius: 5px;
+ color: #ffffff;
+ font-size: 18px;
+ font-weight: 700;
+ padding: 12px;
+ text-shadow: 1px 1px 0 #08276f;
+}
+
+.start-menu button {
+ border-color: transparent;
+ box-shadow: none;
+ min-height: 36px;
+ text-align: left;
+}
+
+.start-menu button:hover {
+ background: #316ac5;
+ color: #ffffff;
+}
+
+.xp-taskbar {
+ align-items: center;
+ background:
+ linear-gradient(180deg, #4f8cff 0%, var(--xp-blue) 22%, var(--xp-blue-dark) 100%);
+ bottom: 0;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
+ display: flex;
+ gap: 7px;
+ height: 40px;
+ left: 0;
+ padding: 4px;
+ position: fixed;
+ right: 0;
+ z-index: 30;
+}
+
+.start-button {
+ background:
+ radial-gradient(circle at 22% 22%, rgba(255, 255, 255, 0.55), transparent 32%),
+ linear-gradient(180deg, #6bd45d, var(--xp-green) 48%, var(--xp-green-dark));
+ border: 1px solid #0d5f0d;
+ border-radius: 16px 8px 8px 16px;
+ box-shadow:
+ inset 1px 1px 0 rgba(255, 255, 255, 0.8),
+ 1px 1px 1px rgba(0, 0, 0, 0.35);
+ color: #ffffff;
+ font-size: 18px;
+ font-style: italic;
+ font-weight: 700;
+ height: 32px;
+ min-width: 92px;
+ text-shadow: 1px 1px 0 #0b4a0b;
+}
+
+.task-button {
+ background: linear-gradient(180deg, #71a8ff, #2f69d8);
+ border: 1px solid #0d3b9b;
+ border-radius: 3px;
+ box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.5);
+ color: #ffffff;
+ height: 30px;
+ max-width: 230px;
+ overflow: hidden;
+ padding: 0 12px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.task-button.active {
+ background: linear-gradient(180deg, #1f55bd, #123f9e);
+}
+
+.tray {
+ align-items: center;
+ background: rgba(100, 180, 255, 0.42);
+ border-left: 1px solid rgba(255, 255, 255, 0.45);
+ color: #ffffff;
+ display: flex;
+ gap: 9px;
+ height: 32px;
+ margin-left: auto;
+ padding: 0 10px;
+}
+
+@media (max-width: 900px) {
+ .xp-screen {
+ padding: 16px 12px 74px;
+ }
+
+ .desktop-icons {
+ display: none;
+ }
+
+ .explorer-layout,
+ .details-window .xp-window-body,
+ .wizard-body {
+ grid-template-columns: 1fr;
+ }
+
+ .details-window {
+ left: 12px;
+ right: 12px;
+ top: auto;
+ bottom: 58px;
+ }
+
+ .rsvp-window {
+ bottom: 58px;
+ left: 12px;
+ top: auto;
+ transform: none;
+ width: auto;
+ }
+
+ .wizard-sidebar {
+ min-height: 110px;
+ }
+
+ .xp-taskbar {
+ gap: 4px;
+ }
+
+
+ .task-button {
+ max-width: 110px;
+ }
+}
diff --git a/astro/before-ai-v2/tsconfig.json b/astro/before-ai-v2/tsconfig.json
new file mode 100644
index 00000000..92a18df9
--- /dev/null
+++ b/astro/before-ai-v2/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react"
+ }
+}
diff --git a/templates.json b/templates.json
index 59f68cb2..9d8bb530 100644
--- a/templates.json
+++ b/templates.json
@@ -64,6 +64,14 @@
"siteTemplateId": "307dedf5-7179-483c-9e6c-6cdcdd6684b3",
"gitPath": "astro/rentals",
"vibeCompatible": false
+ },
+ {
+ "name": "before-ai-v2",
+ "title": "Code Before AI (Wix Events)",
+ "subtitle": "Event listings and RSVPs on an XP desktop",
+ "siteTemplateId": "ca844d81-95d3-4f92-b710-03ddcf9050f4",
+ "gitPath": "astro/before-ai-v2",
+ "vibeCompatible": false
}
]
}