Greenfield scaffold for Linkder, a swipe-to-hire marketplace for local
professional services.
- pnpm/turbo monorepo: apps/web, packages/{shared,db}
- Postgres 16 + PostGIS via docker compose (ports 5442/6389 to avoid
clashing with other local stacks)
- Drizzle schema, 23 tables, geography(Point,4326) with GiST indexes
- Domain core in packages/shared: integer-cent money, status transition
graphs, deck ranking weights, cancellation policy — 46 unit tests
- Deck query: filtering in Postgres on the GiST index, ranking in JS so
the weights stay tunable — 18 integration tests against a seeded DB
- Deterministic seed placing pros at known distances, including three
that must NOT appear on a deck (out of radius, unverified, away)
- Next.js 15 app shell with a working swipe deck
- CI: typecheck, lint, test, build against live postgres+redis
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
45 lines
1.4 KiB
TypeScript
45 lines
1.4 KiB
TypeScript
import { eq } from 'drizzle-orm';
|
|
import { notFound } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import { ArrowLeft } from 'lucide-react';
|
|
import { db, getDeck, schema } from '@linkder/db';
|
|
import { DeckClient } from './deck-client';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
export default async function DeckPage({ params }: { params: Promise<{ jobId: string }> }) {
|
|
const { jobId } = await params;
|
|
|
|
const job = await db.query.jobs.findFirst({
|
|
where: eq(schema.jobs.id, jobId),
|
|
with: { category: true },
|
|
});
|
|
if (!job) notFound();
|
|
|
|
const cards = await getDeck(db, { jobId });
|
|
|
|
return (
|
|
<main className="mx-auto flex min-h-dvh max-w-2xl flex-col px-4 py-6">
|
|
<header className="mb-6">
|
|
<Link
|
|
href="/"
|
|
className="mb-4 inline-flex items-center gap-1.5 text-sm text-[var(--muted)] hover:text-[var(--fg)]"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" aria-hidden />
|
|
Back
|
|
</Link>
|
|
<p className="text-sm text-[var(--muted)]">
|
|
{job.category.name} · {job.addressText}
|
|
</p>
|
|
<h1 className="text-xl font-semibold">{job.title}</h1>
|
|
</header>
|
|
|
|
<DeckClient jobId={jobId} cards={cards} />
|
|
|
|
<p className="mt-8 text-center text-xs text-[var(--muted)]">
|
|
Swipe right to send this job to a pro, left to pass. Drag the card or use the buttons.
|
|
</p>
|
|
</main>
|
|
);
|
|
}
|