From e41fed86fcd91ac158630f063c51fe7ee027e94d Mon Sep 17 00:00:00 2001 From: Manceraider24 Date: Fri, 29 May 2026 18:36:36 +0000 Subject: [PATCH 1/4] feat: add reusable SEO component and integrate across pages --- apps/web/app/auth/layout.tsx | 12 ++++++++++++ apps/web/app/discover/layout.tsx | 12 ++++++++++++ apps/web/app/events/[id]/page.tsx | 18 ++++++++++++++++++ apps/web/app/home/layout.tsx | 12 ++++++++++++ apps/web/components/layout/seo.tsx | 28 ++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 apps/web/app/auth/layout.tsx create mode 100644 apps/web/app/discover/layout.tsx create mode 100644 apps/web/app/home/layout.tsx create mode 100644 apps/web/components/layout/seo.tsx diff --git a/apps/web/app/auth/layout.tsx b/apps/web/app/auth/layout.tsx new file mode 100644 index 00000000..5df4bdc4 --- /dev/null +++ b/apps/web/app/auth/layout.tsx @@ -0,0 +1,12 @@ +import { buildMetadata } from "@/components/layout/seo"; + +export const metadata = buildMetadata({ + title: "Sign In", + description: + "Sign in or create your Agora account to discover events, buy tickets, and connect with communities worldwide.", + path: "/auth", +}); + +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/apps/web/app/discover/layout.tsx b/apps/web/app/discover/layout.tsx new file mode 100644 index 00000000..19b40fcb --- /dev/null +++ b/apps/web/app/discover/layout.tsx @@ -0,0 +1,12 @@ +import { buildMetadata } from "@/components/layout/seo"; + +export const metadata = buildMetadata({ + title: "Discover Events", + description: + "Browse and discover the best tech, crypto, wellness, and community events happening near you and around the world.", + path: "/discover", +}); + +export default function DiscoverLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/apps/web/app/events/[id]/page.tsx b/apps/web/app/events/[id]/page.tsx index dd86c681..889e70c8 100644 --- a/apps/web/app/events/[id]/page.tsx +++ b/apps/web/app/events/[id]/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from "next"; import Image from "next/image"; import { Navbar } from "@/components/layout/navbar"; import { Footer } from "@/components/layout/footer"; @@ -5,6 +6,23 @@ import { dataEvents } from "@/components/events/mockups"; import { RegistrationBox } from "@/components/events/registration-box"; import { notFound } from "next/navigation"; import MapClient from "@/components/events/map-client"; +import { buildMetadata } from "@/components/layout/seo"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ id: string }>; +}): Promise { + const { id } = await params; + const event = dataEvents.find((e) => e.id === parseInt(id)); + if (!event) return {}; + return buildMetadata({ + title: event.title, + description: `Join us for ${event.title} on ${event.date} in ${event.location}. ${event.price === "Free" ? "Free entry." : `Tickets from $${event.price}.`} Secure your spot on Agora.`, + image: event.imageUrl, + path: `/events/${id}`, + }); +} export default async function EventDetailPage({ params, diff --git a/apps/web/app/home/layout.tsx b/apps/web/app/home/layout.tsx new file mode 100644 index 00000000..723f7bf4 --- /dev/null +++ b/apps/web/app/home/layout.tsx @@ -0,0 +1,12 @@ +import { buildMetadata } from "@/components/layout/seo"; + +export const metadata = buildMetadata({ + title: "Home", + description: + "Your personalized Agora feed — upcoming events, events you're hosting, and community picks tailored for you.", + path: "/home", +}); + +export default function HomeLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/apps/web/components/layout/seo.tsx b/apps/web/components/layout/seo.tsx new file mode 100644 index 00000000..60bb7bf6 --- /dev/null +++ b/apps/web/components/layout/seo.tsx @@ -0,0 +1,28 @@ +import type { Metadata } from "next"; + +const BASE_URL = "https://agora.events"; +const DEFAULT_IMAGE = "/og-image.png"; + +interface SEOProps { + title: string; + description: string; + image?: string; + path?: string; +} + +export function buildMetadata({ title, description, image, path }: SEOProps): Metadata { + const url = path ? `${BASE_URL}${path}` : BASE_URL; + const ogImage = image ?? DEFAULT_IMAGE; + + return { + title, + description, + openGraph: { + title, + description, + url, + images: [{ url: ogImage, width: 1200, height: 630, alt: title }], + type: "website", + }, + }; +} From 5f6ab037b46568ada071a3d69e0a896cbc051163 Mon Sep 17 00:00:00 2001 From: Manceraider24 Date: Fri, 29 May 2026 18:56:51 +0000 Subject: [PATCH 2/4] feat: replace lucide-react with inline SVG icon components --- apps/web/app/create-event/page.tsx | 2 +- apps/web/app/events/create/page.tsx | 2 +- .../help/[category]/[slug]/client-sidebar.tsx | 2 +- apps/web/components/events/TicketModal.tsx | 2 +- apps/web/components/ui/icons.tsx | 112 ++++++++++++++++++ apps/web/package.json | 1 - 6 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 apps/web/components/ui/icons.tsx diff --git a/apps/web/app/create-event/page.tsx b/apps/web/app/create-event/page.tsx index cb145d8c..d0418e2d 100644 --- a/apps/web/app/create-event/page.tsx +++ b/apps/web/app/create-event/page.tsx @@ -7,7 +7,7 @@ import { motion } from "framer-motion"; import { Navbar } from "@/components/layout/navbar"; import { Footer } from "@/components/layout/footer"; import { Button } from "@/components/ui/button"; -import { CheckCircle2, Home, ExternalLink } from "lucide-react"; +import { CheckCircle2, Home, ExternalLink } from "@/components/ui/icons"; import { toast } from "sonner"; export default function CreateEventPage() { diff --git a/apps/web/app/events/create/page.tsx b/apps/web/app/events/create/page.tsx index 653c53b9..d2ca1997 100644 --- a/apps/web/app/events/create/page.tsx +++ b/apps/web/app/events/create/page.tsx @@ -1,5 +1,5 @@ import CreateEventForm from "@/components/events/create-event-form"; -import { Camera } from "lucide-react"; +import { Camera } from "@/components/ui/icons"; export default function CreateEventPage() { return ( diff --git a/apps/web/app/help/[category]/[slug]/client-sidebar.tsx b/apps/web/app/help/[category]/[slug]/client-sidebar.tsx index 62b53f18..fe73cb5b 100644 --- a/apps/web/app/help/[category]/[slug]/client-sidebar.tsx +++ b/apps/web/app/help/[category]/[slug]/client-sidebar.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import Link from "next/link"; -import { ChevronDown, ChevronUp } from "lucide-react"; +import { ChevronDown, ChevronUp } from "@/components/ui/icons"; import type { Article } from "../../data"; interface ClientSidebarProps { diff --git a/apps/web/components/events/TicketModal.tsx b/apps/web/components/events/TicketModal.tsx index ec5242a9..96629e64 100644 --- a/apps/web/components/events/TicketModal.tsx +++ b/apps/web/components/events/TicketModal.tsx @@ -4,7 +4,7 @@ import React, { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { QRCodeSVG } from "qrcode.react"; import { toast } from "sonner"; -import { X, Minus, Plus, Ticket, ArrowRight, CheckCircle2, Gift } from "lucide-react"; +import { X, Minus, Plus, Ticket, ArrowRight, CheckCircle2, Gift } from "@/components/ui/icons"; import Image from "next/image"; import { Button } from "@/components/ui/button"; diff --git a/apps/web/components/ui/icons.tsx b/apps/web/components/ui/icons.tsx new file mode 100644 index 00000000..0be68210 --- /dev/null +++ b/apps/web/components/ui/icons.tsx @@ -0,0 +1,112 @@ +interface IconProps { + size?: number; + className?: string; +} + +const base = (size = 24, className?: string) => ({ + width: size, + height: size, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + strokeWidth: 2, + strokeLinecap: "round" as const, + strokeLinejoin: "round" as const, + className, +}); + +export function ChevronDown({ size, className }: IconProps) { + return ; +} + +export function ChevronUp({ size, className }: IconProps) { + return ; +} + +export function Camera({ size, className }: IconProps) { + return ( + + + + + ); +} + +export function CheckCircle2({ size, className }: IconProps) { + return ( + + + + + ); +} + +export function Home({ size, className }: IconProps) { + return ( + + + + + ); +} + +export function ExternalLink({ size, className }: IconProps) { + return ( + + + + + + ); +} + +export function X({ size, className }: IconProps) { + return ( + + + + + ); +} + +export function Minus({ size, className }: IconProps) { + return ; +} + +export function Plus({ size, className }: IconProps) { + return ( + + + + + ); +} + +export function Ticket({ size, className }: IconProps) { + return ( + + + + ); +} + +export function ArrowRight({ size, className }: IconProps) { + return ( + + + + + ); +} + +export function Gift({ size, className }: IconProps) { + return ( + + + + + + + + ); +} diff --git a/apps/web/package.json b/apps/web/package.json index 550d62b1..388b2597 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,7 +17,6 @@ "framer-motion": "^12.26.2", "jsonwebtoken": "^9.0.3", "leaflet": "^1.9.4", - "lucide-react": "^0.575.0", "next": "16.1.3", "nprogress": "^0.2.0", "qrcode.react": "^4.2.0", From 3b391f1ec20d3e846aab2c03a845d961e51c1d4a Mon Sep 17 00:00:00 2001 From: Manceraider24 Date: Mon, 1 Jun 2026 17:23:15 +0000 Subject: [PATCH 3/4] refactor: replace inline SVG icons with public/icons img tags, drop lucide-react - Rewrite components/ui/icons.tsx to use tags pointing to /icons/*.svg (fixes no-restricted-syntax ESLint rule banning inline elements) - Add 8 missing SVG assets: chevron-down, chevron-up, close, minus, plus, gift, check-circle, external-link - Regenerate pnpm-lock.yaml without lucide-react (removed from package.json in prior commit) Closes #712 --- apps/web/components/ui/icons.tsx | 113 +++++++----------------- apps/web/public/icons/check-circle.svg | 1 + apps/web/public/icons/chevron-down.svg | 1 + apps/web/public/icons/chevron-up.svg | 1 + apps/web/public/icons/close.svg | 1 + apps/web/public/icons/external-link.svg | 1 + apps/web/public/icons/gift.svg | 1 + apps/web/public/icons/minus.svg | 1 + apps/web/public/icons/plus.svg | 1 + pnpm-lock.yaml | 18 +--- 10 files changed, 41 insertions(+), 98 deletions(-) create mode 100644 apps/web/public/icons/check-circle.svg create mode 100644 apps/web/public/icons/chevron-down.svg create mode 100644 apps/web/public/icons/chevron-up.svg create mode 100644 apps/web/public/icons/close.svg create mode 100644 apps/web/public/icons/external-link.svg create mode 100644 apps/web/public/icons/gift.svg create mode 100644 apps/web/public/icons/minus.svg create mode 100644 apps/web/public/icons/plus.svg diff --git a/apps/web/components/ui/icons.tsx b/apps/web/components/ui/icons.tsx index 0be68210..ef8b22f5 100644 --- a/apps/web/components/ui/icons.tsx +++ b/apps/web/components/ui/icons.tsx @@ -3,110 +3,57 @@ interface IconProps { className?: string; } -const base = (size = 24, className?: string) => ({ - width: size, - height: size, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - strokeWidth: 2, - strokeLinecap: "round" as const, - strokeLinejoin: "round" as const, - className, -}); +function Icon({ src, alt, size = 24, className }: IconProps & { src: string; alt: string }) { + return ( + // eslint-disable-next-line @next/next/no-img-element + {alt} + ); +} -export function ChevronDown({ size, className }: IconProps) { - return ; +export function ChevronDown(props: IconProps) { + return ; } -export function ChevronUp({ size, className }: IconProps) { - return ; +export function ChevronUp(props: IconProps) { + return ; } -export function Camera({ size, className }: IconProps) { - return ( - - - - - ); +export function Camera(props: IconProps) { + return ; } -export function CheckCircle2({ size, className }: IconProps) { - return ( - - - - - ); +export function CheckCircle2(props: IconProps) { + return ; } -export function Home({ size, className }: IconProps) { - return ( - - - - - ); +export function Home(props: IconProps) { + return ; } -export function ExternalLink({ size, className }: IconProps) { - return ( - - - - - - ); +export function ExternalLink(props: IconProps) { + return ; } -export function X({ size, className }: IconProps) { - return ( - - - - - ); +export function X(props: IconProps) { + return ; } -export function Minus({ size, className }: IconProps) { - return ; +export function Minus(props: IconProps) { + return ; } -export function Plus({ size, className }: IconProps) { - return ( - - - - - ); +export function Plus(props: IconProps) { + return ; } -export function Ticket({ size, className }: IconProps) { - return ( - - - - ); +export function Ticket(props: IconProps) { + return ; } -export function ArrowRight({ size, className }: IconProps) { - return ( - - - - - ); +export function ArrowRight(props: IconProps) { + return ; } -export function Gift({ size, className }: IconProps) { - return ( - - - - - - - - ); +export function Gift(props: IconProps) { + return ; } diff --git a/apps/web/public/icons/check-circle.svg b/apps/web/public/icons/check-circle.svg new file mode 100644 index 00000000..72a76130 --- /dev/null +++ b/apps/web/public/icons/check-circle.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/icons/chevron-down.svg b/apps/web/public/icons/chevron-down.svg new file mode 100644 index 00000000..52573dfb --- /dev/null +++ b/apps/web/public/icons/chevron-down.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/icons/chevron-up.svg b/apps/web/public/icons/chevron-up.svg new file mode 100644 index 00000000..57776076 --- /dev/null +++ b/apps/web/public/icons/chevron-up.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/icons/close.svg b/apps/web/public/icons/close.svg new file mode 100644 index 00000000..491b86bf --- /dev/null +++ b/apps/web/public/icons/close.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/icons/external-link.svg b/apps/web/public/icons/external-link.svg new file mode 100644 index 00000000..328d5409 --- /dev/null +++ b/apps/web/public/icons/external-link.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/icons/gift.svg b/apps/web/public/icons/gift.svg new file mode 100644 index 00000000..71bdbdc1 --- /dev/null +++ b/apps/web/public/icons/gift.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/icons/minus.svg b/apps/web/public/icons/minus.svg new file mode 100644 index 00000000..19d74b0f --- /dev/null +++ b/apps/web/public/icons/minus.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/icons/plus.svg b/apps/web/public/icons/plus.svg new file mode 100644 index 00000000..6a334be5 --- /dev/null +++ b/apps/web/public/icons/plus.svg @@ -0,0 +1 @@ + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0eb23d3b..ec7f2bb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,9 +66,6 @@ importers: leaflet: specifier: ^1.9.4 version: 1.9.4 - lucide-react: - specifier: ^0.575.0 - version: 0.575.0(react@19.2.3) next: specifier: 16.1.3 version: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.4.1)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2846,11 +2843,6 @@ packages: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - lucide-react@0.575.0: - resolution: {integrity: sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -6029,7 +6021,7 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) @@ -6062,7 +6054,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -6077,7 +6069,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -6951,10 +6943,6 @@ snapshots: lru.min@1.1.4: {} - lucide-react@0.575.0(react@19.2.3): - dependencies: - react: 19.2.3 - lz-string@1.5.0: {} magic-string@0.30.21: From c23f16470e481db0ba9ad2c8844929727f723979 Mon Sep 17 00:00:00 2001 From: Manceraider24 Date: Mon, 1 Jun 2026 17:35:41 +0000 Subject: [PATCH 4/4] fix: skip PrismaClient instantiation when DATABASE_URL is absent Prevents build crash in CI and local environments without a database. The sitemap's try/catch already handles missing data gracefully. --- apps/web/lib/prisma.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/web/lib/prisma.ts b/apps/web/lib/prisma.ts index 67979b2d..f78e501b 100644 --- a/apps/web/lib/prisma.ts +++ b/apps/web/lib/prisma.ts @@ -2,10 +2,14 @@ import { PrismaClient } from "@prisma/client"; const globalForPrisma = global as unknown as { prisma: PrismaClient }; -// If we are in the GitHub CI pipeline, return a dummy object so the build doesn't crash. -// Otherwise, boot up the real Prisma Client. -export const prisma = - globalForPrisma.prisma || - (process.env.CI ? ({} as PrismaClient) : new PrismaClient()); +function createPrismaClient(): PrismaClient { + // Skip real instantiation when there's no database (CI builds, static generation) + if (!process.env.DATABASE_URL) { + return {} as PrismaClient; + } + return new PrismaClient(); +} -if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; \ No newline at end of file +export const prisma = globalForPrisma.prisma || createPrismaClient(); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;