wedding invitation with RSVP form
This commit is contained in:
parent
aefeac1880
commit
658969224c
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
data.json
|
||||
.vibe42-run.log
|
||||
.vibe42-run.pid
|
||||
226
index.html
226
index.html
@ -4,8 +4,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Где поступать — навигатор специальностей</title>
|
||||
<meta name="description" content="Выбирай специальность, сравнивай вузы, баллы и стипендии — и знай, куда подать документы.">
|
||||
<title>Приглашение на свадьбу — 1 ноября</title>
|
||||
<meta name="description" content="Приглашение на свадьбу: 1 ноября, ресторан «Хан Шатır». Подтвердите, пожалуйста, ваше присутствие.">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||||
@ -14,189 +14,131 @@
|
||||
<script src="design-system/icons/kt-ai-icons.js"></script>
|
||||
<style>
|
||||
a.kt-ai-btn { text-decoration: none; }
|
||||
.kt-ai-hero-mark { display: inline; padding: 2px 10px; margin: 0 2px; background: var(--vibe-tint); border-radius: 8px; }
|
||||
.kt-ai-searchrow { display: flex; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-7); max-width: 560px; margin-left: auto; margin-right: auto; flex-wrap: wrap; }
|
||||
.kt-ai-searchrow .kt-ai-field { flex: 1 1 240px; margin: 0; }
|
||||
.kt-ai-searchrow .kt-ai-input { width: 100%; }
|
||||
.kt-ai-searchrow .kt-ai-btn { align-self: flex-end; }
|
||||
.kt-ai-prog { height: 6px; border-radius: 99px; background: var(--kt-ai-border); overflow: hidden; margin-top: var(--kt-ai-space-4); }
|
||||
.kt-ai-prog-in { height: 100%; background: var(--vibe-accent); border-radius: 99px; }
|
||||
.kt-ai-prog-l { display: flex; justify-content: space-between; font-size: 12px; color: var(--kt-ai-fg-muted); margin-bottom: 6px; }
|
||||
.kt-ai-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
.kt-ai-table th { text-align: left; font-weight: 500; color: var(--kt-ai-fg-muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; padding: 10px 12px; border-bottom: 1px solid var(--kt-ai-border); }
|
||||
.kt-ai-table td { padding: 14px 12px; border-bottom: 1px solid var(--kt-ai-border); vertical-align: middle; }
|
||||
.kt-ai-table tr:last-child td { border-bottom: none; }
|
||||
.kt-ai-table .num { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.kt-ai-scroll { overflow-x: auto; border: 1px solid var(--kt-ai-border); border-radius: 16px; background: var(--kt-ai-card-bg); }
|
||||
.kt-ai-faq { border: 1px solid var(--kt-ai-border); border-radius: 10px; background: var(--kt-ai-card-bg); padding: 16px 18px; }
|
||||
.kt-ai-faq + .kt-ai-faq { margin-top: var(--kt-ai-space-4); }
|
||||
.kt-ai-faq h3 { margin: 0 0 6px; font-size: 15px; }
|
||||
.kt-ai-faq p { margin: 0; color: var(--kt-ai-fg-muted); font-size: 14px; }
|
||||
.kt-ai-note { color: var(--kt-ai-fg-muted); font-size: 13px; margin-top: var(--kt-ai-space-4); }
|
||||
.kt-ai-tile-top { display: flex; justify-content: space-between; align-items: center; gap: var(--kt-ai-space-2); }
|
||||
.kt-ai-invite-date { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-4); display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap; }
|
||||
.kt-ai-invite-date b { font-size: 15px; color: var(--kt-ai-fg); text-transform: none; letter-spacing: 0; }
|
||||
.kt-ai-invite-names { display: flex; align-items: center; justify-content: center; gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-4); }
|
||||
.kt-ai-invite-am { font-size: 120px; line-height: 1; color: var(--vibe-accent); font-weight: 300; }
|
||||
.kt-ai-hero-name { font-size: clamp(40px, 7vw, 72px); font-weight: 400; letter-spacing: -.035em; }
|
||||
.kt-ai-countdown { display: flex; gap: var(--kt-ai-space-3); justify-content: center; margin-top: var(--kt-ai-space-8); flex-wrap: wrap; }
|
||||
.kt-ai-cd-cell { min-width: 72px; text-align: center; border: 1px solid var(--kt-ai-border); border-radius: 10px; background: var(--kt-ai-card-bg); padding: 12px 10px; }
|
||||
.kt-ai-cd-num { font-size: 26px; font-variant-numeric: tabular-nums; color: var(--kt-ai-fg); }
|
||||
.kt-ai-cd-lab { font-size: 11px; color: var(--kt-ai-fg-muted); margin-top: 4px; }
|
||||
.kt-ai-form-card { max-width: 460px; margin: 0 auto; padding: var(--kt-ai-space-8); }
|
||||
.kt-ai-form-card .kt-ai-field { margin-bottom: var(--kt-ai-space-4); }
|
||||
.kt-ai-form-card .kt-ai-btn { width: 100%; }
|
||||
.kt-ai-form-msg { margin-top: var(--kt-ai-space-4); text-align: center; font-size: 14px; color: var(--kt-ai-fg-muted); min-height: 20px; }
|
||||
.kt-ai-detail-icon span { display: inline-flex; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="kt-ai-pagehead">
|
||||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||||
<strong>Где поступать</strong>
|
||||
<strong>Мы женимся!</strong>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<a class="kt-ai-btn" data-variant="ghost" href="#specialties">Специальности</a>
|
||||
<a class="kt-ai-btn" data-variant="primary" href="#universities">Список вузов</a>
|
||||
<a class="kt-ai-btn" data-variant="primary" href="#rsvp">Подтвердить</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="kt-ai-hero" data-align="center">
|
||||
<div class="kt-ai-wrap">
|
||||
<h1>Знай, <span class="kt-ai-hero-mark">куда</span> подать документы</h1>
|
||||
<p class="kt-ai-hero-sub">Сравни специальность, баллы, бюджетные места и стипендии — без путеводителя «спроси у старших».</p>
|
||||
<div class="kt-ai-searchrow">
|
||||
<div class="kt-ai-field">
|
||||
<label for="f-specialty">Специальность</label>
|
||||
<input class="kt-ai-input" id="f-specialty" type="text" placeholder="IT, медицина, юриспруденция…">
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label for="f-score">Твой балл</label>
|
||||
<input class="kt-ai-input" id="f-score" type="number" min="0" max="200" placeholder="напр. 150">
|
||||
</div>
|
||||
<button class="kt-ai-btn" data-variant="primary" id="btn-search" type="button">Найти вариант</button>
|
||||
<div class="kt-ai-invite-date">
|
||||
<span class="kt-ai-chip">Приглашение</span>
|
||||
<span>суббота</span>
|
||||
<b>1 ноября 2026</b>
|
||||
</div>
|
||||
<p class="kt-ai-note" id="search-note" hidden>Ищем подходящие варианты…</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="specialties" class="kt-ai-section" data-divider="true">
|
||||
<div class="kt-ai-wrap">
|
||||
<div class="kt-ai-section-head">
|
||||
<h2>Как выбрать специальность</h2>
|
||||
<p>Пройди по шагам, и список вариантов сузится с сотен до трёх-пяти.</p>
|
||||
</div>
|
||||
<div class="kt-ai-grid" style="margin-top:var(--kt-ai-space-9)">
|
||||
<article class="kt-ai-tile">
|
||||
<div class="kt-ai-tile-top">
|
||||
<span class="kt-ai-tile-icon"><svg class="kt-icon" aria-hidden="true"><use href="#search"></use></svg></span>
|
||||
<span class="kt-ai-chip">Шаг 1</span>
|
||||
</div>
|
||||
<h3>Что не надоест через полгода</h3>
|
||||
<p>Список интересов и предметов-любимцев. Честнее любой анкеты «кем ты хочешь стать».</p>
|
||||
</article>
|
||||
<article class="kt-ai-tile">
|
||||
<div class="kt-ai-tile-top">
|
||||
<span class="kt-ai-tile-icon"><svg class="kt-icon" aria-hidden="true"><use href="#chart"></use></svg></span>
|
||||
<span class="kt-ai-chip">Шаг 2</span>
|
||||
</div>
|
||||
<h3>Проверь баллы</h3>
|
||||
<p>Смотри проходные баллы 2025 года, а не «примерно сто с хвостиком». Здесь всё по цифрам.</p>
|
||||
<div class="kt-ai-prog"><div class="kt-ai-prog-in" id="prog-avg" style="width:75%"></div></div>
|
||||
<div class="kt-ai-prog-l"><span>средний балл по таблице</span><span id="prog-avg-num">75</span></div>
|
||||
</article>
|
||||
<article class="kt-ai-tile">
|
||||
<div class="kt-ai-tile-top">
|
||||
<span class="kt-ai-tile-icon"><svg class="kt-icon" aria-hidden="true"><use href="#building"></use></svg></span>
|
||||
<span class="kt-ai-chip">Шаг 3</span>
|
||||
</div>
|
||||
<h3>Выбери город и формат</h3>
|
||||
<p>Столица, родной город, бюджет или платно, стипендия, общежитие — всё в одном месте.</p>
|
||||
</article>
|
||||
<article class="kt-ai-tile">
|
||||
<div class="kt-ai-tile-top">
|
||||
<span class="kt-ai-tile-icon"><svg class="kt-icon" aria-hidden="true"><use href="#checklist"></use></svg></span>
|
||||
<span class="kt-ai-chip">Шаг 4</span>
|
||||
</div>
|
||||
<h3>Собери документы</h3>
|
||||
<p>Чек-лист: результат экзамена, направления, приоритеты. Без дедлайна в последний день.</p>
|
||||
</article>
|
||||
<h1 class="kt-ai-invite-names">
|
||||
<span class="kt-ai-hero-name">Айсулу</span>
|
||||
<span class="kt-ai-invite-am"> и </span>
|
||||
<span class="kt-ai-hero-name">Ержан</span>
|
||||
</h1>
|
||||
<p class="kt-ai-hero-sub">просят вас разделить с ними этот день — банкет начнётся в 16:00.</p>
|
||||
<div class="kt-ai-countdown" id="countdown"></div>
|
||||
<div class="kt-ai-hero-actions" style="margin-top:var(--kt-ai-space-8)">
|
||||
<a class="kt-ai-btn" data-variant="primary" data-size="lg" href="#rsvp">Подтвердить визит</a>
|
||||
<a class="kt-ai-btn" data-variant="ghost" data-size="lg" href="#details">Подробнее</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="universities" class="kt-ai-section" data-divider="true">
|
||||
<section id="details" class="kt-ai-section" data-divider="true">
|
||||
<div class="kt-ai-wrap">
|
||||
<div class="kt-ai-section-head">
|
||||
<h2>Вузы и проходные баллы</h2>
|
||||
<p>Примерные цифры 2025 года: минимальный проходной балл, бюджетные места и стипендия.</p>
|
||||
</div>
|
||||
<div class="kt-ai-scroll" style="margin-top:var(--kt-ai-space-7)">
|
||||
<table class="kt-ai-table" id="uni-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Вуз</th>
|
||||
<th>Специальность</th>
|
||||
<th>Город</th>
|
||||
<th class="num">Мин. балл</th>
|
||||
<th class="num">Бюджет</th>
|
||||
<th class="num">Стипендия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="uni-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="kt-ai-note" id="uni-note">Показаны все варианты.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="kt-ai-section" data-divider="true">
|
||||
<div class="kt-ai-wrap">
|
||||
<div class="kt-ai-section-head">
|
||||
<h2>Что дальше</h2>
|
||||
<p>Короткий маршрут на лето — что и когда сделать.</p>
|
||||
<h2>Где и когда</h2>
|
||||
<p>Всё в одном месте — адрес, время и что взять с собой.</p>
|
||||
</div>
|
||||
<div class="kt-ai-grid" data-cols="3" style="margin-top:var(--kt-ai-space-9)">
|
||||
<article class="kt-ai-tile">
|
||||
<h3>Май — выбор</h3>
|
||||
<p>Три варианта по приоритету. Не «все 10 вузов сразу», а список, с которым спокойно.</p>
|
||||
<div class="kt-ai-detail-icon kt-ai-tile-icon"><svg class="kt-icon" aria-hidden="true"><use href="#pin"></use></svg></div>
|
||||
<h3>Ресторан «Хан Шатır»</h3>
|
||||
<p>Укажите полный адрес и карту, как только уточните — например, «г. Алматы, ул. ТОО «Хан», 5», с парковкой у входа.</p>
|
||||
</article>
|
||||
<article class="kt-ai-tile">
|
||||
<h3>Июнь — документы</h3>
|
||||
<p>Подача в приёмную комиссию, подтверждение документов и направление приоритетов.</p>
|
||||
<div class="kt-ai-detail-icon kt-ai-tile-icon"><svg class="kt-icon" aria-hidden="true"><use href="#clock"></use></svg></div>
|
||||
<h3>16:00 — банкет</h3>
|
||||
<p>Прошу не опаздывать: торжественная часть проходит до 18:00, банкет до 23:00.</p>
|
||||
</article>
|
||||
<article class="kt-ai-tile">
|
||||
<h3>Июль — ответ</h3>
|
||||
<p>Дедлайн подачи заявления на зачисление обычно в конце июля. Проверь дважды дату.</p>
|
||||
<div class="kt-ai-detail-icon kt-ai-tile-icon"><svg style="width:18px;height:18px"><svg class="kt-icon" aria-hidden="true"><use href="#users"></use></svg></div>
|
||||
<h3>Дресс-код</h3>
|
||||
<p>Смарт-кэжуал / вечерний. Приходите в удобной обуви — будет танцевать.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="kt-ai-section" data-divider="true">
|
||||
<div class="kt-ai-wrap">
|
||||
<section id="rsvp" class="kt-ai-section" data-divider="true">
|
||||
<div class="kt-ai-wrap" data-width="narrow">
|
||||
<div class="kt-ai-section-head">
|
||||
<h2>Частые вопросы</h2>
|
||||
<h2>Подтвердите визит</h2>
|
||||
<p>До 25 октября, пожалуйста — ресторану нужно знать точное количество гостей.</p>
|
||||
</div>
|
||||
<div style="margin-top:var(--kt-ai-space-7)">
|
||||
<div class="kt-ai-faq">
|
||||
<h3>Можно ли поступить по 2–3 специальностям?</h3>
|
||||
<p>Да, направление можно заявить в нескольких вузах одновременно. Приоритет отдаётся первому варианту, по которому пойдёшь на зачисление.</p>
|
||||
</div>
|
||||
<div class="kt-ai-faq">
|
||||
<h3>Балл ниже проходного — ничего?</h3>
|
||||
<p>Посмотри платное направление, смежные специальности и вузы из других городов: там проходной часто ниже на 10–20 баллов.</p>
|
||||
</div>
|
||||
<div class="kt-ai-faq">
|
||||
<h3>Сколько будет стипендия?</h3>
|
||||
<p>Социальная стипендия плюс академическая. Размер зависит от вуза и курса — в таблице указана базовая цифра на начало обучения.</p>
|
||||
</div>
|
||||
<div class="kt-ai-faq">
|
||||
<h3>А если не попаду никуда?</h3>
|
||||
<p>Останутся допнабор и платные направления. Это нормальная часть сезона, а не конец истории.</p>
|
||||
</div>
|
||||
<div class="kt-ai-card kt-ai-form-card" style="margin-top:var(--kt-ai-space-7)">
|
||||
<form id="rsvp-form">
|
||||
<div class="kt-ai-field">
|
||||
<label for="r-name">Имя (и фамилия, если удобно)</label>
|
||||
<input class="kt-ai-input" id="r-name" name="name" type="text" required placeholder="Айгерим Сериккызы">
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label for="r-guests">Вы и ещё сколько гостей?</label>
|
||||
<select class="kt-ai-select" id="r-guests" name="guests">
|
||||
<option value="1">1 — я</option>
|
||||
<option value="2">Мы вдвоём (2)</option>
|
||||
<option value="3">Мы и ещё кто-то (3)</option>
|
||||
<option value="4">4 человека</option>
|
||||
<option value="5">5 человек</option>
|
||||
<option value="6">6 человек</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label for="r-attendance">Сможете ли вы прийти?</label>
|
||||
<select class="kt-ai-select" id="r-attendance" name="attendance">
|
||||
<option value="yes">Да, с радостью приду</option>
|
||||
<option value="no">К сожалению, не смогу</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label for="r-note">Комментарий (необязательно)</label>
|
||||
<textarea class="kt-ai-textarea" id="r-note" name="note" rows="2" placeholder="Аллергии, пожелания по столу, любимая песня на танцах…"></textarea>
|
||||
</div>
|
||||
<button class="kt-ai-btn" data-variant="primary" data-size="lg" type="submit">Отправить</button>
|
||||
<p class="kt-ai-form-msg" id="rsvp-msg"></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contacts" class="kt-ai-cta">
|
||||
<section class="kt-ai-cta">
|
||||
<div class="kt-ai-wrap">
|
||||
<h2>Остался вопрос по конкретной ситуации?</h2>
|
||||
<p>Напиши — разберём твой балл, город и интересы, и подскажем 2–3 реальных варианта.</p>
|
||||
<a class="kt-ai-btn" data-variant="primary" data-size="lg" href="https://telegram.me">Написать в Telegram</a>
|
||||
<h2>Ждём вас 1 ноября!</h2>
|
||||
<p>Если появятся вопросы — напишите: +7 700 000 00 00 (организатор). До встречи!</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||||
<span>© 2026 Где поступать</span>
|
||||
<span>1 ноября 2026 · ресторан «Хан Шатır»</span>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<span>Данные примерные — сверься с официальным сайтом вуза</span>
|
||||
<span>С любовью</span>
|
||||
</footer>
|
||||
|
||||
<script src="script.js"></script>
|
||||
|
||||
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "wedding-invite",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": { "start": "node server.js" }
|
||||
}
|
||||
100
script.js
100
script.js
@ -1,61 +1,49 @@
|
||||
// Данные примерные: их легко заменить на реальные (массив UNIVERSITIES).
|
||||
const UNIVERSITIES = [
|
||||
{ name: "КазНУ им. аль-Фараби", specialty: "Прикладная математика", city: "Алматы", min: 162, budget: 85, scholarship: 14500 },
|
||||
{ name: "КазНУ им. аль-Фараби", specialty: "Высшая математика в финансах", city: "Алматы", min: 159, budget: 40, scholarship: 14500 },
|
||||
{ name: "КазНУ им. аль-Фараби", specialty: "Математика и анализ данных", city: "Алматы", min: 156, budget: 55, scholarship: 14000 },
|
||||
{ name: "Eurasian National University", specialty: "Высшая математика", city: "Астана", min: 152, budget: 35, scholarship: 13500 },
|
||||
{ name: "Kazakh-British Technical University", specialty: "Высшая математика и моделирование", city: "Астана", min: 155, budget: 30, scholarship: 13500 },
|
||||
{ name: "КазНТУ им. Т. Рысқұлова", specialty: "Высшая математика", city: "Алматы", min: 148, budget: 68, scholarship: 12500 },
|
||||
{ name: "АУ «Сарыарка»", specialty: "Высшая математика", city: "Караганда", min: 138, budget: 25, scholarship: 10000 },
|
||||
{ name: "ЮЗГУ", specialty: "Математика и статистика", city: "Костанай", min: 132, budget: 30, scholarship: 9500 },
|
||||
];
|
||||
// Пригласительный: обратный отсчёт + форма RSVP (отправляет в /api/rsvp)
|
||||
const WEDDING_DATE = new Date("2026-11-01T16:00:00");
|
||||
|
||||
const body = document.getElementById("uni-body");
|
||||
const note = document.getElementById("uni-note");
|
||||
|
||||
function renderTable(rows) {
|
||||
if (!body) return;
|
||||
body.innerHTML = "";
|
||||
for (const r of rows) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML =
|
||||
'<td>' + r.name + "</td>" +
|
||||
'<td>' + r.specialty + "</td>" +
|
||||
'<td>' + r.city + "</td>" +
|
||||
'<td class="num">' + r.min + "</td>" +
|
||||
'<td class="num">' + r.budget + "</td>" +
|
||||
'<td class="num">' + r.scholarship.toLocaleString("ru-RU") + " тг</td>";
|
||||
body.appendChild(tr);
|
||||
const cd = document.getElementById("countdown");
|
||||
if (cd) {
|
||||
function tick() {
|
||||
const diff = WEDDING_DATE - new Date();
|
||||
if (diff <= 0) {
|
||||
cd.innerHTML = '<span class="kt-ai-chip">Сегодня наш день!</span>';
|
||||
return;
|
||||
}
|
||||
const d = Math.floor(diff / 86400000);
|
||||
const h = Math.floor(diff / 3600000) % 24;
|
||||
const m = Math.floor(diff / 60000) % 60;
|
||||
const s = Math.floor(diff / 1000) % 60;
|
||||
cd.innerHTML =
|
||||
cell(d, "дней") + cell(h, "часов") + cell(m, "минут") + cell(s, "секунд");
|
||||
}
|
||||
note.textContent = rows.length
|
||||
? "Показано вариантов: " + rows.length + "."
|
||||
: "Ничего не нашлось — попробуй другой запрос или сбрось балл.";
|
||||
function cell(n, lab) {
|
||||
return '<div class="kt-ai-cd-cell"><div class="kt-ai-cd-num">' + n + "</div><div class=\"kt-ai-cd-lab\">" + lab + "</div></div>";
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
}
|
||||
|
||||
function fmt(v) {
|
||||
return v == null ? "" : String(v).trim().toLowerCase();
|
||||
}
|
||||
|
||||
document.getElementById("btn-search")?.addEventListener("click", function () {
|
||||
const q = fmt(document.getElementById("f-specialty").value);
|
||||
const score = parseInt(document.getElementById("f-score").value, 10);
|
||||
let rows = UNIVERSITIES.filter(function (u) {
|
||||
const okQ = !q || u.specialty.toLowerCase().includes(q) || u.name.toLowerCase().includes(q);
|
||||
const okScore = isNaN(score) || u.min <= score;
|
||||
return okQ && okScore;
|
||||
const form = document.getElementById("rsvp-form");
|
||||
const msg = document.getElementById("rsvp-msg");
|
||||
if (form) {
|
||||
form.addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
msg.textContent = "Отправляю…";
|
||||
try {
|
||||
const body = Object.fromEntries(new FormData(form).entries());
|
||||
const res = await fetch("api/rsvp", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) throw new Error("server " + res.status);
|
||||
const attendance = form.attendance.value;
|
||||
msg.textContent = attendance === "yes"
|
||||
? "Спасибо! До встречи 1 ноября."
|
||||
: "Спасибо, что сообщили. Мы вас поздравим на расстоянии.";
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
msg.textContent = "Не получилось отправить. Попробуйте ещё раз или напишите организатору напрямую.";
|
||||
}
|
||||
});
|
||||
if (!isNaN(score)) rows = rows.slice().sort(function (a, b) { return a.min - b.min; });
|
||||
renderTable(rows);
|
||||
document.getElementById("search-note").hidden = true;
|
||||
document.getElementById("universities").scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
|
||||
(function () {
|
||||
const avg = Math.round(UNIVERSITIES.reduce(function (s, u) { return s + u.min; }, 0) / UNIVERSITIES.length);
|
||||
const el = document.getElementById("prog-avg");
|
||||
const num = document.getElementById("prog-avg-num");
|
||||
if (el) el.style.width = Math.min(100, Math.round(avg / 2)) + "%";
|
||||
if (num) num.textContent = avg;
|
||||
})();
|
||||
|
||||
renderTable(UNIVERSITIES);
|
||||
}
|
||||
|
||||
51
server.js
Normal file
51
server.js
Normal file
@ -0,0 +1,51 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA = path.join(__dirname, "data.json");
|
||||
|
||||
function readAll() {
|
||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { rsvps: [] }; }
|
||||
}
|
||||
function writeAll(d) {
|
||||
fs.writeFileSync(DATA, JSON.stringify(d, null, 2));
|
||||
}
|
||||
function body(req) {
|
||||
return new Promise((resolve) => {
|
||||
let s = "";
|
||||
req.on("data", (c) => (s += c));
|
||||
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
|
||||
});
|
||||
}
|
||||
|
||||
http.createServer(async (req, res) => {
|
||||
if (req.method === "POST" && req.url.split("?")[0] === "/api/rsvp") {
|
||||
const item = await body(req);
|
||||
const d = readAll();
|
||||
d.rsvps.push({
|
||||
id: Date.now(),
|
||||
name: String(item.name || "").slice(0, 80),
|
||||
guests: parseInt(item.guests, 10) || 1,
|
||||
attendance: item.attendance === "no" ? "no" : "yes",
|
||||
note: String(item.note || "").slice(0, 300),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
writeAll(d);
|
||||
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
||||
return res.end(JSON.stringify({ ok: true }));
|
||||
}
|
||||
if (req.method === "GET" && req.url.split("?")[0] === "/api/rsvp") {
|
||||
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
||||
return res.end(JSON.stringify(readAll().rsvps));
|
||||
}
|
||||
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
||||
const full = path.join(__dirname, file);
|
||||
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
||||
const ext = path.extname(full);
|
||||
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : ext === ".svg" ? "image/svg+xml" : "text/html";
|
||||
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
||||
return res.end(fs.readFileSync(full));
|
||||
}
|
||||
res.writeHead(404); res.end("Not found");
|
||||
}).listen(PORT, () => console.log("Server on port " + PORT));
|
||||
Loading…
Reference in New Issue
Block a user