Skip to content

JacobMitchell088/swipe-date

Repository files navigation

swipe date

A two-person "what should we do tonight?" picker. Each person swipes through activity ideas on their own phone, in real time. When you both swipe right on the same thing it shows up as a match. When you can't agree, spin a wheel across the mutual likes — or long-press one to commit. Either path ends on a celebratory lock-in screen with nearby spots pulled from Foursquare.

No accounts. A 4-letter room code is the only credential.

Features

  • Real-time two-player swiping. Both phones stay in sync over Firestore; no refresh, no polling.
  • Mobile-first PWA. Locked viewport, safe-area-aware, swipes never get confused with scrolling. Add to home screen for the full app feel.
  • 130+ curated default activities plus a + button to drop custom ideas into the deck. Custom adds land a few cards deep so the image has time to preload before either partner reaches it.
  • One-shot top pick. Each user gets exactly one star — locks in immediately and is guaranteed to surface for the partner regardless of how they swipe.
  • Undo the last pass. Likes are off-limits (rewinding could nullify a match the partner already saw), but the most recent discard is one tap away.
  • Match grid with preview / commit. Tap a match thumbnail for a full preview; press and hold for 3 seconds (with a building shake) to lock that match in directly without spinning.
  • Tiebreaker wheel. When two or more matches are tied, both partners opt in and spin a weighted wheel that lands on one of the mutual likes.
  • Lock-in celebration. The winning activity gets a full-screen victory screen: hero photo, confetti, sparkles, and a Foursquare-powered list of nearby spots matching the activity.
  • Global image cache. Every photo ever fetched for an activity is stored in a top-level Firestore collection. After the first ~130 hits across the app's lifetime, default activities serve from cache.
  • Photo fallback chain. Unsplash primary, Pexels fallback, with a module-level throttle that backs off for an hour after a 429/403 from either vendor.
  • Built-in feedback. A "give feedback" button on the home and lock-in screens opens a small rating + text form. Submissions are posted as tagged issues on the project's GitHub repo via a server-side token, so no third-party form service is involved.
  • Hardened for public release. Firestore rules enforce a 30-day write TTL and per-doc size caps; both /api routes are origin-locked and IP rate-limited (feedback 3/min, places 30/min). Vercel Analytics tracks funnel events (room creation, partner-joined, matches, lock-ins) without any third-party trackers.

Tech

Vite + React 19 + React Router 7 + Framer Motion 12. Firestore for realtime sync. Unsplash + Pexels for activity photos. Foursquare for nearby places (proxied through a Vercel serverless function so the key stays server-side and CORS doesn't matter). Pure CSS — no Tailwind, no styled-components.

1. One-time setup

Firebase (real-time sync — free tier)

  1. https://console.firebase.google.com/Add project.
  2. Skip Google Analytics.
  3. In the project, click the Web (</>) icon to register a web app. Copy the firebaseConfig values — you'll paste them in the env file below.
  4. Sidebar → Build → Firestore Database → Create database. Pick a region. Start in production mode.
  5. Rules tab → paste the contents of firestore.rulesPublish.

Unsplash (activity photos — free tier)

  1. https://unsplash.com/developers → sign up.
  2. New Application → accept the API guidelines → name it swipe-date.
  3. Copy the Access Key. Demo tier is 50 req/hr; the global image cache makes that more than enough.

Pexels (photo fallback — free tier, optional but recommended)

  1. https://www.pexels.com/api/ → sign up.
  2. Copy the API key. Free tier is 200 req/hr + 20k/month.

Foursquare (nearby spots — free tier)

  1. https://foursquare.com/developers/ → create a project.
  2. Generate a Service API Key under Service API keys. Don't fill in the OAuth section — that's only for end-user "log in with Foursquare" flows we don't use.
  3. Copy the key.

GitHub (feedback — optional)

The in-app "give feedback" button files a tagged issue on a GitHub repo. If you don't configure this, the button still renders but submissions fail with no-token — completely optional for a personal/local install.

  1. https://github.com/settings/tokens?type=betaGenerate new token (fine-grained).
  2. Limit to the single repo you want feedback issues filed to.
  3. Permissions → Issues: read and write. That's all it needs.
  4. Copy the token. Set GITHUB_TOKEN in your env. Optionally set GITHUB_REPO=owner/repo if you're forking and want issues to land in your own fork instead of upstream.

Environment file

cp .env.example .env

Fill in:

  • VITE_FIREBASE_* (six keys, from Firebase web-app config)
  • VITE_UNSPLASH_ACCESS_KEY
  • VITE_PEXELS_API_KEY (optional fallback)
  • VITE_FOURSQUARE_API_KEY (for local dev; production uses FOURSQUARE_API_KEY)
  • GITHUB_TOKEN (optional — enables the feedback button)
  • GITHUB_REPO (optional — defaults to upstream, set to your fork's owner/repo if forking)
  • ALLOWED_ORIGINS (optional — comma-separated origin allowlist for the /api/* routes; if unset, only same-origin requests are accepted)

Vite only reads .env at process start — restart npm run dev after editing.

2. Run it

npm install
npm run dev

Open the printed URL on your phone (same Wi-Fi as your laptop — start with npm run dev -- --host so Vite binds to your network IP).

3. Use it

  1. Person A taps Start a new session, enters their name → gets a 4-letter code.
  2. Person B opens the app, taps Join session, types the code.
  3. Both swipe — right to like, left to pass. Tap the corner star on a card to mark it as your one top pick.
  4. Tap the heart in the top-right (or See matches when you're done) to see mutual likes and each other's stars.
  5. Tap the + floating button to add custom activities mid-session.
  6. From the matches screen, tap a match to peek at nearby spots, or press and hold for 3 seconds to lock it in directly. For ties, tap Decision Maker — once both partners opt in, the wheel spins.
  7. The lock-in screen shows the winner with confetti and nearby places. Tap we're out → to terminate the session for both partners.

4. Deploy to Vercel

  1. Push to GitHub.
  2. https://vercel.comAdd New → Project → import the repo.
  3. Framework preset: Vite.
  4. Environment Variables:
    • All VITE_FIREBASE_* keys (six)
    • VITE_UNSPLASH_ACCESS_KEY
    • VITE_PEXELS_API_KEY (optional)
    • FOURSQUARE_API_KEY (no VITE_ prefix — kept server-side only)
    • GITHUB_TOKEN (optional, no VITE_ prefix — enables the feedback button)
    • GITHUB_REPO (optional, no VITE_ prefix — owner/repo for feedback issues)
    • ALLOWED_ORIGINS (optional, no VITE_ prefix — set this if you serve the app from a custom domain different to the Vercel deployment URL)
  5. Deploy. Both /api/places and /api/feedback routes are automatically served by Vercel's serverless functions from api/places.js and api/feedback.js. Both routes are origin-locked (same-origin only by default) and IP-rate-limited at the function entry. To enable Vercel's first-party analytics dashboard, toggle Analytics on for the project in the Vercel dashboard — the <Analytics /> component is already wired up in src/App.jsx.

If you previously set VITE_FOURSQUARE_API_KEY on Vercel, swap it for the non-prefixed FOURSQUARE_API_KEY so the key never ships in the client bundle. The same rule applies to GITHUB_TOKEN — never prefix it with VITE_.

Project layout

api/
├── places.js                    Vercel function — Foursquare proxy (CORS workaround)
└── feedback.js                  Vercel function — files GitHub issues from the feedback modal
src/
├── firebase.js                  Firestore client init
├── data/defaultActivities.js    130+ curated activity defaults
├── hooks/
│   ├── useRoom.js               Room CRUD, realtime subscription, swipe/star/tiebreaker writes
│   ├── useUnsplash.js           Vendor-agnostic photo fetch (Unsplash → Pexels → global cache)
│   └── usePlaces.js             Geolocation + /api/places client
├── components/
│   ├── SwipeCard.jsx            One draggable card
│   ├── SwipeDeck.jsx            Top-3 stack + prefetcher
│   ├── MatchesView.jsx          Match grid + star row + hold-to-commit gesture
│   ├── ActivityPreviewModal.jsx Tap-to-peek sheet
│   ├── Tiebreaker.jsx           Spin / result / lock-in three-phase overlay
│   ├── FeedbackDialog.jsx       Rating + free-text feedback modal
│   ├── Hint.jsx                 Retro tap-to-dismiss contextual tips
│   └── PlacesList.jsx           Foursquare results renderer
├── screens/
│   ├── Home.jsx                 Create / join
│   ├── Room.jsx                 Main swiping + celebration burst
│   ├── Matches.jsx              Standalone matches screen
│   └── AddActivity.jsx          Modal for custom ideas
├── App.jsx                      Router
└── index.css / App.css          Styles
vite.config.js                   Dev middleware that mounts api/*.js at /api/*
firestore.rules                  No-auth design — anyone with a code can read/write the room

How the image cache works

Photos are looked up in this order:

  1. In-memory promise dedupe — concurrent callers share one round trip.
  2. imageCache/{activityId} — global Firestore collection, persists across every room ever created.
  3. Unsplash search, with a 1-hour throttle that engages after any 429/403 to keep the demo tier alive.
  4. Pexels search as fallback, same throttle mechanism.
  5. On a successful fetch, the photo is written back to imageCache/{activityId} and into rooms/{code}.images.{id} so the partner sees it through the existing room snapshot without a separate getDoc.

SwipeDeck prefetches metadata + bytes for the next 6 cards. The top card's <img> uses fetchPriority="high"; the stack behind uses "low". All use decoding="async".

Unsplash URLs are tuned at w=600&h=800&fit=crop&fm=jpg&q=75&auto=format&dpr=2 for ~80–150 KB WebP/AVIF. Pexels uses its src.portrait (800×1200 pre-cropped).

Privacy / safety

  • Anyone with the 4-letter code can read & write the room — that's by design. Treat the code like a shared secret. Room writes are capped at a 30-day TTL in firestore.rules so abandoned codes can't be hijacked indefinitely.
  • Photos are credited inline per Unsplash's API guidelines.
  • Foursquare and GitHub keys never ship to the browser when deployed (both /api routes hold them server-side).
  • The serverless API routes (/api/feedback, /api/places) are origin-locked (same-origin only by default; configurable via ALLOWED_ORIGINS) and IP-rate-limited (3/min and 30/min respectively).
  • Feedback submissions go directly to the configured GitHub repo as tagged issues. The contact field is optional and only stored where the issue is filed — no third-party form service is involved.
  • Vercel Analytics is enabled if you toggle it on in the Vercel dashboard. It collects anonymous funnel events (room created, partner joined, match found, lock-in) with no personally identifying information. No third-party trackers are loaded.

Reset a room

In the Firestore console, delete the doc under rooms/{CODE}. To wipe the global image cache, delete the imageCache collection.

License

MIT — see LICENSE.

About

A two person "what should we do tonight?" Swipe activity ideas on your phones, matches surface in real time.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors