SortMyLife is a calm, local-first productivity dashboard built with React + TypeScript + Vite.
- Vite
- React + React Router
- TypeScript
- Tailwind CSS
- Zustand
- Framer Motion
- Recharts
- Netlify Functions
- localStorage persistence (no backend database)
SortMyLife now includes a few extra gentle touches to make daily use feel calmer, clearer, and more supportive.
-
Memory Match mini game
- A simple 4x4 card match game with move tracking and best-score history.
- Component:
src/components/games/MemoryMatchGame.tsx - Appears inside Pause Corner on:
src/pages/DashboardPage.tsxsrc/pages/GamesPage.tsx- via
src/components/pause/PauseCornerSection.tsx
-
Pause Corner
- A soft “take-a-breath” area with quick reset tools (breathing bubble + Memory Match), plus hide/show controls.
- Component:
src/components/pause/PauseCornerSection.tsx - Rendered on dashboard and games pages:
src/pages/DashboardPage.tsxsrc/pages/GamesPage.tsx
-
Engagement cards (“Guidance for you”)
- Rotating guidance cards with Save / Like / Hide actions and fallback states.
- Component:
src/components/dashboard/GuidanceForYouSection.tsx - Added to:
src/pages/DashboardPage.tsx
-
Toasts (gentle feedback messages)
- Lightweight toast notifications for saves, encouragement, and celebration moments.
- Store:
src/store/useToastStore.ts - Viewport:
src/components/common/ToastViewport.tsx - Mounted globally in:
src/App.tsx
-
Skeleton loading states
- Soft skeleton placeholders for loading moments on key screens/cards.
- Base skeleton:
src/components/common/Skeleton.tsx - Presets:
src/components/common/PageSkeletons.tsx - Used in places like:
src/components/dashboard/GuidanceForYouSection.tsxsrc/pages/PeoplePage.tsxsrc/pages/ProfilePage.tsx
-
Page + component transitions
- Calm fade/slide transitions across routes, layout content, cards, toasts, and selected mini-game interactions.
- Key files:
src/App.tsx(route transitions)src/components/layout/AppLayout.tsx(page container transitions)src/components/common/Card.tsx(card entrance motion)src/components/common/ToastViewport.tsx(toast enter/exit motion)src/components/games/MemoryMatchGame.tsx(tile feedback + win message motion)
-
Saved locally (device/browser)
- Core personal app state is persisted with Zustand + localStorage in
src/store/useAppStore.ts(sort-my-life-v3). - Pause Corner visibility preference uses
useLocalStorageinsrc/components/pause/PauseCornerSection.tsx. - Memory Match last/best scores are saved in localStorage in
src/components/games/MemoryMatchGame.tsx. - Toasts are intentionally in-memory/temporary for quick feedback (
src/store/useToastStore.ts).
- Core personal app state is persisted with Zustand + localStorage in
-
Saved via backend (when shared features are used)
- Shared workspace/auth flows use Supabase helpers in
src/lib/supabase.tsand related workspace/auth pages. - Examples:
src/pages/WorkspacePage.tsx,src/pages/TasksPage.tsx,src/pages/HabitsPage.tsx,src/pages/ProfilePage.tsx. - Practical model: personal wellbeing flow stays local-first; collaborative/shared data syncs through Supabase.
- Shared workspace/auth flows use Supabase helpers in
- Animations are implemented with Framer Motion (
motion,AnimatePresence,useReducedMotion) for route changes, cards, toasts, and interactive micro-feedback. - If someone prefers reduced motion, components switch to minimal or no movement (for example, opacity-only transitions or disabled hover/tap transforms).
- CSS also respects reduced-motion for non-Framer effects (like skeleton shimmer and decorative animation) using
@media (prefers-reduced-motion: reduce)insrc/index.css.
- No new dependency beyond current stack is required for these additions.
- Framer Motion is the primary animation dependency used more broadly across the new interactive experiences:
- package:
framer-motion - current version in this repo:
^12.23.24(seepackage.json)
- package:
npm install
npm run devOpen: http://localhost:5173
- Install Netlify CLI (if needed):
npm i -g netlify-cli
- Create a
.envfile (or export env var in shell):Or set it in your current shell session:OPENAI_API_KEY=your_key_here
export OPENAI_API_KEY="your_key_here"
- Run Netlify dev:
npm run netlify:dev
- Open the local URL from Netlify dev and click Plan My Day on the dashboard.
Important: the AI button calls a Netlify Function at /api/plan-my-day, so npm run dev alone will return 404 for that feature.
/api/* is redirected to Netlify Functions in netlify.toml to avoid SPA fallback returning HTML to the AI fetch call.
npm run buildVite output directory: dist/.
- Check
netlify devterminal: Vite must be running on 5173. - Open
http://localhost:5173directly. If it works there, refreshhttp://localhost:8888. - If 5173 is busy, stop the conflicting process and restart
npm run netlify:dev. - Hard refresh browser (
Ctrl+Shift+R).
This app entrypoint is src/main.tsx and must be transformed by Vite.
If you open the project with a plain static server (for example python -m http.server, Live Server, or similar), the browser receives .tsx without a JS module MIME type and the page stays blank.
Use one of these instead:
npm run dev
# then open http://localhost:5173or
npm run netlify:dev
# then open the URL printed by Netlify (often http://localhost:8888)Quick verification:
http://localhost:5173/src/main.tsxshould returnContent-Type: text/javascriptwhen Vite is running.- If that URL is 404 or has an empty/wrong MIME type, restart the Vite/Netlify dev process.
If your Netlify UI still has @netlify/plugin-nextjs enabled from older builds, disable it in: Site settings → Build & deploy → Build plugins.
This repo is a Vite app, not Next.js. netlify.toml also sets NEXT_PLUGIN_FORCE_RUN=false as a safeguard.
Add these environment variables in Netlify (and locally for netlify dev):
OPENAI_API_KEY=...
TURNSTILE_SECRET_KEY=...
VITE_TURNSTILE_SITE_KEY=...The AI endpoints enforce:
- cooldown between requests
- short-window and daily caps
- quick verification via Cloudflare Turnstile
- prompt/input size limits and safe timeouts
If users see "Unable to connect to website" or repeated verification failures, run this checklist in order:
-
Check Turnstile widget hostnames in Cloudflare
- Go to Cloudflare Dashboard → Turnstile → Widgets → (your widget).
- Under Hostname management, make sure every live hostname is listed (for example:
sortmylife.com.au,www.sortmylife.com.au, and any temporary domain in use). - Save changes.
-
Confirm key pairing is correct
- In Cloudflare, copy the widget Site Key and Secret Key.
- In Netlify go to Site configuration → Environment variables and confirm:
VITE_TURNSTILE_SITE_KEY= Site KeyTURNSTILE_SECRET_KEY= Secret Key
- If you rotate keys, update both values and redeploy.
-
Set backend hostname allowlist in Netlify
- In Netlify environment variables, set:
TURNSTILE_ALLOWED_HOSTNAMES=sortmylife.com.au,www.sortmylife.com.au,sortmylife.netlify.app
- Include any additional production/staging hostname users actually open.
- In Netlify environment variables, set:
-
Redeploy after env var changes
- In Netlify, trigger Deploys → Trigger deploy → Deploy site.
- Environment variable changes are not active until a new deploy finishes.
-
Verify in browser and in-app browser
- Test on Safari/Chrome first.
- If failure only happens inside apps like Instagram/Messenger webviews, ask users to open in Safari/Chrome as a fallback.
-
Check function logs for verification detail
- Netlify: Functions → support-chat (or tarot/astrology functions) → Logs.
- Look for Turnstile verification errors and error codes returned by Cloudflare.
To enable email sign-in, workspaces, email invites, and synced shared entries:
- Create a Supabase project.
- Run
supabase/schema.sqlin the Supabase SQL editor. - Add environment variables locally and in Netlify:
VITE_SUPABASE_URL=...
VITE_SUPABASE_ANON_KEY=...- In Supabase Auth settings, set the site URL and redirect URL to your app:
- Site URL:
https://sortmylife.netlify.app - Additional Redirect URLs:
https://sortmylife.netlify.app/**http://localhost:3000/**
- Site URL:
Workspace/auth routes:
/workspace→ shared workspace home (create space, invite, shared tasks/routines/check-ins)/join?token=...→ accepts invite token and joins workspace/auth/confirm→ consumes Supabase hash session and continues redirect flow/workspace?workspace=<id>→ opens a specific workspace after join flow/login→ email + password sign in/signup→ email + password sign up with username/forgot-passwordand/reset-password→ password reset flow
The app currently uses Supabase REST endpoints (not Supabase JS auth client):
POST /auth/v1/signupfor sign upPOST /auth/v1/token?grant_type=passwordfor loginPOST /auth/v1/recover+/reset-passwordroute for password reset
Recommended Supabase Auth redirect settings:
- Site URL:
https://sortmylife.netlify.app - Additional Redirect URLs:
https://sortmylife.netlify.app/auth/confirmhttp://localhost:3000/auth/confirmhttps://sortmylife.netlify.app/join**(invite links)http://localhost:3000/join**(local invite links)
By default, friend requests appear as in-app notifications only. To also send email notifications when a friend request is created:
- Add these Netlify environment variables:
RESEND_API_KEY(from Resend)RESEND_FROM_EMAIL(verified sender, e.g.SortMyLife <hello@sortmylife.com.au>)
- Redeploy the site after adding env vars.
- Edit the template in
netlify/functions/_shared/emailTemplates.ts(buildFriendRequestEmailTemplate) to customize colors/copy.
Implementation notes:
- Function endpoint:
netlify/functions/friend-request-email.ts - Trigger point:
sendFriendRequest(...)insrc/lib/supabase.ts(non-blocking call so request creation still succeeds if email fails).
Apply migration supabase/migrations/20260413_posts_media_feed.sql.
This migration adds:
poststable withmedia jsonbarray,visibility(friendsorworkspace), timestamps, indexes, and RLS.post_reactionstable with one reaction per user per post (like,love,care,haha) and RLS.post-mediaSupabase storage bucket with a 50MB object limit and MIME allowlist:- images: png, jpg/jpeg, webp, heic, heif
- videos: mp4, webm, mov (
video/quicktime)
Client behavior:
- HEIC/HEIF is converted to JPEG before upload.
- Images are compressed client-side.
- Videos can include uploaded poster thumbnails.
- Instagram/TikTok URLs use an external-open fallback button (not native
<video>playback).