Stock the trades we serve, and stop offering the ones we don't
A demo hit "That's everyone nearby" on Electrician after four swipes. That was the deck working — there were exactly four — but the shape of the catalogue was worse than it looked: 42 of the 50 trades had NO pros at all, so tapping Roofer or Cleaner hit the dead end immediately rather than after seven swipes. Two halves to the fix, and the second matters more. Depth: 56 more pros, so the fifteen live trades now run 3–15 deep instead of 1–12. Every photo was picked by reading Unsplash's written description and keeping only images that show the trade being DONE, then checking each URL resolves — one of 33 was a 404 and was dropped rather than seeded broken. Where a photo already belonged to someone in this file the new pro takes a name of the same gender: the face and the name on a card have to agree. Honesty: the other 35 trades are seeded isActive:false. A category with nobody behind it is not a feature, it is the product claiming to do something it cannot — and no amount of invented supply fixes that, it just moves the lie one screen later. LIVE_TRADES is the list, and widening it means recruiting pros first and adding the slug second. Locksmith is called out in the file: photo searches return padlocks, not locksmiths, so two of its cards carry door hardware. A lock is at least a locksmith's work. A stock portrait of somebody who is plainly not one is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+110
-1
@@ -74,6 +74,23 @@ function offset(lat: number, lng: number, metres: number, bearingDeg: number) {
|
||||
* stable key (pros, jobs and tests reference them); names and icons are cosmetic.
|
||||
* Icons are lucide names, kebab-case.
|
||||
*/
|
||||
/**
|
||||
* The trades this marketplace can actually serve.
|
||||
*
|
||||
* A category with no pros behind it is not a feature, it is a dead end: the
|
||||
* customer picks it, the deck is empty, and the product has told them it does
|
||||
* something it does not do. Everything outside this set is seeded `isActive:
|
||||
* false` and never reaches the trade strip or search.
|
||||
*
|
||||
* This is the honest shape of a launch market. Widening it means recruiting
|
||||
* supply first, then adding the slug here — in that order.
|
||||
*/
|
||||
const LIVE_TRADES = new Set([
|
||||
'plumber', 'electrician', 'handyman', 'painter', 'carpenter', 'locksmith',
|
||||
'appliance-repair', 'hvac', 'cleaner', 'gardener', 'mover', 'builder',
|
||||
'roofer', 'window-cleaner', 'pest-control',
|
||||
]);
|
||||
|
||||
const CATEGORIES = [
|
||||
// Core trades — the eight that carry most of the volume.
|
||||
{ slug: 'plumber', name: 'Plumber', icon: 'shower-head' },
|
||||
@@ -211,6 +228,98 @@ const PROS: SeedPro[] = [
|
||||
{ name: 'Unverified Ulises', cat: 'plumber', photo: 'photo-1749532125405-70950966b0e5', distanceM: 1_000, rating: null, reviews: 0, radius: 15_000, unverified: true },
|
||||
// Verified but on holiday — must never reach a deck.
|
||||
{ name: 'Away Arturo', cat: 'plumber', photo: 'photo-1676210134188-4c05dd172f89', distanceM: 1_100, rating: 4.9, reviews: 20, radius: 15_000, away: true },
|
||||
|
||||
/*
|
||||
* Depth, added after a demo ran a trade dry in four swipes.
|
||||
*
|
||||
* Every photo below was chosen by reading Unsplash's written description and
|
||||
* keeping only images that show the trade being done, then checking that each
|
||||
* URL resolves. Where a trade reuses a photo already in this file, the new pro
|
||||
* takes a name of the same gender as the existing one — the face and the name
|
||||
* on a card have to agree.
|
||||
*/
|
||||
|
||||
// ---- more of the trades that already had supply ----
|
||||
{ name: 'Ramiro Escalante', cat: 'plumber', photo: 'photo-1676210133055-eab6ef033ce3', distanceM: 3_700, rating: 4.6, reviews: 26, radius: 18_000 },
|
||||
{ name: 'Efraín Nava', cat: 'plumber', photo: 'photo-1749532125405-70950966b0e5', distanceM: 6_200, rating: 4.8, reviews: 51, radius: 22_000 },
|
||||
{ name: 'Ismael Zúñiga', cat: 'plumber', photo: 'photo-1676210134190-3f2c0d5cf58d', distanceM: 1_300, rating: 4.7, reviews: 34, radius: 15_000 },
|
||||
|
||||
{ name: 'Rubén Alcántara', cat: 'electrician', photo: 'photo-1660330589693-99889d60181e', distanceM: 5_400, rating: 4.7, reviews: 39, radius: 22_000 },
|
||||
{ name: 'Teresa Alcalá', cat: 'electrician', photo: 'photo-1646640381839-02748ae8ddf0', distanceM: 3_100, rating: 4.8, reviews: 45, radius: 20_000 },
|
||||
{ name: 'Hugo Peralta', cat: 'electrician', photo: 'photo-1621905251189-08b45d6a269e', distanceM: 7_600, rating: 4.5, reviews: 18, radius: 25_000 },
|
||||
|
||||
{ name: 'Fidel Barrera', cat: 'handyman', photo: 'photo-1621905251918-48416bd8575a', distanceM: 2_700, rating: 4.6, reviews: 30, radius: 15_000 },
|
||||
{ name: 'Ramón Ocampo', cat: 'handyman', photo: 'photo-1698998882494-57c3e043f340', distanceM: 5_800, rating: 4.7, reviews: 43, radius: 20_000 },
|
||||
{ name: 'Ernesto Lira', cat: 'handyman', photo: 'photo-1621905251918-48416bd8575a', distanceM: 8_900, rating: 4.4, reviews: 13, radius: 25_000 },
|
||||
|
||||
{ name: 'Verónica Pacheco', cat: 'painter', photo: 'photo-1717281234297-3def5ae3eee1', distanceM: 4_200, rating: 4.8, reviews: 48, radius: 20_000 },
|
||||
{ name: 'Salvador Ibarra', cat: 'painter', photo: 'photo-1652829069834-2c05031199c5', distanceM: 6_700, rating: 4.6, reviews: 25, radius: 22_000 },
|
||||
{ name: 'Lorena Quintero', cat: 'painter', photo: 'photo-1717281234297-3def5ae3eee1', distanceM: 1_700, rating: 4.9, reviews: 59, radius: 15_000 },
|
||||
|
||||
{ name: 'Emilio Cervantes', cat: 'carpenter', photo: 'photo-1544164560-adac3045edb2', distanceM: 3_800, rating: 4.7, reviews: 36, radius: 20_000 },
|
||||
{ name: 'Patricia Aguirre', cat: 'carpenter', photo: 'photo-1659930087003-2d64e33181f7', distanceM: 6_300, rating: 4.8, reviews: 54, radius: 25_000 },
|
||||
{ name: 'Raúl Mejía', cat: 'carpenter', photo: 'photo-1544164560-adac3045edb2', distanceM: 2_200, rating: 4.5, reviews: 20, radius: 18_000 },
|
||||
|
||||
// Locksmith is the thinnest trade for photography — the searches return
|
||||
// padlocks, not locksmiths. Two door-hardware shots carry the extra pros
|
||||
// rather than a stock portrait of somebody plainly not a locksmith.
|
||||
{ name: 'Aarón Valdés', cat: 'locksmith', photo: 'photo-1579382311054-0701b08da226', distanceM: 2_900, rating: 4.7, reviews: 33, radius: 22_000 },
|
||||
{ name: 'Óscar Lozano', cat: 'locksmith', photo: 'photo-1616358284394-acded77dcea5', distanceM: 6_100, rating: 4.6, reviews: 24, radius: 25_000 },
|
||||
{ name: 'Felipe Carrillo', cat: 'locksmith', photo: 'photo-1676630656246-3047520adfdf', distanceM: 1_400, rating: 4.9, reviews: 58, radius: 18_000 },
|
||||
|
||||
{ name: 'Alfonso Arriaga', cat: 'appliance-repair', photo: 'photo-1621905251918-48416bd8575a', distanceM: 4_600, rating: 4.7, reviews: 32, radius: 20_000 },
|
||||
{ name: 'Mauricio Tapia', cat: 'appliance-repair', photo: 'photo-1698998882494-57c3e043f340', distanceM: 7_200, rating: 4.6, reviews: 22, radius: 25_000 },
|
||||
{ name: 'Octavio Lugo', cat: 'appliance-repair', photo: 'photo-1621905251918-48416bd8575a', distanceM: 2_500, rating: 4.8, reviews: 46, radius: 18_000 },
|
||||
|
||||
{ name: 'Damián Solórzano', cat: 'hvac', photo: 'photo-1642749776312-aa42ce20c9f5', distanceM: 3_300, rating: 4.7, reviews: 37, radius: 22_000 },
|
||||
{ name: 'Fermín Villaseñor', cat: 'hvac', photo: 'photo-1705579605238-24a90c8799c5', distanceM: 6_900, rating: 4.8, reviews: 50, radius: 25_000 },
|
||||
{ name: 'Joaquín Bañuelos', cat: 'hvac', photo: 'photo-1642749776312-aa42ce20c9f5', distanceM: 1_100, rating: 4.5, reviews: 21, radius: 15_000 },
|
||||
|
||||
// ---- trades that had no supply at all until now ----
|
||||
{ name: 'Guadalupe Rentería', cat: 'cleaner', photo: 'photo-1646980241033-cd7abda2ee88', distanceM: 1_400, rating: 4.8, reviews: 63, radius: 15_000,
|
||||
skills: ['Deep cleans', 'Move-out cleans'] },
|
||||
{ name: 'Alma Trejo', cat: 'cleaner', photo: 'photo-1647381518264-97ff1835026f', distanceM: 3_200, rating: 4.7, reviews: 41, radius: 18_000 },
|
||||
{ name: 'Rocío Delgado', cat: 'cleaner', photo: 'photo-1758273238415-01ec03d9ef27', distanceM: 5_600, rating: 4.9, reviews: 88, radius: 20_000 },
|
||||
{ name: 'Isabel Mora', cat: 'cleaner', photo: 'photo-1758523670739-0d26a3ee976d', distanceM: 2_100, rating: 4.6, reviews: 24, radius: 12_000 },
|
||||
{ name: 'Yolanda Cárdenas', cat: 'cleaner', photo: 'photo-1758272421751-963195322eaa', distanceM: 7_300, rating: 4.5, reviews: 17, radius: 25_000 },
|
||||
|
||||
{ name: 'Tomás Vega', cat: 'gardener', photo: 'photo-1637531347055-4fa8aa80c111', distanceM: 2_300, rating: 4.8, reviews: 47, radius: 20_000,
|
||||
skills: ['Tree surgery', 'Irrigation'] },
|
||||
{ name: 'Beatriz Olvera', cat: 'gardener', photo: 'photo-1555955208-94f6fafea771', distanceM: 4_700, rating: 4.7, reviews: 29, radius: 18_000 },
|
||||
{ name: 'Aureliano Sosa', cat: 'gardener', photo: 'photo-1605117882932-f9e32b03fea9', distanceM: 6_100, rating: 4.5, reviews: 15, radius: 25_000 },
|
||||
{ name: 'Margarita Nieto', cat: 'gardener', photo: 'photo-1728706613021-e447801e1ea6', distanceM: 1_900, rating: 4.9, reviews: 66, radius: 15_000 },
|
||||
{ name: 'Everardo Rangel', cat: 'gardener', photo: 'photo-1621460249485-4e4f92c9de5d', distanceM: 8_200, rating: 4.3, reviews: 9, radius: 30_000 },
|
||||
|
||||
{ name: 'Baltazar Nájera', cat: 'mover', photo: 'photo-1698917414969-feade59e3343', distanceM: 3_400, rating: 4.6, reviews: 38, radius: 40_000 },
|
||||
{ name: 'Rigoberto Cuevas', cat: 'mover', photo: 'photo-1694715669993-ea0022b470f7', distanceM: 5_900, rating: 4.8, reviews: 55, radius: 45_000,
|
||||
skills: ['Piano moving', 'Packing service'] },
|
||||
{ name: 'Nicolás Frías', cat: 'mover', photo: 'photo-1523543659209-5c57c05834aa', distanceM: 2_600, rating: 4.5, reviews: 19, radius: 35_000 },
|
||||
{ name: 'Hilario Ordaz', cat: 'mover', photo: 'photo-1642756457381-930fdc1e2e2e', distanceM: 7_700, rating: 4.7, reviews: 44, radius: 50_000 },
|
||||
{ name: 'Damián Bravo', cat: 'mover', photo: 'photo-1554620158-d8d5c2f3a27b', distanceM: 1_800, rating: 4.4, reviews: 11, radius: 30_000 },
|
||||
|
||||
{ name: 'Heriberto Alanís', cat: 'builder', photo: 'photo-1587582423116-ec07293f0395', distanceM: 4_300, rating: 4.6, reviews: 27, radius: 35_000 },
|
||||
{ name: 'Anselmo Padilla', cat: 'builder', photo: 'photo-1563166423-482a8c14b2d6', distanceM: 7_100, rating: 4.8, reviews: 61, radius: 40_000,
|
||||
skills: ['Extensions', 'Structural work'] },
|
||||
{ name: 'Ezequiel Maldonado', cat: 'builder', photo: 'photo-1593313637552-29c2c0dacd35', distanceM: 2_400, rating: 4.7, reviews: 35, radius: 25_000 },
|
||||
{ name: 'Leonardo Gallardo', cat: 'builder', photo: 'photo-1667207591431-0366faabde1a', distanceM: 10_300, rating: 4.5, reviews: 20, radius: 45_000 },
|
||||
{ name: 'Porfirio Escobar', cat: 'builder', photo: 'photo-1530639834082-05bafb67fbbe', distanceM: 1_500, rating: 4.9, reviews: 73, radius: 30_000 },
|
||||
|
||||
{ name: 'Abel Montoya', cat: 'roofer', photo: 'photo-1635424824849-1b09bdcc55b1', distanceM: 4_100, rating: 4.7, reviews: 33, radius: 25_000 },
|
||||
{ name: 'Cristóbal Ruelas', cat: 'roofer', photo: 'photo-1726589004565-bedfba94d3a2', distanceM: 6_800, rating: 4.6, reviews: 21, radius: 30_000 },
|
||||
{ name: 'Genaro Ibáñez', cat: 'roofer', photo: 'photo-1633759593085-1eaeb724fc88', distanceM: 2_900, rating: 4.9, reviews: 52, radius: 20_000,
|
||||
skills: ['Flat roofs', 'Leak tracing'] },
|
||||
{ name: 'Moisés Contreras', cat: 'roofer', photo: 'photo-1635424709961-f3a150459ad4', distanceM: 9_400, rating: 4.4, reviews: 12, radius: 35_000 },
|
||||
{ name: 'Ulises Barrios', cat: 'roofer', photo: 'photo-1635424824800-692767998d07', distanceM: 1_600, rating: 4.8, reviews: 40, radius: 18_000 },
|
||||
|
||||
{ name: 'Efrén Aparicio', cat: 'window-cleaner', photo: 'photo-1635445818409-64a0ff92eb39', distanceM: 2_800, rating: 4.7, reviews: 31, radius: 20_000 },
|
||||
{ name: 'Salomón Terán', cat: 'window-cleaner', photo: 'photo-1775654063422-54e0a71578ad', distanceM: 5_100, rating: 4.8, reviews: 49, radius: 25_000 },
|
||||
{ name: 'Bernardo Quiroz', cat: 'window-cleaner', photo: 'photo-1719499757876-346424e8f67d', distanceM: 8_600, rating: 4.5, reviews: 16, radius: 30_000 },
|
||||
|
||||
{ name: 'Rodolfo Zavala', cat: 'pest-control', photo: 'photo-1747659629851-a92bd71149f6', distanceM: 3_600, rating: 4.7, reviews: 42, radius: 25_000 },
|
||||
{ name: 'Ignacio Bermúdez', cat: 'pest-control', photo: 'photo-1674485135526-b5a686b33dfe', distanceM: 6_400, rating: 4.6, reviews: 23, radius: 30_000 },
|
||||
{ name: 'Faustino Ayala', cat: 'pest-control', photo: 'photo-1749030415358-f533ad412767', distanceM: 2_000, rating: 4.8, reviews: 57, radius: 20_000 },
|
||||
{ name: 'Gustavo Meraz', cat: 'pest-control', photo: 'photo-1670989292166-8b20b9530438', distanceM: 9_100, rating: 4.4, reviews: 14, radius: 35_000 },
|
||||
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -377,7 +486,7 @@ async function main() {
|
||||
|
||||
const cats = await db
|
||||
.insert(schema.categories)
|
||||
.values(CATEGORIES.map((c, i) => ({ ...c, position: i })))
|
||||
.values(CATEGORIES.map((c, i) => ({ ...c, position: i, isActive: LIVE_TRADES.has(c.slug) })))
|
||||
.returning();
|
||||
const catBySlug = new Map(cats.map((c) => [c.slug, c.id]));
|
||||
console.log(` ${cats.length} categories`);
|
||||
|
||||
@@ -55,12 +55,15 @@ describe('getDeck', () => {
|
||||
expect(names).toEqual([
|
||||
'Alejandro Dávila',
|
||||
'Antón Bautista',
|
||||
'Efraín Nava',
|
||||
'Gabriel Torres',
|
||||
'Gerardo Solís',
|
||||
'Ismael Zúñiga',
|
||||
'Jorge Pineda',
|
||||
// Sorted by code unit, so accented letters land after plain ASCII ones.
|
||||
'Norma Salgado',
|
||||
'Néstor Bosque',
|
||||
'Ramiro Escalante',
|
||||
'Rogelio Amaya',
|
||||
'Sergio Fabela',
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user