From 1a710aa3c61985c641898866c004242939ba4fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ckirukib=E2=80=9D?= <“kirubeljkl679@gmail.com”> Date: Wed, 20 May 2026 11:57:21 +0300 Subject: [PATCH] first commit + project setup --- .dev.vars | 1 + .env.example | 3 + .../workflows/cloudflare-workers-opennext.yml | 28 + .gitignore | 38 + app/api/inquiry/route.ts | 19 + app/api/payment/route.ts | 37 + app/apple-icon.tsx | 41 + app/calendar/route.ts | 12 + app/contact/page.tsx | 46 + app/exhibit/page.tsx | 79 + app/globals.css | 191 + app/icon.tsx | 54 + app/layout.tsx | 39 + app/page.tsx | 38 + app/partners/page.tsx | 75 + app/payment/page.tsx | 71 + app/payment/success/page.tsx | 49 + app/pitch-competition/page.tsx | 61 + app/privacy/page.tsx | 35 + app/program/page.tsx | 65 + app/robots.ts | 13 + app/sitemap.ts | 11 + app/speakers/page.tsx | 68 + app/sponsor/page.tsx | 70 + components.json | 21 + components/brand/BrandLogo.tsx | 106 + components/brand/FooterTopographicBand.tsx | 108 + components/brand/PartnerLogoPlaceholder.tsx | 28 + components/brand/RiftFlowLines.tsx | 116 + components/brand/RiftPageFlow.tsx | 70 + components/brand/RiftSectionAccent.tsx | 133 + components/brand/rift-patterns.ts | 9 + components/event/AddToCalendar.tsx | 57 + components/exhibit/BoothPackages.tsx | 28 + components/exhibit/ExhibitorBoothForm.tsx | 264 + components/forms/DataConsentField.tsx | 51 + components/forms/InquiryForm.tsx | 108 + components/grants/CyclingGrantAmount.tsx | 74 + components/grants/GrantHeadline.tsx | 16 + components/home/AttendSummitSection.tsx | 71 + components/home/BoothAcquisitionBand.tsx | 37 + components/home/ExperienceCards.tsx | 48 + components/home/Faq.tsx | 27 + components/home/Hero.tsx | 56 + components/home/HeroGrantLine.tsx | 17 + components/home/Newsletter.tsx | 69 + components/home/PartnerMarquee.tsx | 20 + components/home/PurposeBand.tsx | 37 + components/home/PurposeGrantText.tsx | 15 + components/home/Speakers.tsx | 73 + components/home/SponsorTiers.tsx | 37 + components/home/StatsGrid.tsx | 35 + components/home/TicketsBand.tsx | 41 + components/home/TopicMarquee.tsx | 25 + components/home/Venue.tsx | 38 + components/layout/FooterNewsletter.tsx | 108 + components/layout/NavTicketsCta.tsx | 30 + components/layout/Section.tsx | 42 + components/layout/SiteFooter.tsx | 96 + components/layout/SiteHeader.tsx | 127 + components/partners/ChampionStartupModal.tsx | 172 + components/partners/PartnerCard.tsx | 37 + components/partners/PartnerSectionBlock.tsx | 31 + components/partners/PartnershipCtaBand.tsx | 30 + .../partners/PartnershipInquiryForm.tsx | 122 + components/payment/PaymentForm.tsx | 215 + components/seo/JsonLd.tsx | 65 + components/speakers/SpeakerCard.tsx | 39 + components/tickets/TicketCard.tsx | 87 + components/ui/accordion.tsx | 66 + components/ui/badge.tsx | 48 + components/ui/button.tsx | 64 + components/ui/card.tsx | 92 + components/ui/carousel.tsx | 241 + components/ui/checkbox.tsx | 32 + components/ui/dialog.tsx | 158 + components/ui/dropdown-menu.tsx | 257 + components/ui/input.tsx | 21 + components/ui/label.tsx | 24 + components/ui/navigation-menu.tsx | 168 + components/ui/select.tsx | 190 + components/ui/separator.tsx | 28 + components/ui/sheet.tsx | 143 + components/ui/tabs.tsx | 91 + components/ui/textarea.tsx | 18 + content/attend.ts | 57 + content/consent.ts | 9 + content/exhibit.ts | 51 + content/faq.ts | 50 + content/grants.ts | 9 + content/inquiries.ts | 26 + content/legal.ts | 28 + content/page-seo.ts | 69 + content/partners.ts | 109 + content/people.ts | 180 + content/pitch.ts | 19 + content/program.ts | 61 + content/site.ts | 31 + content/tickets.ts | 59 + content/tracks.ts | 40 + eslint.config.mjs | 14 + lib/calendar.ts | 57 + lib/inquiry.ts | 275 + lib/payment.ts | 65 + lib/seo.ts | 101 + lib/utils.ts | 6 + next.config.ts | 5 + package-lock.json | 8412 +++++++++++++++++ package.json | 47 + postcss.config.mjs | 7 + public/branding/booth-mockup.png | Bin 0 -> 1369952 bytes public/branding/logo-icon.png | Bin 0 -> 31127 bytes public/branding/logo-wordmark.jpg | Bin 0 -> 78414 bytes public/branding/logo.png | Bin 0 -> 23599 bytes public/branding/speakers/abraham.png | Bin 0 -> 183032 bytes public/branding/speakers/abrhame.png | Bin 0 -> 145163 bytes public/branding/speakers/adam.png | Bin 0 -> 162309 bytes public/branding/speakers/amity.png | Bin 0 -> 181921 bytes public/branding/speakers/beamlak.png | Bin 0 -> 216961 bytes public/branding/speakers/biruh.png | Bin 0 -> 167499 bytes public/branding/speakers/brook.png | Bin 0 -> 173780 bytes public/branding/speakers/dagmawit.png | Bin 0 -> 174386 bytes public/branding/speakers/lulite.png | Bin 0 -> 141110 bytes public/branding/speakers/mekdim.png | Bin 0 -> 176045 bytes public/branding/speakers/samiya.png | Bin 0 -> 153038 bytes public/branding/speakers/sarma.png | Bin 0 -> 186510 bytes public/branding/speakers/solomon.png | Bin 0 -> 205521 bytes public/branding/speakers/sunil.png | Bin 0 -> 187941 bytes public/branding/speakers/tewabech.png | Bin 0 -> 194585 bytes public/branding/speakers/tigist.png | Bin 0 -> 234073 bytes public/branding/speakers/yared.png | Bin 0 -> 164817 bytes scripts/download-assets.mjs | 49 + tsconfig.json | 21 + wrangler.jsonc | 17 + 134 files changed, 15695 insertions(+) create mode 100644 .dev.vars create mode 100644 .env.example create mode 100644 .github/workflows/cloudflare-workers-opennext.yml create mode 100644 .gitignore create mode 100644 app/api/inquiry/route.ts create mode 100644 app/api/payment/route.ts create mode 100644 app/apple-icon.tsx create mode 100644 app/calendar/route.ts create mode 100644 app/contact/page.tsx create mode 100644 app/exhibit/page.tsx create mode 100644 app/globals.css create mode 100644 app/icon.tsx create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/partners/page.tsx create mode 100644 app/payment/page.tsx create mode 100644 app/payment/success/page.tsx create mode 100644 app/pitch-competition/page.tsx create mode 100644 app/privacy/page.tsx create mode 100644 app/program/page.tsx create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts create mode 100644 app/speakers/page.tsx create mode 100644 app/sponsor/page.tsx create mode 100644 components.json create mode 100644 components/brand/BrandLogo.tsx create mode 100644 components/brand/FooterTopographicBand.tsx create mode 100644 components/brand/PartnerLogoPlaceholder.tsx create mode 100644 components/brand/RiftFlowLines.tsx create mode 100644 components/brand/RiftPageFlow.tsx create mode 100644 components/brand/RiftSectionAccent.tsx create mode 100644 components/brand/rift-patterns.ts create mode 100644 components/event/AddToCalendar.tsx create mode 100644 components/exhibit/BoothPackages.tsx create mode 100644 components/exhibit/ExhibitorBoothForm.tsx create mode 100644 components/forms/DataConsentField.tsx create mode 100644 components/forms/InquiryForm.tsx create mode 100644 components/grants/CyclingGrantAmount.tsx create mode 100644 components/grants/GrantHeadline.tsx create mode 100644 components/home/AttendSummitSection.tsx create mode 100644 components/home/BoothAcquisitionBand.tsx create mode 100644 components/home/ExperienceCards.tsx create mode 100644 components/home/Faq.tsx create mode 100644 components/home/Hero.tsx create mode 100644 components/home/HeroGrantLine.tsx create mode 100644 components/home/Newsletter.tsx create mode 100644 components/home/PartnerMarquee.tsx create mode 100644 components/home/PurposeBand.tsx create mode 100644 components/home/PurposeGrantText.tsx create mode 100644 components/home/Speakers.tsx create mode 100644 components/home/SponsorTiers.tsx create mode 100644 components/home/StatsGrid.tsx create mode 100644 components/home/TicketsBand.tsx create mode 100644 components/home/TopicMarquee.tsx create mode 100644 components/home/Venue.tsx create mode 100644 components/layout/FooterNewsletter.tsx create mode 100644 components/layout/NavTicketsCta.tsx create mode 100644 components/layout/Section.tsx create mode 100644 components/layout/SiteFooter.tsx create mode 100644 components/layout/SiteHeader.tsx create mode 100644 components/partners/ChampionStartupModal.tsx create mode 100644 components/partners/PartnerCard.tsx create mode 100644 components/partners/PartnerSectionBlock.tsx create mode 100644 components/partners/PartnershipCtaBand.tsx create mode 100644 components/partners/PartnershipInquiryForm.tsx create mode 100644 components/payment/PaymentForm.tsx create mode 100644 components/seo/JsonLd.tsx create mode 100644 components/speakers/SpeakerCard.tsx create mode 100644 components/tickets/TicketCard.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 content/attend.ts create mode 100644 content/consent.ts create mode 100644 content/exhibit.ts create mode 100644 content/faq.ts create mode 100644 content/grants.ts create mode 100644 content/inquiries.ts create mode 100644 content/legal.ts create mode 100644 content/page-seo.ts create mode 100644 content/partners.ts create mode 100644 content/people.ts create mode 100644 content/pitch.ts create mode 100644 content/program.ts create mode 100644 content/site.ts create mode 100644 content/tickets.ts create mode 100644 content/tracks.ts create mode 100644 eslint.config.mjs create mode 100644 lib/calendar.ts create mode 100644 lib/inquiry.ts create mode 100644 lib/payment.ts create mode 100644 lib/seo.ts create mode 100644 lib/utils.ts create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/branding/booth-mockup.png create mode 100644 public/branding/logo-icon.png create mode 100644 public/branding/logo-wordmark.jpg create mode 100644 public/branding/logo.png create mode 100644 public/branding/speakers/abraham.png create mode 100644 public/branding/speakers/abrhame.png create mode 100644 public/branding/speakers/adam.png create mode 100644 public/branding/speakers/amity.png create mode 100644 public/branding/speakers/beamlak.png create mode 100644 public/branding/speakers/biruh.png create mode 100644 public/branding/speakers/brook.png create mode 100644 public/branding/speakers/dagmawit.png create mode 100644 public/branding/speakers/lulite.png create mode 100644 public/branding/speakers/mekdim.png create mode 100644 public/branding/speakers/samiya.png create mode 100644 public/branding/speakers/sarma.png create mode 100644 public/branding/speakers/solomon.png create mode 100644 public/branding/speakers/sunil.png create mode 100644 public/branding/speakers/tewabech.png create mode 100644 public/branding/speakers/tigist.png create mode 100644 public/branding/speakers/yared.png create mode 100644 scripts/download-assets.mjs create mode 100644 tsconfig.json create mode 100644 wrangler.jsonc diff --git a/.dev.vars b/.dev.vars new file mode 100644 index 0000000..a2a6158 --- /dev/null +++ b/.dev.vars @@ -0,0 +1 @@ +NEXTJS_ENV=development diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6424f93 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Future email integration (e.g. Resend) +# RESEND_API_KEY= +# INQUIRY_TO_EMAIL=partnerships@grvsummit.com diff --git a/.github/workflows/cloudflare-workers-opennext.yml b/.github/workflows/cloudflare-workers-opennext.yml new file mode 100644 index 0000000..a3de6d7 --- /dev/null +++ b/.github/workflows/cloudflare-workers-opennext.yml @@ -0,0 +1,28 @@ +name: Deploy to Cloudflare Workers (OpenNext) + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + + - name: Install + run: npm ci + + - name: Build and deploy with OpenNext + run: npm run deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6830bda --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ +.vercel/ +.open-next/ + + +# production +/build + +# misc +.DS_Store +*.pem +.env*.local +.env + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/app/api/inquiry/route.ts b/app/api/inquiry/route.ts new file mode 100644 index 0000000..dfe080c --- /dev/null +++ b/app/api/inquiry/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from "next/server"; +import { validateInquiry } from "@/lib/inquiry"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const result = validateInquiry(body); + + if (!result.ok) { + return NextResponse.json({ ok: false, error: result.error }, { status: 400 }); + } + + console.info("[GRV Summit Inquiry]", JSON.stringify(result.data, null, 2)); + + return NextResponse.json({ ok: true }); + } catch { + return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } +} diff --git a/app/api/payment/route.ts b/app/api/payment/route.ts new file mode 100644 index 0000000..b89e803 --- /dev/null +++ b/app/api/payment/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import { calculateTotal } from "@/lib/payment"; +import { validatePayment } from "@/lib/payment"; +import { ticketTiers } from "@/content/tickets"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const result = validatePayment(body); + + if (!result.ok) { + return NextResponse.json({ ok: false, error: result.error }, { status: 400 }); + } + + const tier = ticketTiers.find((t) => t.id === result.data.ticketId)!; + const totalUsd = calculateTotal(result.data.ticketId, result.data.quantity); + + const record = { + ...result.data, + ticketName: tier.name, + totalUsd, + status: "pending", + note: "v1 stub — wire payment provider (Stripe/Chapa) in production", + }; + + console.info("[GRV Summit Payment]", JSON.stringify(record, null, 2)); + + return NextResponse.json({ + ok: true, + orderId: `GRV-${Date.now()}`, + totalUsd, + paymentMethod: result.data.paymentMethod, + }); + } catch { + return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); + } +} diff --git a/app/apple-icon.tsx b/app/apple-icon.tsx new file mode 100644 index 0000000..96168f2 --- /dev/null +++ b/app/apple-icon.tsx @@ -0,0 +1,41 @@ +import { ImageResponse } from "next/og"; + +export const size = { width: 180, height: 180 }; +export const contentType = "image/png"; + +export default function AppleIcon() { + return new ImageResponse( + ( +
+
+
+ GREAT RIFT +
+
VALLEY
+
+ Innovation Summit +
+
+ ), + { ...size } + ); +} diff --git a/app/calendar/route.ts b/app/calendar/route.ts new file mode 100644 index 0000000..8c4d5aa --- /dev/null +++ b/app/calendar/route.ts @@ -0,0 +1,12 @@ +import { buildIcsFileContent } from "@/lib/calendar"; + +export function GET() { + const ics = buildIcsFileContent(); + + return new Response(ics, { + headers: { + "Content-Type": "text/calendar; charset=utf-8", + "Content-Disposition": 'attachment; filename="grv-summit.ics"', + }, + }); +} diff --git a/app/contact/page.tsx b/app/contact/page.tsx new file mode 100644 index 0000000..3bc60d2 --- /dev/null +++ b/app/contact/page.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from "next"; +import { Section } from "@/components/layout/Section"; +import { InquiryForm } from "@/components/forms/InquiryForm"; +import { inquiryChannels } from "@/content/inquiries"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +export const metadata: Metadata = createPageMetadata(pageSeo.contact); + +export default function ContactPage() { + return ( + <> +
+

Contact us

+

+ Reach the right team for registration, exhibitions, sponsorship, or media inquiries. +

+
+ {inquiryChannels.map((ch) => ( + + + {ch.label} + {ch.description} + + + + {ch.email} + + + + ))} +
+
+
+

Send a message

+
+ +
+
+ + ); +} diff --git a/app/exhibit/page.tsx b/app/exhibit/page.tsx new file mode 100644 index 0000000..215b7ad --- /dev/null +++ b/app/exhibit/page.tsx @@ -0,0 +1,79 @@ +import type { Metadata } from "next"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import Image from "next/image"; +import { exhibitCopy } from "@/content/exhibit"; +import { Section } from "@/components/layout/Section"; +import { BoothPackages } from "@/components/exhibit/BoothPackages"; +import { ExhibitorBoothForm } from "@/components/exhibit/ExhibitorBoothForm"; +import { Faq } from "@/components/home/Faq"; + +export const metadata: Metadata = createPageMetadata(pageSeo.exhibit); + +const benefits = [ + "Connect with 500+ investors, startups, and professionals", + "Showcase and advertise your products in the exhibitor hall", + "Meet emerging talent in agriculture, health, and education", + "Brand visibility across summit programming and materials", +]; + +export default function ExhibitPage() { + return ( + <> +
+
+
+

+ {exhibitCopy.eyebrow} +

+

{exhibitCopy.headline}

+

{exhibitCopy.subheadline}

+
    + {benefits.map((b) => ( +
  • + + {b} +
  • + ))} +
+
+
+ Exhibition booth +
+
+
+ +
+

Booth packages

+

+ Choose a footprint that fits how you want to present your brand and products. Final + placement and pricing are confirmed by our exhibitions team. +

+
+ +
+
+ +
+
+

Acquire your booth

+

+ Complete the form below with details about your company, the products you want to + advertise, and your booth requirements. We will follow up with availability and next + steps. +

+
+ +
+
+
+ + + + ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..0402e69 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,191 @@ +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --font-sans: var(--font-body); + --font-display: var(--font-display); + --color-brand-green: #1a5c38; + --color-brand-green-dark: #0d3d26; + --color-brand-gold: #ffb300; + --color-brand-blue: #1f3d7e; + --color-brand-navy: #0f0404; + --color-surface-muted: #f7f7f7; + --color-text-muted: #767676; +} + +:root { + --radius: 0.75rem; + --background: #ffffff; + --foreground: #0d3d26; + --card: #ffffff; + --card-foreground: #0d3d26; + --popover: #ffffff; + --popover-foreground: #0d3d26; + /* Primary: brand green · Secondary: white */ + --primary: #1a5c38; + --primary-foreground: #ffffff; + --secondary: #ffffff; + --secondary-foreground: #1a5c38; + --muted: #f0f5f2; + --muted-foreground: #5a6b62; + --accent: #ffb300; + --accent-foreground: #0d3d26; + --destructive: #dc2626; + --border: #dce8e0; + --input: #dce8e0; + --ring: #1a5c38; + --hero: #0d3d26; + --section-muted: #f0f5f2; + --section-inverse: #1a5c38; +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground antialiased; + font-family: var(--font-body), system-ui, sans-serif; + } + h1, + h2, + h3, + h4 { + font-family: var(--font-display), system-ui, sans-serif; + letter-spacing: -0.02em; + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } + .section-inverse { + background: var(--section-inverse); + color: #fafafa; + } + .section-muted { + background: var(--section-muted); + } + .grain { + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E"); + } + .marquee { + animation: marquee 40s linear infinite; + } + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } + } + @media (prefers-reduced-motion: reduce) { + .marquee { + animation: none; + } + } + .ticket-notch::before, + .ticket-notch::after { + content: ""; + position: absolute; + left: 50%; + width: 1.5rem; + height: 1.5rem; + transform: translateX(-50%); + border-radius: 9999px; + background: var(--section-inverse, #0a0a0a); + } + .ticket-notch::before { + top: -0.75rem; + } + .ticket-notch::after { + bottom: -0.75rem; + } + + /* Navbar tickets CTA — glow + text + arrow */ + @keyframes ticket-glow { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(255, 179, 0, 0.4); + } + 50% { + box-shadow: 0 0 0 10px rgba(255, 179, 0, 0); + } + } + @keyframes ticket-text-pulse { + 0%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.06); + } + } + @keyframes ticket-arrow-nudge { + 0%, + 100% { + transform: translateX(0); + } + 50% { + transform: translateX(4px); + } + } + .ticket-cta-pulse { + animation: ticket-glow 2.2s ease-in-out infinite; + } + .ticket-cta-text { + display: inline-block; + animation: ticket-text-pulse 2.2s ease-in-out infinite; + } + .ticket-cta-arrow { + animation: ticket-arrow-nudge 2.2s ease-in-out infinite; + } + + @keyframes ticket-card-enter { + from { + opacity: 0; + transform: translateY(24px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + .ticket-card-enter { + animation: ticket-card-enter 0.6s ease-out both; + } + + @media (prefers-reduced-motion: reduce) { + .ticket-cta-pulse, + .ticket-cta-text, + .ticket-cta-arrow, + .ticket-card-enter { + animation: none; + } + } +} diff --git a/app/icon.tsx b/app/icon.tsx new file mode 100644 index 0000000..0d5949b --- /dev/null +++ b/app/icon.tsx @@ -0,0 +1,54 @@ +import { ImageResponse } from "next/og"; + +export const size = { width: 32, height: 32 }; +export const contentType = "image/png"; + +export default function Icon() { + return new ImageResponse( + ( +
+
+ GRV +
+
+ GREAT RIFT +
+
+ ), + { ...size } + ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..267a1bb --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,39 @@ +import { Syne, DM_Sans } from "next/font/google"; +import { RiftPageFlow } from "@/components/brand/RiftPageFlow"; +import { JsonLd } from "@/components/seo/JsonLd"; +import { SiteHeader } from "@/components/layout/SiteHeader"; +import { SiteFooter } from "@/components/layout/SiteFooter"; +import { rootMetadata } from "@/lib/seo"; +import "./globals.css"; + +export const metadata = rootMetadata; + +const display = Syne({ + subsets: ["latin"], + variable: "--font-display", +}); + +const body = DM_Sans({ + subsets: ["latin"], + variable: "--font-body", +}); + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + +
+ +
{children}
+
+ + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..dcce251 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import { Hero } from "@/components/home/Hero"; +import { PartnerMarquee } from "@/components/home/PartnerMarquee"; +import { StatsGrid } from "@/components/home/StatsGrid"; +import { PurposeBand } from "@/components/home/PurposeBand"; +import { TopicMarquee } from "@/components/home/TopicMarquee"; +import { ExperienceCards } from "@/components/home/ExperienceCards"; +import { BoothAcquisitionBand } from "@/components/home/BoothAcquisitionBand"; +import { AttendSummitSection } from "@/components/home/AttendSummitSection"; +import { Speakers } from "@/components/home/Speakers"; +import { SponsorTiers } from "@/components/home/SponsorTiers"; +import { TicketsBand } from "@/components/home/TicketsBand"; +import { Faq } from "@/components/home/Faq"; +import { Venue } from "@/components/home/Venue"; + +export const metadata: Metadata = createPageMetadata(pageSeo.home); + +export default function HomePage() { + return ( + <> + + + + + + + + + + + + + + + ); +} diff --git a/app/partners/page.tsx b/app/partners/page.tsx new file mode 100644 index 0000000..2527eef --- /dev/null +++ b/app/partners/page.tsx @@ -0,0 +1,75 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import { + partnersIntro, + sponsorSections, + exhibitorSections, + supporterSections, + mediaPartnerSections, +} from "@/content/partners"; +import { Section } from "@/components/layout/Section"; +import { PartnerSectionBlock } from "@/components/partners/PartnerSectionBlock"; +import { PartnershipCtaBand } from "@/components/partners/PartnershipCtaBand"; +import { Button } from "@/components/ui/button"; +import { ChampionStartupModal } from "@/components/partners/ChampionStartupModal"; + +export const metadata: Metadata = createPageMetadata(pageSeo.partners); + +export default function PartnersPage() { + return ( + <> +
+

+ {partnersIntro.eyebrow} +

+

+ {partnersIntro.headline} +

+

+ {partnersIntro.subheadline} +

+
+ + +
+
+ +
+
+ {sponsorSections.map((section, index) => ( + + ))} +
+
+ +
+
+ {exhibitorSections.map((section) => ( + + ))} +
+
+ +
+
+ {supporterSections.map((section) => ( + + ))} + {mediaPartnerSections.map((section) => ( + + ))} +
+
+ + + + ); +} diff --git a/app/payment/page.tsx b/app/payment/page.tsx new file mode 100644 index 0000000..348654b --- /dev/null +++ b/app/payment/page.tsx @@ -0,0 +1,71 @@ +import type { Metadata } from "next"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import { Suspense } from "react"; +import Link from "next/link"; +import { site } from "@/content/site"; +import { ticketTiers } from "@/content/tickets"; +import { Section } from "@/components/layout/Section"; +import { PaymentForm } from "@/components/payment/PaymentForm"; +import { AddToCalendar } from "@/components/event/AddToCalendar"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + +export const metadata: Metadata = createPageMetadata(pageSeo.payment); + +export default function PaymentPage() { + return ( + <> +
+
+
+

+ {site.dates.label} +

+

Tickets & payment

+

+ Secure your place at {site.venue.name}, {site.venue.address}. Choose a pass and + complete checkout below. +

+
+ +
+ +
+ {ticketTiers.map((tier) => ( + + + {tier.name} + {tier.description} + + +

${tier.priceUsd}

+

per ticket · USD

+
    + {tier.features.slice(0, 3).map((f) => ( +
  • · {f}
  • + ))} +
+
+ + + +
+ ))} +
+
+ +
+

Checkout

+

Complete your registration in a few steps.

+
+ Loading checkout…

}> + +
+
+
+ + ); +} diff --git a/app/payment/success/page.tsx b/app/payment/success/page.tsx new file mode 100644 index 0000000..31d291e --- /dev/null +++ b/app/payment/success/page.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from "next"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import Link from "next/link"; +import { CheckCircle2 } from "lucide-react"; +import { Section } from "@/components/layout/Section"; +import { Button } from "@/components/ui/button"; +import { AddToCalendar } from "@/components/event/AddToCalendar"; + +export const metadata: Metadata = createPageMetadata(pageSeo.paymentSuccess); + +type Props = { + searchParams: Promise<{ order?: string; total?: string }>; +}; + +export default async function PaymentSuccessPage({ searchParams }: Props) { + const params = await searchParams; + const orderId = params.order ?? "GRV-ORDER"; + const total = params.total ? `$${params.total} USD` : null; + + return ( +
+
+ +

Thank you for your order

+

+ Your registration has been received. Order reference:{" "} + {orderId} + {total && ( + <> + {" "} + · Total: {total} + + )} +

+

+ A confirmation email will be sent once payment processing is connected. For now, our + team has logged your request. +

+
+ + +
+
+
+ ); +} diff --git a/app/pitch-competition/page.tsx b/app/pitch-competition/page.tsx new file mode 100644 index 0000000..ca25ec4 --- /dev/null +++ b/app/pitch-competition/page.tsx @@ -0,0 +1,61 @@ +import type { Metadata } from "next"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import Link from "next/link"; +import { pitchCompetition } from "@/content/pitch"; +import { site } from "@/content/site"; +import { Section } from "@/components/layout/Section"; +import { GrantHeadline } from "@/components/grants/GrantHeadline"; +import { Button } from "@/components/ui/button"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; + +export const metadata: Metadata = createPageMetadata(pageSeo.pitch); + +export default function PitchCompetitionPage() { + return ( + <> +
+

+ Pitch competition +

+

+ +

+

{pitchCompetition.subheadline}

+

+ {pitchCompetition.description} +

+ +
+
+

Award criteria

+
    + {pitchCompetition.criteria.map((c) => ( +
  • + + {c} +
  • + ))} +
+
+
+

Timeline

+ + {pitchCompetition.timeline.map((t) => ( + + {t.phase} + {t.date} + + ))} + +
+ + ); +} diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx new file mode 100644 index 0000000..3d9da63 --- /dev/null +++ b/app/privacy/page.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { privacyPolicy } from "@/content/legal"; +import { pageSeo } from "@/content/page-seo"; +import { Section } from "@/components/layout/Section"; +import { createPageMetadata } from "@/lib/seo"; +import { Button } from "@/components/ui/button"; + +export const metadata: Metadata = createPageMetadata(pageSeo.privacy); + +export default function PrivacyPage() { + return ( +
+

Legal

+

{privacyPolicy.title}

+

Last updated: {privacyPolicy.updated}

+

+ {privacyPolicy.intro} +

+
+ {privacyPolicy.sections.map((section) => ( +
+

{section.heading}

+

{section.body}

+
+ ))} +
+
+ +
+
+ ); +} diff --git a/app/program/page.tsx b/app/program/page.tsx new file mode 100644 index 0000000..c7b0164 --- /dev/null +++ b/app/program/page.tsx @@ -0,0 +1,65 @@ +import type { Metadata } from "next"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import Image from "next/image"; +import Link from "next/link"; +import { programDays } from "@/content/program"; +import { pillars } from "@/content/tracks"; +import { Section } from "@/components/layout/Section"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +export const metadata: Metadata = createPageMetadata(pageSeo.program); + +export default function ProgramPage() { + return ( + <> +
+

Event program

+

+ Two days of workshops, panels, exhibition, and the Great Rift Valley Pitch Competition at + Skylight Hotel, Addis Ababa. +

+
+ {programDays.map((day) => ( + +
+ {day.title} +
+ +

{day.date}

+ {day.title} + {day.description} +
+ +
    + {day.highlights.map((h) => ( +
  • · {h}
  • + ))} +
+
+
+ ))} +
+
+

Innovation pillars

+
+ {pillars.map((p) => ( + + + {p.title} + + +

{p.description}

+
+
+ ))} +
+
+ +
+ + ); +} diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..784a8da --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,13 @@ +import type { MetadataRoute } from "next"; +import { SITE_URL } from "@/lib/seo"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + disallow: ["/api/", "/payment/success"], + }, + sitemap: `${SITE_URL}/sitemap.xml`, + }; +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..1185cce --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,11 @@ +import type { MetadataRoute } from "next"; +import { staticRoutes, SITE_URL } from "@/lib/seo"; + +export default function sitemap(): MetadataRoute.Sitemap { + return staticRoutes.map((route) => ({ + url: `${SITE_URL}${route.path === "/" ? "" : route.path}`, + lastModified: new Date(), + changeFrequency: route.changeFrequency, + priority: route.priority, + })); +} diff --git a/app/speakers/page.tsx b/app/speakers/page.tsx new file mode 100644 index 0000000..bb2ebcd --- /dev/null +++ b/app/speakers/page.tsx @@ -0,0 +1,68 @@ +import type { Metadata } from "next"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import Link from "next/link"; +import { + speakers, + speakerGroupLabels, + speakerGroupOrder, + type SpeakerGroup, +} from "@/content/people"; +import { site } from "@/content/site"; +import { Section } from "@/components/layout/Section"; +import { SpeakerCard } from "@/components/speakers/SpeakerCard"; +import { Button } from "@/components/ui/button"; + +export const metadata: Metadata = createPageMetadata(pageSeo.speakers); + +export default function SpeakersPage() { + const grouped = speakerGroupOrder.reduce( + (acc, group) => { + const list = speakers.filter((s) => s.group === group); + if (list.length) acc[group] = list; + return acc; + }, + {} as Partial> + ); + + return ( + <> +
+

Lineup

+

+ Summit speakers & judges +

+

+ {site.dates.label} · {site.venue.name} +

+
+ +
+
+ {(Object.entries(grouped) as [SpeakerGroup, typeof speakers][]).map( + ([group, list]) => ( +
+
+
+

{speakerGroupLabels[group]}

+

{site.dates.label}

+
+
+
+ {list.map((speaker) => ( + + ))} +
+
+ ) + )} +
+
+ +
+
+ + ); +} diff --git a/app/sponsor/page.tsx b/app/sponsor/page.tsx new file mode 100644 index 0000000..6230fd6 --- /dev/null +++ b/app/sponsor/page.tsx @@ -0,0 +1,70 @@ +import type { Metadata } from "next"; +import { pageSeo } from "@/content/page-seo"; +import { createPageMetadata } from "@/lib/seo"; +import { Section } from "@/components/layout/Section"; +import { InquiryForm } from "@/components/forms/InquiryForm"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +export const metadata: Metadata = createPageMetadata(pageSeo.sponsor); + +const tiers = [ + { + name: "Lead Sponsor", + description: "Premier visibility across all summit touchpoints and keynote branding.", + perks: ["Logo on all materials", "Keynote introduction", "VIP hospitality", "Speaking slot"], + }, + { + name: "Gold Sponsor", + description: "High-impact brand presence in exhibition and programming.", + perks: ["Exhibition branding", "Panel sponsorship", "Digital promotion", "4 VIP passes"], + }, + { + name: "Supporting Sponsor", + description: "Align your brand with Ethiopia's innovation mission.", + perks: ["Website listing", "Program mention", "2 passes", "Newsletter feature"], + }, +]; + +export default function SponsorPage() { + return ( + <> +
+

Sponsor

+

+ Partner with Ethiopia's flagship innovation summit +

+

+ Support the Ethiopian Diaspora Trust Fund's mission to foster tech-enabled innovation. + Sponsorship connects your organization with investors, founders, and leaders across + agriculture, healthcare, and education. +

+
+ {tiers.map((tier) => ( + + + {tier.name} + {tier.description} + + +
    + {tier.perks.map((p) => ( +
  • · {p}
  • + ))} +
+
+
+ ))} +
+
+
+

Sponsorship inquiry

+

+ Share your goals and we'll tailor a partnership package for your organization. +

+
+ +
+
+ + ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/brand/BrandLogo.tsx b/components/brand/BrandLogo.tsx new file mode 100644 index 0000000..5417faf --- /dev/null +++ b/components/brand/BrandLogo.tsx @@ -0,0 +1,106 @@ +import Image from "next/image"; +import Link from "next/link"; +import { cn } from "@/lib/utils"; + +const siteName = "Great Rift Valley Innovation Summit"; + +type BrandLogoProps = { + className?: string; + href?: string; + /** Header/footer wordmark, or icon-only */ + variant?: "default" | "footer" | "icon"; + /** Tighter sizing for the floating navbar */ + compact?: boolean; +}; + +export function BrandLogo({ + className, + href = "/", + variant = "default", + compact = false, +}: BrandLogoProps) { + const isFooter = variant === "footer"; + + const markSize = compact ? "size-8" : "size-9 md:size-10"; + const markPad = isFooter ? "rounded-md bg-white p-1 shadow-sm" : "rounded-md"; + + const mark = ( + + + + ); + + if (variant === "icon") { + return ( + + {mark} + + ); + } + + const primaryClass = isFooter + ? "text-white" + : "text-[#1a5c38]"; + const secondaryClass = isFooter + ? "text-white/85" + : "text-[#0d3d26]/90"; + + const primarySize = compact + ? "text-[11px] sm:text-xs md:text-[13px]" + : isFooter + ? "text-sm md:text-base" + : "text-xs sm:text-sm md:text-base"; + const secondarySize = compact + ? "text-[8px] sm:text-[9px]" + : "text-[9px] sm:text-[10px]"; + + const content = ( + + {mark} + + + Great Rift Valley + + + Innovation Summit + + + + ); + + if (!href) return content; + + return ( + + {content} + + ); +} diff --git a/components/brand/FooterTopographicBand.tsx b/components/brand/FooterTopographicBand.tsx new file mode 100644 index 0000000..b55db70 --- /dev/null +++ b/components/brand/FooterTopographicBand.tsx @@ -0,0 +1,108 @@ +import { cn } from "@/lib/utils"; + +type Props = { + className?: string; +}; + +/** + * Curved green / white bands like the GRV logo topography. + * Overlaps the top of the footer — no straight stripe bar. + */ +export function FooterTopographicBand({ className }: Props) { + const contour = "rgba(255,255,255,0.35)"; + + return ( +
+ + {/* Alternating curved bands (logo-style flow, not vertical stripes) */} + + + + + + + {/* Logo-like contour strokes */} + + + + + + + {/* Fade into solid footer green */} +
+
+ ); +} diff --git a/components/brand/PartnerLogoPlaceholder.tsx b/components/brand/PartnerLogoPlaceholder.tsx new file mode 100644 index 0000000..54cc947 --- /dev/null +++ b/components/brand/PartnerLogoPlaceholder.tsx @@ -0,0 +1,28 @@ +import { cn } from "@/lib/utils"; + +type Props = { + className?: string; + size?: "sm" | "md" | "lg"; +}; + +const sizeClasses = { + sm: "h-12 min-w-[100px] px-4 text-xs", + md: "h-16 min-w-[140px] px-6 text-sm", + lg: "h-20 min-w-[180px] px-8 text-base", +}; + +export function PartnerLogoPlaceholder({ className, size = "md" }: Props) { + return ( +
+ Your logo here +
+ ); +} diff --git a/components/brand/RiftFlowLines.tsx b/components/brand/RiftFlowLines.tsx new file mode 100644 index 0000000..b428438 --- /dev/null +++ b/components/brand/RiftFlowLines.tsx @@ -0,0 +1,116 @@ +import { cn } from "@/lib/utils"; + +type FlowProps = { + className?: string; + variant?: "divider" | "card" | "section" | "footer"; + inverse?: boolean; +}; + +/** Small-scale decorative lines (dividers, footer band) — keep subtle */ +export function RiftFlowLines({ + className, + variant = "divider", + inverse = false, +}: FlowProps) { + const green = inverse ? "rgba(255,255,255,0.35)" : "rgba(26,92,56,0.25)"; + const soft = inverse ? "rgba(255,255,255,0.15)" : "rgba(26,92,56,0.12)"; + + if (variant === "card") { + return ( + + + + ); + } + + if (variant === "section") { + return ( + + + + ); + } + + if (variant === "footer") { + return ( + + + + + + ); + } + + return ( + + + + + ); +} + +/** Subtle link between ticket card blocks */ +export function RiftCardConnector({ className }: { className?: string }) { + return ( +
+
+
+ ); +} diff --git a/components/brand/RiftPageFlow.tsx b/components/brand/RiftPageFlow.tsx new file mode 100644 index 0000000..ea18b43 --- /dev/null +++ b/components/brand/RiftPageFlow.tsx @@ -0,0 +1,70 @@ +import { cn } from "@/lib/utils"; + +/** + * Continuous valley lines from top to bottom of the page. + * Sits behind content; sections use lighter accents on top. + */ +export function RiftPageFlow() { + return ( +
+ {/* Primary spine — left */} + + + + + + {/* Secondary thread — right (different rhythm) */} + + + + + + {/* Center whisper — only on xl, very faint */} + +
+ ); +} diff --git a/components/brand/RiftSectionAccent.tsx b/components/brand/RiftSectionAccent.tsx new file mode 100644 index 0000000..d3377ad --- /dev/null +++ b/components/brand/RiftSectionAccent.tsx @@ -0,0 +1,133 @@ +import type { RiftPattern } from "@/components/brand/rift-patterns"; +import { cn } from "@/lib/utils"; + +type Props = { + pattern: RiftPattern; + inverse?: boolean; +}; + +/** Light section-local accent — does not compete with page spine */ +export function RiftSectionAccent({ pattern, inverse }: Props) { + if (pattern === "none") return null; + + const mainStroke = inverse ? "rgba(255,255,255,0.25)" : "rgba(26,92,56,0.2)"; + const softStroke = inverse ? "rgba(255,255,255,0.12)" : "rgba(26,92,56,0.1)"; + + if (pattern === "whisper") { + return ( +
+ ); + } + + if (pattern === "vein-left") { + return ( + + + + ); + } + + if (pattern === "vein-right") { + return ( + + + + ); + } + + if (pattern === "arc-top") { + return ( + + + + + ); + } + + if (pattern === "arc-bottom") { + return ( + + + + ); + } + + if (pattern === "fork") { + return ( + + + + ); + } + + return null; +} diff --git a/components/brand/rift-patterns.ts b/components/brand/rift-patterns.ts new file mode 100644 index 0000000..e3800ac --- /dev/null +++ b/components/brand/rift-patterns.ts @@ -0,0 +1,9 @@ +/** Subtle per-section line treatments — paired with page-level RiftPageFlow */ +export type RiftPattern = + | "none" + | "whisper" + | "arc-top" + | "arc-bottom" + | "vein-left" + | "vein-right" + | "fork"; diff --git a/components/event/AddToCalendar.tsx b/components/event/AddToCalendar.tsx new file mode 100644 index 0000000..7b83b76 --- /dev/null +++ b/components/event/AddToCalendar.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { CalendarPlus, Download } from "lucide-react"; +import { buildGoogleCalendarUrl, buildOutlookCalendarUrl } from "@/lib/calendar"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +type Props = { + className?: string; + variant?: "default" | "outline" | "inverse"; +}; + +export function AddToCalendar({ className, variant = "outline" }: Props) { + const googleUrl = buildGoogleCalendarUrl(); + const outlookUrl = buildOutlookCalendarUrl(); + + const buttonClass = + variant === "inverse" + ? "rounded-full border-white/30 bg-transparent text-white hover:bg-white/10" + : variant === "default" + ? "rounded-full bg-[#ffb300] text-[#0f0404] hover:bg-[#ffb300]/90" + : "rounded-full"; + + return ( + + + + + + + + Google Calendar + + + + + Outlook + + + + + + Apple / iCal (.ics) + + + + + ); +} diff --git a/components/exhibit/BoothPackages.tsx b/components/exhibit/BoothPackages.tsx new file mode 100644 index 0000000..c33c8c5 --- /dev/null +++ b/components/exhibit/BoothPackages.tsx @@ -0,0 +1,28 @@ +import { boothPackages } from "@/content/exhibit"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +export function BoothPackages() { + return ( +
+ {boothPackages.map((pkg) => ( + + + {pkg.name} + {pkg.size} + + +

{pkg.description}

+
    + {pkg.highlights.map((h) => ( +
  • + + {h} +
  • + ))} +
+
+
+ ))} +
+ ); +} diff --git a/components/exhibit/ExhibitorBoothForm.tsx b/components/exhibit/ExhibitorBoothForm.tsx new file mode 100644 index 0000000..b082342 --- /dev/null +++ b/components/exhibit/ExhibitorBoothForm.tsx @@ -0,0 +1,264 @@ +"use client"; + +import { useState } from "react"; +import { boothSizes, exhibitorSectors } from "@/content/exhibit"; +import { DataConsentField } from "@/components/forms/DataConsentField"; +import { dataConsent } from "@/content/consent"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +export function ExhibitorBoothForm() { + const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); + const [error, setError] = useState(""); + const [consent, setConsent] = useState(false); + const [sector, setSector] = useState(""); + const [boothSize, setBoothSize] = useState(""); + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!consent) { + setError(dataConsent.errorMessage); + return; + } + if (!sector) { + setError("Please select your industry / sector."); + return; + } + if (!boothSize) { + setError("Please select a preferred booth size."); + return; + } + + setStatus("loading"); + setError(""); + + const form = e.currentTarget; + const data = new FormData(form); + + try { + const res = await fetch("/api/inquiry", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + intent: "exhibitor", + firstName: data.get("firstName"), + lastName: data.get("lastName"), + name: `${data.get("firstName")} ${data.get("lastName")}`, + email: data.get("email"), + phone: data.get("phone"), + jobTitle: data.get("jobTitle"), + company: data.get("company"), + companyWebsite: data.get("companyWebsite") || undefined, + companyDescription: data.get("companyDescription"), + sector, + productsToAdvertise: data.get("productsToAdvertise"), + boothSize, + powerRequired: data.get("powerRequired") === "on", + internetRequired: data.get("internetRequired") === "on", + staffCount: data.get("staffCount") || undefined, + marketingMaterials: data.get("marketingMaterials") || undefined, + specialRequirements: data.get("specialRequirements") || undefined, + message: data.get("boothGoals"), + consent: true, + }), + }); + const json = await res.json(); + if (!res.ok || !json.ok) { + throw new Error(json.error || "Something went wrong"); + } + setStatus("success"); + form.reset(); + setConsent(false); + setSector(""); + setBoothSize(""); + } catch (err) { + setStatus("error"); + setError(err instanceof Error ? err.message : "Failed to send"); + } + } + + return ( +
+
+

Reserve your booth

+

+ Tell us about your company and what you plan to showcase. Our team will confirm + availability and send package details. +

+
+ +
+ + Contact + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ +
+ + Company & products + +
+ + +
+
+ + +
+
+ +