/* global React, CrownMark */ const { useState, useEffect, useRef, useCallback } = React; // ============================================================================= // SUPABASE CLIENT // ============================================================================= const SUPABASE_URL = "https://okdickifdjxqbezxbkhd.supabase.co"; const SUPABASE_KEY = "sb_publishable_qGLmJK_HkbI77zkaELoqwQ_ja0F-XgQ"; const supa = (window.supabase && window.supabase.createClient) ? window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY, { auth: { persistSession: true, autoRefreshToken: true, storageKey: "rdc-auth" } }) : null; if (!supa) { console.warn("Supabase client no se pudo inicializar. Revisar que supabase-js esté cargado."); } // ============================================================================= // STORE — Supabase-backed reactive hook // ============================================================================= const STORE_KEYS = { blog: "blog", videos: "videos", fotos: "fotos" }; // Schema mappers (DB usa snake_case; el código usa camelCase para videos.youtube_id) const MAPPERS = { blog: { fromDb: r => r, toDb: r => r }, videos: { fromDb: r => ({ ...r, youtubeId: r.youtube_id }), toDb: r => { const { youtubeId, ...rest } = r; return { ...rest, youtube_id: youtubeId }; }, }, fotos: { fromDb: r => r, toDb: r => r }, }; const ORDER_BY = { blog: "date", videos: "created_at", fotos: "created_at" }; // Defaults — usados como semilla inicial y mientras Supabase responde const DEFAULT_BLOG = [ { title: "Vuelve el torneo 3x3 a La Pintana", excerpt: "Después de dos años, volvemos a la cancha donde todo empezó.", body: "Inscripciones abiertas hasta el 30 de junio.", cover: "assets/court-mockup.jpeg", featured: true, date: "2026-05-15", category: "Torneos" }, { title: "Drop 02 — Camo Pack disponible", excerpt: "La estampa más icónica del street chileno.", body: "Edición limitada de 200 unidades.", cover: "assets/shorts-camo.jpeg", featured: true, date: "2026-05-08", category: "Merch" }, { title: "Clínica gratuita en Cerro Navia", excerpt: "Joaco 'Sniper' Núñez dicta una clínica para sub-15.", body: "Cupo limitado a 30 participantes.", cover: "assets/squad-sunset.jpeg", featured: false, date: "2026-04-28", category: "Comunidad" }, ]; const DEFAULT_VIDEOS = [ { youtubeId: "dQw4w9WgXcQ", thumb: "assets/court-mockup.jpeg", title: "Mixtape Verano 2024", duration: "5:21", size: "wide" }, { youtubeId: "dQw4w9WgXcQ", thumb: "assets/squad-sunset.jpeg", title: "Torneo La Pintana", duration: "3:42", size: "" }, ]; const DEFAULT_FOTOS = [ { src: "assets/court-mockup.jpeg", title: "La Cancha", subtitle: "Mockup oficial", size: "wide" }, { src: "assets/squad-sunset.jpeg", title: "El Equipo", subtitle: "Documental", size: "" }, { src: "assets/king-painting.jpeg", title: "El Rey", subtitle: "Editorial", size: "" }, { src: "assets/king-blue-shorts.jpeg", title: "Crossover", subtitle: "Ilustración", size: "" }, { src: "assets/orange-stance.jpeg", title: "Defensa", subtitle: "Movimiento", size: "" }, ]; const _initialData = {}; const useStore = (table, defaults) => { const [data, setData] = useState(() => _initialData[table] || defaults); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const refresh = useCallback(async () => { if (!supa) { setLoading(false); return; } const { data: rows, error } = await supa .from(table) .select("*") .order(ORDER_BY[table], { ascending: false }); if (error) { setError(error.message); setLoading(false); return; } const mapped = rows.map(MAPPERS[table].fromDb); _initialData[table] = mapped; // cache for next mount setData(mapped); setLoading(false); }, [table]); useEffect(() => { refresh(); if (!supa) return; // Cross-instance refresh: cuando una instancia escribe, todas se enteran. const onLocalChange = (e) => { if (e.detail && e.detail.table === table) refresh(); }; window.addEventListener("rdc:supabase-change", onLocalChange); // Realtime via Supabase (si está habilitado en el proyecto). const channelName = `rt-${table}-${Math.random().toString(36).slice(2, 8)}`; const ch = supa.channel(channelName); ch.on("postgres_changes", { event: "*", schema: "public", table }, () => refresh()); ch.subscribe(); return () => { window.removeEventListener("rdc:supabase-change", onLocalChange); supa.removeChannel(ch); }; }, [table, refresh]); const notify = () => window.dispatchEvent(new CustomEvent("rdc:supabase-change", { detail: { table } })); const add = useCallback(async (item) => { if (!supa) return null; const { id, created_at, ...rest } = item; const { data: row, error } = await supa.from(table).insert(MAPPERS[table].toDb(rest)).select().single(); if (error) { alert("Error al crear: " + error.message); return null; } notify(); return MAPPERS[table].fromDb(row); }, [table]); const update = useCallback(async (id, patch) => { if (!supa) return null; const payload = MAPPERS[table].toDb(patch); delete payload.id; delete payload.created_at; const { data: row, error } = await supa.from(table).update(payload).eq("id", id).select().single(); if (error) { alert("Error al actualizar: " + error.message); return null; } notify(); return MAPPERS[table].fromDb(row); }, [table]); const remove = useCallback(async (id) => { if (!supa) return; const { error } = await supa.from(table).delete().eq("id", id); if (error) alert("Error al eliminar: " + error.message); else notify(); }, [table]); return [data, { add, update, remove, refresh, loading, error }]; }; // ============================================================================= // IMAGE UPLOAD — Supabase Storage // ============================================================================= async function uploadImage(file) { if (!supa) { alert("Supabase no inicializado."); return null; } if (!file) return null; if (file.size > 5 * 1024 * 1024) { alert("La imagen supera 5MB."); return null; } const ext = (file.name.split(".").pop() || "jpg").toLowerCase(); const path = `uploads/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; const { error } = await supa.storage.from("media").upload(path, file, { cacheControl: "31536000", upsert: false, contentType: file.type || `image/${ext}`, }); if (error) { alert("Error subiendo: " + error.message); return null; } const { data } = supa.storage.from("media").getPublicUrl(path); return data.publicUrl; } // ============================================================================= // PUBLIC — Novedades section // ============================================================================= const formatDate = (iso) => { if (!iso) return ""; const d = new Date(iso); if (isNaN(d.getTime())) return iso; const months = ["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"]; return `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`; }; const Novedades = ({ onOpenPost }) => { const [posts] = useStore(STORE_KEYS.blog, DEFAULT_BLOG); const featured = posts.filter(p => p.featured).slice(0, 2); if (featured.length === 0) return null; return (
· Novedades

LO ÚLTIMO
EN LA CALLE.

Ver todas
{featured.map((p, i) => (
onOpenPost && onOpenPost(p)} className="group relative overflow-hidden bg-black border border-white/10 hover:border-[#FF6D0D]/60 transition-all cursor-pointer" style={{ animation: `fadeUp 0.6s ${i * 0.1}s both` }}>
{p.title}
{p.category}
{formatDate(p.date)} {Math.max(1, Math.ceil((p.body || "").length / 800))} min lectura

{p.title}

{p.excerpt}

Leer nota
))}
); }; const BlogReader = ({ post, onClose }) => { if (!post) return null; return (
e.stopPropagation()} className="relative bg-[#0A0807] border border-white/10 w-full max-w-3xl my-8" style={{ animation: "pop 0.4s cubic-bezier(.34,1.56,.64,1)" }}>
{post.title}
{post.category} {formatDate(post.date)}

{post.title}

{post.excerpt}

{post.body}
); }; // ============================================================================= // ADMIN — Login (Supabase Auth) + Panel // ============================================================================= const AdminGate = () => { const [open, setOpen] = useState(false); const [session, setSession] = useState(null); const [sessionReady, setSessionReady] = useState(false); useEffect(() => { if (!supa) { setSessionReady(true); return; } supa.auth.getSession().then(({ data }) => { setSession(data.session); setSessionReady(true); }); const { data: sub } = supa.auth.onAuthStateChange((_e, s) => setSession(s)); return () => sub.subscription.unsubscribe(); }, []); useEffect(() => { const check = () => { if (window.location.hash === "#admin") setOpen(true); }; check(); window.addEventListener("hashchange", check); const onTrigger = () => setOpen(true); window.addEventListener("rdc:open-admin", onTrigger); return () => { window.removeEventListener("hashchange", check); window.removeEventListener("rdc:open-admin", onTrigger); }; }, []); const close = () => { setOpen(false); if (window.location.hash === "#admin") { history.replaceState(null, "", window.location.pathname + window.location.search); } }; if (!open) return null; if (!sessionReady) return null; if (!session) return ; return { await supa.auth.signOut(); }} />; }; const AdminLogin = ({ onClose }) => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [err, setErr] = useState(""); const [busy, setBusy] = useState(false); const submit = async (e) => { e.preventDefault(); if (!supa) { setErr("Supabase no está conectado."); return; } setBusy(true); setErr(""); const { error } = await supa.auth.signInWithPassword({ email: email.trim(), password }); setBusy(false); if (error) { setErr(error.message); return; } // session listener triggers re-render }; return (

PANEL ADMIN

Acceso para el equipo de Reyes.

{ setEmail(e.target.value); setErr(""); }} className="w-full bg-transparent border-b-2 border-white/20 focus:border-[#FF6D0D] outline-none py-3 text-white text-base" style={{ fontFamily: "Inter, sans-serif" }} />
{ setPassword(e.target.value); setErr(""); }} className="w-full bg-transparent border-b-2 border-white/20 focus:border-[#FF6D0D] outline-none py-3 text-white text-base" style={{ fontFamily: "Inter, sans-serif" }} />
{err &&
{err}
}
); }; const AdminPanel = ({ onClose, onLogout }) => { const [tab, setTab] = useState("blog"); const [blog, blogOps] = useStore(STORE_KEYS.blog, []); const [videos, videosOps] = useStore(STORE_KEYS.videos, []); const [fotos, fotosOps] = useStore(STORE_KEYS.fotos, []); const tabs = [ { k: "blog", label: "Novedades", count: blog.length, badge: blog.filter(p => p.featured).length + " destacadas" }, { k: "videos", label: "Videos", count: videos.length }, { k: "fotos", label: "Galería", count: fotos.length }, ]; const totalRows = blog.length + videos.length + fotos.length; const allLoading = blogOps.loading && videosOps.loading && fotosOps.loading; const showSeed = !allLoading && totalRows === 0; const seed = async () => { if (!confirm("¿Insertar contenido de ejemplo? Solo recomendado si la base está vacía.")) return; for (const p of DEFAULT_BLOG) await blogOps.add(p); for (const v of DEFAULT_VIDEOS) await videosOps.add(v); for (const f of DEFAULT_FOTOS) await fotosOps.add(f); }; return (
Panel Admin
Reyes de la Calle · Supabase
{showSeed && ( )}
{tab === "blog" && } {tab === "videos" && } {tab === "fotos" && }
); }; // ============================================================================= // FORM HELPERS // ============================================================================= const TextField = ({ label, value, onChange, type = "text", placeholder, required, hint }) => ( ); const TextArea = ({ label, value, onChange, rows = 4, placeholder, required }) => (