- Extrae App.jsx en módulos reutilizables: constants/ (categorias, provincias), utils/geo, styles/shared, hooks/usePwaInstall y components/ (StarRating, LocalCard, CampoUbicacion, BannerMovil, CategoriaBadge) - AdminPanel reutiliza constantes y componentes compartidos y corrige la limpieza de suscripciones de Firebase al desmontar - Añade logos de marca (logo-localesp.png + @2x) y regenera los iconos PWA con tamaños menores - sw.js: sube la caché del shell a v2 e incluye los nuevos iconos y logos
68 lines
2.5 KiB
React
68 lines
2.5 KiB
React
import { useState } from "react";
|
|
|
|
const ESTRELLAS = [1, 2, 3, 4, 5];
|
|
|
|
/**
|
|
* Muestra siempre 5 estrellas (usando el font icon de Tabler ya cargado en
|
|
* index.html, para no depender de una fuente/librería extra). Si no hay
|
|
* valoración (0, null, undefined o un valor no numérico) se muestran las
|
|
* 5 estrellas vacías en lugar de no renderizar nada.
|
|
*
|
|
* - Modo lectura (readOnly, por defecto): <div role="img"> con aria-label
|
|
* describiendo la puntuación, para lectores de pantalla.
|
|
* - Modo edición (onChange): botones reales con role="radio" para que sea
|
|
* utilizable con teclado y tenga foco visible.
|
|
*/
|
|
export default function StarRating({ value = 0, onChange, readOnly = false, size = 20 }) {
|
|
const [valorHover, setValorHover] = useState(0);
|
|
const esInteractivo = !readOnly && typeof onChange === "function";
|
|
|
|
const valorSeguro = Math.min(5, Math.max(0, Math.round(Number(value)) || 0));
|
|
const valorMostrado = esInteractivo && valorHover ? valorHover : valorSeguro;
|
|
|
|
const contenedorProps = esInteractivo
|
|
? { role: "radiogroup", "aria-label": "Selecciona una puntuación de 1 a 5 estrellas" }
|
|
: { role: "img", "aria-label": `Valoración: ${valorSeguro} de 5 estrellas` };
|
|
|
|
return (
|
|
<div
|
|
{...contenedorProps}
|
|
style={{ display: "inline-flex", gap: 2 }}
|
|
onMouseLeave={() => esInteractivo && setValorHover(0)}
|
|
>
|
|
{ESTRELLAS.map((estrella) => {
|
|
const rellena = estrella <= valorMostrado;
|
|
const iconStyle = {
|
|
fontSize: size,
|
|
color: rellena ? "#D4AF37" : "var(--color-border-secondary)",
|
|
transition: "color 0.15s",
|
|
lineHeight: 0,
|
|
};
|
|
|
|
if (!esInteractivo) {
|
|
return (
|
|
<i key={estrella} className={`ti ${rellena ? "ti-star-filled" : "ti-star"}`} style={iconStyle} aria-hidden="true" />
|
|
);
|
|
}
|
|
|
|
return (
|
|
<button
|
|
key={estrella}
|
|
type="button"
|
|
role="radio"
|
|
aria-checked={valorSeguro === estrella}
|
|
aria-label={`${estrella} estrella${estrella === 1 ? "" : "s"}`}
|
|
onClick={() => onChange(estrella)}
|
|
onMouseEnter={() => setValorHover(estrella)}
|
|
onFocus={() => setValorHover(estrella)}
|
|
onBlur={() => setValorHover(0)}
|
|
style={{ background: "none", border: "none", padding: 2, margin: 0, cursor: "pointer", lineHeight: 0 }}
|
|
>
|
|
<i className={`ti ${rellena ? "ti-star-filled" : "ti-star"}`} style={iconStyle} aria-hidden="true" />
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|