Modulo makers3d desarrollado con codex V 0.0.1
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import FavoriteToggle from "../../components/FavoriteToggle";
|
||||
import { serverFetch } from "../../lib/api";
|
||||
|
||||
function toText(value: unknown, fallback = "") {
|
||||
return typeof value === "string" && value ? value : fallback;
|
||||
}
|
||||
|
||||
function toNumber(value: unknown, fallback = 0) {
|
||||
return typeof value === "number" ? value : Number(value) || fallback;
|
||||
}
|
||||
|
||||
function asArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
||||
}
|
||||
|
||||
function availabilityLabel(value: string) {
|
||||
if (value === "available") {
|
||||
return "Aceptando trabajos";
|
||||
}
|
||||
if (value === "limited") {
|
||||
return "Disponibilidad limitada";
|
||||
}
|
||||
if (value === "unavailable") {
|
||||
return "No acepta trabajos";
|
||||
}
|
||||
return value || "Aceptando trabajos";
|
||||
}
|
||||
|
||||
function workImage(work: Record<string, unknown> | undefined, fallback = "/demo/work-custom.svg") {
|
||||
return work ? toText(work.image_url, fallback) : fallback;
|
||||
}
|
||||
|
||||
export default async function MakerPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const data = await serverFetch<{
|
||||
maker: Record<string, unknown>;
|
||||
services: Array<Record<string, unknown>>;
|
||||
showcases: Array<Record<string, unknown>>;
|
||||
works: Array<Record<string, unknown>>;
|
||||
reviews: Array<Record<string, unknown>>;
|
||||
}>(`/makers/${slug}`);
|
||||
|
||||
const { maker, services, showcases, works, reviews } = data;
|
||||
const makerName = toText(maker.business_name, "Maker");
|
||||
const makerImage = toText(maker.main_image_url, workImage(works[0], "/demo/maker-hero-1.svg"));
|
||||
const rating = toNumber(maker.avg_rating, 4.9).toFixed(1);
|
||||
const allowVerifiedReviews = maker.allow_verified_reviews !== false;
|
||||
const showSatisfactionScore = maker.show_satisfaction_score !== false;
|
||||
const showReviewTags = maker.show_review_tags !== false;
|
||||
const showPastReviews = maker.show_past_reviews !== false;
|
||||
const visibleReviews = showPastReviews ? reviews : [];
|
||||
const reviewCount = showPastReviews ? Math.max(toNumber(maker.review_count, reviews.length), reviews.length) : 0;
|
||||
const contactHref = `/consultas/nueva?makerId=${encodeURIComponent(String(maker.id))}&sourceType=profile&makerName=${encodeURIComponent(makerName)}&contextTitle=${encodeURIComponent(makerName)}&returnTo=${encodeURIComponent(`/makers/${slug}`)}`;
|
||||
const tabs = [
|
||||
"Destacado",
|
||||
`Trabajos (${works.length})`,
|
||||
`Servicios (${services.length})`,
|
||||
`Escaparates (${showcases.length})`,
|
||||
`Opiniones (${visibleReviews.length})`,
|
||||
"Informacion"
|
||||
];
|
||||
const statCards = [
|
||||
{ value: rating, label: "Reputacion", icon: "*" },
|
||||
...(showSatisfactionScore ? [{ value: "96%", label: "Satisfaccion", icon: "ok" }] : []),
|
||||
{ value: "~1 h", label: "Respuesta", icon: "rz" },
|
||||
{ value: "Alta", label: "Confianza", icon: "cf" }
|
||||
];
|
||||
const publicBadges = [
|
||||
...(reviewCount ? ["Trabajo verificado"] : []),
|
||||
...(reviewCount >= 2 ? ["Cliente recurrente"] : []),
|
||||
"Respuesta rapida",
|
||||
"Confianza + Transparencia"
|
||||
];
|
||||
const socialLinks = [
|
||||
["Web", toText(maker.website_url)],
|
||||
["Instagram", toText(maker.instagram_url)],
|
||||
["TikTok", toText(maker.tiktok_url)],
|
||||
["X", toText(maker.twitter_url)],
|
||||
["Facebook", toText(maker.facebook_url)],
|
||||
["YouTube", toText(maker.youtube_url)],
|
||||
["LinkedIn", toText(maker.linkedin_url)]
|
||||
].filter(([, href]) => href);
|
||||
|
||||
return (
|
||||
<main className="page-shell maker-public-page">
|
||||
<section className="maker-public-phone">
|
||||
<section className="maker-public-hero">
|
||||
<img src={makerImage} alt={makerName} />
|
||||
<div className="maker-public-top-actions">
|
||||
<Link href="/discover" className="maker-public-circle-action" aria-label="Volver a descubrir"><</Link>
|
||||
<div className="maker-public-action-pair">
|
||||
<FavoriteToggle targetType="maker" targetId={String(maker.id)} targetSlug={slug} compact />
|
||||
<button className="maker-public-circle-action" type="button" aria-label="Compartir">sh</button>
|
||||
</div>
|
||||
</div>
|
||||
<span className="maker-public-gallery-count">1/6</span>
|
||||
</section>
|
||||
|
||||
<section className="maker-public-main-card">
|
||||
<div className="maker-public-identity">
|
||||
<div className="maker-public-logo">
|
||||
<span>{makerName.slice(0, 2).toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="maker-public-title-block">
|
||||
<div className="maker-public-name-row">
|
||||
<h1>{makerName}</h1>
|
||||
<span className="maker-public-online-dot" />
|
||||
<span className="maker-public-availability">{availabilityLabel(toText(maker.availability, "available"))}</span>
|
||||
</div>
|
||||
<div className="maker-public-rating">
|
||||
<span className="map-home-star">*</span>
|
||||
<strong>{rating}</strong>
|
||||
<span>({reviewCount} resenas verificadas)</span>
|
||||
</div>
|
||||
<p>{toText(maker.city, "Cordoba")}, {toText(maker.province, "Argentina")} | 1,2 km</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="maker-public-stats">
|
||||
{statCards.map((stat) => (
|
||||
<article key={stat.label}>
|
||||
<strong>{stat.value}</strong>
|
||||
<span>{stat.label}</span>
|
||||
<small>{stat.icon}</small>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showReviewTags ? (
|
||||
<div className="maker-public-trust-badges">
|
||||
{publicBadges.map((badge) => <span key={badge}>{badge}</span>)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="maker-public-tags">
|
||||
{services.slice(0, 5).map((service) => (
|
||||
<span key={String(service.id)}>{toText(service.category, "Servicio")}</span>
|
||||
))}
|
||||
{services.length === 0 ? <span>Impresion 3D</span> : null}
|
||||
</div>
|
||||
|
||||
<div className="maker-public-actions-row">
|
||||
<a href={contactHref} className="button button-primary">Escribir al maker</a>
|
||||
<a href={`https://wa.me/${toText(maker.public_whatsapp, "")}`} className="maker-public-whatsapp" aria-label="WhatsApp">wa</a>
|
||||
</div>
|
||||
|
||||
<section className="maker-public-recommendation">
|
||||
<span className="maker-public-section-icon">i</span>
|
||||
<div>
|
||||
<strong>Por que te lo recomendamos?</strong>
|
||||
<ul>
|
||||
<li>A 1,2 km de tu ubicacion</li>
|
||||
<li>Especialista en trabajos funcionales</li>
|
||||
<li>{works.length} trabajos verificados</li>
|
||||
<li>Excelente calidad/precio</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<nav className="maker-public-tabs" aria-label="Secciones del perfil">
|
||||
{tabs.map((tab, index) => (
|
||||
<a key={tab} href={index === 0 ? "#destacado" : index === 1 ? "#trabajos" : index === 2 ? "#servicios" : index === 3 ? "#escaparates" : index === 4 ? "#opiniones" : "#informacion"}>
|
||||
{tab}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<section id="destacado" className="maker-public-section">
|
||||
<span className="eyebrow">Destacado</span>
|
||||
<h2>Especialidades</h2>
|
||||
<div className="maker-public-chip-grid">
|
||||
{["Repuestos", "Ingenieria", "Automotor", "Diseno CAD", "Prototipos"].map((item) => (
|
||||
<span key={item}>{item}</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="maker-public-section">
|
||||
<div className="maker-public-section-head">
|
||||
<h2>Escaparates</h2>
|
||||
<Link href="#escaparates">Ver todos</Link>
|
||||
</div>
|
||||
<div id="escaparates" className="maker-public-showcases">
|
||||
{showcases.slice(0, 4).map((showcase) => (
|
||||
<Link key={String(showcase.id)} href={`/showcases/${toText(showcase.slug)}`} style={{ backgroundImage: `url(${toText(showcase.cover_image_url, workImage(works[0], "/demo/feed-print-technical.svg"))})` }}>
|
||||
<strong>{toText(showcase.title, "Escaparate")}</strong>
|
||||
<span>{Array.isArray(showcase.selected_work_ids) ? showcase.selected_work_ids.length : 0} trabajos</span>
|
||||
</Link>
|
||||
))}
|
||||
{showcases.length === 0 ? (
|
||||
<Link href={works[0] ? `/works/${toText(works[0].slug)}` : "#"} style={{ backgroundImage: `url(${workImage(works[0], "/demo/feed-print-technical.svg")})` }}>
|
||||
<strong>Trabajos destacados</strong>
|
||||
<span>{Math.max(works.length, 1)} trabajos</span>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="trabajos" className="maker-public-section">
|
||||
<div className="maker-public-section-head">
|
||||
<h2>Trabajos destacados</h2>
|
||||
<Link href="/discover">Ver todos</Link>
|
||||
</div>
|
||||
<div className="maker-public-work-rail">
|
||||
{works.map((work) => (
|
||||
<Link key={String(work.id)} href={`/works/${toText(work.slug)}`} className="maker-public-work-card">
|
||||
<img src={workImage(work)} alt={toText(work.title)} />
|
||||
<strong>{toText(work.title)}</strong>
|
||||
<span>{toText(work.technology, "FDM")} | {toText(work.material, "PLA")}</span>
|
||||
<small><span className="map-home-star">*</span> {rating}</small>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="servicios" className="maker-public-section">
|
||||
<div className="maker-public-section-head">
|
||||
<h2>Servicios y precios</h2>
|
||||
<Link href="#servicios">Ver todos</Link>
|
||||
</div>
|
||||
<div className="maker-public-service-list">
|
||||
{services.map((service) => (
|
||||
<Link key={String(service.id)} href={`/services/${toText(service.slug)}`}>
|
||||
<img src={workImage(works[0], "/demo/feed-print-gear.svg")} alt={toText(service.title)} />
|
||||
<div>
|
||||
<strong>{toText(service.title)}</strong>
|
||||
<span>{asArray(service.materials).slice(0, 3).join(" | ") || toText(service.category)}</span>
|
||||
<small>Desde ${Math.round(toNumber(service.price_from_cents, 0) / 100).toLocaleString("es-AR")}</small>
|
||||
</div>
|
||||
<b>></b>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="opiniones" className="maker-public-section">
|
||||
<div className="maker-public-section-head">
|
||||
<h2>Opinion destacada</h2>
|
||||
<Link href="#opiniones">Ver todas</Link>
|
||||
</div>
|
||||
{!allowVerifiedReviews ? (
|
||||
<div className="empty-state">Este maker pauso temporalmente las nuevas reseñas verificadas.</div>
|
||||
) : null}
|
||||
{!showPastReviews ? (
|
||||
<div className="empty-state">Este maker mantiene privadas sus resenas anteriores.</div>
|
||||
) : null}
|
||||
{showPastReviews ? visibleReviews.slice(0, 2).map((review) => (
|
||||
<article key={String(review.id)} className="maker-public-review">
|
||||
<div>
|
||||
<strong>Trabajo verificado</strong>
|
||||
<span>{toNumber(review.rating_overall, 5).toFixed(1)} / 5</span>
|
||||
</div>
|
||||
<p>{toText(review.comment, "Excelente comunicacion y calidad. Cumplio con el plazo y el resultado fue perfecto.")}</p>
|
||||
</article>
|
||||
)) : null}
|
||||
{showPastReviews && visibleReviews.length === 0 ? <div className="empty-state">Todavia no hay opiniones publicadas.</div> : null}
|
||||
</section>
|
||||
|
||||
<section id="informacion" className="maker-public-section maker-public-info-bottom">
|
||||
<h2>Informacion del maker</h2>
|
||||
<p>{toText(maker.description, "Especialista en piezas funcionales, prototipos y trabajos con contexto real.")}</p>
|
||||
{socialLinks.length ? (
|
||||
<div className="maker-public-social-links">
|
||||
{socialLinks.map(([label, href]) => {
|
||||
const safeHref = href.startsWith("http") ? href : `https://${href}`;
|
||||
return (
|
||||
<a key={label} href={safeHref} target="_blank" rel="noreferrer">
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div className="maker-public-bottom-cta">
|
||||
<a href={contactHref} className="button button-primary">Escribir al maker</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user