Community platform for photographers built with Next.js and Supabase. Features event management, photo galleries, and user profiles.
- Event listings (upcoming/past)
- Scene events: community-curated photography events (exhibitions, photowalks, talks, workshops, festivals, meetups) at /scene
- RSVP system with email confirmation
- Automated email reminders:
- RSVP reminders (5 days before) for non-attendees
- Attendee reminders (1 day before) for confirmed RSVPs
- Calendar integration (Google, Outlook, Apple/iCal)
- Attendee list with avatars
- Attendance tracking for admins
- "You went!" badge on past events you attended
- reCAPTCHA protection
- User albums with drag-and-drop reordering
- Community photo albums: shared albums (open or closed membership), event albums auto-created per event (ownerless, any member can add), contributor attribution
- Photo uploads with EXIF extraction (camera, lens, settings, GPS)
- Photo captions
- Album comments and tags with reply threading
- Public/private visibility
- Full-size viewing (PhotoSwipe)
- Masonry grid layout
- Likes on photos and albums
- View tracking with "Most viewed this week" sections
- Bulk photo and album editing (multi-select, batch operations)
- Add photos to albums modal
- Challenge badges on photos (shows which challenges a photo was accepted in)
- Copyright & licensing: per-photo license selection (All Rights Reserved, CC BY, CC BY-NC, CC BY-NC-ND, CC0), automatic watermarking (corner or diagonal text), EXIF copyright embedding
- Admin moderation (suspend/unsuspend)
- Content reporting system (report photos, albums, profiles, comments)
- Themed challenges with prompts created by admins
- Members submit photos (upload or from library)
- Submission limits per user (optional)
- Admin review queue (accept/reject with reasons)
- Bulk review actions
- Public gallery of accepted submissions
- Challenge comments
- Email announcements to members
- Notifications for submission results
- Profile badges for challenge participation
- "Featured in" section on photo detail pages shows accepted challenges
- Public profile pages (
/@username) - Custom avatar uploads
- Bio, website, social links (with auto-detected icons)
- Interests (up to 10 per profile)
- Activity stats
- Self-service account deletion (30-day retention with admin revert)
- Google OAuth
- Discord OAuth
- Email/password
- Password reset
- Sign-up CTAs on gallery, events, challenges, and members pages
- Event CRUD with attendance tracking
- Scene events moderation at /admin/scene
- Photo challenge management (create, edit, announce)
- Submission review queue with bulk actions
- Album moderation (suspend/unsuspend)
- Content reports review queue with resolution workflow
- Newsletter tool: compose and send newsletters to members (respects opt-out preferences)
- Global search (Cmd/Ctrl+K)
- Search across albums, photos, members, events, and tags
- Keyboard navigation support
- Debounced queries with loading states
- Dark/light/system theme
- Responsive design with mobile menu
- Tag-based caching with
use cache - Loading skeletons
- Custom 404 page
- Help & FAQ pages with contextual help links (including licenses guide)
- Email notifications (React Email + Resend)
- In-app notifications with real-time toast notifications (Supabase Realtime)
- Activity feed page with notification management
- Weekly notification digest emails
- Automated cron jobs (Vercel Cron): reminders, digest, hourly events revalidation, weekly content cleanup
- Changelog page with version history
| Category | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript |
| UI Library | React 19 |
| Database | Supabase (PostgreSQL) |
| Auth | Supabase Auth |
| Storage | Supabase Storage |
| Data Fetching | TanStack Query (React Query) |
| Styling | Tailwind CSS 4 |
| React Email + Resend | |
| Cron Jobs | Vercel Cron |
| Gallery | PhotoSwipe |
| Drag & Drop | dnd-kit |
| Toasts | Sonner |
| Analytics | Vercel Analytics |
- Node.js 18+
- Supabase project
- (Optional) Resend account, reCAPTCHA keys
Copy .env.example to .env.local and fill in your values:
cp .env.example .env.localSee .env.example for all required variables. Key ones:
SUPABASE_SERVICE_ROLE_KEY- Found in Supabase Dashboard → Project Settings → APIRESEND_API_KEY- From your Resend dashboardEMAIL_ASSETS_URL- Your production URL (for email images to work)ENCRYPTION_KEY- Generate withopenssl rand -hex 32CRON_SECRET- Secret token for cron job authentication (generate a secure random string)
For Production (Supabase Dashboard):
Configure Google and Discord credentials in Supabase Dashboard → Authentication → Providers.
For Local Development (Supabase CLI):
Set these environment variables before running supabase start:
# Google OAuth
export SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID="your-google-client-id"
export SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET="your-google-secret"
# Discord OAuth
export SUPABASE_AUTH_EXTERNAL_DISCORD_CLIENT_ID="your-discord-client-id"
export SUPABASE_AUTH_EXTERNAL_DISCORD_SECRET="your-discord-secret"To get these credentials:
- Google: Google Cloud Console → APIs & Services → Credentials → Create OAuth 2.0 Client ID
- Discord: Discord Developer Portal → New Application → OAuth2
Set the redirect URI to: http://localhost:54321/auth/v1/callback
npm install
npm run devOpen http://localhost:3000.
npm run dev # Dev server (Turbopack)
npm run build # Production build
npm run start # Production server
npm run lint # ESLint
npm run lint:fix # ESLint with auto-fix
npm run typecheck # TypeScript type checking
npm run test:run # Run unit tests once
npm run test:ui # Unit tests with UI
npm run test:e2e # Playwright E2E tests
npm run analyze # Bundle analysis (webpack)The project uses husky to run checks before each commit:
- Lint staged files - ESLint runs on staged
.tsand.tsxfiles (warnings allowed, errors block) - Type check - Full TypeScript check (
tsc --noEmit) - warnings allowed, errors block - Unit tests - Runs all Vitest tests
Warnings are allowed, but errors will block the commit. To bypass (not recommended):
git commit --no-verify -m "message"On pull requests, GitHub Actions runs jobs in sequence:
- Lint & Type Check - ESLint and TypeScript validation
- Unit Tests - Vitest tests (runs after lint/typecheck)
- E2E Tests - Playwright tests with production build (runs after unit tests)
src/
├── app/ # Pages (App Router)
│ ├── [nickname]/ # User profiles (@username)
│ ├── account/ # Account settings, user galleries
│ ├── admin/ # Admin dashboard
│ ├── api/ # API routes
│ ├── challenges/ # Photo challenges
│ ├── changelog/ # Version history pages
│ ├── events/ # Event pages
│ ├── scene/ # Scene events (community-curated photography events)
│ ├── gallery/ # Public gallery and tag pages
│ ├── members/ # Member discovery pages
│ └── ...
├── components/
│ ├── admin/ # Admin components
│ ├── album/ # Album display components
│ ├── auth/ # Auth components
│ ├── challenges/ # Challenge components
│ ├── events/ # Event components
│ ├── gallery/ # Paginated gallery views
│ ├── layout/ # Header, footer, etc.
│ ├── manage/ # Photo/album management (edit, bulk actions)
│ ├── notifications/ # In-app notifications
│ ├── onboarding/ # New user onboarding flow
│ ├── photo/ # Photo display and lightbox
│ ├── search/ # Global search modal
│ └── shared/ # Reusable UI
├── config/ # Routes, socials
├── context/ # React context (Auth, Manage, UnsavedChanges)
├── emails/ # Email templates (React Email)
├── hooks/ # Custom hooks
├── lib/
│ ├── actions/ # Server actions (likes, notifications)
│ ├── data/ # Server-side data fetching functions
│ ├── notifications/ # Notification creation logic
│ └── sync/ # Real-time sync handlers (Supabase Realtime)
├── types/ # TypeScript types
└── utils/ # Utilities, Supabase clients
supabase/
└── migrations/ # Database migrations
| Table | Description |
|---|---|
profiles |
User profiles |
events |
Events/meetups |
events_rsvps |
RSVPs and attendance |
event_announcements |
Event email announcement tracking |
event_comments |
Event-comment junction |
albums |
Photo albums (with likes_count, view_count, sharing settings) |
album_photos |
Album-photo junction (with added_by for attribution) |
album_comments |
Album-comment junction |
album_likes |
Album likes |
album_tags |
Album tags |
album_views |
Individual album view events for trending queries |
shared_album_members |
Shared album membership |
shared_album_requests |
Join requests and invites for shared albums |
photos |
Photo metadata + EXIF (with likes_count, view_count) |
photo_comments |
Photo-comment junction |
photo_tags |
Photo tags |
photo_likes |
Photo likes |
photo_views |
Individual photo view events for trending queries |
tags |
Central tags table with usage counts |
challenges |
Photo challenges with prompts and deadlines |
challenge_submissions |
User photo submissions (pending/accepted/rejected) |
challenge_announcements |
Challenge email announcement tracking |
challenge_color_draws |
Challenge color draw records (user/guest color assignments) |
challenge_comments |
Challenge-comment junction |
comments |
Comment content with threading support |
notifications |
In-app notifications |
email_types |
Email preference types |
email_preferences |
User email opt-in/out |
feedback |
User-submitted feedback (bug reports, feature requests) |
interests |
Central interests table with usage counts |
profile_interests |
Profile-interests junction |
auth_tokens |
Email verification & password reset tokens |
reports |
User-submitted content reports (photos, albums, profiles, comments, scene events) |
scene_events |
Community-curated photography events (exhibitions, photowalks, talks, etc.) |
scene_event_comments |
Links comments to scene events |
scene_event_interests |
Users who marked interest in scene events |
cpg-public- General public assetsuser-avatars- Profile picturesuser-photos- Gallery photosevent-covers- Event and challenge cover images
npx supabase gen types typescript --project-id "your-project-id" > ./src/database.types.tsOr via SSH:
ssh user@domain "npx supabase gen types typescript --db-url 'postgresql://postgres.[tenant-id]:[password]@localhost:5432/postgres' --schema public" > ./src/database.types.tsHeavy dependencies are lazy-loaded to reduce initial bundle size:
| Library | Size | Loaded When |
|---|---|---|
| PhotoSwipe | ~25 KiB | User clicks to view full-size image |
| @dnd-kit | ~40 KiB | User opens photo/album management |
| exifr | ~15 KiB | User uploads photos |
| Swiper | ~30 KiB | Activities slider rendered |
- Modern formats enabled (AVIF, WebP)
- Responsive
sizesattributes on all images - Priority loading for LCP images
npm run analyzeOpens an interactive treemap to visualize bundle composition. See docs/performance-optimization.md for details.
Deploy to Vercel:
- Connect repository
- Set environment variables (including
CRON_SECRETfor reminder emails) - Configure Supabase OAuth redirect URLs for production
- Set up RLS and storage policies
- Cron jobs are automatically configured via
vercel.json(reminders daily 8:00 AM, events revalidation daily 5:01 PM, content cleanup Sundays 3:00 AM UTC)
Deployment Strategy:
- Main branch commits and PR previews do not trigger production deployments directly
- Release Please creates releases from
mainand the release workflow deploys those released commits to Vercel production explicitly
- Event comments
- Photo challenges
- Member management
- Statistics/analytics dashboard
- Admin tools
- Display EXIF data to viewers
- Bulk photo actions (delete, edit)
- Member profile photo stream
- Manual album cover selection
- Tags on individual photos
- Copyright & licensing (per-photo licenses, watermarking, EXIF copyright)
- Mobile-friendly drag/drop
- Email reminders before events
- "Happening now" status badge with timezone-aware event times (Europe/Amsterdam)
- Event photo albums (auto-created per event, any member can contribute)
- Challenge creation with cover images
- Submission flow (upload or library)
- Admin review queue with bulk actions
- Email announcements
- Submission result notifications
- Profile achievement badges
- Challenge badges on photos (manage grid + photo detail pages)
- Photo/album likes
- View tracking and stats
- Follow photographers
- Activity feed
- In-app notifications
- Community photo albums
- Reply to comments
- Member interests in profile
- Members page (only for logged-in users)
- Browse by tags
- Browse by interests
- Member discovery sections:
- Popular interests
- Recently active members
- New members
- Explore by photo style (tags with member counts)
- Random interests with member samples
- Search (albums, photos, members, events, tags)
- Featured/trending galleries (Most viewed this week)
- Social share buttons
- Forum (discussion boards, threads, categories)
- Report content