Entry screen is the app: phone frame with a live deck inside
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>
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { and, count, eq } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { getDeck, getDeckCount, schema } from '@linkder/db';
|
||||
import { getDeck, getDeckCount, getShowcaseDeck, schema } from '@linkder/db';
|
||||
import {
|
||||
DECK_PAGE_SIZE,
|
||||
MAX_OPEN_REQUESTS_PER_JOB,
|
||||
REQUEST_TTL_HOURS,
|
||||
swipeSchema,
|
||||
} from '@linkder/shared';
|
||||
import { clientProcedure, router } from '../trpc';
|
||||
import { clientProcedure, publicProcedure, router } from '../trpc';
|
||||
import type { Context } from '../context';
|
||||
|
||||
/**
|
||||
@@ -33,6 +33,31 @@ async function requireOwnedJob(ctx: Context, jobId: string) {
|
||||
}
|
||||
|
||||
export const deckRouter = router({
|
||||
/**
|
||||
* The demo deck on the entry screen — real verified pros, no job, no session.
|
||||
*
|
||||
* The ONLY public procedure in this router: `list` and `swipe` both stay
|
||||
* behind clientProcedure. This one reads nothing about the caller and writes
|
||||
* nothing, so a right swipe here is purely local — it advances the card and
|
||||
* stops. Nobody is contacted until the visitor posts an actual job.
|
||||
*/
|
||||
showcase: publicProcedure
|
||||
.input(z.object({ limit: z.number().int().min(1).max(DECK_PAGE_SIZE).optional() }).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
const lat = Number(process.env.NEXT_PUBLIC_CITY_LAT);
|
||||
const lng = Number(process.env.NEXT_PUBLIC_CITY_LNG);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
||||
// An unset city would silently make every pro "out of radius" and show
|
||||
// an empty deck that looks like "we have no pros".
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'NEXT_PUBLIC_CITY_LAT / NEXT_PUBLIC_CITY_LNG are not set.',
|
||||
});
|
||||
}
|
||||
const cards = await getShowcaseDeck(ctx.db, { lat, lng, limit: input?.limit });
|
||||
return { cards };
|
||||
}),
|
||||
|
||||
/** The next cards for one of the caller's own jobs. */
|
||||
list: clientProcedure
|
||||
.input(z.object({ jobId: z.string().uuid(), limit: z.number().int().min(1).max(50).optional() }))
|
||||
|
||||
Reference in New Issue
Block a user