This repository has been archived on 2026-11-14. You can view files and clone it, but cannot push or open issues or pull requests.
kuzbaeva/index.html
kuzbaeva ce4107cf80 Make site installable as PWA on iPhone
- Add manifest.webmanifest with standalone display, brand colors, icons
- Generate 180/192/512 PNG icons from kids.jpg via sips
- Add Apple PWA meta tags (apple-touch-icon, apple-mobile-web-app-*)
- Add viewport-fit=cover + safe-area insets for notch handling

On iPhone: open site in Safari → Share → Add to Home Screen.
Launches fullscreen (no browser chrome) with custom icon and title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:21:15 +05:00

805 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Семейный сайт Жанар</title>
<link rel="manifest" href="manifest.webmanifest" />
<meta name="theme-color" content="#0c1733" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="icon-192.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Наша семья" />
<meta name="format-detection" content="telephone=no" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--bg-1: #f97316;
--bg-2: #14b8a6;
--bg-3: #e11d48;
--bg-base: #0c1733;
--fg: #f8fafc;
--muted: #a5b4cb;
--accent: #a5b4fc;
--accent-2: #f9a8d4;
--accent-3: #67e8f9;
--accent-soft: rgba(165, 180, 252, 0.16);
--card: rgba(255, 255, 255, 0.04);
--card-solid: rgba(255, 255, 255, 0.06);
--border: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.16);
--radius: 20px;
--radius-sm: 12px;
--shadow: 0 1px 2px rgba(0,0,0,0.2), 0 12px 32px rgba(0,0,0,0.35);
--shadow-lg: 0 4px 12px rgba(0,0,0,0.25), 0 24px 60px rgba(0,0,0,0.45);
--grad: linear-gradient(135deg, #a78bfa 0%, #f472b6 35%, #fb923c 65%, #facc15 100%);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-feature-settings: "cv11", "ss01";
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
color: var(--fg);
font-size: 16px; line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background:
radial-gradient(1000px 700px at 10% -10%, var(--bg-1) 0%, transparent 55%),
radial-gradient(900px 600px at 100% 10%, var(--bg-2) 0%, transparent 55%),
radial-gradient(1100px 800px at 50% 115%, var(--bg-3) 0%, transparent 60%),
radial-gradient(700px 500px at 75% 55%, rgba(250, 204, 21, 0.18) 0%, transparent 65%),
var(--bg-base);
background-attachment: fixed;
min-height: 100vh;
}
main {
max-width: 780px;
margin: 0 auto;
padding:
calc(32px + env(safe-area-inset-top))
calc(20px + env(safe-area-inset-right))
calc(96px + env(safe-area-inset-bottom))
calc(20px + env(safe-area-inset-left));
}
.hidden { display: none !important; }
header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 28px;
padding: 4px 4px 0;
}
h1 {
font-family: 'Manrope', 'Inter', sans-serif;
font-size: 2rem; font-weight: 800;
letter-spacing: -0.02em;
margin: 0;
background: var(--grad);
background-size: 200% 200%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
animation: shimmer 8s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
h2 {
font-family: 'Manrope', 'Inter', sans-serif;
font-size: 1.15rem; font-weight: 700;
letter-spacing: -0.01em;
margin: 0 0 16px;
color: var(--fg);
display: flex; align-items: center; gap: 8px;
}
section {
background: var(--card);
backdrop-filter: blur(20px) saturate(140%);
-webkit-backdrop-filter: blur(20px) saturate(140%);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
margin-bottom: 20px;
box-shadow: var(--shadow);
transition: box-shadow .25s ease, transform .25s ease;
}
section:hover { box-shadow: var(--shadow-lg); }
.muted { color: var(--muted); }
.small { font-size: 0.82rem; }
.error { color: #dc2626; }
button {
background: var(--grad);
color: white; border: none;
border-radius: 12px;
padding: 11px 20px;
font-size: 0.95rem; font-weight: 600;
font-family: inherit;
cursor: pointer;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.25);
transition: transform .15s ease, box-shadow .2s ease, opacity .15s;
letter-spacing: -0.01em;
}
button:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(99, 102, 241, 0.35); }
button:active { transform: translateY(0); }
button.link {
background: none; color: var(--accent);
text-decoration: none;
padding: 6px 10px;
border-radius: 8px;
box-shadow: none;
font-weight: 500;
font-size: 0.88rem;
}
.wa-btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
background: linear-gradient(135deg, #25d366, #128c7e);
color: white; text-decoration: none;
padding: 12px 18px;
border-radius: 12px;
font-weight: 600; font-size: 0.95rem;
box-shadow: 0 4px 14px rgba(37, 211, 102, 0.3);
transition: transform .15s ease, box-shadow .2s ease;
}
.wa-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(37, 211, 102, 0.45); }
.wa-btn:active { transform: translateY(0); }
button.link:hover {
background: var(--accent-soft);
transform: none;
box-shadow: none;
}
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; }
.card {
background: var(--card-solid);
border: 1px solid var(--border);
border-radius: 16px;
padding: 18px 14px;
text-align: center;
transition: transform .2s ease, box-shadow .2s ease, border-color .2s;
position: relative;
overflow: hidden;
}
.card::before {
content: ""; position: absolute; inset: 0;
background: linear-gradient(135deg, rgba(99,102,241,0.05), rgba(236,72,153,0.04));
opacity: 0; transition: opacity .25s;
pointer-events: none;
}
.card:hover {
transform: translateY(-3px);
box-shadow: 0 16px 36px rgba(0,0,0,0.4);
border-color: var(--border-strong);
}
.card:hover::before { opacity: 1; }
.card > * { position: relative; }
.card .photo-wrap { position: relative; display: inline-block; cursor: pointer; }
.card img {
width: 96px; height: 96px;
object-fit: cover; border-radius: 50%;
margin-bottom: 10px;
background: #f1f5f9;
display: block;
box-shadow: 0 6px 20px rgba(0,0,0,0.4), 0 0 0 2px rgba(255,255,255,0.08), 0 0 0 4px rgba(129,140,248,0.35);
}
.card .photo-wrap::after {
content: "📷 заменить";
position: absolute; left: 0; right: 0; bottom: 10px;
background: rgba(15,23,42,0.7);
backdrop-filter: blur(6px);
color: white;
font-size: 0.7rem; font-weight: 500;
padding: 4px 0;
border-radius: 0 0 50% 50%;
opacity: 0; transition: opacity .2s;
}
.card .photo-wrap:hover::after { opacity: 1; }
.card.wide { grid-column: 1 / -1; }
.card.wide img {
width: 100%; height: 260px;
border-radius: 14px;
box-shadow: 0 12px 32px rgba(0,0,0,0.45);
}
.card.wide .photo-wrap { display: block; }
.card.wide .photo-wrap::after { border-radius: 0 0 14px 14px; bottom: 0; font-size: 0.85rem; padding: 8px 0; }
.card .file-input { display: none; }
.card .name { font-weight: 700; font-size: 1rem; letter-spacing: -0.01em; }
.card .role { color: var(--muted); font-size: 0.82rem; margin-top: 2px; }
.card .about { font-size: 0.85rem; margin-top: 8px; color: var(--fg); opacity: 0.85; }
.ig {
margin-top: 18px; padding: 14px 16px;
background: linear-gradient(135deg, rgba(244,114,182,0.14), rgba(129,140,248,0.12));
border: 1px solid var(--border);
border-radius: 14px;
font-size: 0.9rem;
}
.ig a {
color: var(--accent-2); font-weight: 600;
text-decoration: none;
}
.ig a:hover { text-decoration: underline; }
.big-counter {
font-family: 'Manrope', sans-serif;
font-size: 3.5rem; font-weight: 800;
letter-spacing: -0.04em;
text-align: center;
padding: 16px 0 8px;
background: var(--grad);
background-size: 200% 200%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
animation: shimmer 6s ease-in-out infinite;
}
#trip-dates, #trip-weather { text-align: center; margin: 6px 0; font-size: 0.9rem; }
.cat {
margin-bottom: 18px;
padding: 14px 16px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
border-radius: 14px;
}
.cat h3 {
margin: 0 0 10px; font-size: 0.92rem;
font-weight: 600; color: var(--accent);
text-transform: uppercase; letter-spacing: 0.04em;
}
.cat ul { list-style: none; padding-left: 0; margin: 0; }
.cat li {
display: flex; align-items: center; gap: 10px;
padding: 6px 0;
font-size: 0.93rem;
}
.cat input[type="checkbox"] {
width: 18px; height: 18px;
accent-color: var(--accent);
cursor: pointer;
}
.cat label { cursor: pointer; flex: 1; }
.cat label.done { text-decoration: line-through; color: var(--muted); opacity: 0.6; }
#guide-table {
width: 100%; border-collapse: separate; border-spacing: 0;
font-size: 0.9rem;
}
#guide-table th, #guide-table td {
text-align: left; padding: 12px 10px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
#guide-table th {
color: var(--muted); font-weight: 600;
font-size: 0.72rem;
text-transform: uppercase; letter-spacing: 0.06em;
background: rgba(129,140,248,0.12);
position: sticky; top: 0;
}
#guide-table tbody tr { transition: background .15s; }
#guide-table tbody tr:hover { background: rgba(129,140,248,0.08); }
#guide-table a { color: var(--accent); text-decoration: none; }
#guide-table a:hover { text-decoration: underline; }
#auth-dialog {
border: 1px solid var(--border-strong);
border-radius: 24px;
padding: 32px 28px;
max-width: 360px;
color: var(--fg);
background: rgba(20, 20, 35, 0.85);
backdrop-filter: blur(24px) saturate(140%);
box-shadow: 0 25px 80px rgba(0,0,0,0.6);
}
#auth-dialog::backdrop {
background: rgba(0,0,0,0.6);
backdrop-filter: blur(10px);
}
#auth-dialog h2 { margin-top: 0; font-size: 1.3rem; }
#auth-dialog input {
width: 100%; padding: 12px 14px;
font-size: 1rem; font-family: inherit;
background: rgba(255,255,255,0.06);
color: var(--fg);
border: 1.5px solid var(--border-strong);
border-radius: 12px;
margin: 10px 0 16px;
transition: border-color .15s, box-shadow .15s, background .15s;
}
#auth-dialog input::placeholder { color: var(--muted); }
#auth-dialog input:focus {
outline: none;
background: rgba(255,255,255,0.1);
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(129,140,248,0.2);
}
#auth-dialog button { width: 100%; }
footer {
text-align: center; margin-top: 32px;
padding: 16px;
color: var(--muted);
}
@media (max-width: 540px) {
main { padding: 20px 14px 80px; }
section { padding: 18px; border-radius: 18px; }
.big-counter { font-size: 2.6rem; }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.05rem; }
#guide-table th, #guide-table td { padding: 10px 6px; font-size: 0.82rem; }
}
</style>
</head>
<body>
<dialog id="auth-dialog">
<form id="auth-form" method="dialog">
<h2>Личный семейный портал</h2>
<p>Введите пароль:</p>
<input type="password" id="auth-input" autocomplete="current-password" required />
<button type="submit">Войти</button>
<p id="auth-error" class="error hidden">Неверный пароль</p>
</form>
</dialog>
<main id="main" class="hidden">
<header>
<h1>🏠 Наша семья</h1>
<button id="logout" class="link">Выйти</button>
</header>
<section id="family">
<h2>Кто мы</h2>
<div id="family-list" class="cards"></div>
<p class="ig">
📷 Instagram:
<a id="ig-link" target="_blank" rel="noopener"></a>
</p>
</section>
<section id="countdown">
<h2>⏳ До поездки в Батуми</h2>
<div id="countdown-box" class="big-counter">…</div>
<p id="trip-dates" class="muted"></p>
<p id="trip-weather" class="muted"></p>
</section>
<section id="reminders">
<h2>🔔 Напоминания в WhatsApp</h2>
<p class="muted small">Жми кнопку — откроется WhatsApp с готовым текстом, отправь себе.</p>
<div style="display:flex; flex-direction:column; gap:10px; margin-top:12px;">
<a id="wa-steps" class="wa-btn" target="_blank" rel="noopener">👟 <span id="wa-steps-label">Шаги сегодня</span></a>
<a id="wa-ig" class="wa-btn" target="_blank" rel="noopener">📷 Опубликовать историю в Instagram</a>
</div>
</section>
<section id="checklist">
<h2>🧳 Чек-лист поездки</h2>
<div id="checklist-list"></div>
<button id="reset-checklist" class="link">Сбросить все галочки</button>
</section>
<section id="guide">
<h2>🗺️ Гид по Батуми</h2>
<p class="muted small">Отметь галочкой посещённые места.</p>
<table id="guide-table">
<thead><tr><th>✓</th><th>#</th><th>Место</th><th>Описание</th><th>Адрес</th><th>Время</th></tr></thead>
<tbody></tbody>
</table>
<p class="small"><span id="guide-progress" class="muted"></span> · <button id="reset-guide" class="link">Сбросить отметки</button></p>
</section>
<footer>
<p class="muted small">Создано с помощью Claude · Май 2026</p>
</footer>
</main>
<script>
/* ===== DATA ===== */
const FAMILY = {
members: [
{
name: "Жанар",
role: "мама, 37 лет",
photo: "zhanara.jpg",
about: "Главный пользователь портала. Любит путешествия и заботится о семье."
},
{
name: "Азамат",
role: "папа, 39 лет",
photo: "https://placehold.co/200x200/ffe8d6/d97b3a?text=%D0%90",
about: "Работает, помогает планировать семейные поездки."
},
{
name: "Наши дети",
role: "трое — старшая, младшая и малыш",
photo: "WhatsApp Image 2026-05-22 at 09.31.52.jpeg",
about: "Любимые сердечки нашей семьи.",
wide: true
}
],
instagram: "https://instagram.com/zhanara2964",
instagramHandle: "@zhanara2964"
};
const TRIP = {
destination: "Батуми, Грузия",
departure: "2026-06-22",
return: "2026-06-29",
weather: "☀️ Днём +23…+25°C, ночью +21°C · влажность 80% · возможны короткие дожди · море +22…+23°C"
};
const CHECKLIST = [
{ category: "👕 Одежда", items: [
"Лёгкие платья / шорты / футболки (3–4 комплекта)",
"Лёгкий кардиган или ветровка",
"Дождевик или компактный зонт",
"Нарядный образ для ресторана"
]},
{ category: "🏖 Пляж", items: [
"Купальник / плавки (2 шт)",
"Шлёпанцы для пляжа",
"Полотенце"
]},
{ category: "👟 Обувь", items: [
"Сандалии",
"Удобные кроссовки для прогулок"
]},
{ category: "🕶 Аксессуары", items: [
"Солнцезащитные очки",
"Шляпа или панама",
"Солнцезащитный крем SPF 50"
]},
{ category: "📄 Документы и деньги", items: [
"Паспорта (все члены семьи)",
"Билеты / брони отелей (распечатать)",
"Банковские карты + немного налички",
"Медицинская страховка"
]}
];
const GUIDE = [
{ name: "Приморский бульвар", desc: "Главная набережная города, старейшая достопримечательность — гулять обязательно.",
addr: "Батуми, набережная Чёрного моря (центр города, 7 км вдоль берега)",
hours: "круглосуточно, лучше на закате (19:00–21:00)" },
{ name: "Скульптура «Али и Нино»", desc: "Знаменитые движущиеся фигуры влюблённых на набережной.",
addr: "Приморский бульвар, у площади Чудес, рядом с Алфавитной башней",
hours: "движение каждые 10 мин, 19:00 — лучший вид" },
{ name: "Алфавитная башня", desc: "Архитектурная икона Батуми, высокая башня в форме ДНК.",
addr: "ул. Шерифа Химшиашвили, 1, Приморский парк",
hours: "пн–вс 11:00–23:00, кафе наверху до 22:00" },
{ name: "Колесо обозрения", desc: "Вид на море с высоты, ~10 лари, у порта.",
addr: "Приморский бульвар, рядом с Морским портом",
hours: "пн–вс 12:00–24:00" },
{ name: "Ботанический сад", desc: "Огромный сад с видом на море, тропические растения.",
addr: "пос. Мцване Концхи (Зелёный мыс), 9 км к северу от Батуми",
hours: "пн–вс 09:00–20:00 (касса до 19:00), ~3 ч на осмотр" },
{ name: "Старый город", desc: "Исторические улицы, мечеть, атмосфера.",
addr: "район Старого Батуми, между ул. Мемед Абашидзе и набережной",
hours: "круглосуточно, оптимально 17:00–22:00" },
{ name: "Пещера Прометея", desc: "Экскурсия ~1,5 ч от Батуми, сталактиты, подземная река.",
addr: "с. Кумистави, Цхалтубский муниципалитет (~140 км от Батуми, трасса на Кутаиси)",
hours: "пн–вс 10:00–18:00 (вт выходной), экскурсии каждые 30 мин" },
{ name: "Каньон Мартвили", desc: "Природный каньон, лодки по воде, очень красиво.",
addr: "с. Гачедили, Мартвильский муниципалитет (~135 км от Батуми)",
hours: "пн–вс 10:00–18:00, заплыв на лодке 15 мин" },
{ name: "Водопад Махунцети", desc: "Природная достопримечательность за городом.",
addr: "с. Махунцети, Кеда (~30 км к востоку от Батуми, трасса Батуми–Ахалцихе)",
hours: "круглосуточно, лучше до 12:00 пока мало людей" },
{ name: "Фонтан Нептуна", desc: "Театральная площадь, копия итальянского фонтана.",
addr: "Театральная площадь, Старый город, у драмтеатра им. Чавчавадзе",
hours: "круглосуточно, подсветка после 20:00" }
];
const REMINDER_MOTTO = "💪 Дисциплина к успеху";
const REMINDERS = {
weekdaySteps: { label: "Пройти 5 000 шагов сегодня", text: "Пройти 5 000 шагов сегодня 👟\n" + REMINDER_MOTTO },
weekendSteps: { label: "Пройти 10 000 шагов сегодня", text: "Пройти 10 000 шагов сегодня 👟\n" + REMINDER_MOTTO },
instagram: { label: "История в Instagram", text: "Опубликовать одну историю в Instagram (@zhanara2964) 📷\n" + REMINDER_MOTTO }
};
const WHATSAPP_NUMBER = "77477854246";
const PASSWORD = "zhanar2026";
/* ===== AUTH ===== */
(function () {
const dialog = document.getElementById("auth-dialog");
const form = document.getElementById("auth-form");
const input = document.getElementById("auth-input");
const error = document.getElementById("auth-error");
const main = document.getElementById("main");
const logout = document.getElementById("logout");
function unlock() {
main.classList.remove("hidden");
if (dialog.open) dialog.close();
}
function lock() {
main.classList.add("hidden");
if (!dialog.open) dialog.showModal();
input.value = "";
input.focus();
}
if (localStorage.getItem("auth") === "1") {
unlock();
} else {
lock();
}
form.addEventListener("submit", function (e) {
e.preventDefault();
if (input.value === PASSWORD) {
localStorage.setItem("auth", "1");
error.classList.add("hidden");
unlock();
window.dispatchEvent(new Event("auth:unlocked"));
} else {
error.classList.remove("hidden");
input.value = "";
input.focus();
}
});
logout.addEventListener("click", function () {
localStorage.removeItem("auth");
lock();
});
})();
/* ===== COUNTDOWN ===== */
(function () {
const box = document.getElementById("countdown-box");
const dates = document.getElementById("trip-dates");
const wx = document.getElementById("trip-weather");
function daysBetween(from, to) {
const ms = to.setHours(0,0,0,0) - from.setHours(0,0,0,0);
return Math.round(ms / (1000 * 60 * 60 * 24));
}
function fmt(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString("ru-RU", { day: "numeric", month: "long", year: "numeric" });
}
function plural(n, one, few, many) {
const mod10 = n % 10;
const mod100 = n % 100;
if (mod10 === 1 && mod100 !== 11) return one;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few;
return many;
}
window.renderCountdown = function () {
const today = new Date();
const dep = new Date(TRIP.departure);
const ret = new Date(TRIP.return);
const daysToDep = daysBetween(new Date(today), new Date(dep));
const daysToRet = daysBetween(new Date(today), new Date(ret));
if (daysToDep > 0) {
box.textContent = `${daysToDep} ${plural(daysToDep, "день","дня","дней")}`;
} else if (daysToRet >= 0) {
box.textContent = "🌴 Вы в Батуми!";
} else {
box.textContent = "✈️ Поездка завершена";
}
dates.textContent = `Вылет ${fmt(TRIP.departure)} → Возвращение ${fmt(TRIP.return)} · ${TRIP.destination}`;
wx.textContent = TRIP.weather;
};
})();
/* ===== CHECKLIST ===== */
(function () {
const root = document.getElementById("checklist-list");
const reset = document.getElementById("reset-checklist");
const KEY = "checklist-state";
function loadState() {
try { return JSON.parse(localStorage.getItem(KEY) || "{}"); }
catch (e) { return {}; }
}
function saveState(state) {
localStorage.setItem(KEY, JSON.stringify(state));
}
window.renderChecklist = function () {
const state = loadState();
root.innerHTML = "";
CHECKLIST.forEach(function (cat, ci) {
const block = document.createElement("div");
block.className = "cat";
const title = document.createElement("h3");
title.textContent = cat.category;
block.appendChild(title);
const ul = document.createElement("ul");
cat.items.forEach(function (item, ii) {
const id = `c-${ci}-${ii}`;
const li = document.createElement("li");
const cb = document.createElement("input");
cb.type = "checkbox";
cb.id = id;
cb.checked = !!state[id];
const label = document.createElement("label");
label.htmlFor = id;
label.textContent = item;
if (cb.checked) label.classList.add("done");
cb.addEventListener("change", function () {
const s = loadState();
s[id] = cb.checked;
saveState(s);
label.classList.toggle("done", cb.checked);
});
li.appendChild(cb);
li.appendChild(label);
ul.appendChild(li);
});
block.appendChild(ul);
root.appendChild(block);
});
};
reset.addEventListener("click", function () {
if (confirm("Сбросить все отметки в чек-листе?")) {
localStorage.removeItem(KEY);
window.renderChecklist();
}
});
})();
/* ===== REMINDERS (WhatsApp manual) ===== */
(function () {
const stepsBtn = document.getElementById("wa-steps");
const stepsLabel = document.getElementById("wa-steps-label");
const igBtn = document.getElementById("wa-ig");
function waLink(text) {
return "https://wa.me/" + WHATSAPP_NUMBER + "?text=" + encodeURIComponent(text);
}
function refresh() {
const dow = new Date().getDay();
const isWeekend = dow === 0 || dow === 6;
const steps = isWeekend ? REMINDERS.weekendSteps : REMINDERS.weekdaySteps;
stepsBtn.href = waLink(steps.text);
stepsLabel.textContent = steps.label;
igBtn.href = waLink(REMINDERS.instagram.text);
}
refresh();
setInterval(refresh, 60 * 60 * 1000);
})();
/* ===== APP (render) ===== */
(function () {
function renderFamily() {
const root = document.getElementById("family-list");
root.innerHTML = "";
const PHOTO_KEY = "family-photos";
const stored = (function () {
try { return JSON.parse(localStorage.getItem(PHOTO_KEY) || "{}"); }
catch (e) { return {}; }
})();
FAMILY.members.forEach(function (m, idx) {
const card = document.createElement("div");
card.className = "card" + (m.wide ? " wide" : "");
const slot = "m" + idx;
const src = stored[slot] || m.photo;
card.innerHTML = `
<label class="photo-wrap" title="Кликните, чтобы загрузить фото">
<img src="${src}" alt="${m.name}" onerror="this.src='https://placehold.co/200x200?text=%3F'" />
<input type="file" class="file-input" accept="image/*" data-slot="${slot}" />
</label>
<div class="name">${m.name}</div>
<div class="role">${m.role}</div>
${m.about ? `<div class="about">${m.about}</div>` : ""}
`;
root.appendChild(card);
});
root.querySelectorAll(".file-input").forEach(function (input) {
input.addEventListener("change", function (e) {
const file = e.target.files && e.target.files[0];
if (!file) return;
const slot = input.dataset.slot;
const reader = new FileReader();
reader.onload = function (ev) {
const dataUrl = ev.target.result;
resizeImage(dataUrl, 600, function (small) {
const s = JSON.parse(localStorage.getItem(PHOTO_KEY) || "{}");
s[slot] = small;
try {
localStorage.setItem(PHOTO_KEY, JSON.stringify(s));
const img = input.parentElement.querySelector("img");
img.src = small;
} catch (err) {
alert("Не удалось сохранить фото (превышен лимит хранилища). Попробуйте фото меньшего размера.");
}
});
};
reader.readAsDataURL(file);
});
});
const ig = document.getElementById("ig-link");
ig.href = FAMILY.instagram;
ig.textContent = FAMILY.instagramHandle || FAMILY.instagram;
}
function resizeImage(dataUrl, maxSide, cb) {
const img = new Image();
img.onload = function () {
const scale = Math.min(1, maxSide / Math.max(img.width, img.height));
const w = Math.round(img.width * scale);
const h = Math.round(img.height * scale);
const canvas = document.createElement("canvas");
canvas.width = w; canvas.height = h;
canvas.getContext("2d").drawImage(img, 0, 0, w, h);
cb(canvas.toDataURL("image/jpeg", 0.85));
};
img.src = dataUrl;
}
function getGuideVisited() {
try { return JSON.parse(localStorage.getItem("guide-visited") || "{}"); }
catch (e) { return {}; }
}
function setGuideVisited(state) {
localStorage.setItem("guide-visited", JSON.stringify(state));
}
function updateGuideProgress() {
const v = getGuideVisited();
const done = GUIDE.filter((_, i) => v[i]).length;
const el = document.getElementById("guide-progress");
if (el) el.textContent = `Посещено: ${done} из ${GUIDE.length}`;
}
function renderGuide() {
const tbody = document.querySelector("#guide-table tbody");
tbody.innerHTML = "";
const visited = getGuideVisited();
GUIDE.forEach(function (g, i) {
const tr = document.createElement("tr");
const mapUrl = "https://maps.google.com/?q=" + encodeURIComponent(g.name + ", Batumi, Georgia");
const checked = visited[i] ? "checked" : "";
tr.innerHTML = `
<td><input type="checkbox" data-guide-idx="${i}" ${checked} style="width:18px;height:18px;accent-color:var(--accent);cursor:pointer;" /></td>
<td>${i + 1}</td>
<td><strong>${g.name}</strong></td>
<td>${g.desc}</td>
<td>${g.addr || ""} <a href="${mapUrl}" target="_blank" rel="noopener" title="Открыть в Google Maps">🗺️</a></td>
<td class="small muted">${g.hours || ""}</td>
`;
if (visited[i]) tr.style.opacity = "0.55";
tbody.appendChild(tr);
});
tbody.querySelectorAll('input[type="checkbox"][data-guide-idx]').forEach(function (cb) {
cb.addEventListener("change", function () {
const idx = cb.getAttribute("data-guide-idx");
const state = getGuideVisited();
if (cb.checked) state[idx] = true; else delete state[idx];
setGuideVisited(state);
cb.closest("tr").style.opacity = cb.checked ? "0.55" : "1";
updateGuideProgress();
});
});
updateGuideProgress();
const resetBtn = document.getElementById("reset-guide");
if (resetBtn && !resetBtn.dataset.bound) {
resetBtn.dataset.bound = "1";
resetBtn.addEventListener("click", function () {
if (!confirm("Сбросить все отметки посещённых мест?")) return;
setGuideVisited({});
renderGuide();
});
}
}
function renderAll() {
renderFamily();
renderGuide();
if (window.renderCountdown) window.renderCountdown();
if (window.renderChecklist) window.renderChecklist();
}
if (localStorage.getItem("auth") === "1") renderAll();
window.addEventListener("auth:unlocked", renderAll);
})();
</script>
</body>
</html>