Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | 52x 52x 47x 47x 63x 35x 34x 26x 29x 15x 37x 15x 6x 14x 6x 94x 34x 46x 46x 46x 31x 46x 11x 34x 34x 34x 19x 16x 10x 10x 10x 10x 16x 8x 6x 6x 4x 11x 4x 2x | import { count, eq } from "drizzle-orm";
import { MIN_PUBLISHED_PROS_TO_LAUNCH } from "../../config/city-launch";
import type { getDb } from "../../db";
import { cities, pros } from "../../db/schema";
type Db = ReturnType<typeof getDb>;
type CityRow = typeof cities.$inferSelect;
export type CityListingStatus = "listed" | "unlisted";
/** Columns the live rule and public payload need. Extra city columns are ignored. */
export type CityProfileSource = {
id: string;
name: string;
state?: string;
sortOrder?: number;
listingStatus?: CityListingStatus | string | null;
publicName?: string | null;
urlSlug?: string | null;
wikidataId?: string | null;
pricesApproved?: boolean | number | null;
alwaysLive?: boolean | number | null;
};
/** Mirrored for consumers as `MarketplaceCity` in `@interioring/api-types`. */
export type MarketplaceCity = {
id: string;
slug: string;
publicName: string;
state: string;
sortOrder: number;
marketplaceStatus: "live" | "not_live";
publishedProCount: number;
pricesApproved: boolean;
wikidataId: string | null;
};
export function slugFromCity(city: CityProfileSource): string {
const slug = city.urlSlug?.trim();
return slug ? slug : city.id;
}
export function publicNameFromCity(city: CityProfileSource): string {
const name = city.publicName?.trim();
return name ? name : city.name;
}
function asBool(value: boolean | number | null | undefined): boolean {
return value === true || value === 1;
}
/**
* A city is live when it is a real listed city and either pinned
* (`always_live`) or it has enough published designers.
* Published means `pros.status = 'published'` — the same filter as `/pros`.
*/
export function isCityLive(
city: CityProfileSource,
publishedProCount: number,
threshold: number = MIN_PUBLISHED_PROS_TO_LAUNCH,
): boolean {
if ((city.listingStatus ?? "listed") !== "listed") return false;
if (asBool(city.alwaysLive)) return true;
return publishedProCount >= threshold;
}
export function toMarketplaceCity(
city: CityProfileSource,
publishedProCount: number,
threshold: number = MIN_PUBLISHED_PROS_TO_LAUNCH,
): MarketplaceCity {
return {
id: city.id,
slug: slugFromCity(city),
publicName: publicNameFromCity(city),
state: city.state ?? "",
sortOrder: city.sortOrder ?? 0,
marketplaceStatus: isCityLive(city, publishedProCount, threshold)
? "live"
: "not_live",
publishedProCount,
pricesApproved: asBool(city.pricesApproved),
wikidataId: city.wikidataId ?? null,
};
}
/** Resolve a public slug or a DB id to the city id. Unknown → null. */
export function cityIdFromSlug(
idOrSlug: string,
directory: CityProfileSource[],
): string | null {
const key = idOrSlug.trim().toLowerCase();
const byId = directory.find((city) => city.id === key);
if (byId) return byId.id;
const bySlug = directory.find(
(city) => slugFromCity(city).toLowerCase() === key,
);
return bySlug?.id ?? null;
}
type QueryRows = Promise<unknown[]> & { all?: () => Promise<unknown[]> };
async function readRows(query: QueryRows): Promise<unknown[]> {
if (typeof query.all === "function") return query.all();
return query;
}
/**
* Published designers per city id, in one `GROUP BY` query. Published means
* `pros.status = 'published'` — the same filter as `/pros`. Pros with no city
* are not counted; a city with none is absent from the map.
*/
export async function countPublishedProsByCity(
db: Pick<Db, "select">,
): Promise<Map<string, number>> {
const rows = (await readRows(
db
.select({ cityId: pros.cityId, count: count() })
.from(pros)
.where(eq(pros.status, "published"))
.groupBy(pros.cityId) as unknown as QueryRows,
)) as { cityId: string | null; count: number }[];
const counts = new Map<string, number>();
for (const row of rows) {
if (row.cityId) counts.set(row.cityId, Number(row.count));
}
return counts;
}
/** Published designers in one city (0 when it has none). */
export async function countPublishedPros(
db: Pick<Db, "select">,
cityId: string,
): Promise<number> {
return (await countPublishedProsByCity(db)).get(cityId) ?? 0;
}
/**
* Every listed city, with the live rule applied.
* Placeholder rows (`unlisted`, `overseas`) are omitted.
*/
export async function listMarketplaceCities(
db: Db,
threshold: number = MIN_PUBLISHED_PROS_TO_LAUNCH,
): Promise<MarketplaceCity[]> {
const cityRows = (await readRows(
db.select().from(cities) as unknown as QueryRows,
)) as CityProfileSource[];
const counts = await countPublishedProsByCity(db);
return cityRows
.filter((city) => (city.listingStatus ?? "listed") === "listed")
.map((city) => toMarketplaceCity(city, counts.get(city.id) ?? 0, threshold))
.sort(
(a, b) =>
a.sortOrder - b.sortOrder || a.publicName.localeCompare(b.publicName),
);
}
export async function getMarketplaceCity(
db: Db,
idOrSlug: string,
threshold: number = MIN_PUBLISHED_PROS_TO_LAUNCH,
): Promise<{ profile: MarketplaceCity; source: CityRow } | null> {
const cityRows = (await readRows(
db.select().from(cities) as unknown as QueryRows,
)) as CityRow[];
const id = cityIdFromSlug(idOrSlug, cityRows);
if (!id) return null;
const source = cityRows.find((city) => city.id === id);
if (!source || (source.listingStatus ?? "listed") !== "listed") return null;
const publishedProCount = await countPublishedPros(db, id);
return {
source,
profile: toMarketplaceCity(source, publishedProCount, threshold),
};
}
/**
* Public name of a listed city by database id (`bangalore` → `Bengaluru`),
* or null for an unknown id or a placeholder row. Lead forms that name no
* city fall back to the designer's city with this.
*/
export async function listedCityPublicName(
db: Pick<Db, "select">,
cityId: string,
): Promise<string | null> {
const rows = (await readRows(
db
.select()
.from(cities)
.where(eq(cities.id, cityId)) as unknown as QueryRows,
)) as CityProfileSource[];
const city = rows.find((row) => row.id === cityId);
if (!city || (city.listingStatus ?? "listed") !== "listed") return null;
return publicNameFromCity(city);
}
|