fixing role ,apping. added accessories to ui

This commit is contained in:
2025-12-13 06:02:05 -05:00
parent 3ddb48fe58
commit 978b9df96c
4 changed files with 243 additions and 194 deletions
+123 -58
View File
@@ -29,6 +29,20 @@ type BuildState = Partial<Record<CategoryId, string>>; // categoryId -> partId
const STORAGE_KEY = "gunbuilder-build-state"; const STORAGE_KEY = "gunbuilder-build-state";
// Categories that should NOT be filtered by platform (universal accessories / gear)
const UNIVERSAL_CATEGORIES = new Set<CategoryId>([
"magazine",
"weapon-light",
"foregrip",
"bipod",
"sling",
"rail-accessory",
"tools",
// Optics & sights are intentionally treated as universal
"optic",
"sights",
]);
// Category groups for the main grid // Category groups for the main grid
const CATEGORY_GROUPS: { const CATEGORY_GROUPS: {
id: string; id: string;
@@ -43,8 +57,8 @@ const CATEGORY_GROUPS: {
"Everything from the serialized lower to small parts, fire control, and core controls.", "Everything from the serialized lower to small parts, fire control, and core controls.",
categoryIds: [ categoryIds: [
"lower", "lower",
"completeLower", "complete-lower",
"lowerParts", "lower-parts",
"trigger", "trigger",
"grip", "grip",
"safety", "safety",
@@ -59,26 +73,34 @@ const CATEGORY_GROUPS: {
"Barrel, upper, gas system, and the parts that keep the rifle cycling.", "Barrel, upper, gas system, and the parts that keep the rifle cycling.",
categoryIds: [ categoryIds: [
"upper", "upper",
"completeUpper", "complete-upper",
"bcg", "bcg",
"barrel", "barrel",
"gasBlock", "gas-block",
"gasTube", "gas-tube",
"muzzleDevice", "muzzle-device",
"suppressor", "suppressor",
"handguard", "handguard",
"chargingHandle", "charging-handle",
"sights", "sights",
"optic", "optic",
], ],
}, },
// Optional: future group for accessories/furniture if needed {
// { id: "accessories-group",
// id: "accessories-group", label: "Accessories & Gear",
// label: "Accessories & Furniture", description:
// description: "Optics, lights, slings, and other supporting gear.", "Universal add-ons that work across platforms — mags, lights, slings, tools, and more.",
// categoryIds: ["optic", "sights", ...] as CategoryId[], categoryIds: [
// }, "magazine",
"weapon-light",
"foregrip",
"bipod",
"sling",
"rail-accessory",
"tools",
],
},
]; ];
export default function GunbuilderPage() { export default function GunbuilderPage() {
@@ -121,47 +143,78 @@ export default function GunbuilderPage() {
setLoading(true); setLoading(true);
setError(null); setError(null);
const res = await fetch( // 1) Platform-scoped products
`${API_BASE_URL}/api/products?platform=${encodeURIComponent( const scopedUrl = `${API_BASE_URL}/api/products?platform=${encodeURIComponent(
platform platform
)}`, )}`;
{ signal: controller.signal }
);
if (!res.ok) { // 2) Universal products (no platform filter)
throw new Error(`Failed to load products (${res.status})`); // NOTE: This assumes your backend supports /api/products with no platform param.
// If it doesn't yet, this call will fail quietly and you'll still see scoped results.
const universalUrl = `${API_BASE_URL}/api/products`;
const [scopedRes, universalRes] = await Promise.all([
fetch(scopedUrl, { signal: controller.signal }),
fetch(universalUrl, { signal: controller.signal }).catch(() => null as any),
]);
if (!scopedRes || !scopedRes.ok) {
const status = scopedRes?.status ?? "";
throw new Error(`Failed to load products (${status})`);
} }
const data: GunbuilderProductFromApi[] = await res.json(); const scopedData: GunbuilderProductFromApi[] = await scopedRes.json();
const normalized: Part[] = data let universalData: GunbuilderProductFromApi[] = [];
.map((p): Part | null => { if (universalRes && universalRes.ok) {
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole]; universalData = await universalRes.json();
if (!categoryId) { }
// Skip any parts we don't know how to map yet
return null;
}
const buyUrl = p.buyUrl ?? undefined; // Normalize both lists
const normalize = (data: GunbuilderProductFromApi[]): Part[] =>
data
.map((p): Part | null => {
const categoryId = PART_ROLE_TO_CATEGORY[p.partRole];
if (!categoryId) return null;
return { // Only keep truly-universal categories from the universal feed
id: String(p.id), // ALWAYS string // so we don't accidentally mix platforms for platform-scoped parts.
categoryId, if (data === universalData && !UNIVERSAL_CATEGORIES.has(categoryId)) {
name: p.name, return null;
brand: p.brand, }
price: p.price ?? 0,
imageUrl: p.mainImageUrl ?? undefined,
affiliateUrl: buyUrl, // main link
url: buyUrl, // 👈optional alias if you still reference .url. maybe pretty url?
notes: undefined,
};
})
.filter((p): p is Part => p !== null);
setParts(normalized); const buyUrl = p.buyUrl ?? undefined;
return {
id: String(p.id),
categoryId,
name: p.name,
brand: p.brand,
price: p.price ?? 0,
imageUrl: p.mainImageUrl ?? undefined,
affiliateUrl: buyUrl,
url: buyUrl,
notes: undefined,
};
})
.filter((p): p is Part => p !== null);
const scopedParts = normalize(scopedData);
const universalParts = normalize(universalData);
// Merge + de-dupe by (categoryId + id)
const seen = new Set<string>();
const merged: Part[] = [];
for (const p of [...scopedParts, ...universalParts]) {
const key = `${p.categoryId}:${p.id}`;
if (seen.has(key)) continue;
seen.add(key);
merged.push(p);
}
setParts(merged);
} catch (err: any) { } catch (err: any) {
if (err.name === "AbortError") return; if (err?.name === "AbortError") return;
setError(err.message ?? "Failed to load products"); setError(err?.message ?? "Failed to load products");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -173,12 +226,18 @@ export default function GunbuilderPage() {
}, [platform]); }, [platform]);
useEffect(() => { useEffect(() => {
// When the platform changes, reset the current build so we don't // When the platform changes, clear ONLY platform-scoped selections.
// show stale part selections from a different platform. // Keep universal accessories (optic/light/sling/etc) so the user doesn't lose them.
setBuild({}); setBuild((prev) => {
if (typeof window !== "undefined") { const next: BuildState = {};
localStorage.removeItem(STORAGE_KEY); for (const [categoryId, partId] of Object.entries(prev)) {
} const cid = categoryId as CategoryId;
if (UNIVERSAL_CATEGORIES.has(cid)) {
next[cid] = partId;
}
}
return next;
});
}, [platform]); }, [platform]);
// Handle URL query parameter for part selection (?select=upper:165) // Handle URL query parameter for part selection (?select=upper:165)
@@ -280,10 +339,16 @@ export default function GunbuilderPage() {
}, [build]); }, [build]);
const handleSelectPart = (categoryId: CategoryId, partId: string) => { const handleSelectPart = (categoryId: CategoryId, partId: string) => {
setBuild((prev) => ({ setBuild((prev) => {
...prev, const updated = {
[categoryId]: partId, ...prev,
})); [categoryId]: partId,
};
if (typeof window !== "undefined") {
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
}
return updated;
});
}; };
// Use group ordering for the summary as well // Use group ordering for the summary as well
@@ -302,7 +367,7 @@ export default function GunbuilderPage() {
} }
const lines: string[] = []; const lines: string[] = [];
lines.push("Shadow Standard — The Armory Build"); lines.push("B Build");
lines.push(""); lines.push("");
lines.push(`Total: $${totalPrice.toFixed(2)}`); lines.push(`Total: $${totalPrice.toFixed(2)}`);
lines.push(""); lines.push("");
+21 -21
View File
@@ -1,9 +1,9 @@
import type { Category } from "@/types/gunbuilder"; import type { Category } from "@/types/gunbuilder";
export const CATEGORY_SLUGS = { export const CATEGORY_SLUGS: Record<string, string> = {
lower: "lower", lower: "lower",
"complete-lower": "complete-lower", completeLower: "complete-lower",
"lower-parts": "lower-parts", lowerParts: "lower-parts",
trigger: "trigger", trigger: "trigger",
grip: "grip", grip: "grip",
safety: "safety", safety: "safety",
@@ -11,32 +11,32 @@ export const CATEGORY_SLUGS = {
stock: "stock", stock: "stock",
upper: "upper", upper: "upper",
"complete-upper": "complete-upper", completeUpper: "complete-upper",
barrel: "barrel", barrel: "barrel",
handguard: "handguard", handguard: "handguard",
"gas-block": "gas-block", gasBlock: "gas-block",
"gas-tube": "gas-tube", gasTube: "gas-tube",
"muzzle-device": "muzzle-device", muzzleDevice: "muzzle-device",
bcg: "bcg", bcg: "bcg",
sights: "sights", sights: "sights",
"charging-handle": "charging-handle", chargingHandle: "charging-handle",
suppressor: "suppressor", suppressor: "suppressor",
optic: "optic", optic: "optic",
magazine: "magazine", magazine: "magazine",
"weapon-light": "weapon-light", weaponLight: "weapon-light",
foregrip: "foregrip", foregrip: "foregrip",
bipod: "bipod", bipod: "bipod",
sling: "sling", sling: "sling",
"rail-accessory": "rail-accessory", railAccessory: "rail-accessory",
tools: "tools", tools: "tools",
} as const; };
export const CATEGORIES: Category[] = [ export const CATEGORIES: Category[] = [
// Lower group // Lower group
{ id: "lower", name: "Stripped Lowers", group: "lower", slug: CATEGORY_SLUGS.lower }, { id: "lower", name: "Stripped Lowers", group: "lower", slug: CATEGORY_SLUGS.lower },
{ id: "complete-lower", name: "Complete Lower", group: "lower", slug: CATEGORY_SLUGS["complete-lower"] }, { id: "completeLower", name: "Complete Lower", group: "lower", slug: CATEGORY_SLUGS.completeLower },
{ id: "lower-parts", name: "Lower Parts Kit", group: "lower", slug: CATEGORY_SLUGS["lower-parts"] }, { id: "lowerParts", name: "Lower Parts Kit", group: "lower", slug: CATEGORY_SLUGS.lowerParts },
{ id: "trigger", name: "Trigger / Fire Control", group: "lower", slug: CATEGORY_SLUGS.trigger }, { id: "trigger", name: "Trigger / Fire Control", group: "lower", slug: CATEGORY_SLUGS.trigger },
{ id: "grip", name: "Pistol Grip", group: "lower", slug: CATEGORY_SLUGS.grip }, { id: "grip", name: "Pistol Grip", group: "lower", slug: CATEGORY_SLUGS.grip },
{ id: "safety", name: "Safety / Selector", group: "lower", slug: CATEGORY_SLUGS.safety }, { id: "safety", name: "Safety / Selector", group: "lower", slug: CATEGORY_SLUGS.safety },
@@ -45,24 +45,24 @@ export const CATEGORIES: Category[] = [
// Upper group // Upper group
{ id: "upper", name: "Stripped Upper", group: "upper", slug: CATEGORY_SLUGS.upper }, { id: "upper", name: "Stripped Upper", group: "upper", slug: CATEGORY_SLUGS.upper },
{ id: "complete-upper", name: "Complete Upper", group: "upper", slug: CATEGORY_SLUGS["complete-upper"] }, { id: "completeUpper", name: "Complete Upper", group: "upper", slug: CATEGORY_SLUGS.completeUpper },
{ id: "barrel", name: "Barrels", group: "upper", slug: CATEGORY_SLUGS.barrel }, { id: "barrel", name: "Barrels", group: "upper", slug: CATEGORY_SLUGS.barrel },
{ id: "handguard", name: "Handguards / Rails", group: "upper", slug: CATEGORY_SLUGS.handguard }, { id: "handguard", name: "Handguards / Rails", group: "upper", slug: CATEGORY_SLUGS.handguard },
{ id: "gas-block", name: "Gas Block", group: "upper", slug: CATEGORY_SLUGS["gas-block"] }, { id: "gasBlock", name: "Gas Block", group: "upper", slug: CATEGORY_SLUGS.gasBlock },
{ id: "gas-tube", name: "Gas Tube", group: "upper", slug: CATEGORY_SLUGS["gas-tube"] }, { id: "gasTube", name: "Gas Tube", group: "upper", slug: CATEGORY_SLUGS.gasTube },
{ id: "muzzle-device", name: "Muzzle Device", group: "upper", slug: CATEGORY_SLUGS["muzzle-device"] }, { id: "muzzleDevice", name: "Muzzle Device", group: "upper", slug: CATEGORY_SLUGS.muzzleDevice },
{ id: "sights", name: "Iron Sights", group: "upper", slug: CATEGORY_SLUGS.sights }, { id: "sights", name: "Iron Sights", group: "upper", slug: CATEGORY_SLUGS.sights },
{ id: "bcg", name: "Bolt Carrier Group", group: "upper", slug: CATEGORY_SLUGS.bcg }, { id: "bcg", name: "Bolt Carrier Group", group: "upper", slug: CATEGORY_SLUGS.bcg },
{ id: "charging-handle", name: "Charging Handle", group: "upper", slug: CATEGORY_SLUGS["charging-handle"] }, { id: "chargingHandle", name: "Charging Handle", group: "upper", slug: CATEGORY_SLUGS.chargingHandle },
{ id: "suppressor", name: "Suppressors", group: "upper", slug: CATEGORY_SLUGS.suppressor }, { id: "suppressor", name: "Suppressors", group: "upper", slug: CATEGORY_SLUGS.suppressor },
{ id: "optic", name: "Optics", group: "upper", slug: CATEGORY_SLUGS.optic }, { id: "optic", name: "Optics", group: "upper", slug: CATEGORY_SLUGS.optic },
// Accessories // Accessories (platform-agnostic)
{ id: "magazine", name: "Magazines", group: "accessories", slug: CATEGORY_SLUGS.magazine }, { id: "magazine", name: "Magazines", group: "accessories", slug: CATEGORY_SLUGS.magazine },
{ id: "weapon-light", name: "Weapon Lights & Lasers", group: "accessories", slug: CATEGORY_SLUGS["weapon-light"] }, { id: "weaponLight", name: "Weapon Lights & Lasers", group: "accessories", slug: CATEGORY_SLUGS.weaponLight },
{ id: "foregrip", name: "Vertical / Angled Grips", group: "accessories", slug: CATEGORY_SLUGS.foregrip }, { id: "foregrip", name: "Vertical / Angled Grips", group: "accessories", slug: CATEGORY_SLUGS.foregrip },
{ id: "bipod", name: "Bipods", group: "accessories", slug: CATEGORY_SLUGS.bipod }, { id: "bipod", name: "Bipods", group: "accessories", slug: CATEGORY_SLUGS.bipod },
{ id: "sling", name: "Slings & Mounts", group: "accessories", slug: CATEGORY_SLUGS.sling }, { id: "sling", name: "Slings & Mounts", group: "accessories", slug: CATEGORY_SLUGS.sling },
{ id: "rail-accessory", name: "Rail Accessories", group: "accessories", slug: CATEGORY_SLUGS["rail-accessory"] }, { id: "railAccessory", name: "Rail Accessories", group: "accessories", slug: CATEGORY_SLUGS.railAccessory },
{ id: "tools", name: "Tools & Maintenance", group: "accessories", slug: CATEGORY_SLUGS.tools }, { id: "tools", name: "Tools & Maintenance", group: "accessories", slug: CATEGORY_SLUGS.tools },
]; ];
+24 -85
View File
@@ -1,102 +1,41 @@
import type { CategoryId } from "@/types/gunbuilder"; import type { CategoryId } from "@/types/gunbuilder";
// CategoryId -> partRoles
export const CATEGORY_TO_PART_ROLES: Record<CategoryId, string[]> = { export const CATEGORY_TO_PART_ROLES: Record<CategoryId, string[]> = {
// CORE / EXISTING upper: ["upper-receiver", "upper"],
upper: ["upper-receiver"],
barrel: ["barrel"],
handguard: ["handguard"],
chargingHandle: ["charging-handle"],
buffer: ["buffer-kit"],
lowerParts: ["lower-parts-kit"],
sights: ["sight"],
lower: ["LOWER_RECEIVER_STRIPPED"],
optic: ["optic"],
stock: ["stock"],
// NEW LOWER PARTS
trigger: ["trigger", "trigger-kit"],
grip: ["pistol-grip", "grip"],
safety: ["safety", "safety-selector"],
completeLower: ["complete-lower"],
// NEW UPPER PARTS
completeUpper: ["complete-upper"], completeUpper: ["complete-upper"],
bcg: ["bcg", "bolt-carrier-group"], barrel: ["barrel"],
gasBlock: ["gas-block"], gasBlock: ["gas-block"],
gasTube: ["gas-tube"], gasTube: ["gas-tube"],
muzzleDevice: ["muzzle-device", "compensator", "brake"], muzzleDevice: ["muzzle-device", "compensator", "brake"],
suppressor: ["suppressor"], suppressor: ["suppressor"],
handguard: ["handguard"],
chargingHandle: ["charging-handle"],
bcg: ["bcg", "bolt-carrier-group"],
// ===== ACCESSORIES ===== lower: ["lower-receiver", "lower", "LOWER_RECEIVER_STRIPPED"],
completeLower: ["complete-lower"],
lowerParts: ["lower-parts-kit", "lower-parts"],
trigger: ["trigger", "trigger-kit"],
grip: ["pistol-grip", "grip"],
safety: ["safety", "safety-selector"],
buffer: ["buffer-kit", "buffer"],
stock: ["stock"],
// Magazines sights: ["sight", "sights", "iron-sights"],
magazine: [ optic: ["optic", "optics"],
"magazine",
"mag",
"magazine-ar15",
"magazine-308",
"drum-magazine",
],
// Lights / Lasers magazine: ["magazine", "mag", "magazine-ar15", "magazine-308", "drum-magazine"],
weaponLight: [ weaponLight: ["weapon-light", "light", "weapon-light-laser", "light-laser-combo", "laser"],
"weapon-light", foregrip: ["vertical-grip", "angled-foregrip", "foregrip", "handstop"],
"light", bipod: ["bipod"],
"weapon-light-laser", sling: ["sling", "sling-mount", "sling-swivel", "qd-sling-mount"],
"light-laser-combo", railAccessory: ["rail-section", "picatinny-rail-section", "m-lok-rail-section", "keymod-rail-section", "rail-cover", "rail-panel"],
"laser", tools: ["tool", "armorer-tool", "armorer-wrench", "cleaning-kit", "bore-snake", "vise-block", "torque-wrench"],
],
// Foregrips
foregrip: [
"vertical-grip",
"angled-foregrip",
"foregrip",
"handstop",
],
// Bipods
bipod: [
"bipod",
],
// Slings & mounts
sling: [
"sling",
"sling-mount",
"sling-swivel",
"qd-sling-mount",
],
// Rail sections & covers
railAccessory: [
"rail-section",
"picatinny-rail-section",
"m-lok-rail-section",
"keymod-rail-section",
"rail-cover",
"rail-panel",
],
// Tools & maintenance
tools: [
"tool",
"armorer-tool",
"armorer-wrench",
"cleaning-kit",
"bore-snake",
"vise-block",
"torque-wrench",
],
}; };
// Invert to get partRole -> CategoryId
export const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = Object.entries( export const PART_ROLE_TO_CATEGORY: Record<string, CategoryId> = Object.entries(
CATEGORY_TO_PART_ROLES, CATEGORY_TO_PART_ROLES
).reduce((acc, [categoryId, roles]) => { ).reduce((acc, [categoryId, roles]) => {
for (const role of roles) { for (const role of roles) acc[role] = categoryId as CategoryId;
acc[role] = categoryId as CategoryId;
}
return acc; return acc;
}, {} as Record<string, CategoryId>); }, {} as Record<string, CategoryId>);
+75 -30
View File
@@ -1,35 +1,58 @@
// Grouping for nav + layout // Grouping for nav + layout
export type CategoryGroup = "lower" | "upper" | "accessories"; export type CategoryGroup = "lower" | "upper" | "accessories";
/**
* CategoryId is the canonical identifier used throughout the UI + localStorage.
*
* ✅ Canonical IDs are kebab-case to match route segments and `gunbuilderParts.ts`.
*
* 🧯 Legacy camelCase IDs are temporarily supported to avoid breaking older
* localStorage/build-state and older code paths. Prefer kebab-case going forward.
*/
export type CategoryId = export type CategoryId =
| "upper" // ===== LOWER =====
| "completeUpper"
| "barrel"
| "gasBlock"
| "gasTube"
| "muzzleDevice"
| "suppressor"
| "handguard"
| "chargingHandle"
| "bcg"
| "buffer"
| "lower" | "lower"
| "completeLower" | "complete-lower"
| "lowerParts" | "lower-parts"
| "trigger" | "trigger"
| "grip" | "grip"
| "safety" | "safety"
| "buffer"
| "stock"
// ===== UPPER =====
| "upper"
| "complete-upper"
| "barrel"
| "handguard"
| "gas-block"
| "gas-tube"
| "muzzle-device"
| "bcg"
| "charging-handle"
| "suppressor"
| "sights" | "sights"
| "optic" | "optic"
| "stock"
// NEW ACCESSORY CATEGORIES // ===== ACCESSORIES =====
| "magazine" | "magazine"
| "weaponLight" | "weapon-light"
| "foregrip" | "foregrip"
| "bipod" | "bipod"
| "sling" | "sling"
| "railAccessory" | "rail-accessory"
| "tools"; | "tools"
// ===== LEGACY (temporary) =====
| "completeUpper"
| "gasBlock"
| "gasTube"
| "muzzleDevice"
| "chargingHandle"
| "completeLower"
| "lowerParts"
| "weaponLight"
| "railAccessory";
export interface Category { export interface Category {
id: CategoryId; id: CategoryId;
@@ -37,6 +60,8 @@ export interface Category {
description?: string; description?: string;
/** used for nav & grouping */ /** used for nav & grouping */
group?: CategoryGroup; group?: CategoryGroup;
/** optional slug override (mostly for legacy IDs) */
slug?: string;
} }
export interface Part { export interface Part {
@@ -46,39 +71,59 @@ export interface Part {
categoryId: CategoryId; categoryId: CategoryId;
price: number; price: number;
affiliateUrl?: string; affiliateUrl?: string;
url?: string; url?: string;
imageUrl?: string; imageUrl?: string;
notes?: string; notes?: string;
} }
export const CATEGORY_SLUGS: Record<CategoryId, string> = { /**
* Route / UI slug for each CategoryId.
*
* - Canonical kebab-case ids map 1:1.
* - Legacy camelCase ids map to the same slug.
*/
export const CATEGORY_SLUGS = {
// LOWER
lower: "lower", lower: "lower",
completeLower: "complete-lower", "complete-lower": "complete-lower",
lowerParts: "lower-parts", "lower-parts": "lower-parts",
trigger: "trigger", trigger: "trigger",
grip: "grip", grip: "grip",
safety: "safety", safety: "safety",
buffer: "buffer", buffer: "buffer",
stock: "stock", stock: "stock",
// UPPER
upper: "upper", upper: "upper",
completeUpper: "complete-upper", "complete-upper": "complete-upper",
barrel: "barrel", barrel: "barrel",
handguard: "handguard", handguard: "handguard",
gasBlock: "gas-block", "gas-block": "gas-block",
gasTube: "gas-tube", "gas-tube": "gas-tube",
muzzleDevice: "muzzle-device", "muzzle-device": "muzzle-device",
sights: "sights", sights: "sights",
bcg: "bcg", bcg: "bcg",
chargingHandle: "charging-handle", "charging-handle": "charging-handle",
suppressor: "suppressor", suppressor: "suppressor",
optic: "optic", optic: "optic",
// ACCESSORIES
magazine: "magazine", magazine: "magazine",
weaponLight: "weapon-light", "weapon-light": "weapon-light",
foregrip: "foregrip", foregrip: "foregrip",
bipod: "bipod", bipod: "bipod",
sling: "sling", sling: "sling",
railAccessory: "rail-accessory", "rail-accessory": "rail-accessory",
tools: "tools", tools: "tools",
};
// LEGACY (temporary)
completeUpper: "complete-upper",
gasBlock: "gas-block",
gasTube: "gas-tube",
muzzleDevice: "muzzle-device",
chargingHandle: "charging-handle",
completeLower: "complete-lower",
lowerParts: "lower-parts",
weaponLight: "weapon-light",
railAccessory: "rail-accessory",
} as const satisfies Record<CategoryId, string>;