Files

269 lines
8.8 KiB
TypeScript

import InteractiveMakerMapClient from "./InteractiveMakerMapClient";
import { MapHomeMakerList, type MapHomeMakerListItem } from "./MapHomeMakerList";
import { serverFetch } from "../lib/api";
type PublicMaker = {
id: string;
slug: string;
business_name: string;
description: string;
province: string;
city: string;
main_image_url: string | null;
avg_rating: number | string;
review_count: number;
service_count: number;
work_count: number;
public_latitude: number | null;
public_longitude: number | null;
public_locations?: Array<{
id: string;
label?: string | null;
city?: string | null;
province?: string | null;
latitude: number | null;
longitude: number | null;
}>;
categories?: string[];
};
type SearchParams = Record<string, string | string[] | undefined>;
type HomeCategory = "all" | "print-3d" | "design-3d" | "resin" | "fdm";
const quickCategories: Array<{ id: HomeCategory; label: string }> = [
{ id: "print-3d", label: "Impresion 3D" },
{ id: "design-3d", label: "Diseno 3D" },
{ id: "resin", label: "Resina" },
{ id: "fdm", label: "FDM" }
];
const preferredOrder = [
"MakerLab 3D",
"PrintCraft",
"3D Ideas",
"ImpresionAR",
"ProtoWorks",
"Resin Forge"
];
const distanceLabels = ["1,2 km", "1,8 km", "2,1 km", "2,4 km", "2,6 km", "2,9 km", "3,2 km"];
function getMakerImage(maker: PublicMaker) {
return maker.main_image_url || "/demo/maker-hero-1.svg";
}
function readParam(value: string | string[] | undefined) {
return typeof value === "string" ? value : "";
}
function getRating(value: number | string) {
const parsed = typeof value === "number" ? value : Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
function getSatisfaction(value: number | string) {
const rating = getRating(value);
return `${Math.max(92, Math.round(rating * 20))}% satisfaccion`;
}
function normalizeCategory(value: string): HomeCategory {
if (value === "print-3d" || value === "design-3d" || value === "resin" || value === "fdm") {
return value;
}
return "all";
}
function hasCategory(maker: PublicMaker, expected: string) {
return (maker.categories || []).some((category) => category.toLowerCase() === expected.toLowerCase());
}
function filterByCategory(makers: PublicMaker[], category: HomeCategory) {
if (category === "all") {
return makers;
}
if (category === "print-3d") {
return makers.filter((maker) => hasCategory(maker, "Impresion FDM") || hasCategory(maker, "Impresion Resina") || hasCategory(maker, "Prototipado"));
}
if (category === "design-3d") {
return makers.filter((maker) => hasCategory(maker, "Diseno 3D"));
}
if (category === "resin") {
return makers.filter((maker) => hasCategory(maker, "Impresion Resina"));
}
return makers.filter((maker) => hasCategory(maker, "Impresion FDM"));
}
function orderMakers(makers: PublicMaker[]) {
return [...makers].sort((left, right) => {
const leftIndex = preferredOrder.indexOf(left.business_name);
const rightIndex = preferredOrder.indexOf(right.business_name);
if (leftIndex !== -1 || rightIndex !== -1) {
return (leftIndex === -1 ? 999 : leftIndex) - (rightIndex === -1 ? 999 : rightIndex);
}
return getRating(right.avg_rating) - getRating(left.avg_rating);
});
}
function getMakerTags(maker: PublicMaker) {
const tags = new Set<string>();
for (const category of maker.categories || []) {
if (category === "Impresion FDM") {
tags.add("FDM");
}
if (category === "Impresion Resina") {
tags.add("Resina");
}
if (category === "Diseno 3D") {
tags.add("Diseno 3D");
}
if (category === "Prototipado") {
tags.add("Prototipos");
}
}
if (tags.size === 0) {
tags.add("Impresion 3D");
}
return Array.from(tags).slice(0, 3);
}
function buildHomeHref(query: string, category: HomeCategory) {
const params = new URLSearchParams();
if (query) {
params.set("q", query);
}
if (category !== "all") {
params.set("category", category);
}
const serialized = params.toString();
return serialized ? `/?${serialized}` : "/";
}
function buildDiscoverHref(query: string, category: HomeCategory) {
const params = new URLSearchParams();
if (query) {
params.set("q", query);
}
if (category === "design-3d") {
params.set("serviceCategory", "Diseno 3D");
}
if (category === "resin") {
params.set("serviceCategory", "Impresion Resina");
}
if (category === "fdm") {
params.set("serviceCategory", "Impresion FDM");
}
const serialized = params.toString();
return serialized ? `/discover?${serialized}` : "/discover";
}
export async function MapHomeScreen({ searchParams = {} }: { searchParams?: SearchParams }) {
const query = readParam(searchParams.q);
const activeCategory = normalizeCategory(readParam(searchParams.category));
const requestParams = new URLSearchParams();
if (query) {
requestParams.set("q", query);
}
const data = await serverFetch<{ makers: PublicMaker[] }>(`/makers?${requestParams.toString()}`);
const makers = orderMakers(filterByCategory(data.makers, activeCategory));
const visibleMakers = makers.slice(0, 10);
const makerListItems: MapHomeMakerListItem[] = visibleMakers.map((maker, index) => ({
id: maker.id,
slug: maker.slug,
name: maker.business_name,
city: maker.city,
province: maker.province,
imageUrl: getMakerImage(maker),
distance: distanceLabels[index % distanceLabels.length],
rating: getRating(maker.avg_rating).toFixed(1),
reviewCount: Math.max(maker.review_count * 32, 12),
satisfaction: getSatisfaction(maker.avg_rating),
tags: getMakerTags(maker)
}));
const discoverHref = buildDiscoverHref(query, activeCategory);
const mapMarkers = makers
.flatMap((maker) => {
const publicLocations = (maker.public_locations || []).filter((location) => location.latitude !== null && location.longitude !== null);
const locations = publicLocations.length
? publicLocations
: maker.public_latitude !== null && maker.public_longitude !== null
? [{ id: maker.id, city: maker.city, province: maker.province, latitude: maker.public_latitude, longitude: maker.public_longitude }]
: [];
return locations.map((location, index) => ({
id: `${maker.id}:${location.id || index}`,
slug: maker.slug,
businessName: maker.business_name,
city: location.city || maker.city,
province: location.province || maker.province,
latitude: location.latitude as number,
longitude: location.longitude as number
}));
});
return (
<section className="map-home-shell">
<div className="map-home-device">
<form action="/" className="map-home-search-stack">
<div className="map-home-search-row">
<label className="map-home-searchbar">
<span className="map-home-search-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="6.6" />
<path d="m20 20-3.6-3.6" />
</svg>
</span>
<input name="q" placeholder="Buscar makers, servicios, trabajos..." defaultValue={query} />
</label>
{activeCategory !== "all" ? <input type="hidden" name="category" value={activeCategory} /> : null}
<a href={discoverHref} className="map-home-filter-link" aria-label="Abrir filtros">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 7h16" />
<path d="M7 12h10" />
<path d="M10 17h4" />
</svg>
</a>
</div>
</form>
<div className="map-home-chip-row">
<a href={discoverHref} className="map-home-chip map-home-chip-primary">Filtros</a>
{quickCategories.map((category) => (
<a
key={category.id}
href={buildHomeHref(query, category.id)}
className={`map-home-chip ${activeCategory === category.id ? "active" : ""}`}
>
{category.label}
</a>
))}
</div>
<InteractiveMakerMapClient markers={mapMarkers} />
<section className="map-home-results">
<div className="map-home-results-head">
<div className="stack" style={{ gap: 4 }}>
<strong className="map-home-results-title">Makers cerca de ti</strong>
<span className="mini-note">Cordoba, Argentina | 1,2 km</span>
</div>
<a href={discoverHref} className="map-home-view-all">Ver todos</a>
</div>
<MapHomeMakerList makers={makerListItems} />
</section>
</div>
</section>
);
}