Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Waypoint — Collaborative Trip Planner

Plan trips together. A real-time, visual itinerary builder with a drag-and-drop timeline, animated map routes, live budget tracking, and multi-user collaboration.

Next.js TypeScript Supabase Vercel


Live Demo

🌐 get-waypoint.vercel.app

Click Try Demo on the landing page or login screen to explore with a pre-seeded account — no sign-up required.


Visual & Technical Enhancements (v2)

  • Editorial Visual Tone: Upgraded color system to deep petrol teal with desaturated brass/gold accents, custom layered shadows, and subtle noise texturing (.bg-grain).
  • Google Sans Typography: Body and typography powered by self-hosted Google Sans via next/font/local.
  • Split Auth Layout: Desktop login and signup pages feature a split visual panel with animated route arcs and rotating value-prop copy.
  • Interactive Landing Sandbox: Inline live drag-and-drop itinerary demo allowing visitors to reorder activities and test live budget rollups directly on the landing page without signing in.
  • GDPR Privacy Policy: Accessible accordion policy at /privacy covering sub-processors (Supabase, Vercel, MapTiler), data retention, and user rights.

Screenshots

Landing Page & Interactive Sandbox Drag-and-Drop Timeline Animated Map Route
Hero with Three.js globe, interactive DnD sandbox & FAQ accordions Day-by-day card editor with @dnd-kit reorder animations MapLibre GL route drawn in real time as activities update

Technical Challenges & Debugging Insights

1. SVG + GSAP transform-origin Coordinate Discrepancy

The problem: In the scroll-driven feature walkthrough (ScrollRoute.tsx), pin dot circles calculated their SVG cx/cy coordinates, but when animated with GSAP scale: 1, pins jumped across the screen during scroll.
Root cause: GSAP overrides element transform state when tweening CSS transform: scale, recalculating SVG transform-origin around its internal bounding box center rather than honoring the inline CSS transform-origin.
The fix: Instead of scaling SVG elements via CSS transforms, the initial circle geometry attribute r is set directly (r={0}) and animated using GSAP's native attribute plugin (attr: { r: 8 }), resolving origin calculation conflicts across all browsers.

2. Last-Write-Wins Collaboration over Supabase Realtime

Building multi-user collaboration without a full CRDT implementation required a deliberate, defensible conflict resolution strategy.

The problem: Two users can simultaneously edit the same activity. Supabase Realtime broadcasts Postgres row changes to all connected clients, but there's no built-in merge — the client has to decide what to do when an incoming UPDATE conflicts with a local in-flight save.

The solution: Each activity row carries an updated_at timestamp set by a BEFORE UPDATE Postgres trigger (not the client). When an UPDATE arrives over the Realtime channel:

  1. Compare incoming row's updated_at with local row's updated_at.
  2. If incoming timestamp is older than local, silently discard the remote event — local version wins.
  3. If incoming timestamp is newer, replace local row and show a non-blocking toast: "Updated by [name]".

The presence layer (Supabase Realtime Presence) shows who's currently on the page, reducing the chance of collision in the first place.


Architecture

                         ┌─────────────────────────────────────────┐
                         │             Browser (Next.js 15)         │
                         │                                          │
                         │  App Router (RSC + Client Components)    │
                         │  ┌───────────┐  ┌──────────────────────┐│
                         │  │ Three.js  │  │ GSAP ScrollTrigger   ││
                         │  │ Hero Globe│  │ Landing animations   ││
                         │  └───────────┘  └──────────────────────┘│
                         │  ┌───────────┐  ┌──────────────────────┐│
                         │  │ @dnd-kit  │  │ MapLibre GL JS       ││
                         │  │ Timeline  │  │ Animated route map   ││
                         │  └───────────┘  └──────────────────────┘│
                         └────────┬────────────────┬────────────────┘
                                  │  HTTPS          │ WebSocket
                    ┌─────────────▼─────────────────▼──────────────┐
                    │                  Supabase                     │
                    │  ┌──────────┐  ┌──────────┐  ┌───────────┐  │
                    │  │ Postgres │  │   Auth   │  │ Realtime  │  │
                    │  │ (trips,  │  │ (JWT +   │  │ (Postgres │  │
                    │  │  days,   │  │  PKCE)   │  │  Changes  │  │
                    │  │ activities│ │          │  │ Presence) │  │
                    │  └──────────┘  └──────────┘  └───────────┘  │
                    └───────────────────────────────────────────────┘
                                          │
                    ┌─────────────────────▼─────────────────────────┐
                    │               MapTiler                         │
                    │    (Vector tile server for MapLibre GL)        │
                    └───────────────────────────────────────────────┘

Tech Stack

Layer Technology
Framework Next.js 15 (App Router, React Server Components)
Language TypeScript 5 (strict mode)
Styling Tailwind CSS + semantic token design system (petrol teal / brass)
Typography Google Sans (self-hosted via next/font/local)
3D / WebGL Three.js via @react-three/fiber + @react-three/drei
Animations GSAP 3 + ScrollTrigger (gated on prefers-reduced-motion)
Drag & Drop @dnd-kit/core + @dnd-kit/sortable
Maps MapLibre GL JS + MapTiler vector tiles
Backend Supabase (Postgres, Auth PKCE, Realtime, Storage)
Deployment Vercel (Edge middleware for auth, Speed Insights)
Testing Vitest (unit) + Playwright (e2e smoke)
CI GitHub Actions (lint → typecheck → unit → build → e2e)

Local Setup

Prerequisites

Steps

# 1. Clone the repo
git clone https://github.com/x0911/waypoint.git
cd waypoint

# 2. Install dependencies
npm install

# 3. Configure environment variables
cp .env.example .env.local
# Edit .env.local — fill in your real values (never commit this file)

Open .env.example to see which variables are required:

NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
NEXT_PUBLIC_MAPTILER_KEY=
SUPABASE_SECRET_KEY=
SUPABASE_JWKS_URL=

⚠️ Real credentials are never committed. .env.local is in .gitignore. Only .env.example with empty placeholder values is tracked.

# 4. Run database migrations (in Supabase SQL editor or CLI)
# Apply the SQL from supabase/migrations/ in order

# 5. Start the dev server
npm run dev
# → http://localhost:3000

Running Tests

npm test              # Vitest unit tests (fast, no browser)
npm run test:e2e      # Playwright smoke tests (requires running dev server)
npm run test:coverage # Vitest with v8 coverage report

Project Structure

waypoint/
├── app/
│   ├── (app)/              # Authenticated routes (trips, dashboard)
│   ├── (auth)/             # Login, signup pages (split layout)
│   ├── (marketing)/        # Landing page & /privacy page
│   └── t/[shareSlug]/      # Public read-only share view
├── components/
│   ├── features/
│   │   ├── budget/         # Budget rollup panel + utils
│   │   ├── collaboration/  # Presence avatars + live indicator
│   │   ├── landing/        # Three.js hero, scroll route, comparison, sandbox, FAQ
│   │   ├── map/            # MapLibre route map + layout
│   │   ├── sharing/        # Publish toggle
│   │   └── timeline/       # DnD itinerary editor
│   ├── theme/              # Theme toggle, provider
│   └── ui/                 # Button, Card, Badge, Modal, Toast, Input
├── hooks/                  # useRealtimeTrip, useTripPresence, useReducedMotion
├── lib/
│   ├── supabase/           # Client, server, types
│   └── validation/         # Zod schemas
├── public/fonts/           # Google Sans woff2 files
├── supabase/migrations/    # Postgres schema + RLS policies
└── __tests__/
    ├── unit/               # Vitest unit tests
    └── e2e/                # Playwright smoke tests

Key Design Decisions

  • No CRDT: Last-write-wins with updated_at comparison. Simpler, explainable, appropriate for this domain.
  • SVG Attribute Animation: GSAP animates SVG geometry attributes (r) directly to prevent transform-origin misalignment.
  • SSR-first: Trip editor data is fetched in Server Components and passed as props. Realtime updates are overlaid by client hooks.
  • Reduced motion everywhere: Every GSAP animation (hero, scroll route, drag reorder, map line) is gated on useReducedMotion(). Three.js auto-rotation stops. CSS transitions are handled by prefers-reduced-motion in globals.css.
  • Semantic color tokens only: No raw hex, no raw Tailwind palette classes anywhere in components. Every color goes through the design token layer in tailwind.config.ts.
  • Public repo safety: Only .env.example (empty values) is committed. The CI pipeline uses stub env vars for the build step.

License

MIT — see LICENSE

About

Animated Collaborative Trip Planner

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages