- 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>
805 lines
32 KiB
HTML
805 lines
32 KiB
HTML
<!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>
|