From 1bb4aa8b8149bcef873ecab300e7a6d713456907 Mon Sep 17 00:00:00 2001 From: suharshit singh Date: Sat, 14 Mar 2026 00:26:28 +0530 Subject: [PATCH 1/3] feat: Implement the initial web application structure, including a dashboard, event management, and core UI components. --- AGENTS.md | 111 -------- apps/vibely-web/app/(auth)/login/page.tsx | 10 +- .../app/api/dashboard/stats/route.ts | 68 +++++ .../app/api/notifications/[id]/route.ts | 9 +- .../vibely-web/app/api/notifications/route.ts | 44 +++- apps/vibely-web/app/dashboard/layout.tsx | 17 ++ apps/vibely-web/app/dashboard/page.tsx | 249 +++++++----------- apps/vibely-web/app/layout.tsx | 4 +- .../components/dashboard/KPIGroup.tsx | 43 +++ .../components/dashboard/OverviewHeader.tsx | 17 ++ .../components/events/CreateEventCard.tsx | 18 ++ .../components/events/EventCard.tsx | 196 ++++++-------- .../components/events/EventGrid.tsx | 13 + apps/vibely-web/components/layout/Footer.tsx | 22 ++ apps/vibely-web/components/layout/NavBar.tsx | 188 ++----------- .../layout/NotificationDropdown.tsx | 113 ++++++++ .../components/layout/ProfileButton.tsx | 149 +++++++++++ .../components/layout/SettingsButton.tsx | 13 + apps/vibely-web/components/ui/IconButton.tsx | 24 ++ apps/vibely-web/components/ui/KPIBadge.tsx | 19 ++ apps/vibely-web/components/ui/Logo.tsx | 26 ++ apps/vibely-web/components/ui/SearchBar.tsx | 32 +++ apps/vibely-web/hooks/useDashboardStats.ts | 53 ++++ apps/vibely-web/hooks/useNotifications.ts | 62 +++++ apps/vibely-web/lib/notify.ts | 16 +- apps/vibely-web/package.json | 1 + packages/shared/types/database.types.ts | 2 +- pnpm-lock.yaml | 7 + 28 files changed, 949 insertions(+), 577 deletions(-) delete mode 100644 AGENTS.md create mode 100644 apps/vibely-web/app/api/dashboard/stats/route.ts create mode 100644 apps/vibely-web/app/dashboard/layout.tsx create mode 100644 apps/vibely-web/components/dashboard/KPIGroup.tsx create mode 100644 apps/vibely-web/components/dashboard/OverviewHeader.tsx create mode 100644 apps/vibely-web/components/events/CreateEventCard.tsx create mode 100644 apps/vibely-web/components/events/EventGrid.tsx create mode 100644 apps/vibely-web/components/layout/Footer.tsx create mode 100644 apps/vibely-web/components/layout/NotificationDropdown.tsx create mode 100644 apps/vibely-web/components/layout/ProfileButton.tsx create mode 100644 apps/vibely-web/components/layout/SettingsButton.tsx create mode 100644 apps/vibely-web/components/ui/IconButton.tsx create mode 100644 apps/vibely-web/components/ui/KPIBadge.tsx create mode 100644 apps/vibely-web/components/ui/Logo.tsx create mode 100644 apps/vibely-web/components/ui/SearchBar.tsx create mode 100644 apps/vibely-web/hooks/useDashboardStats.ts create mode 100644 apps/vibely-web/hooks/useNotifications.ts diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 7248ace..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,111 +0,0 @@ -# Vibely — Agent Instructions - -## Project Overview - -Vibely is an event-centric photo sharing platform. Hosts create events and share -QR codes. Guests upload photos without creating an account. Photos auto-expire -after events end unless saved to a personal vault. - -## Monorepo Structure - -- `apps/vibely-web` — Next.js 14 App Router (web frontend + ALL API routes) -- `apps/vibely-mobile` — Expo React Native app -- `packages/shared` — Shared TypeScript types, Zod schemas, utility functions -- `supabase/migrations/` — Numbered SQL migration files (001–005 exist) -- `supabase/functions/` — Supabase Edge Functions (Deno runtime) - -## Backend Scope for Jules - -Jules ONLY works on: - -- `supabase/migrations/` — new migration files -- `supabase/functions/` — new or updated Edge Functions -- `apps/vibely-web/app/api/` — Next.js API route handlers -- `apps/vibely-web/lib/` — server-side utilities -- `apps/vibely-web/hooks/` — data fetching hooks (no UI) -- `packages/shared/` — types, schemas, utilities - -Jules NEVER modifies: - -- `apps/vibely-web/app/` page files (anything not inside `api/`) -- `apps/vibely-web/components/` -- `apps/vibely-mobile/` (entire directory) -- `apps/vibely-web/app/layout.tsx` -- `apps/vibely-web/proxy.ts` - -## Tech Stack - -- Database: Supabase (PostgreSQL) with Row Level Security on all tables -- Auth: Supabase Auth (JWT Bearer tokens) -- Storage: Supabase Storage (event-photos, avatars, event-covers buckets) -- CDN: ImageKit (URL endpoint in NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT) -- Rate limiting: Upstash Redis (see apps/vibely-web/lib/rate-limit.ts) -- Serverless: Next.js API routes on Vercel -- Edge Functions: Deno runtime (supabase/functions/) - -## Authentication Pattern - -Every authenticated API route uses this exact pattern: - -```ts -import { createClient, createAdminClient } from "@/lib/supabase/server"; -const supabase = await createClient(); -const { - data: { user }, -} = await supabase.auth.getUser(); -if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); -// Use adminSupabase for queries that need to bypass RLS -const adminSupabase = createAdminClient(); -``` - -## API Route Conventions - -- All routes: `apps/vibely-web/app/api/{resource}/route.ts` -- Nested: `apps/vibely-web/app/api/{resource}/[id]/route.ts` -- Always validate request body with Zod before processing -- Return `{ error: string }` for errors with appropriate HTTP status -- Return data directly (not wrapped in `{ data: ... }`) for success -- Use `createAdminClient()` for writes and RLS-bypassing reads -- Use `createClient()` for reads that respect RLS - -## Database Conventions - -- All tables have RLS enabled -- UUIDs as primary keys (`gen_random_uuid()`) -- `created_at TIMESTAMPTZ NOT NULL DEFAULT now()` -- Soft deletes via `status = 'deleted'` and `deleted_at` (not hard DELETE) -- Migration files are numbered sequentially: 006_feature_name.sql - -## Shared Package - -- Import types: `import type { Event, Photo } from '@repo/shared/types'` -- Import schemas: `import { someSchema } from '@repo/shared/validation/...'` -- Import utils: `import { thumbnailUrl } from '@repo/shared/utils/storage'` -- When adding new types/schemas, add to the appropriate file in packages/shared/ - -## Storage Key Patterns - -- Photos: `events/{eventId}/{photoId}/{filename}` -- Avatars: `{userId}/avatar.{ext}` (fixed name — overwrites on update) -- Covers: `{eventId}/cover.{ext}` (fixed name — overwrites on update) - -## Rate Limiting - -Use pre-configured limiters from `apps/vibely-web/lib/rate-limit.ts`. -Add new limiters there if needed. Always check `if (!rl.success) return rl.response!;` - -## ImageKit URL Helpers - -Use helpers from `packages/shared/utils/storage.ts`: - -- `thumbnailUrl(storageKey)` — 400px thumbnail -- `previewUrl(storageKey)` — 1200px preview -- `fullUrl(storageKey)` — full quality - -## What Good PRs Look Like - -- One migration file per PR (increment the number) -- API routes include inline comments explaining WHY (not what) -- Zod validation for every request body -- RLS policies in the migration for any new tables -- No `console.log` left in production code (use `console.error` for actual errors only) diff --git a/apps/vibely-web/app/(auth)/login/page.tsx b/apps/vibely-web/app/(auth)/login/page.tsx index 33a2356..76c33c5 100644 --- a/apps/vibely-web/app/(auth)/login/page.tsx +++ b/apps/vibely-web/app/(auth)/login/page.tsx @@ -10,15 +10,7 @@ import { Suspense, useState, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { useAuth } from "@/context/AuthContext"; -import { - Zap, - Mail, - Lock, - Eye, - EyeOff, - ArrowRight, - Terminal, -} from "lucide-react"; +import { Zap, Mail, Lock, Eye, EyeOff, ArrowRight } from "lucide-react"; function LoginForm() { const router = useRouter(); diff --git a/apps/vibely-web/app/api/dashboard/stats/route.ts b/apps/vibely-web/app/api/dashboard/stats/route.ts new file mode 100644 index 0000000..e2e0f8f --- /dev/null +++ b/apps/vibely-web/app/api/dashboard/stats/route.ts @@ -0,0 +1,68 @@ +// ============================================================ +// apps/vibely-web/app/api/dashboard/stats/route.ts +// ============================================================ +// GET /api/dashboard/stats +// Aggregates total photos across all events the user is a member of, +// and total bytes for photos saved in their personal vault. +// ============================================================ + +import { NextResponse } from "next/server"; +import { createClient } from "@/lib/supabase/server"; + +export async function GET() { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // 1. Get total photos in all events the user is part of + const { data: userEvents, error: eventsError } = await supabase + .from("event_members") + .select("event_id") + .eq("user_id", user.id); + + if (eventsError) { + return NextResponse.json( + { error: "Failed to fetch user events" }, + { status: 500 } + ); + } + + const eventIds = userEvents.map((em) => em.event_id); + let totalPhotos = 0; + + if (eventIds.length > 0) { + const { count: photosCount, error: photosError } = await supabase + .from("photos") + .select("*", { count: "exact", head: true }) + .in("event_id", eventIds) + .eq("status", "active"); + + if (!photosError && photosCount !== null) { + totalPhotos = photosCount; + } + } + + // 2. Get total size of all items in personal vault + const { data: vaultItems, error: vaultError } = await supabase + .from("personal_vault") + .select("photo:photos(file_size)") + .eq("user_id", user.id); + + let totalBytes = 0; + if (!vaultError && vaultItems) { + totalBytes = vaultItems.reduce( + (acc, item) => acc + (item.photo?.file_size ?? 0), + 0 + ); + } + + return NextResponse.json({ + totalPhotos, + totalBytes, + }); +} diff --git a/apps/vibely-web/app/api/notifications/[id]/route.ts b/apps/vibely-web/app/api/notifications/[id]/route.ts index 763b7a7..ed798e5 100644 --- a/apps/vibely-web/app/api/notifications/[id]/route.ts +++ b/apps/vibely-web/app/api/notifications/[id]/route.ts @@ -13,7 +13,9 @@ type RouteParams = { params: Promise<{ id: string }> }; export async function PATCH(_req: Request, { params }: RouteParams) { const { id } = await params; const supabase = await createClient(); - const { data: { user } } = await supabase.auth.getUser(); + const { + data: { user }, + } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -27,7 +29,10 @@ export async function PATCH(_req: Request, { params }: RouteParams) { .eq("user_id", user.id); if (updateError) { - return NextResponse.json({ error: "Failed to update notification" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to update notification" }, + { status: 500 } + ); } return NextResponse.json({ success: true }); diff --git a/apps/vibely-web/app/api/notifications/route.ts b/apps/vibely-web/app/api/notifications/route.ts index 793e153..33df34e 100644 --- a/apps/vibely-web/app/api/notifications/route.ts +++ b/apps/vibely-web/app/api/notifications/route.ts @@ -12,16 +12,20 @@ import { NextResponse } from "next/server"; import { createClient } from "@/lib/supabase/server"; import { z } from "zod"; -const markReadSchema = z.object({ - all: z.boolean().optional(), - ids: z.array(z.string().uuid()).optional() -}).refine(data => data.all || (data.ids && data.ids.length > 0), { - message: "Must provide either 'all: true' or a non-empty array of 'ids'" -}); +const markReadSchema = z + .object({ + all: z.boolean().optional(), + ids: z.array(z.string().uuid()).optional(), + }) + .refine((data) => data.all || (data.ids && data.ids.length > 0), { + message: "Must provide either 'all: true' or a non-empty array of 'ids'", + }); export async function GET() { const supabase = await createClient(); - const { data: { user } } = await supabase.auth.getUser(); + const { + data: { user }, + } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -37,7 +41,10 @@ export async function GET() { .limit(20); if (fetchError) { - return NextResponse.json({ error: "Failed to fetch notifications" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to fetch notifications" }, + { status: 500 } + ); } // Get unread count @@ -48,18 +55,23 @@ export async function GET() { .eq("is_read", false); if (countError) { - return NextResponse.json({ error: "Failed to fetch notification count" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to fetch notification count" }, + { status: 500 } + ); } return NextResponse.json({ notifications: notifications || [], - unread_count: count || 0 + unread_count: count || 0, }); } export async function PATCH(req: Request) { const supabase = await createClient(); - const { data: { user } } = await supabase.auth.getUser(); + const { + data: { user }, + } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -70,7 +82,10 @@ export async function PATCH(req: Request) { const result = markReadSchema.safeParse(body); if (!result.success) { - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + return NextResponse.json( + { error: "Invalid request body" }, + { status: 400 } + ); } const { all, ids } = result.data; @@ -87,7 +102,10 @@ export async function PATCH(req: Request) { const { error: updateError } = await updateQuery; if (updateError) { - return NextResponse.json({ error: "Failed to update notifications" }, { status: 500 }); + return NextResponse.json( + { error: "Failed to update notifications" }, + { status: 500 } + ); } return NextResponse.json({ success: true }); diff --git a/apps/vibely-web/app/dashboard/layout.tsx b/apps/vibely-web/app/dashboard/layout.tsx new file mode 100644 index 0000000..9c3498d --- /dev/null +++ b/apps/vibely-web/app/dashboard/layout.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { ReactNode } from "react"; +import NavBar from "@/components/layout/NavBar"; +import Footer from "@/components/layout/Footer"; + +export default function DashboardLayout({ children }: { children: ReactNode }) { + return ( +
+ +
+ {children} +
+
+
+ ); +} diff --git a/apps/vibely-web/app/dashboard/page.tsx b/apps/vibely-web/app/dashboard/page.tsx index f072f69..99cb722 100644 --- a/apps/vibely-web/app/dashboard/page.tsx +++ b/apps/vibely-web/app/dashboard/page.tsx @@ -1,173 +1,128 @@ "use client"; -// ============================================================ -// apps/web/app/dashboard/page.tsx -// ============================================================ -// Main dashboard: shows all events the user belongs to, -// separated into "upcoming" and "past" sections. -// Has a prominent "Create event" CTA. -// -// WHY 'use client' here? -// The useEvents hook uses useState/useEffect — Client Component -// territory. For a larger app, you'd fetch the initial events -// list in a Server Component and pass it as props to avoid the -// loading flicker. We keep it simple here for the MVP. -// ============================================================ - -import { useState } from "react"; -import Link from "next/link"; +import { useState, useEffect } from "react"; import { useEvents } from "@/hooks/useEvents"; -import { EventCard } from "@/components/events/EventCard"; +import { useDashboardStats } from "@/hooks/useDashboardStats"; +import { createClient } from "@/lib/supabase/client"; + +// New Components +import OverviewHeader from "@/components/dashboard/OverviewHeader"; +import KPIGroup from "@/components/dashboard/KPIGroup"; +import EventGrid from "@/components/events/EventGrid"; +import CreateEventCard from "@/components/events/CreateEventCard"; +import EventCard from "@/components/events/EventCard"; import { isEventExpired } from "@shared/utils/invite"; export default function DashboardPage() { - const { events, isLoading, error, deleteEvent } = useEvents(); - const [deletingId, setDeletingId] = useState(null); - - const handleDelete = async (id: string) => { - const event = events.find((e) => e.id === id); - if (!event) return; + const { events, isLoading: eventsLoading, error: eventsError } = useEvents(); + const { + totalPhotos, + formattedSize, + isLoading: statsLoading, + } = useDashboardStats(); + const [userName, setUserName] = useState("User"); - // Simple browser confirm — Phase 14 (polish) will replace with a modal - const confirmed = window.confirm( - `Delete "${event.title}"? This will permanently delete all photos. This cannot be undone.` - ); - if (!confirmed) return; - - setDeletingId(id); - await deleteEvent(id); - setDeletingId(null); - }; - - // Split events into upcoming (active) and past (expired/archived) - const upcoming = events.filter( - (e) => !isEventExpired(e.expires_at) && e.status === "active" - ); - const past = events.filter( - (e) => isEventExpired(e.expires_at) || e.status !== "active" - ); + // Fetch user for the greeting + useEffect(() => { + const supabase = createClient(); + supabase.auth.getUser().then(({ data: { user } }) => { + if (!user) return; + supabase + .from("users") + .select("name") + .eq("id", user.id) + .single() + .then(({ data }) => { + if (data && data.name) { + // Pick first name + setUserName(data.name.split(" ")[0]); + } + }); + }); + }, []); return ( -
-
- {/* Page header */} -
-
-

Your Events

-

- {events.length === 0 - ? "No events yet" - : `${events.length} event${events.length !== 1 ? "s" : ""}`} -

+
+ {/* Top Section: Greeting & KPIs */} +
+ + {statsLoading || eventsLoading ? ( +
+
+
+
+ ) : ( + + )} +
- - - - - Create event - -
- - {/* Error */} - {error && ( + {/* Main Grid Section */} +
+ {eventsError && (
- {error} + {eventsError}
)} - {/* Loading skeleton */} - {isLoading && ( -
- {[1, 2, 3].map((i) => ( + + {/* Always show the Create card first */} + + + {/* Skeleton Loaders */} + {eventsLoading && + Array.from({ length: 3 }).map((_, i) => (
-
-
-
-
-
-
+ key={`skeleton-${i}`} + className="w-full aspect-[1/2] sm:aspect-auto sm:h-[480px] rounded-[32px] bg-gray-200 animate-pulse" + /> ))} -
- )} - {/* Empty state */} - {!isLoading && events.length === 0 && ( -
-
📷
-

- No events yet -

-

- Create your first event and start collecting photos from everyone - there. -

- - Create your first event - -
- )} + {/* Render real events */} + {!eventsLoading && + events.map((event) => { + const expired = + isEventExpired(event.expires_at) || event.status !== "active"; - {/* Upcoming events */} - {!isLoading && upcoming.length > 0 && ( -
-

- Upcoming -

-
- {upcoming.map((event) => ( -
- -
- ))} -
-
- )} + // Mocking specific design elements that might not be in DB yet + // Fallback cover image if none is provided + const coverImg = + event.cover_image_url || + "https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?q=80&w=800&auto=format&fit=crop"; + const formattedDate = new Date( + event.created_at + ).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); - {/* Past events */} - {!isLoading && past.length > 0 && ( -
-

- Past events -

-
- {past.map((event) => ( + return ( - ))} -
-
- )} -
+ ); + })} + +
); } diff --git a/apps/vibely-web/app/layout.tsx b/apps/vibely-web/app/layout.tsx index 00484fe..9f34d12 100644 --- a/apps/vibely-web/app/layout.tsx +++ b/apps/vibely-web/app/layout.tsx @@ -5,7 +5,6 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; -import { NavBar } from "@/components/layout/NavBar"; import { ToastProvider } from "@/components/layout/Toast"; import { AuthProvider } from "@/context/AuthContext"; @@ -34,8 +33,7 @@ export default function RootLayout({ > - -
{children}
+
{children}
diff --git a/apps/vibely-web/components/dashboard/KPIGroup.tsx b/apps/vibely-web/components/dashboard/KPIGroup.tsx new file mode 100644 index 0000000..032cc5f --- /dev/null +++ b/apps/vibely-web/components/dashboard/KPIGroup.tsx @@ -0,0 +1,43 @@ +"use client"; + +import KPIBadge from "@/components/ui/KPIBadge"; +import { + Cloud as CloudIcon, + EventAvailable as EventIcon, + Image as ImageIcon, +} from "@mui/icons-material"; + +interface KPIGroupProps { + totalEvents: number; + totalPhotos: number; + formattedSize: string; +} + +export default function KPIGroup({ + totalEvents, + totalPhotos, + formattedSize, +}: KPIGroupProps) { + // Simple number formatting for thousands + const formatNum = (num: number) => new Intl.NumberFormat("en-US").format(num); + + return ( +
+ } + value={formatNum(totalEvents)} + label="Events" + /> + } + value={formatNum(totalPhotos)} + label="Photos" + /> + } + value={formattedSize} + label="Vault" + /> +
+ ); +} diff --git a/apps/vibely-web/components/dashboard/OverviewHeader.tsx b/apps/vibely-web/components/dashboard/OverviewHeader.tsx new file mode 100644 index 0000000..d1d2571 --- /dev/null +++ b/apps/vibely-web/components/dashboard/OverviewHeader.tsx @@ -0,0 +1,17 @@ +interface OverviewHeaderProps { + userName: string; +} + +export default function OverviewHeader({ userName }: OverviewHeaderProps) { + return ( +
+

+ Good evening, {userName}{" "} + 👋 +

+

+ Capture every moment, curate every memory. +

+
+ ); +} diff --git a/apps/vibely-web/components/events/CreateEventCard.tsx b/apps/vibely-web/components/events/CreateEventCard.tsx new file mode 100644 index 0000000..93f557d --- /dev/null +++ b/apps/vibely-web/components/events/CreateEventCard.tsx @@ -0,0 +1,18 @@ +import { Plus } from "lucide-react"; +import Link from "next/link"; + +export default function CreateEventCard() { + return ( + +
+ +
+ + Create New Event + + + ); +} diff --git a/apps/vibely-web/components/events/EventCard.tsx b/apps/vibely-web/components/events/EventCard.tsx index 4e4728f..64d9edc 100644 --- a/apps/vibely-web/components/events/EventCard.tsx +++ b/apps/vibely-web/components/events/EventCard.tsx @@ -1,138 +1,100 @@ -"use client"; - -// ============================================================ -// apps/web/components/events/EventCard.tsx -// ============================================================ -// Displays a compact event summary in the dashboard grid. -// Shows: cover image/placeholder, title, date, member count, -// status badge, and the user's role. -// ============================================================ - -import Link from "next/link"; import Image from "next/image"; -import { - formatEventDate, - relativeTime, - isEventExpired, -} from "@shared/utils/invite"; -import type { EventWithRole } from "@/hooks/useEvents"; +import Link from "next/link"; + +// interface EventTag { +// label: string; +// } interface EventCardProps { - event: EventWithRole; - onDelete?: (id: string) => void; + id: string; + title: string; + dateStr: string; + description: string; + imageUrl: string; + status: "ACTIVE" | "EXPIRED"; + tags: string[]; // Simplest format as string array } -const STATUS_STYLES = { - active: "bg-emerald-50 text-emerald-700 border-emerald-100", - expired: "bg-gray-50 text-gray-500 border-gray-100", - archived: "bg-amber-50 text-amber-700 border-amber-100", -} as const; - -const ROLE_LABEL = { - host: "Host", - contributor: "Member", - viewer: "Viewer", -} as const; - -export function EventCard({ event, onDelete }: EventCardProps) { - const expired = isEventExpired(event.expires_at); - const displayStatus = ( - expired && event.status === "active" ? "expired" : event.status - ) as keyof typeof STATUS_STYLES; +export default function EventCard({ + id, + title, + dateStr, + description, + imageUrl, + status, + // tags, +}: EventCardProps) { + const isExpired = status === "EXPIRED"; return ( -
- {/* Cover image or gradient placeholder */} -
- {event.cover_image_url && ( - {event.title} - )} + + {/* Background Image */} + {title} - {/* Status badge overlaid on the image */} - - {displayStatus.charAt(0).toUpperCase() + displayStatus.slice(1)} - + {/* Gradient Overlay for Text Visibility */} +
- {/* Role badge */} - - {ROLE_LABEL[event.user_role]} - -
+ {/* Content Container */} +
+ {/* Top: Status Badges & Basic Info could go here, but design has it lower */} - {/* Content */} -
- -

- {event.title} -

- + {/* Bottom portion */} +
+ {/* Header Row: Title & Status Badge */} +
+

+ {title} +

+
+ {status} +
+
-

- {formatEventDate(event.event_date)} -

+

{dateStr}

- {event.description && ( -

- {event.description} +

+ {description}

- )} - {/* Footer row */} -
-
- {/* Calendar icon */} - - - - {relativeTime(event.event_date)} -
- - {/* Action buttons — only visible on hover */} -
- - View - + {/* TODO Add Tags logics */} + {/* Tags +
+ {tags.map((tag, idx) => ( + + {tag} + + ))} +
*/} - {event.user_role === "host" && ( - <> - - Edit - - - + {/* Action Button */} +
+ {isExpired ? ( + + ) : ( + )}
-
+ ); } diff --git a/apps/vibely-web/components/events/EventGrid.tsx b/apps/vibely-web/components/events/EventGrid.tsx new file mode 100644 index 0000000..37e7492 --- /dev/null +++ b/apps/vibely-web/components/events/EventGrid.tsx @@ -0,0 +1,13 @@ +import { ReactNode } from "react"; + +interface EventGridProps { + children: ReactNode; +} + +export default function EventGrid({ children }: EventGridProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/vibely-web/components/layout/Footer.tsx b/apps/vibely-web/components/layout/Footer.tsx new file mode 100644 index 0000000..a6908e9 --- /dev/null +++ b/apps/vibely-web/components/layout/Footer.tsx @@ -0,0 +1,22 @@ +import Link from "next/link"; + +export default function Footer() { + return ( +
+
+ © 2024 Vibely. All rights reserved. +
+
+ + Privacy Policy + + + Terms of Service + + + Support + +
+
+ ); +} diff --git a/apps/vibely-web/components/layout/NavBar.tsx b/apps/vibely-web/components/layout/NavBar.tsx index 5facff5..a2ae39f 100644 --- a/apps/vibely-web/components/layout/NavBar.tsx +++ b/apps/vibely-web/components/layout/NavBar.tsx @@ -1,58 +1,16 @@ "use client"; -// ============================================================ -// apps/web/components/layout/NavBar.tsx -// ============================================================ -// Responsive top navigation bar. -// Desktop: horizontal links -// Mobile: hamburger menu with slide-down drawer -// ============================================================ - -import { useState, useEffect } from "react"; -import Link from "next/link"; -import { usePathname, useRouter } from "next/navigation"; -import { createClient } from "@/lib/supabase/client"; -import Image from "next/image"; -import { MirrorRectangular } from "lucide-react"; - -const NAV_LINKS = [ - { href: "/dashboard", label: "Events", emoji: "📅" }, - { href: "/vault", label: "Vault", emoji: "🔖" }, - { href: "/profile", label: "Profile", emoji: "👤" }, -]; - -export function NavBar() { +import { usePathname } from "next/navigation"; +import Logo from "@/components/ui/Logo"; +import SearchBar from "@/components/ui/SearchBar"; +import NotificationDropdown from "@/components/layout/NotificationDropdown"; +import SettingsButton from "@/components/layout/SettingsButton"; +import ProfileButton from "@/components/layout/ProfileButton"; + +export default function NavBar() { const pathname = usePathname(); - const router = useRouter(); - const [menuOpen, setMenuOpen] = useState(false); - const [userName, setUserName] = useState(""); - const [avatarUrl, setAvatarUrl] = useState(null); - - useEffect(() => { - const supabase = createClient(); - supabase.auth.getUser().then(({ data: { user } }) => { - if (!user) return; - supabase - .from("users") - .select("name, avatar_url") - .eq("id", user.id) - .single() - .then(({ data }) => { - if (data) { - setUserName(data.name); - setAvatarUrl(data.avatar_url); - } - }); - }); - }, []); - - const handleSignOut = async () => { - const supabase = createClient(); - await supabase.auth.signOut(); - router.push("/login"); - }; - // Hide nav on guest and auth pages + // Hide nav on specific pages, consistent with previous behavior if ( pathname === "/" || pathname.startsWith("/guest") || @@ -61,125 +19,25 @@ export function NavBar() { pathname.startsWith("/join") || pathname.startsWith("/forgot-password") || pathname.startsWith("/pricing") - ) + ) { return null; + } return ( -