feat: CarBrandLogo component + PL24 image pre-download + catalog index fix

- Add CarBrandLogo component with SVG assets for 11 brands (Alpine, Citroën,
  Cupra, Dacia, MAN, Opel, Peugeot, Renault, SEAT, Škoda, Suzuki); falls back
  to logoUrl prop then initial-letter avatar
- Use CarBrandLogo across brand-selector, catalog index, subscription page,
  dashboard home, and vehicle detail page
- PL24: pre-download schema image buffer while auth headers are fresh to avoid
  token expiry on cached URLs; strip buffer before Redis caching
- PL24: fetchVehicleList tries de account first, falls back to main token
- DB: widen catalogVehicles unique index to include serviceName column
- CSS: add Tailwind v4 dark variant declaration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-03-07 15:47:43 +00:00
parent 9d2e534e56
commit 14e43bc808
22 changed files with 439 additions and 39 deletions

View File

@@ -256,7 +256,7 @@ export const catalogVehicles = pgTable(
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
uniqueIndex("catalog_vehicles_source_vid_idx").on(table.source, table.serviceVehicleId),
uniqueIndex("catalog_vehicles_source_svc_vid_idx").on(table.source, table.serviceName, table.serviceVehicleId),
index("catalog_vehicles_brand_name_idx").on(table.brandName),
index("catalog_vehicles_service_name_idx").on(table.serviceName),
],

View File

@@ -315,6 +315,9 @@ export class PL24Service {
const groupName = crumbs[crumbs.length - 1]?.name || "";
const illustrationId =
linkPath.match(/illustrationId=(\d+)/)?.[1] || "";
// Temporary: log raw images field to diagnose 404 issue
const rawImages = (data?.data as any)?.images || data?.images;
if (rawImages) this.logger.log(`[imgdbg] images field: ${JSON.stringify(rawImages).substring(0, 500)}`);
const imageData = this.extractImageData(data);
let parts = this.parsePartsResponse(data);
@@ -340,7 +343,19 @@ export class PL24Service {
hotspots: imageData.hotspots,
};
await this.redis.setJson(cacheKey, result, 3600);
// Pre-download image buffer while auth headers are fresh (avoids token expiry on cached URL)
if (result.schemaImageUrl) {
const downloaded = await this.tryDownloadImageBuffer(result.schemaImageUrl, headers);
if (downloaded) {
result.schemaImageBuffer = downloaded.buffer;
result.schemaImageContentType = downloaded.contentType;
result.schemaImageUrl = undefined; // buffer takes precedence
}
}
// Cache without buffer (binary data not suited for Redis)
const { schemaImageBuffer: _buf, ...toCache } = result as any;
await this.redis.setJson(cacheKey, toCache, 3600);
return result;
} catch (error) {
const err = error as Error;
@@ -1295,6 +1310,36 @@ export class PL24Service {
// ==================== PRIVATE: Image extraction ====================
private async tryDownloadImageBuffer(
imageUrl: string,
headers: Record<string, string>,
): Promise<{ buffer: Buffer; contentType: string } | null> {
this.logger.log(`[imgdl] Downloading image: ${imageUrl}`);
try {
const response = await fetch(imageUrl, {
method: "GET",
headers,
signal: AbortSignal.timeout(this.timeout),
});
if (!response.ok) {
this.logger.warn(`Image pre-download failed: HTTP ${response.status}`);
return null;
}
const contentType = response.headers.get("content-type") || "image/png";
if (contentType.includes("application/json")) {
const json = (await response.json()) as Record<string, any>;
if (json.image && typeof json.image === "string") {
return { buffer: Buffer.from(json.image, "base64"), contentType: "image/png" };
}
return null;
}
return { buffer: Buffer.from(await response.arrayBuffer()), contentType };
} catch (err) {
this.logger.warn(`Image pre-download error: ${(err as Error).message}`);
return null;
}
}
private extractIllustrationUrl(response: unknown): string | null {
const responseData = response as Record<string, unknown>;
const data =
@@ -1816,13 +1861,18 @@ export class PL24Service {
return this.fordLegacyService.fetchVehicleListForVolvo(serviceName);
}
const cacheKey = `${PL24_DEFAULTS.CACHE_PREFIX}vehicle_list:${serviceName}`;
const cached = await this.redis.getJson<any[]>(cacheKey);
if (cached) return cached;
try {
await this.authService.authorizeService(serviceName);
const headers = await this.authService.buildAuthHeaders(serviceName);
// Try de account; fall back to main token (demo mode) if that also fails
let headers: Record<string, string>;
try {
await this.authService.authorizeServiceForAccount(serviceName, "de");
headers = await this.authService.buildAuthHeadersForAccount("de", serviceName);
} catch {
this.logger.warn(
`fetchVehicleList: de auth failed for ${serviceName}, using main token`,
);
headers = await this.authService.buildAuthHeaders();
}
const catalogBase = getServiceApiPath(serviceName);
@@ -1885,7 +1935,6 @@ export class PL24Service {
}
if (vehicles.length > 0) {
await this.redis.setJson(cacheKey, vehicles, 86400); // 24h
return vehicles;
}
}

View File

@@ -16,6 +16,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@cardog-icons/react": "^1.1.1",
"@grafana/faro-web-sdk": "^2.2.4",
"@grafana/faro-web-tracing": "^2.2.4",
"@remotion/player": "^4.0.422",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 0 192.756 192.756"><g fill-rule="evenodd" clip-rule="evenodd"><path fill="#fff" d="M0 0h192.756v192.756H0V0z"/><path fill="#060a0b" d="M62.707 87.96l2.49-2.49h-6.28l-7.794 7.795L39.7 104.688l-2.653 2.652H43.327l7.796-7.795L62.707 87.96zM96.648 85.47h-5.899v21.925l18.079.107v-5.143l-12.18-.054V85.47zM76.62 85.47h-6.28l-7.633 7.633-11.584 11.585-2.653 2.652H54.75l7.957-7.957L76.62 85.47zM28.278 88.123l2.652-2.653h-6.279l-2.057 2.111-19.76 19.759h6.28l13.48-13.533 5.684-5.684zM51.123 88.123l2.652-2.653h-6.28L39.7 93.265l-11.422 11.423-2.599 2.652h6.226l7.795-7.795 11.423-11.422zM39.7 88.123l2.653-2.653H36.073l-7.795 7.795-5.684 5.739-8.337 8.336h6.226l2.111-2.111 5.684-5.684L39.7 88.123zM62.707 104.85l-2.49 2.49h7.957l15.753-15.916v15.971h5.793l.054-21.925h-7.741l-19.326 19.38zM125.99 85.578H115.433v-.108h-5.848v21.925l5.848.055v-7.256H125.99c4.33 0 5.738-2.922 5.738-7.253.001-4.331-1.408-7.363-5.738-7.363zm.215 8.121c0 1.353-1.189 1.353-1.189 1.353h-9.475V90.18s2.762 0 5.305.054h4.006c1.354 0 1.354 1.245 1.354 1.245l-.001 2.22zM188.785 105.066c0-.541-.326-.703-.922-.703h-.811v2.219h.486v-.92h.217l.107.217.379.703h.598l-.598-.975c.327 0 .544-.162.544-.541zm-.812.217h-.433v-.541h.324c.164 0 .434.053.434.27s-.163.271-.325.271z"/><path d="M187.863 103.443c-1.082 0-2.057.756-2.057 2.057 0 1.244.975 2.002 2.057 2.002 1.084 0 2.059-.758 2.059-2.002 0-1.301-.975-2.057-2.059-2.057zm0 3.625v-.053c-.811.053-1.461-.596-1.461-1.516 0-.975.65-1.57 1.461-1.57.812 0 1.463.596 1.463 1.57 0 .921-.65 1.569-1.463 1.569zM163.775 90.18v17.322h20.842v-4.926h-15.158v-4.061h13.316v-4.817h-13.316V90.18h15.158v-4.927H168.43s-4.655 1.029-4.655 4.927zM157.008 98.895c-1.623-2.002-7.74-9.364-9.689-11.8 0 0-1.191-1.678-3.465-1.678h-4.33v21.979l5.521.055V93.482l11.584 13.913 5.848.055V85.47h-5.469v13.425zM132.594 107.395l5.9.054V85.47h-5.9v21.925z" fill="#060a0b"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg height="2369" viewBox="70.8 77.5 453.6 440.3" width="2500" xmlns="http://www.w3.org/2000/svg"><path d="m131.3 517h-36.7c-5.9 0-11.9-2.4-16.4-6.5-4.8-4.3-7.4-10-7.4-16.1s2.6-11.7 7.4-16.1c4.5-4.1 10.5-6.5 16.4-6.5h36.7v8.9h-36.2c-7.9 0-14.1 6.4-14.1 13.6 0 7.3 6.1 13.6 14.1 13.6h36.2zm99-45.1h-61.1v8.9h25.7v36.2h9.7v-36.2h25.7zm-82.6 0h9.7v45h-9.7zm211.6 45.9h-19.7c-12.6 0-23.8-8.9-23.8-22.9 0-14.1 11.5-23.8 24.6-23.8h18c13.1 0 24.6 8.9 24.6 23.8.1 14-11 22.9-23.7 22.9zm-18.8-37.9c-8.2 0-14.5 6.7-14.5 14.9 0 7.7 5.9 14.1 13.6 14.1h19.7c7.7 0 13.6-6.3 13.6-14.1 0-8.2-6.3-14.9-14.5-14.9zm77.5-13.2h-14c-1.9 0-3.4-1.5-3.4-3.4s1.5-3.4 3.4-3.4h14c1.9 0 3.4 1.5 3.4 3.4-.1 1.9-1.6 3.4-3.4 3.4zm25.4 0h-14c-1.9 0-3.4-1.5-3.4-3.4s1.5-3.4 3.4-3.4h14c1.9 0 3.4 1.5 3.4 3.4s-1.6 3.4-3.4 3.4zm7.7 14.1v-8.9h-54.8v45.1h54.8v-8.9h-45.1v-9.2h39.4v-8.9h-39.4v-9.2zm-171.1 18.3h8.7c7.5 0 14.8-3.4 14.8-13.2 0-8.8-7.3-14-14.8-14h-46.8v45h9.7v-36.1h36.6c3 0 5.1 3 5.1 5.5s-2.2 5.5-5.1 5.5h-27.9v3.1l30.9 21.9h14.3zm224.6-27.2h-39.7v45h9.7v-36.1h28.4c6.7 0 12.1 5.4 12.1 12.1v24.1h9.3v-25.2c0-11-8.9-19.9-19.8-19.9zm-111.6-344.8c-25.4-32-59.2-49.6-95.4-49.6s-70 17.6-95.4 49.6c-24.9 31.5-38.7 73.3-38.7 117.6s13.7 86.1 38.7 117.6c25.4 32 59.2 49.6 95.4 49.6s70-17.6 95.4-49.6c24.9-31.5 38.7-73.3 38.7-117.6s-13.8-86.1-38.7-117.6zm-95.4-34.6c64.4 0 117 65.8 118.9 147.6l-118.9-88.9-118.9 88.9c1.9-81.8 54.5-147.6 118.9-147.6zm116.4 184.1c-1.5 9.2-3.7 18.1-6.5 26.7l-109.9-82.1-109.9 82.1c-2.8-8.5-5-17.5-6.5-26.7l116.4-87zm-116.4 120.4c-40.2 0-75.8-25.6-97.3-64.8l97.3-72.8 97.3 72.8c-21.5 39.2-57.1 64.8-97.3 64.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg height="206pt" viewBox=".26 .95 312.19 206.3" width="313pt" xmlns="http://www.w3.org/2000/svg"><path d="m64.87.95c29.9 10.4 59.64 21.28 89.61 31.47 1.06.41 2.11.41 3.17 0 29.92-10.24 59.64-21.03 89.49-31.47-10.59 16.88-21.28 33.7-32.1 50.43-5-2.15-10.15-3.94-15.19-6 2.92-5.14 6.06-10.16 9.02-15.28-9.64 4.85-19.45 9.36-29.02 14.35 6.69 2.55 13.39 5.05 20.03 7.74 3.7 1.51 7.67 4.42 7.92 8.76.59 4.82-2.7 8.76-5.03 12.62-14.42 22.56-28.76 45.18-43.25 67.7.08-5.94-.6-11.94.1-17.84 5.7-18.09 11.33-36.2 16.52-54.44 1.02-2.71-2.01-4.16-3.96-5.11-5.5-2.43-10.65-5.65-16.28-7.73-5.71 2.47-11.19 5.46-16.79 8.17-1.73.8-4.17 2.38-3.18 4.64 5.15 18.25 10.84 36.35 16.5 54.45.63 5.91.03 11.91.06 17.86-14.26-22.32-28.49-44.67-42.73-67-2.48-4.11-6.08-8.31-5.46-13.44.33-4.31 4.28-7.17 7.98-8.67 6.58-2.68 13.26-5.13 19.89-7.72-9.55-4.97-19.32-9.52-28.95-14.33 2.95 5.12 6.09 10.13 9 15.28-5.07 2.02-10.2 3.89-15.25 5.96-10.78-16.75-21.45-33.58-32.1-50.4zm63.36 204.24c.1-9.86-.07-19.71.09-29.57 16.22.11 32.45-.03 48.67.06 3.17-.11 6.87 1.48 7.5 4.9.39 3.35.3 6.75.06 10.1-.2 3.7-4.08 5.94-7.5 5.7-13.23.03-26.46-.02-39.7.01.01 2.96.02 5.91.06 8.87-3.06.08-6.12.04-9.18-.07m9.09-22.88c.04 2.3.06 4.6.11 6.91 12.56-.12 25.12.11 37.68-.11 2.28-1.32.73-4.49 1.16-6.61-12.97-.44-25.97-.09-38.95-.19z"/><path d="m1.16 180.17c1.3-2.94 4.67-4.58 7.8-4.51 15.9-.04 31.8.02 47.71-.02.03 2.72.04 5.45.06 8.17-15.58.11-31.18-.14-46.75.12-.29.23-.86.7-1.15.94-.18 3.91.04 7.84-.12 11.76 1.41.35 2.84.71 4.31.65 14.56-.04 29.12.02 43.68-.04.01 2.63.02 5.26.02 7.89-15.25.44-30.52.06-45.77.22-3.74.15-8.38-.57-9.86-4.57-.81-3.54-.25-7.21-.4-10.8.05-3.26-.43-6.62.47-9.81zm63.15 17.84c-.03-7.46-.05-14.92.04-22.37 3.26.01 6.53-.01 9.8.05.11 7.15-.12 14.31.14 21.46 12.1.24 24.21.1 36.31.08.5-7.18.12-14.38.23-21.56 3.3-.02 6.6-.01 9.9-.05.07 7.45.09 14.91.02 22.37.04 4-3.75 7.34-7.66 7.27-11.36.34-22.73-.11-34.1.09-3.28-.01-6.62.19-9.83-.62-2.72-1.02-5-3.71-4.85-6.72zm127.93-22.37c15.93.04 31.86 0 47.79.01 3.44-.07 7.49 1.72 8.35 5.35.24 3.33.23 6.69-.01 10.02-.5 2.8-2.97 4.27-5.23 5.57 2.16 2.74 4.08 5.65 6.02 8.55-3.94-.25-9.92 2.11-12.12-2.35-1.32-2.1-2.57-4.24-3.85-6.36-10.53-.09-21.06-.04-31.59-.03.02 2.9.03 5.8.06 8.71-3.15.28-6.31.29-9.45.08.05-9.85-.04-19.7.03-29.55m9.46 6.34c-.08 2.4-.08 4.81-.07 7.21 12.44-.09 24.89.17 37.32-.12.31-.22.93-.67 1.25-.9-.18-2.02 1.03-5.07-1.39-6.07-12.36-.26-24.74-.01-37.11-.12zm54.48 1.01c-.13-4.51 4.58-7.65 8.76-7.34 15.78-.01 31.56.03 47.34-.02.05 9.85.17 19.71-.12 29.56-2.74.12-5.51.38-8.24.04-1.24-2.59-.19-5.97-.49-8.84-12.68-.01-25.36 0-38.04-.01-.02 2.96 0 5.93.02 8.9-3.07.08-6.13.04-9.19-.09.09-7.4-.19-14.8-.04-22.2m9.25-.59c-.04 2.27-.03 4.54.01 6.82 12.64-.07 25.29-.02 37.94-.03.02-2.29.03-4.57.06-6.86-12.67 0-25.34-.13-38.01.07z" fill="#010101"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1 @@
<svg height="1365" viewBox="0 0 1875.9290000000005 1024.029" width="2500" xmlns="http://www.w3.org/2000/svg"><g fill="#241f21"><path d="M105.362 0v157.405h582.54v158.727h-582.54v157.405h643.203c14.753 0 24.479-4.093 34.535-14.204l169.345-164.726 169.351 164.726c10.056 10.11 19.782 14.204 34.535 14.204h643.203V316.132H1216.99V157.405h582.544V0H1156.33c-14.753 0-24.479 4.093-34.535 14.203L952.446 178.93 783.1 14.203C773.044 4.093 763.318 0 748.565 0zM0 827.021v65.048l239.407 1.534c5.715 0 8.88 1.311 12.514 4.946l25.46 22.002c1.863 1.862 2.716 3.052 2.716 4.972 0 1.919-.853 3.114-2.716 4.976l-25.46 22.002c-3.635 3.635-6.799 4.946-12.514 4.946L0 958.981v65.048h269.382c9.62 0 14.363-1.573 20.534-7.743l76.975-79.567c2.899-2.899 4.796-7.426 4.796-11.196s-1.897-8.293-4.796-11.192l-76.975-79.572c-6.171-6.17-10.914-7.738-20.534-7.738z" fill-rule="evenodd"/><path d="M607.747 827.021c-4.237 0-6.638.85-8.3 2.511l-188.984 189.76c-1.984 1.994-1.089 4.737 1.995 4.737h91.89l97.46-101.098c2.022-2.022 3.54-2.866 5.939-2.866 2.397 0 3.916.844 5.939 2.866l97.458 101.098h91.891c3.084 0 3.98-2.743 1.995-4.736l-188.984-189.76c-1.662-1.663-4.063-2.512-8.3-2.512z"/><path d="M1217.07 827.021v65.048l-239.407 1.534c-5.715 0-8.879 1.311-12.514 4.946l-25.46 22.002c-1.862 1.862-2.716 3.052-2.716 4.972 0 1.919.854 3.114 2.716 4.976l25.46 22.002c3.635 3.635 6.8 4.946 12.514 4.946l239.407 1.534v65.048H947.688c-9.62 0-14.363-1.573-20.533-7.743l-76.976-79.567c-2.898-2.899-4.796-7.426-4.796-11.196s1.898-8.293 4.796-11.192l76.976-79.572c6.17-6.17 10.913-7.738 20.533-7.738z" fill-rule="evenodd"/><path d="M1329.535 827.021h75.303v197.008h-75.303zM1676.66 827.021c-4.236 0-6.637.85-8.299 2.511l-188.983 189.76c-1.985 1.994-1.09 4.737 1.994 4.737h91.891l97.459-101.098c2.022-2.022 3.541-2.866 5.939-2.866s3.917.844 5.939 2.866l97.458 101.098h91.891c3.084 0 3.98-2.743 1.995-4.736l-188.984-189.76c-1.662-1.663-4.063-2.512-8.3-2.512z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 -13 720 719.998"><path fill="none" d="M0-13h720v719.998H0z"/><path d="M468.828 404.51h-37.054v72.36h37.369v-32.647l44.11 32.647h37.348v-72.36h-37.376v32.938l-44.397-32.938zM358.146 213.959c-131.229 0-237.604 106.379-237.604 237.602 0 8.554.453 16.991 1.332 25.312h24.883a214.88 214.88 0 0 1-1.488-25.312c0-117.566 95.31-212.875 212.878-212.875 117.569 0 212.88 95.309 212.88 212.875 0 8.567-.506 17.012-1.487 25.312h24.881a240.465 240.465 0 0 0 1.332-25.312c-.005-131.223-106.382-237.602-237.607-237.602zm19.941 190.551h-39.733l-47.091 72.36h38.569l5.234-8.096h46.311l5.238 8.096h38.567l-47.095-72.36zm-9.029 45.031h-21.672l10.836-16.742 10.836 16.742zM200.069 404.51h-35.055v72.36h37.377v-28.614l22.327 22.324 22.322-22.324v28.614h37.376v-72.36H249.36l-24.643 24.646-24.648-24.646z" fill="#222"/></svg>

After

Width:  |  Height:  |  Size: 881 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1 @@
<svg height="2500" viewBox=".306 .394 379.652 500" width="1900" xmlns="http://www.w3.org/2000/svg"><path d="m304.375 250.394-133.72 250h-36.628l-133.721-250 133.14-250h37.79l-133.14 250 113.954 214.536 113.953-214.536-66.279-124.997 19.186-35.47zm-56.975-250h-38.372l-132.558 250 85.465 160.467 18.604-35.47-66.279-124.997 113.953-215.117 113.953 215.117-133.14 250h38.374l132.558-250z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 414 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2075" height="2500" viewBox="0 0 153.588 185.084"><path d="M55.487 39.154c1.426-4.656 5.037-2.28 5.037-2.28S69.882 44 95.549 60.333 153.215 54 153.215 54l-76-54c-33.334 35.333-76 48-76 48l.121.047 97.147 68.121c-1.426 4.656-5.037 2.281-5.037 2.281s-9.358-7.127-35.025-23.461C32.754 78.655.755 101.322.755 101.322l76 54c33.334-35.334 76-48 76-48l-.123-.048-97.145-68.12z" fill="#ff0016"/><g fill="#015cd0"><path d="M0 176.761l11.692.062c.201 1.1-.115 1.096.5 1.67.963.896 2.457 1.1 3.711.879 1.027-.18 2.619-.494 2.695-1.769.083-1.384-.875-1.866-2.077-2.024-1.662-.217-3.339-.274-4.996-.539-4.269-.701-6.539-1.015-8.357-2.174-1.834-1.16-1.992-3.686-1.992-5.484 0-1.182.469-2.297 1.407-3.35.938-1.049 2.349-1.875 4.232-2.478 1.884-.601 4.465-.899 7.746-.899 4.024 0 7.095.545 9.206 1.635 2.114 1.09 3.371 3.521 3.772 5.896h-9.941c.074-.141-.195-.777-.238-.936-.381-1.379-1.712-1.781-2.987-2.063-1.045-.231-2.239-.251-3.181.325-.573.352-1.018.723-1.018 1.227 0 .365.236.694.71.987.461.302 1.55.585 3.269.845 4.255.672 7.302 1.348 9.144 2.031 1.84.688 3.179 1.535 4.018 2.548.837 1.013 1.257 2.147 1.257 3.401 0 1.474-.559 2.832-1.677 4.074-1.116 1.242-2.677 2.188-4.682 2.83-2.006.643-4.533.963-7.583.963-5.355 0-9.066-.752-11.127-2.256C1.439 180.66.272 179.083 0 176.761zM47.439 161.087h8.641v16.246c0 3.334-2.071 5.695-6.214 7.09-1.917.636-4.215.66-6.817.66-2.333 0-4.426.043-6.362-.461-4.56-1.822-6.839-4.252-6.839-7.289v-16.246h8.702V175c0 2.533.336 4.418 4.5 4.418 4.248 0 4.333-1.967 4.333-4.5l.056-13.831zM103.938 161.087h8.642v16.246c0 3.334-2.071 5.695-6.214 7.09-1.916.636-4.215.66-6.817.66-2.333 0-4.427.043-6.362-.461-4.561-1.822-6.84-4.252-6.84-7.289v-16.246h8.702V175c0 2.533.337 4.418 4.5 4.418 4.248 0 4.333-1.967 4.333-4.5l.056-13.831zM57.858 161.255h26.766v4.994L71.549 178h13.076v6.541H57.858v-5.623l12.774-11.584h-12.75l-.024-6.079zM114.72 184.5v-23.285h8.579l-.002 8.619 9.253-8.619h10.737L130.966 172.5l12.834 12h-11.584l-8.834-8.332.084 8.332h-8.746zM153.588 161.212V184.5h-8.562v-23.285h8.562v-.003z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -7,6 +7,7 @@ import { cn } from "@sase/ui";
import { useQuery } from "@tanstack/react-query";
import { Check } from "lucide-react";
import { useEffect, useState } from "react";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
interface Brand {
id: string;
@@ -111,17 +112,7 @@ export function BrandSelector({
>
<CardContent className="flex flex-col items-center justify-center p-4">
<div className="relative">
{brand.logoUrl ? (
<img
src={brand.logoUrl}
alt={brand.name}
className="mb-2 h-12 w-12 object-contain"
/>
) : (
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-lg bg-muted text-lg font-bold text-muted-foreground">
{brand.name.charAt(0)}
</div>
)}
<CarBrandLogo brandName={brand.name} logoUrl={brand.logoUrl} size={48} className="mb-2" />
{isSelected && (
<div className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check className="h-3 w-3" />

View File

@@ -0,0 +1,190 @@
import type { SVGProps } from "react";
import {
AlfaRomeoLogo,
AlfaRomeoLogoDark,
AudiLogo,
AudiLogoDark,
BentleyLogo,
BentleyLogoDark,
BMWLogo,
BMWLogoDark,
FiatLogo,
FiatLogoDark,
FordLogo,
FordLogoDark,
GenesisLogo,
GenesisLogoDark,
HondaLogo,
HondaLogoDark,
HyundaiLogo,
HyundaiLogoDark,
InfinitiLogo,
InfinitiLogoDark,
JaguarLogo,
JaguarLogoDark,
KiaLogo,
KiaLogoDark,
LandroverLogo,
LandroverLogoDark,
LexusLogo,
LexusLogoDark,
MazdaLogo,
MazdaLogoDark,
MBLogo,
MBLogoDark,
MiniLogo,
MiniLogoDark,
MitsubishiLogo,
MitsubishiLogoDark,
NissanLogo,
NissanLogoDark,
PolestarLogo,
PolestarLogoDark,
PorscheLogo,
PorscheLogoDark,
SubaruLogo,
SubaruLogoDark,
ToyotaLogo,
ToyotaLogoDark,
VolkswagenLogo,
VolkswagenLogoDark,
VolvoLogo,
VolvoLogoDark,
} from "@cardog-icons/react";
import { cn } from "@sase/ui";
// Static SVG asset imports (brands not in @cardog-icons/react)
import alpineUrl from "@/assets/brand-logos/alpine.svg";
import citroenUrl from "@/assets/brand-logos/citroen.svg";
import cupraUrl from "@/assets/brand-logos/cupra.svg";
import daciaUrl from "@/assets/brand-logos/dacia.svg";
import manUrl from "@/assets/brand-logos/man.svg";
import opelUrl from "@/assets/brand-logos/opel.svg";
import peugeotUrl from "@/assets/brand-logos/peugeot.svg";
import renaultUrl from "@/assets/brand-logos/renault.svg";
import seatUrl from "@/assets/brand-logos/seat.svg";
import skodaUrl from "@/assets/brand-logos/skoda.svg";
import suzukiUrl from "@/assets/brand-logos/suzuki.svg";
type SvgComponent = React.ForwardRefExoticComponent<
Omit<SVGProps<SVGSVGElement>, "ref"> & React.RefAttributes<SVGSVGElement>
>;
interface BrandEntry {
light: SvgComponent;
dark: SvgComponent;
}
interface StaticEntry {
url: string;
invertDark: boolean;
}
const BRAND_MAP: Record<string, BrandEntry> = {
volkswagen: { light: VolkswagenLogo, dark: VolkswagenLogoDark },
vw: { light: VolkswagenLogo, dark: VolkswagenLogoDark },
audi: { light: AudiLogo, dark: AudiLogoDark },
porsche: { light: PorscheLogo, dark: PorscheLogoDark },
bentley: { light: BentleyLogo, dark: BentleyLogoDark },
bmw: { light: BMWLogo, dark: BMWLogoDark },
mini: { light: MiniLogo, dark: MiniLogoDark },
mercedes: { light: MBLogo, dark: MBLogoDark },
"mercedes-benz": { light: MBLogo, dark: MBLogoDark },
toyota: { light: ToyotaLogo, dark: ToyotaLogoDark },
lexus: { light: LexusLogo, dark: LexusLogoDark },
ford: { light: FordLogo, dark: FordLogoDark },
hyundai: { light: HyundaiLogo, dark: HyundaiLogoDark },
kia: { light: KiaLogo, dark: KiaLogoDark },
genesis: { light: GenesisLogo, dark: GenesisLogoDark },
nissan: { light: NissanLogo, dark: NissanLogoDark },
infiniti: { light: InfinitiLogo, dark: InfinitiLogoDark },
volvo: { light: VolvoLogo, dark: VolvoLogoDark },
polestar: { light: PolestarLogo, dark: PolestarLogoDark },
fiat: { light: FiatLogo, dark: FiatLogoDark },
"alfa romeo": { light: AlfaRomeoLogo, dark: AlfaRomeoLogoDark },
"alfa-romeo": { light: AlfaRomeoLogo, dark: AlfaRomeoLogoDark },
alfaromeo: { light: AlfaRomeoLogo, dark: AlfaRomeoLogoDark },
mitsubishi: { light: MitsubishiLogo, dark: MitsubishiLogoDark },
jaguar: { light: JaguarLogo, dark: JaguarLogoDark },
"land rover": { light: LandroverLogo, dark: LandroverLogoDark },
"land-rover": { light: LandroverLogo, dark: LandroverLogoDark },
landrover: { light: LandroverLogo, dark: LandroverLogoDark },
mazda: { light: MazdaLogo, dark: MazdaLogoDark },
subaru: { light: SubaruLogo, dark: SubaruLogoDark },
honda: { light: HondaLogo, dark: HondaLogoDark },
};
const STATIC_LOGO_MAP: Record<string, StaticEntry> = {
renault: { url: renaultUrl, invertDark: true },
alpine: { url: alpineUrl, invertDark: true },
cupra: { url: cupraUrl, invertDark: true },
opel: { url: opelUrl, invertDark: true },
dacia: { url: daciaUrl, invertDark: true },
peugeot: { url: peugeotUrl, invertDark: true },
citroen: { url: citroenUrl, invertDark: true },
"citroën": { url: citroenUrl, invertDark: true },
man: { url: manUrl, invertDark: true },
skoda: { url: skodaUrl, invertDark: false },
"škoda": { url: skodaUrl, invertDark: false },
seat: { url: seatUrl, invertDark: false },
suzuki: { url: suzukiUrl, invertDark: false },
};
interface CarBrandLogoProps {
brandName: string;
size?: number;
className?: string;
logoUrl?: string | null;
}
export function CarBrandLogo({ brandName, size = 40, className, logoUrl }: CarBrandLogoProps) {
const key = brandName.toLowerCase();
const cardog = BRAND_MAP[key];
if (cardog) {
const { light: LightLogo, dark: DarkLogo } = cardog;
return (
<>
<LightLogo width={size} height={size} className={cn("dark:hidden", className)} />
<DarkLogo width={size} height={size} className={cn("hidden dark:block", className)} />
</>
);
}
const staticEntry = STATIC_LOGO_MAP[key];
if (staticEntry) {
return (
<img
src={staticEntry.url}
alt={brandName}
width={size}
height={size}
className={cn("object-contain", staticEntry.invertDark && "dark:invert", className)}
/>
);
}
if (logoUrl) {
return (
<img
src={logoUrl}
alt={brandName}
width={size}
height={size}
className={cn("object-contain", className)}
/>
);
}
return (
<div
style={{ width: size, height: size }}
className={cn(
"flex items-center justify-center rounded-lg bg-muted text-sm font-bold text-muted-foreground",
className,
)}
>
{brandName.charAt(0).toUpperCase()}
</div>
);
}

View File

@@ -1,6 +1,8 @@
@import "tailwindcss";
@source "../../../packages/ui/src";
@variant dark (&:where(.dark, .dark *));
@theme {
--color-background: #ffffff;
--color-foreground: #0a0a0a;

View File

@@ -4,6 +4,7 @@ import { api } from "@/lib/api-client";
import { useTranslation } from "@/lib/i18n";
import { Skeleton } from "@sase/ui";
import { Library, Lock } from "lucide-react";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
export const Route = createFileRoute("/dashboard/catalog/")({
component: CatalogBrandsPage,
@@ -60,7 +61,12 @@ function BrandCard({ brand }: { brand: CatalogBrand }) {
if (!brand.hasAccess) {
return (
<div className="relative flex flex-col items-center justify-center rounded-xl border border-border/50 bg-muted/30 p-4 text-center opacity-60 select-none">
<Lock className="mb-2 size-5 text-muted-foreground" />
<div className="relative mb-2">
<CarBrandLogo brandName={brand.brandName} logoUrl={brand.logoUrl} size={40} />
<div className="absolute -right-1 -bottom-1 flex size-4 items-center justify-center rounded-full bg-muted-foreground/60">
<Lock className="size-2.5 text-background" />
</div>
</div>
<p className="text-sm font-semibold text-foreground">{brand.brandName}</p>
<p className="mt-1 text-xs text-muted-foreground">{t("catalog.locked")}</p>
<Link
@@ -80,17 +86,7 @@ function BrandCard({ brand }: { brand: CatalogBrand }) {
search={{ catalog: undefined }}
className="flex flex-col items-center justify-center rounded-xl border border-border bg-card p-4 text-center transition-colors hover:bg-accent hover:border-accent-foreground/20"
>
{brand.logoUrl ? (
<img
src={brand.logoUrl}
alt={brand.brandName}
className="mb-2 h-10 w-auto object-contain"
/>
) : (
<div className="mb-2 flex size-10 items-center justify-center rounded-full bg-primary/10">
<Library className="size-5 text-primary" />
</div>
)}
<CarBrandLogo brandName={brand.brandName} logoUrl={brand.logoUrl} size={40} className="mb-2" />
<p className="text-sm font-semibold">{brand.brandName}</p>
</Link>
);

View File

@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { api } from "@/lib/api-client";
import { Button, Badge, Skeleton, Separator } from "@sase/ui";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import {
Search,
Car,
@@ -325,7 +326,8 @@ function DashboardHome() {
{subscription.brands && subscription.brands.length > 0 && (
<div className="flex flex-wrap gap-2">
{subscription.brands.map((b) => (
<Badge key={b.brandId} variant="outline">
<Badge key={b.brandId} variant="outline" className="flex items-center gap-1.5">
<CarBrandLogo brandName={b.brandName} size={16} className="shrink-0" />
{b.brandName}
</Badge>
))}

View File

@@ -1,6 +1,7 @@
import { lazy, Suspense, useEffect, useRef, useState } from "react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { api } from "@/lib/api-client";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
import { startAction } from "@/lib/faro";
import { capture, setPeopleProperties } from "@/lib/posthog";
import { useTranslation } from "@/lib/i18n";
@@ -441,7 +442,8 @@ function SubscriptionPage() {
<p className="mb-2 text-sm font-medium">{t("subscription.accessibleBrands")}:</p>
<div className="flex flex-wrap gap-2">
{subscription.brands.map((b) => (
<Badge key={b.brandId} variant="outline">
<Badge key={b.brandId} variant="outline" className="flex items-center gap-1.5">
<CarBrandLogo brandName={b.brandName} size={16} className="shrink-0" />
{b.brandName}
</Badge>
))}

View File

@@ -9,6 +9,7 @@ import { Skeleton } from "@sase/ui";
import { ArrowLeft, LayoutGrid, List } from "lucide-react";
import { Button } from "@sase/ui";
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
export const Route = createFileRoute("/dashboard/vehicles_/$id/")({
component: VehicleDetailPage,
@@ -59,11 +60,16 @@ function VehicleDetailPage() {
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h2 className="text-2xl font-bold">
{vehicle?.brandName} {vehicle?.model} {vehicle?.year && `(${vehicle.year})`}
</h2>
<p className="font-mono text-sm text-muted-foreground">{vehicle?.vin}</p>
<div className="flex items-center gap-3">
{vehicle?.brandName && (
<CarBrandLogo brandName={vehicle.brandName} size={32} className="shrink-0" />
)}
<div>
<h2 className="text-2xl font-bold">
{vehicle?.brandName} {vehicle?.model} {vehicle?.year && `(${vehicle.year})`}
</h2>
<p className="font-mono text-sm text-muted-foreground">{vehicle?.vin}</p>
</div>
</div>
</div>

150
pnpm-lock.yaml generated
View File

@@ -172,6 +172,9 @@ importers:
apps/web:
dependencies:
'@cardog-icons/react':
specifier: ^1.1.1
version: 1.1.1(react@19.2.4)
'@grafana/faro-web-sdk':
specifier: ^2.2.4
version: 2.2.4
@@ -696,6 +699,14 @@ packages:
'@borewit/text-codec@0.2.1':
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
'@cardog-icons/core@1.1.1':
resolution: {integrity: sha512-H/vh6sThJIJbC0nWvC2WrOip5CI0GXZ2+mlmebuDiD1OtAoGFM/Sh1Ob4njzitr0Q5aaFfTpDQB++b7EtruHWg==}
'@cardog-icons/react@1.1.1':
resolution: {integrity: sha512-6S4K7k3iNWjWGHNRl5dQLLvTOIVx2EBpD1g017wgw77gkaYOgrQ4xjgsrVMQBhTsj/gkGnCkDbmYoyROqakA+w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
'@colors/colors@1.5.0':
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
@@ -3358,6 +3369,9 @@ packages:
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
bowser@2.14.1:
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
@@ -3508,6 +3522,10 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
comment-json@4.2.5:
resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==}
engines: {node: '>= 6'}
@@ -3573,13 +3591,32 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-select@5.2.2:
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
css-tree@2.2.1:
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-tree@3.1.0:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-what@6.2.2:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
csso@5.0.5:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
cssstyle@5.3.7:
resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==}
engines: {node: '>=20'}
@@ -3662,9 +3699,22 @@ packages:
dom-accessibility-api@0.6.3:
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.3.1:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dotenv-expand@10.0.0:
resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
engines: {node: '>=12'}
@@ -3793,6 +3843,10 @@ packages:
resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==}
engines: {node: '>=10.13.0'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -4402,6 +4456,12 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
@@ -4552,6 +4612,9 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -4843,6 +4906,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.5.0:
resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==}
engines: {node: '>=11.0.0'}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
@@ -5042,6 +5109,11 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
svgo@3.3.3:
resolution: {integrity: sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==}
engines: {node: '>=14.0.0'}
hasBin: true
swagger-ui-dist@5.18.2:
resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==}
@@ -6260,6 +6332,16 @@ snapshots:
'@borewit/text-codec@0.2.1': {}
'@cardog-icons/core@1.1.1':
dependencies:
glob: 10.4.5
svgo: 3.3.3
'@cardog-icons/react@1.1.1(react@19.2.4)':
dependencies:
'@cardog-icons/core': 1.1.1
react: 19.2.4
'@colors/colors@1.5.0':
optional: true
@@ -8840,6 +8922,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
boolbase@1.0.0: {}
bowser@2.14.1: {}
brace-expansion@1.1.12:
@@ -9001,6 +9085,8 @@ snapshots:
commander@4.1.1: {}
commander@7.2.0: {}
comment-json@4.2.5:
dependencies:
array-timsort: 1.0.3
@@ -9067,13 +9153,37 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
css-select@5.2.2:
dependencies:
boolbase: 1.0.0
css-what: 6.2.2
domhandler: 5.0.3
domutils: 3.2.2
nth-check: 2.1.1
css-tree@2.2.1:
dependencies:
mdn-data: 2.0.28
source-map-js: 1.2.1
css-tree@2.3.1:
dependencies:
mdn-data: 2.0.30
source-map-js: 1.2.1
css-tree@3.1.0:
dependencies:
mdn-data: 2.12.2
source-map-js: 1.2.1
css-what@6.2.2: {}
css.escape@1.5.1: {}
csso@5.0.5:
dependencies:
css-tree: 2.2.1
cssstyle@5.3.7:
dependencies:
'@asamuzakjp/css-color': 4.1.2
@@ -9134,10 +9244,28 @@ snapshots:
dom-accessibility-api@0.6.3: {}
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
domelementtype@2.3.0: {}
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
dompurify@3.3.1:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dotenv-expand@10.0.0: {}
dotenv@16.4.5: {}
@@ -9181,6 +9309,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
entities@4.5.0: {}
entities@6.0.1: {}
env-paths@3.0.0:
@@ -9879,6 +10009,10 @@ snapshots:
math-intrinsics@1.1.0: {}
mdn-data@2.0.28: {}
mdn-data@2.0.30: {}
mdn-data@2.12.2: {}
media-typer@0.3.0: {}
@@ -10011,6 +10145,10 @@ snapshots:
normalize-path@3.0.0: {}
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -10333,6 +10471,8 @@ snapshots:
safer-buffer@2.1.2: {}
sax@1.5.0: {}
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
@@ -10566,6 +10706,16 @@ snapshots:
dependencies:
has-flag: 4.0.0
svgo@3.3.3:
dependencies:
commander: 7.2.0
css-select: 5.2.2
css-tree: 2.3.1
css-what: 6.2.2
csso: 5.0.5
picocolors: 1.1.1
sax: 1.5.0
swagger-ui-dist@5.18.2:
dependencies:
'@scarf/scarf': 1.4.0