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 202 203 204 205 206 207 208 209 210 211 212 | 102x 14x 39x 9x 7x 5x 7x 6x 14x 12x 14x 8x 7x 3x 6x 6x 12x 12x 12x 121x 6x 31x 29x 3x 26x 18x 18x 7x 7x 7x 7x 7x 7x 7x 6x 12x 8x 3x 5x 7x 6x 3x 3x 3x 7x 7x 7x 6x 4x 4x 4x 7x 7x 7x 3x 8x 8x 3x | // Helper functions for API data
import {
ROOM_CATEGORIES,
CITIES,
CUSTOMER_SEGMENT_CONFIG,
PROPERTY_TYPE_LABELS,
BUDGET_RANGE_LABELS,
BUDGET_RANGE_LABELS_FULL,
ROOM_BUDGET_LABELS,
type CustomerSegmentConfig,
} from "./constants";
export function getCategoryLabel(category: string): string {
const found = ROOM_CATEGORIES.find((c) => c.value === category);
return found?.label ?? category;
}
export function getCityLabel(city: string): string {
const found = CITIES.find((c) => c.value === city);
return found?.label ?? city;
}
export function getCustomerSegmentBadge(
customerSegment: string | null,
): CustomerSegmentConfig | null {
if (!customerSegment) return null;
return CUSTOMER_SEGMENT_CONFIG[customerSegment] ?? null;
}
export function getPropertyTypeLabel(
propertyType: string | null,
): string | null {
if (!propertyType) return null;
return PROPERTY_TYPE_LABELS[propertyType] ?? propertyType;
}
export function getBudgetRangeLabel(
budgetRange: string | null,
full = false,
): string | null {
if (!budgetRange) return null;
const labels = full ? BUDGET_RANGE_LABELS_FULL : BUDGET_RANGE_LABELS;
return labels[budgetRange] ?? budgetRange;
}
export function getRoomBudgetLabel(
budgetSpent: string | null,
): string | null {
if (!budgetSpent) return null;
return ROOM_BUDGET_LABELS[budgetSpent] ?? budgetSpent;
}
// Returns the browser-facing API URL (PUBLIC_API_URL if set, otherwise API_URL)
// In Docker, API_URL points to the internal service name (http://api:6001) for SSR fetches,
// while PUBLIC_API_URL points to the host-accessible URL (http://localhost:7001) for browser use.
export function getPublicApiUrl(env: { API_URL: string; PUBLIC_API_URL?: string }): string {
return env.PUBLIC_API_URL || env.API_URL;
}
/**
* Concurrency-capped enrichment helper. Caps Promise.all parallelism to
* stay safely under Cloudflare Workers' 50-subrequest limit per request.
*
* Each chunk runs its items in parallel; chunks run sequentially. Failures
* inside a chunk surface per-item via Promise.allSettled — one bad row
* never fails the whole batch.
*
* Default chunkSize = 20 leaves headroom for the page's other subrequests
* (auth probe, KV reads, additional API calls). Bump down if a page makes
* many other fetches; bump up only if you're certain the worker is on a
* paid plan with the higher 1000-subrequest limit.
*/
export async function enrichChunked<T, U>(
items: T[],
fn: (item: T) => Promise<U>,
chunkSize = 20,
): Promise<Array<U | null>> {
const results: Array<U | null> = [];
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
const settled = await Promise.allSettled(chunk.map(fn));
for (const r of settled) {
results.push(r.status === "fulfilled" ? r.value : null);
}
}
return results;
}
export { getRoomCategorySlug } from "@interioring/utils/constants/room-categories";
// Helper to build full image URL from path
// Handles both formats: with and without /api/images/ prefix
// When imageUrl is provided, uses the dedicated image domain instead of API URL
// Optional resizeParams for Cloudflare Image Resizing (e.g. { w: 600, f: 'auto', q: 80 })
export function getImageUrl(
path: string,
apiUrl: string,
imageUrl?: string,
resizeParams?: { w?: number; h?: number; f?: string; q?: number; fit?: string },
): string {
if (!path) return "";
// If it's already a full URL, return as is
if (path.startsWith("http://") || path.startsWith("https://")) {
return path;
}
if (imageUrl) {
// Use dedicated image domain — strip /api/images/ prefix if present
const storageKey = path.startsWith("/api/images/")
? path.slice("/api/images/".length)
: path;
// If resize params provided, use Cloudflare Image Resizing URL format
if (resizeParams) {
const parts: string[] = [];
if (resizeParams.w) parts.push(`w=${resizeParams.w}`);
if (resizeParams.h) parts.push(`h=${resizeParams.h}`);
if (resizeParams.f) parts.push(`f=${resizeParams.f}`);
if (resizeParams.q) parts.push(`q=${resizeParams.q}`);
if (resizeParams.fit) parts.push(`fit=${resizeParams.fit}`);
if (parts.length > 0) {
return `${imageUrl}/cdn-cgi/image/onerror=redirect,${parts.join(",")}/${storageKey}`;
}
}
return `${imageUrl}/${storageKey}`;
}
// If path already includes /api/images/, just prepend the base URL
if (path.startsWith("/api/images/")) {
return `${apiUrl}${path}`;
}
// Otherwise, construct the full image URL
return `${apiUrl}/api/images/${path}`;
}
// Build a same-origin preload URL matching Astro's <Picture> cloudflare image format.
// Used for <link rel="preload"> to ensure the preloaded resource matches the rendered <img>.
//
// Format chosen = AVIF because Astro's <Picture> is configured with
// formats=['avif','webp'] and browsers pick the first supported <source>.
// Modern browsers (Chrome 85+, Firefox 93+, Safari 16+ — effectively all
// Interioring traffic) support AVIF and will load that, leaving a webp
// preload unused. That triggered a "preloaded but not used within a few
// seconds" console warning on every SSR page. AVIF preload matches what
// the browser actually loads; older browsers without AVIF support skip the
// preload and fall back to the webp <source> at parse time (one extra
// handshake worth of LCP delay on a tiny tail of users).
// Format: /cdn-cgi/image/onerror=redirect,width=W,height=H,format=avif/{full-image-url}
export function getAstroPreloadUrl(
path: string,
apiUrl: string,
imageUrl?: string,
params?: { w?: number; h?: number },
): string | undefined {
if (!path) return undefined;
// Without IMAGE_URL the runtime has no Cloudflare Image Resizing origin,
// so <Picture> renders the plain apiUrl-prefixed path (no /cdn-cgi wrap).
// Emitting a /cdn-cgi preload would point at a URL the browser never
// actually requests; the preload would always go unused and fire the
// "preloaded but not used" warning. Skip it in that case.
if (!imageUrl) return undefined;
// Get the base image URL (no resize params)
const baseUrl = getImageUrl(path, apiUrl, imageUrl);
Iif (!baseUrl) return undefined;
const w = params?.w ?? 800;
const h = params?.h ?? 500;
return `/cdn-cgi/image/onerror=redirect,width=${w},height=${h},format=avif/${baseUrl}`;
}
/**
* Build a responsive `imagesrcset` string for `<link rel="preload" as="image">`.
*
* Returns a comma-joined srcset of CDN URLs at multiple widths (defaults
* 400/800/1200), keyed off the priority `<Picture>`'s aspect ratio so the
* variants match what the browser will actually request via the rendered
* <img>'s srcset. Pair with `imagesizes` so the browser picks the right
* variant per viewport.
*
* Example:
* preloadImageSrcSet={getAstroPreloadSrcSet(path, apiUrl, imgUrl, {
* ratio: '4/3',
* widths: [400, 800, 1200],
* })}
* preloadImageSizes="(max-width: 768px) 100vw, 800px"
*/
export function getAstroPreloadSrcSet(
path: string,
apiUrl: string,
imageUrl?: string,
params?: { widths?: number[]; ratio?: string },
): string | undefined {
if (!path) return undefined;
if (!imageUrl) return undefined;
const baseUrl = getImageUrl(path, apiUrl, imageUrl);
Iif (!baseUrl) return undefined;
const widths = params?.widths ?? [400, 800, 1200];
const ratio = params?.ratio ?? "4/3";
const [rw, rh] = ratio.split("/").map(Number);
if (!rw || !rh) return undefined;
const entries = widths.map((w) => {
const h = Math.round((w * rh) / rw);
return `/cdn-cgi/image/onerror=redirect,width=${w},height=${h},format=avif/${baseUrl} ${w}w`;
});
return entries.join(", ");
}
|