M1: phone app shell, settings, profile, dev login

Everything now renders inside a phone illustration on the entry screen,
with a five-tab bar. The frame lives in the root layout rather than one
page, so sign-in, onboarding and the job form are inside it too.

- Entry screen is the product running, not a marketing page: a live
  swipeable deck of real verified pros with a trade-filter strip above
  the card. deck.showcase is the only public procedure in that router
  and writes nothing, so an anonymous right swipe reaches no one.

- Settings: notification preferences (new table, defaults returned when
  no row exists), signed-in devices, GDPR export, deletion request.

  Closes the setEmail finding: an unverified address is no longer
  written to users.email, which is UNIQUE -- claiming a stranger's
  address used to block them from ever signing up with Google, and the
  uniqueness error leaked whether an address was registered. Now parked
  in email_change_requests until a token proves ownership.

- Profile: for a pro it leads with their REAL deck card, rendered by the
  same exported <Card> clients swipe, so the two cannot drift. Adds
  pro.previewCard (works at draft/pending, where publicProfile 404s) and
  pro.reorderMedia (photo position 0 is the deck card). Warns before an
  edit that would send a verified pro back for review, rather than after
  it silently drops them off the deck. Clients get a thin profile plus a
  route into pro onboarding -- supply is the launch blocker.

- Dev login: +34600000000 / 000000, behind THREE guards (NODE_ENV,
  an explicit ALLOW_DEV_LOGIN flag, and an exact number match). It
  overwrites the stored code rather than skipping verification, so the
  real expiry, attempt cap and single-use consumption still apply.

- Seed uses portrait photos. The cards previously showed picsum stock
  scenery -- a locksmith standing on a railway track.

Fixes found along the way: the card's name rendered ink-950 navy on a
dark photo because globals.css sets h1..h6 colour in @layer base, which
beat the inherited text-white; and the card referenced --color-go-500,
--border and --card, none of which exist, so the SEND JOB stamp had no
colour.

Also adds public/sw.js as a kill-switch: a service worker left
registered on localhost:3000 by a different project was intercepting
this app's chunks.

typecheck, lint clean; 186 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
serfowi
2026-08-21 03:16:27 -04:00
co-authored by Claude Opus 5
parent 582f13fa99
commit 176ba187c8
51 changed files with 15622 additions and 85 deletions
@@ -0,0 +1,22 @@
CREATE TABLE "deletion_requests" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"reason" text,
"actioned_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "notification_preferences" (
"user_id" uuid PRIMARY KEY NOT NULL,
"sms_new_request" boolean DEFAULT true NOT NULL,
"sms_booking_reminder" boolean DEFAULT true NOT NULL,
"sms_marketing" boolean DEFAULT false NOT NULL,
"email_receipts" boolean DEFAULT true NOT NULL,
"email_marketing" boolean DEFAULT false NOT NULL,
"push_messages" boolean DEFAULT true NOT NULL,
"push_requests" boolean DEFAULT true NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "deletion_requests" ADD CONSTRAINT "deletion_requests_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification_preferences" ADD CONSTRAINT "notification_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
@@ -0,0 +1,12 @@
CREATE TABLE "email_change_requests" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"email" text NOT NULL,
"token" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"consumed_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "email_change_requests_token_unique" UNIQUE("token")
);
--> statement-breakpoint
ALTER TABLE "email_change_requests" ADD CONSTRAINT "email_change_requests_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
@@ -0,0 +1,3 @@
ALTER TABLE "users" ADD COLUMN "location" geography(Point,4326);--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "location_text" text;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "search_radius_m" integer DEFAULT 15000 NOT NULL;
@@ -0,0 +1 @@
ALTER TABLE "pro_profiles" ADD COLUMN "skills" text[] DEFAULT '{}'::text[] NOT NULL;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+28
View File
@@ -8,6 +8,34 @@
"when": 1787252153406,
"tag": "0000_material_shadow_king",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1787292806114,
"tag": "0001_bouncy_sally_floyd",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1787292942440,
"tag": "0002_jazzy_mac_gargan",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1787295054810,
"tag": "0003_amazing_turbo",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1787296176203,
"tag": "0004_closed_nextwave",
"breakpoints": true
}
]
}