269 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|