import { useState, useEffect } from "react";
// ── Helpers ──────────────────────────────────────────────────────────────────
function normalizePhone(raw) {
let c = raw.replace(/[\s\-\(\)\+\.]/g, "");
if (/^\d{9}$/.test(c)) return "420" + c;
if (c.startsWith("420") && c.length === 12) return c;
if (c.startsWith("00420")) return c.slice(2);
return c;
}
function waLink(phone, message) {
return `https://wa.me/${normalizePhone(phone)}?text=${encodeURIComponent(message)}`;
}
function fillMsg(template, vars) {
return Object.entries(vars).reduce(
(t, [k, v]) => t.replaceAll(`{${k}}`, v || "—"),
template
);
}
function fmtDate(dateStr) {
if (!dateStr) return "";
return new Date(dateStr + "T12:00").toLocaleDateString("cs-CZ", {
day: "numeric", month: "long", year: "numeric",
});
}
// ── Default Templates ─────────────────────────────────────────────────────────
const DEFAULT_INV = `Dobrý den {jmeno},
rádi bychom Vás pozvali na exkluzivní testování závodních vozů:
🏁 Akce: {akce}
📅 Datum: {datum}
⏰ Čas: {cas}
📍 Místo: {misto}
Těšíme se na Vaši účast! V případě dotazů nás neváhejte kontaktovat.
S pozdravem,
Tým GTSPORTS`;
const DEFAULT_REM = `Dobrý den {jmeno},
připomínáme Vám nadcházející testování závodních vozů:
🏁 Akce: {akce}
📅 Datum: {datum}
⏰ Čas: {cas}
📍 Místo: {misto}
Pokud máte jakékoli dotazy nebo potřebujete změnu, kontaktujte nás.
Těšíme se na Vás!
Tým GTSPORTS`;
const STORAGE_KEY = "gtsports_racingmsg_v2";
// ── App ───────────────────────────────────────────────────────────────────────
export default function App() {
const [tab, setTab] = useState("zakaznici");
const [loaded, setLoaded] = useState(false);
const [data, setData] = useState({
event: { name: "", date: "", time: "", location: "" },
customers: [],
inviteT: DEFAULT_INV,
reminderT: DEFAULT_REM,
});
const [newName, setNewName] = useState("");
const [newPhone, setNewPhone] = useState("");
const [bulkText, setBulkText] = useState("");
const [openPreview, setOpenPreview] = useState(null);
useEffect(() => {
(async () => {
try {
const r = await window.storage.get(STORAGE_KEY);
if (r) setData(JSON.parse(r.value));
} catch {}
setLoaded(true);
})();
}, []);
async function save(next) {
setData(next);
try { await window.storage.set(STORAGE_KEY, JSON.stringify(next)); } catch {}
}
const patchEvent = (f, v) => save({ ...data, event: { ...data.event, [f]: v } });
function addOne() {
if (!newName.trim() || !newPhone.trim()) return;
const c = { id: Date.now(), name: newName.trim(), phone: newPhone.trim(), sentInvite: false, sentReminder: false };
save({ ...data, customers: [...data.customers, c] });
setNewName(""); setNewPhone("");
}
function importBulk() {
const lines = bulkText.trim().split("\n").filter(Boolean);
const added = lines.flatMap((line, i) => {
const [name, phone] = line.split(/[,;\t]+/);
if (!phone?.trim()) return [];
return [{ id: Date.now() + i, name: name.trim(), phone: phone.trim(), sentInvite: false, sentReminder: false }];
});
if (added.length) save({ ...data, customers: [...data.customers, ...added] });
setBulkText("");
}
const removeCustomer = (id) => save({ ...data, customers: data.customers.filter(c => c.id !== id) });
const markSent = (id, field) => save({ ...data, customers: data.customers.map(c => c.id === id ? { ...c, [field]: true } : c) });
const resetSent = (id) => save({ ...data, customers: data.customers.map(c => c.id === id ? { ...c, sentInvite: false, sentReminder: false } : c) });
function getVars(cust) {
return {
jmeno: cust.name,
datum: fmtDate(data.event.date),
cas: data.event.time || "",
misto: data.event.location || "",
akce: data.event.name || "Testování závodních vozů",
};
}
function cardBorderColor(c) {
if (c.sentInvite && c.sentReminder) return "#16a34a";
if (c.sentInvite) return "#d97706";
return "#3f3f46";
}
const sentInv = data.customers.filter(c => c.sentInvite).length;
const sentRem = data.customers.filter(c => c.sentReminder).length;
// ── Styles ──
const inp = {
width: "100%", boxSizing: "border-box",
padding: "10px 14px", background: "#151820",
border: "1px solid #1E2130", borderRadius: 8,
color: "#EEE9E2", fontSize: 14, outline: "none",
};
const sec = { fontSize: 11, fontWeight: 800, color: "#DC2020", letterSpacing: "2px", textTransform: "uppercase", marginBottom: 14 };
if (!loaded) return (
Načítám…
);
return (
{/* ── HEADER ── */}
{data.event.date && (
)}
{/* ── TAB BAR ── */}
🏁
GTSPORTS
Racing Event Messenger
Příští akce
{data.event.name || "Testování"}
{fmtDate(data.event.date)}{data.event.time ? ` · ${data.event.time}` : ""}
{[["akce","🏎️ Akce"],["sablony","📝 Šablony"],["zakaznici","👥 Zákazníci"],["odeslat","📤 Odeslat"]].map(([id, label]) => (
))}
{/* ════ AKCE ════ */}
{tab === "akce" && (
)}
)}
{/* ════ ŠABLONY ════ */}
{tab === "sablony" && (
)}
{/* ════ ZÁKAZNÍCI ════ */}
{tab === "zakaznici" && (
{/* Add one */}
setBulkText(e.target.value)} rows={4}
style= />
{/* List */}
{data.customers.length === 0 && (
)}
{data.customers.map(c => (
))}
)}
{/* ════ ODESLAT ════ */}
{tab === "odeslat" && (
)}
{!data.event.date && (
)}
{data.customers.map(c => {
const vars = getVars(c);
const invMsg = fillMsg(data.inviteT, vars);
const remMsg = fillMsg(data.reminderT, vars);
const isOpen = openPreview === c.id;
return (
{/* Action buttons */}
);
})}
)}
);
}Detail akce
{[
["name","Název akce","text","např. Trackday Brno 2025"],
["date","Datum","date",""],
["time","Čas začátku","time",""],
["location","Místo / Okruh","text","např. Autodrom Brno"],
].map(([f, label, type, ph]) => (
patchEvent(f, e.target.value)} style={inp} />
))}
{data.event.name && data.event.date && (
✓ AKCE NASTAVENA
{data.event.name}
{fmtDate(data.event.date)}
{data.event.time && ` · ${data.event.time}`}
{data.event.location && ` · ${data.event.location}`}
Šablony zpráv
Proměnné:{" "}
{["{jmeno}","{datum}","{cas}","{misto}","{akce}"].map(v => (
{v}
))}
Zákazníci
{data.customers.length} celkem
setNewName(e.target.value)}
onKeyDown={e => e.key === "Enter" && addOne()}
style= />
setNewPhone(e.target.value)}
onKeyDown={e => e.key === "Enter" && addOne()}
style= />
{/* Bulk import */}
Hromadný import — jeden zákazník na řádek
Formát:
Jméno, číslo (oddělovač: čárka, středník nebo tabulátor)👥
Přidejte zákazníky ručně nebo hromadným importem
{c.name}
{c.phone}
{c.sentInvite ? "✓ Pozv." : "Pozv."}
{c.sentReminder ? "✓ Připom." : "Připom."}
Odeslat zprávy
{/* Stats */}
{[
{ l: "Zákazníků", v: data.customers.length, c: "#EEE9E2" },
{ l: "Pozváno", v: `${sentInv} / ${data.customers.length}`, c: "#86efac" },
{ l: "Připomenuto", v: `${sentRem} / ${data.customers.length}`, c: "#7dd3fc" },
].map(s => (
))}
{/* Progress bar */}
{data.customers.length > 0 && (
{s.l}
{s.v}
POKRYTÍ POZVÁNKAMI
{Math.round(sentInv / data.customers.length * 100)}%
⚠️ Nejdříve vyplňte detail akce v záložce „Akce"
)}
{data.customers.length === 0 && (
📤
Přidejte zákazníky v záložce „Zákazníci"
{c.name}
{c.phone}
markSent(c.id, "sentInvite")}
style=>
{c.sentInvite ? "✓ Pozvánka odeslána" : "📱 Odeslat pozvánku"}
markSent(c.id, "sentReminder")}
style=>
{c.sentReminder ? "✓ Připomenutí odesláno" : "🔔 Odeslat připomenutí"}
{/* Preview toggle */}
{isOpen && (
{[["POZVÁNKA", "#4ade80", invMsg], ["PŘIPOMENUTÍ", "#7dd3fc", remMsg]].map(([title, color, msg]) => (
))}
)}
{title}
{msg}