The homepage was a marketing brochure -- a hero paragraph, a "three steps" explainer and a tag list. It described the gesture in prose while the actual Tinder deck sat behind /deck/[jobId], reachable only after signing in AND posting a job. Nobody opening the app ever saw the product. Now / renders a phone illustration with the real app running inside it: the same <Deck>, the same drag physics, real verified pros. On a phone the bezel collapses and the deck simply fills the viewport -- drawing a picture of a phone on a phone is absurd, and it would eat the width the cards need. - Removed the fixed app bar and bottom tab bar. AppShell now renders the screen title as an in-flow h1; the bar owned the only h1 on every screen, so dropping it silently would have left every page headingless. The /jobs "post" action moved from bar chrome into the content, since the tab bar was its only other route there. - getShowcaseDeck(): a deck with no job behind it. getDeck is job-scoped (joins jobs for category and location, anti-joins swipes), which an anonymous visitor has none of, so this centres on the launch city. Eligibility rules are copied verbatim -- nobody may appear in the shop window who could not appear on a real deck. - deck.showcase: the only public procedure in the router. list and swipe stay behind clientProcedure. It reads nothing about the caller and writes nothing, so a right swipe on the entry screen is purely local. No real tradesperson is contacted until a job is posted. - <Deck> filled a hardcoded 560px desktop box; it now fills its container. The card counter moved out from between the two action buttons so the thumb zone holds nothing but the two controls. Verified against the seeded database: 22 eligible pros returned, and all three seeded traps excluded for the right reason -- Pau Ribas (22km out, 5km radius), Unverified Ulla (pending), Away Arnau (not accepting). 7 new integration tests cover exactly that. Also corrects a label I had written as "Plumbers near you" -- the showcase deck is not category-filtered and shows every trade. Includes concurrent edits to the mobile shell, ui/ primitives and DESIGN.md made outside this session. typecheck, lint, build clean; 141 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
40 lines
1.2 KiB
TypeScript
40 lines
1.2 KiB
TypeScript
import { redirect } from 'next/navigation';
|
|
import { getApi } from '@/server/caller';
|
|
import { BareShell } from '@/components/chrome/app-shell';
|
|
import { RoleChooser } from './role-chooser';
|
|
|
|
export const metadata = { title: 'Welcome' };
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
/**
|
|
* Role selection.
|
|
*
|
|
* Google signup lands everyone on the `client` default, so a tradesperson would
|
|
* otherwise reach the customer deck with the wrong account type. Phone signup
|
|
* has the same problem. This is the fork.
|
|
*/
|
|
export default async function OnboardingPage() {
|
|
const api = await getApi();
|
|
|
|
let me: Awaited<ReturnType<typeof api.user.me>>;
|
|
try {
|
|
me = await api.user.me();
|
|
} catch {
|
|
redirect('/sign-in?next=/onboarding');
|
|
}
|
|
|
|
// Someone who already committed to a role does not need to see this again.
|
|
if (me.role === 'admin') redirect('/admin');
|
|
if (me.role === 'pro') redirect(me.hasProProfile ? '/pro' : '/pro/onboarding');
|
|
|
|
return (
|
|
<BareShell>
|
|
<h1 className="text-h1">What brings you here?</h1>
|
|
<p className="mt-3 text-body text-muted">
|
|
You can only pick once, so choose the one that fits.
|
|
</p>
|
|
<RoleChooser />
|
|
</BareShell>
|
|
);
|
|
}
|