From 1557e239b4765ded19573d4f794c7ba8e49cdb44 Mon Sep 17 00:00:00 2001 From: tuvit Date: Tue, 25 Aug 2026 17:30:32 +0300 Subject: [PATCH 1/2] =?UTF-8?q?Add=20astro/before-ai-v2=20template=20?= =?UTF-8?q?=E2=80=94=20Wix=20Events=20RSVPs=20on=20an=20XP=20desktop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Windows XP desktop metaphor as an events site: one React island lists upcoming Wix Events and takes RSVPs through an XP-style wizard. Restructured to the repo layout and reviewed against AGENTS.md: - Adapter: @wix/cloud-provider-fetch-adapter behind an `isBuild` conditional replaced with @wix/astro-wix-hosting-adapter (the banned pattern) - package.json: renamed to wix-astro-before-ai-v2, scripts reduced to astro/dev/build, dropped @wix/cli + @astrojs/cloudflare + @astrojs/node, caret-pinned react/react-dom - Wix is the source of truth: removed the bundled seed-events fallback that shadowed queryEvents, along with the "live vs demo" UI it fed. A site with no upcoming events now renders an empty state - Dropped the constant fake "speaker" byline (Wix Events has no such field) and the hardcoded 7:42 PM taskbar clock, now a real client-side clock - Errors: /api/rsvp no longer leaks SDK detail or references API-key scopes that do not exist under @wix/astro — it logs server-side and returns a generic message - No dead UI: removed the handler-less folder buttons and the minimize/maximize buttons that did nothing - Removed astro.config.local.mjs and TESTING.md (the local @astrojs/node escape hatch went away with the CLI scripts) Verified from a standalone copy: npm install, astro build, and tsc --noEmit all pass; largest built file is 248K, well under the registry's ~4MB limit. Co-Authored-By: Claude Opus 5 --- astro/before-ai-v2/.gitignore | 28 + astro/before-ai-v2/README.md | 50 ++ astro/before-ai-v2/astro.config.mjs | 23 + astro/before-ai-v2/package.json | 29 + astro/before-ai-v2/public/favicon.svg | 6 + .../before-ai-v2/src/components/XpDesktop.tsx | 388 ++++++++++ astro/before-ai-v2/src/layouts/Layout.astro | 29 + astro/before-ai-v2/src/lib/types.ts | 13 + astro/before-ai-v2/src/lib/wix-events.ts | 149 ++++ astro/before-ai-v2/src/pages/api/events.ts | 10 + astro/before-ai-v2/src/pages/api/rsvp.ts | 37 + astro/before-ai-v2/src/pages/index.astro | 14 + astro/before-ai-v2/src/styles/global.css | 665 ++++++++++++++++++ astro/before-ai-v2/tsconfig.json | 9 + 14 files changed, 1450 insertions(+) create mode 100644 astro/before-ai-v2/.gitignore create mode 100644 astro/before-ai-v2/README.md create mode 100644 astro/before-ai-v2/astro.config.mjs create mode 100644 astro/before-ai-v2/package.json create mode 100644 astro/before-ai-v2/public/favicon.svg create mode 100644 astro/before-ai-v2/src/components/XpDesktop.tsx create mode 100644 astro/before-ai-v2/src/layouts/Layout.astro create mode 100644 astro/before-ai-v2/src/lib/types.ts create mode 100644 astro/before-ai-v2/src/lib/wix-events.ts create mode 100644 astro/before-ai-v2/src/pages/api/events.ts create mode 100644 astro/before-ai-v2/src/pages/api/rsvp.ts create mode 100644 astro/before-ai-v2/src/pages/index.astro create mode 100644 astro/before-ai-v2/src/styles/global.css create mode 100644 astro/before-ai-v2/tsconfig.json 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) => ( + + ))} + +
+ +
+
+ Code Before AI - Event Explorer + +
+ +
+ + + +
+
+ +
+ {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) => ( +
+ +
+

{event.status}

+

{event.title}

+ {event.summary ?

{event.summary}

: null} +
+
+
Date
+
{event.date}
+
+
+
Venue
+
{event.venue}
+
+
+
+ + +
+
+
+ )) + )} +
+
+
+ + {isDetailsOpen && selectedEvent ? ( +
+
+ {selectedEvent.title}.exe +
+ +
+
+
+
{selectedEvent.icon}
+
+

Opening story archive

+

{selectedEvent.title}

+ {selectedEvent.summary ?

{selectedEvent.summary}

: null} +
+ {selectedEvent.date} + {selectedEvent.time} + {selectedEvent.venue} +
+
+ + {selectedEvent.eventPageUrl ? ( + + Wix Event Page + + ) : null} + +
+
+
+
+ ) : null} + + {isRsvpOpen && selectedEvent ? ( +
+
+ RSVP Wizard +
+ +
+
+
+
+ +

Wix RSVP

+
+
+

Reserve a chair and a story

+

{selectedEvent.title}

+ + + + {rsvpStatus.message ? ( +

+ {rsvpStatus.message} +

+ ) : null} +
+ + +
+
+
+
+ ) : null} + + {isHelpOpen ? ( + + ) : null} + + {isStartOpen ? ( +
+
👴 Code Before AI
+ + + + +
+ ) : null} + +
+ + + {isDetailsOpen && selectedEvent ? ( + + ) : null} + {isRsvpOpen ? ( + + ) : null} +
+ + + +
+
+
+ ); +} + +/** + * 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 ; +} 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" + } +} From 5fcf70881c60a989aa524508e91a6d580c0b2ae7 Mon Sep 17 00:00:00 2001 From: tuvit Date: Tue, 25 Aug 2026 18:09:31 +0300 Subject: [PATCH 2/2] Register before-ai-v2 template (templates.json + README) Co-Authored-By: Claude Opus 5 --- README.md | 6 ++++++ templates.json | 8 ++++++++ 2 files changed, 14 insertions(+) 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/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 } ] }