From a94526f5c04b7fada924ad302f2e605d30585b82 Mon Sep 17 00:00:00 2001 From: ashirova_gulnur Date: Fri, 26 Jun 2026 05:49:02 +0000 Subject: [PATCH] =?UTF-8?q?v1:=20=D1=81=D0=B5=D0=BC=D0=B5=D0=B9=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=82=D0=B0=D1=81=D0=BA-=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=B4=D0=B6=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 173 ++++++++++++++++++ app.js | 520 +++++++++++++++++++++++++++++++++++++++++++++++++++++ design.md | 110 ++++++++++++ index.html | 204 +++++++++++++++++++++ style.css | 57 ++++++ 5 files changed, 1064 insertions(+) create mode 100644 AGENTS.md create mode 100644 app.js create mode 100644 design.md create mode 100644 index.html create mode 100644 style.css diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c602b1d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,173 @@ + +# Vibe42 — учебная песочница для лендингов + +Workspace юзера `ashirova_gulnur`. Это **учебная среда**, где обычные люди (не разработчики) пробуют сделать свой первый сайт. + +--- + +## 🎯 ТВОЯ РОЛЬ + +Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с: +1. **рабочим лендингом**, опубликованным по адресу `https://pages.git.vibe42.kz/ashirova_gulnur//`, +2. ощущением «это было легко» — без серверов, БД, токенов, конфигов. + +Юзер не разработчик. Ему важен **результат, который видно в браузере**, а не код. + +--- + +## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет) + +1. Поздоровайся коротко: «Привет! Тут за 10 минут собираем лендинг и публикуем его в интернете. О чём хочешь сделать?» +2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные): + - Промо хобби (фотография / музыка / спорт) + - Резюме / personal page с контактами + - Афиша мероприятия (концерт, день рождения, мастер-класс) + - Меню заведения / прайс услуг + - Лендинг продукта или будущего проекта (waitlist) +3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы). +4. Сразу делай `./new-project ` и собирай страницу. Не спрашивай разрешения на каждый шаг. + +--- + +## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО + +Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**. + +Скажи: «Давай определимся, я задам 3 коротких вопроса: +1. Это для тебя лично, для проекта/бизнеса, или для события? +2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио? +3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?» + +После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать. + +--- + +## 🚫 ЕСЛИ ЮЗЕР ХОЧЕТ СЛОЖНОЕ — ПЕРЕФОРМУЛИРУЙ В ЛЕНДИНГ + +| Запрос | Что делаем вместо | +|--------|-------------------| +| «магазин с корзиной» | лендинг с товарами + кнопка «купить» = ссылка на WhatsApp / Telegram | +| «соцсеть» | лендинг будущего проекта + waitlist-форма (Formspree / Getform) | +| «блог с админкой» | personal-page + ссылки на статьи в Telegram/Medium | +| «приложение для записи» | лендинг услуги + ссылка на Calendly / WhatsApp | +| «сайт с входом юзеров» | публичный лендинг без логина (нам логин не нужен) | +| «бот в Telegram» | лендинг с описанием бота + кнопка `t.me/...` | + +**Не говори «это невозможно».** Скажи: «У нас песочница только для статических сайтов. Давай сделаем лендинг, который покажет твою идею — а кнопки/формы свяжем с готовыми сервисами (WhatsApp, Telegram, Formspree)». Юзер счастлив, результат за 15 минут. + +--- + +## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера) + +### A — Промо продукта/услуги +**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер). + +### B — Personal / резюме +**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки). + +### C — Афиша мероприятия +**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт). + +### D — Меню / прайс +**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка). + +### E — Waitlist для будущего проекта +**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма). + +Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического. + +--- + +## ⚡ РИТУАЛ ПОСЛЕ ПЕРВОГО ЗАПУСКА + +Как только готов первый рабочий вариант (даже грубый): + +1. **Сразу запушь:** + ```bash + git add -A + git commit -m "v1" + git push origin HEAD:pages + ``` +2. **ОБЯЗАТЕЛЬНО** дай юзеру ссылку **жирно**: + > 🎉 Готово! Твой лендинг здесь: **https://pages.git.vibe42.kz/ashirova_gulnur//** +3. Скажи: «Открой в новой вкладке, посмотри. Что хочешь поменять?» +4. Дальше короткие итерации: правка → push → новый URL-показ. Каждые 2-3 правки — push. + +--- + +## ⚠️ ЖЕЛЕЗНЫЕ ПРАВИЛА (НЕ нарушать никогда) + +1. **Только статика — HTML + CSS + JS в браузере.** +2. **Никакого бэкенда.** Никаких Node/Express/FastAPI/Django/PHP/Go-серверов. Никаких БД. Никакого Redis. +3. **Никакой аутентификации / OAuth / JWT.** +4. **Никакого Docker, nginx, sudo, системных настроек.** +5. **Никаких тяжёлых сборщиков** (`npm install` дерево на 500МБ). Tailwind — только через CDN. +6. **НИКОГДА `git init` в workspace root (`/workspaces/ashirova_gulnur`)** — это папка-контейнер юзера, не репозиторий. + +--- + +## ✅ ВСЕГДА работай через `./new-project` + +Если юзер сказал «сделай сайт NAME» / «создай проект NAME»: + +```bash +cd /workspaces/ashirova_gulnur +./new-project NAME # создаёт repo в Gitea + клонит локально в ./NAME/ +cd NAME +# теперь создавай index.html / style.css / script.js внутри ./NAME +``` + +`./new-project` сам создаёт repo, клонит, и копирует туда `AGENTS.md` + `design.md`. + +--- + +## 🌐 Git и публикация + +**НЕТ GitHub.** Self-hosted git: **https://git.vibe42.kz** + +- Профиль юзера: https://git.vibe42.kz/ashirova_gulnur +- Pages (живые лендинги): https://pages.git.vibe42.kz/ashirova_gulnur// +- Креды уже в `/workspaces/ashirova_gulnur/.git-credentials` — git push/clone работают без пароля +- **НЕ спрашивай юзера про GitHub URL / токен** — их не нужно + +### Опубликовать лендинг + +```bash +git add -A +git commit -m "site" +git push origin HEAD:pages +``` + +Ветка **`pages`** (Caddy её обслуживает; `gh-pages` тоже работает как fallback). Push → лендинг доступен мгновенно. + +--- + +## 🔧 Когда что-то идёт не так + +- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f` +- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет +- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages` +- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html + +--- + +## ❌ Чего НЕ делать НИКОГДА + +- ❌ `git init` в workspace root +- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt) +- ❌ Создавать `server.js` / `app.py` / `main.go` как backend +- ❌ Использовать `gh` CLI или GitHub API +- ❌ Вызывать Gitea Pages-API (его нет) +- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages» +- ❌ Просить юзера ввести токен/URL/пароль — всё уже настроено +- ❌ Задавать юзеру 10 вопросов подряд (максимум 2-3 за раз) +- ❌ Показывать юзеру голый код больше 1 раза — ему важен результат, а не как написано +- ❌ Предлагать «давай сначала дизайн в Figma» — мы делаем сразу в HTML +- ❌ Говорить «это сложно» — переформулируй в простое +- ❌ Зависать в обсуждениях — сделай первый вариант грубо, потом итерируй + +--- + +## 🎨 design.md + +Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие. diff --git a/app.js b/app.js new file mode 100644 index 0000000..f1a7cd9 --- /dev/null +++ b/app.js @@ -0,0 +1,520 @@ +/* ===== Семейный таск-менеджер (чистая статика, localStorage) ===== */ + +const KEY = 'family_tasks_v1'; +const SESSION = 'family_tasks_user'; +const AVATAR_COLORS = ['#ec4899','#3b82f6','#10b981','#f59e0b','#8b5cf6','#ef4444','#06b6d4','#84cc16','#f97316','#6366f1']; +const AVATAR_EMOJIS = ['👩','👨','👧','👦','🧑','👵','👴','🧒','👶','🧔','👩‍🦰','👨‍🦱']; + +let data = null; // {familyName, members:[], tasks:[]} +let me = null; // текущий пользователь (member id) +let view = 'mine'; +let editingId = null; // id задачи в модалке (null = создание) +let postponeId = null; // id задачи для переноса + +/* ---------- storage ---------- */ +function load(){ try{ return JSON.parse(localStorage.getItem(KEY)); }catch(e){ return null; } } +function save(){ localStorage.setItem(KEY, JSON.stringify(data)); } +function uid(){ return 'id'+Date.now().toString(36)+Math.random().toString(36).slice(2,6); } + +/* ---------- helpers ---------- */ +const $ = s => document.querySelector(s); +const $$ = s => [...document.querySelectorAll(s)]; +function member(id){ return (data.members||[]).find(m=>m.id===id) || null; } +function escapeHtml(s){ return (s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } +function todayStr(){ const d=new Date(); return fmt(d); } +function fmt(d){ return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0'); } +function parseDate(s){ if(!s) return null; const [y,m,d]=s.split('-').map(Number); return new Date(y,m-1,d); } +function prettyDate(s){ const d=parseDate(s); if(!d) return '—'; return d.toLocaleDateString('ru-RU',{day:'numeric',month:'short}); } +function prettyDateTime(s,t){ let r=prettyDate(s); if(t) r+=' · '+t; return r; } +function relativeDue(s){ if(!s) return ''; const d=parseDate(s); const now=new Date(); now.setHours(0,0,0,0); const diff=Math.round((d-now)/86400000); if(diff<0) return 'просрочено'; if(diff===0) return 'сегодня'; if(diff===1) return 'завтра'; if(diff<7) return `через ${diff} дн.`; return prettyDate(s); } +function initials(name){ return (name||'?').trim().slice(0,1).toUpperCase(); } +function colorFor(id){ const m=member(id); return m?m.color:'#94a3b8'; } + +function avatarHtml(m, size){ + if(!m) return `?`; + const content = m.emoji || initials(m.name); + return `${content}`; +} + +function toast(msg){ const t=$('#toast'); t.textContent=msg; t.classList.remove('hidden'); clearTimeout(t._t); t._t=setTimeout(()=>t.classList.add('hidden'),2200); } + +const STATUS = { + todo: {label:'В работе', cls:'bg-amber-100 text-amber-700'}, + done: {label:'Готово', cls:'bg-emerald-100 text-emerald-700'}, + postponed: {label:'Перенесено',cls:'bg-sky-100 text-sky-700'}, + cancelled: {label:'Отменено', cls:'bg-gray-200 text-gray-600'}, +}; +const PRIORITY = { + low: {label:'🟢 Низкая', cls:'bg-gray-100 text-gray-600'}, + medium: {label:'🟡 Средняя', cls:'bg-amber-50 text-amber-600'}, + high: {label:'🔴 Высокая', cls:'bg-red-50 text-red-600'}, +}; + +/* ============================================================ + ИНИЦИАЛИЗАЦИЯ + ============================================================ */ +function init(){ + data = load(); + if(!data || !data.members || !data.members.length){ showOnboard(); return; } + me = localStorage.getItem(SESSION); + if(!me || !member(me)) me = data.members[0].id; + enterApp(); +} + +/* ---------- онбординг ---------- */ +function showOnboard(){ + $('#onboard').classList.remove('hidden'); + $('#app').classList.add('hidden'); + const ob = $('#ob-members'); + ob.innerHTML=''; + addObMember(); addObMember(); +} +function addObMember(){ + const idx = $('#ob-members').children.length; + const color = AVATAR_COLORS[idx % AVATAR_COLORS.length]; + const emoji = AVATAR_EMOJIS[idx % AVATAR_EMOJIS.length]; + const row = document.createElement('div'); + row.className='flex items-center gap-2'; + row.innerHTML = ` + ${emoji} + + `; + row.querySelector('.ob-del').onclick=()=>{ if($('#ob-members').children.length>1) row.remove(); }; + ob.appendChild(row); +} + +$('#ob-add-member').onclick = addObMember; +$('#ob-create').onclick = ()=>{ + const familyName = $('#ob-family').value.trim() || 'Наша семья'; + const names = $$('#ob-members .ob-name').map(i=>i.value.trim()).filter(Boolean); + if(names.length<1){ toast('Добавьте хотя бы одного члена семьи'); return; } + const members = names.map((n,i)=>({ id:uid(), name:n, color:AVATAR_COLORS[i%AVATAR_COLORS.length], emoji:AVATAR_EMOJIS[i%AVATAR_EMOJIS.length] })); + data = { familyName, members, tasks:[] }; + me = members[0].id; + localStorage.setItem(SESSION, me); + save(); + $('#onboard').classList.add('hidden'); + enterApp(); + toast('Семья создана! 🎉'); +}; + +/* ============================================================ + ВХОД В ПРИЛОЖЕНИЕ + ============================================================ */ +function enterApp(){ + $('#onboard').classList.add('hidden'); + $('#app').classList.remove('hidden'); + $('#hd-family').textContent = data.familyName; + renderUserMenu(); + setCurrentUser(me); + setView('mine'); +} + +/* ---------- переключение пользователя («вход») ---------- */ +function renderUserMenu(){ + const list = $('#hd-user-list'); + list.innerHTML = data.members.map(m=>` + `).join(''); + list.querySelectorAll('.menu-row').forEach(r=>r.onclick=()=>{ + setCurrentUser(r.dataset.uid); + $('#hd-menu').classList.add('hidden'); + setView(view); + }); +} +function setCurrentUser(id){ + me = id; + localStorage.setItem(SESSION, id); + const m = member(id); + $('#hd-avatar').outerHTML = `${m.emoji||initials(m.name)}`; + $('#hd-avatar').style.background = m.color; + $('#hd-name').textContent = m.name; + $('#f-from').textContent = m.name; +} + +/* выпадающее меню пользователя */ +$('#hd-user').onclick = e=>{ e.stopPropagation(); $('#hd-menu').classList.toggle('hidden'); }; +document.addEventListener('click', e=>{ if(!e.target.closest('#hd-user') && !e.target.closest('#hd-menu')) $('#hd-menu').classList.add('hidden'); }); +$('#hd-manage').onclick = ()=>{ $('#hd-menu').classList.add('hidden'); openFamilyModal(); }; + +/* ---------- навигация ---------- */ +$$('.navtab').forEach(b=>b.onclick=()=>setView(b.dataset.view)); +function setView(v){ + view = v; + $$('.navtab').forEach(b=>b.classList.toggle('active', b.dataset.view===v)); + render(); +} + +/* ---------- кнопка новая задача ---------- */ +$('#hd-new').onclick = $('#hd-new-m').onclick = ()=>openTaskModal(null); + +/* ============================================================ + ГЛАВНЫЙ РЕНДЕР + ============================================================ */ +function render(){ + $('#hd-family').textContent = data.familyName; + if(view==='mine') return renderMine(); + if(view==='assigned') return renderAssigned(); + if(view==='all') return renderAll(); + if(view==='schedule') return renderSchedule(); + if(view==='family') return renderFamily(); +} + +function taskRow(t){ + const a = member(t.assigneeId); + const from = member(t.assignerId); + const overdue = t.status==='todo' && t.dueDate && parseDate(t.dueDate) < new Date(todayStr()); + const st = STATUS[t.status] || STATUS.todo; + return ` +
+
+ +
+
+
${escapeHtml(t.title)}
+ ${st.label} +
+ ${t.note?`
${escapeHtml(t.note)}
`:''} +
+ ${avatarHtml(a,16)} ${escapeHtml(a?a.name:'—')} + ${from?`от ${escapeHtml(from.name)}`:''} + ${t.dueDate?`⏰ ${prettyDateTime(t.dueDate,t.time)} · ${relativeDue(t.dueDate)}`:''} + ${PRIORITY[t.priority].label} +
+
+
+
+ ${t.status!=='done'?``:``} + ${t.status!=='cancelled' && t.status!=='done'?``:''} + ${t.status!=='cancelled' && t.status!=='done'?``:''} + +
+
`; +} + +function bindTaskRow(el){ + const id = el.dataset.id; + const t = data.tasks.find(x=>x.id===id); if(!t) return; + const on = (sel, fn)=>el.querySelectorAll(sel).forEach(b=>{ if(!b.disabled) b.addEventListener('click', fn); }); + on('.t-done', ()=>setStatus(id,'done')); + on('.t-reopen', ()=>setStatus(id,'todo')); + on('.t-postpone', ()=>openPostpone(id)); + on('.t-cancel', ()=>setStatus(id,'cancelled')); + on('.t-edit', ()=>openTaskModal(id)); +} + +/* ---------- Мои задачи ---------- */ +function renderMine(){ + const me2 = member(me); + const tasks = data.tasks.filter(t=>t.assigneeId===me && (t.status==='todo'||t.status==='postponed')) + .sort((a,b)=> (a.dueDate||'9999').localeCompare(b.dueDate||'9999')); + const doneCount = data.tasks.filter(t=>t.assigneeId===me && t.status==='done').length; + const main = $('#main'); + main.innerHTML = ` +
+
+

Мои задачи

+

${avatarHtml(me2,20)} ${escapeHtml(me2.name)} · выполнено за всё время: ${doneCount}

+
+
+
`; + const list = $('#list'); + if(!tasks.length){ list.innerHTML = emptyHtml('🎉','Активных задач нет. Можно отдохнуть!'); return; } + list.innerHTML = tasks.map(taskRow).join(''); + list.querySelectorAll('.task').forEach(bindTaskRow); +} + +/* ---------- Я поручил ---------- */ +function renderAssigned(){ + const tasks = data.tasks.filter(t=>t.assignerId===me) + .sort((a,b)=> (a.dueDate||'9999').localeCompare(b.dueDate||'9999')); + const active = tasks.filter(t=>t.status==='todo'||t.status==='postponed').length; + $('#main').innerHTML = ` +

Я поручил

+

Задачи, которые вы поставили другим. Активных: ${active}

+
`; + const list = $('#list'); + if(!tasks.length){ list.innerHTML = emptyHtml('📤','Вы ещё никому не поручали задач.','+ Новая задача','hd-new'); return; } + list.innerHTML = tasks.map(taskRow).join(''); + list.querySelectorAll('.task').forEach(bindTaskRow); +} + +/* ---------- Все задачи ---------- */ +function renderAll(){ + const statuses = ['todo','postponed','done','cancelled']; + $('#main').innerHTML = ` +

Все задачи семьи

+

Всего: ${data.tasks.length}

+
+ + +
+
`; + const apply = ()=>{ + const fm = $('#flt-member').value, fs = $('#flt-status').value; + let tasks = data.tasks.slice().sort((a,b)=>(b.createdAt||0)-(a.createdAt||0)); + if(fm) tasks = tasks.filter(t=>t.assigneeId===fm||t.assignerId===fm); + if(fs) tasks = tasks.filter(t=>t.status===fs); + const list = $('#list'); + if(!tasks.length){ list.innerHTML = emptyHtml('🗂','Задач не найдено по фильтру.'); return; } + list.innerHTML = tasks.map(taskRow).join(''); + list.querySelectorAll('.task').forEach(bindTaskRow); + }; + $('#flt-member').onchange = apply; $('#flt-status').onchange = apply; + apply(); +} + +/* ---------- Расписание (календарь) ---------- */ +let calRef = new Date(); +function renderSchedule(){ + const y = calRef.getFullYear(), m = calRef.getMonth(); + const first = new Date(y,m,1); + const startDow = (first.getDay()+6)%7; // понедельник = 0 + const daysInMonth = new Date(y,m+1,0).getDate(); + const monthName = first.toLocaleDateString('ru-RU',{month:'long', year:'numeric'}); + const tasksByDate = {}; + data.tasks.filter(t=>t.dueDate && t.status!=='cancelled').forEach(t=>{ + (tasksByDate[t.dueDate] = tasksByDate[t.dueDate]||[]).push(t); + }); + let cells = ''; + const dows = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс']; + cells += dows.map(d=>`
${d}
`).join(''); + for(let i=0;i`; + const today = todayStr(); + for(let d=1; d<=daysInMonth; d++){ + const ds = fmt(new Date(y,m,d)); + const tlist = tasksByDate[ds]||[]; + cells += `
+
${d}
+ ${tlist.slice(0,3).map(t=>`
${escapeHtml(t.title)}
`).join('')} + ${tlist.length>3?`
+${tlist.length-3} ещё
`:''} +
`; + } + $('#main').innerHTML = ` +
+

${monthName}

+
+ + + +
+
+
${cells}
+
+ ${data.members.map(m=>`${avatarHtml(m,14)} ${escapeHtml(m.name)}`).join('')} +
`; + $('#cal-prev').onclick=()=>{ calRef=new Date(y,m-1,1); renderSchedule(); }; + $('#cal-next').onclick=()=>{ calRef=new Date(y,m+1,1); renderSchedule(); }; + $('#cal-today').onclick=()=>{ calRef=new Date(); renderSchedule(); }; + $$('#main .cal-task').forEach(el=>el.onclick=()=>openTaskModal(el.dataset.id)); +} + +/* ---------- Семья ---------- */ +function renderFamily(){ + const cards = data.members.map(m=>{ + const assigned = data.tasks.filter(t=>t.assigneeId===m.id && (t.status==='todo'||t.status==='postponed')).length; + const done = data.tasks.filter(t=>t.assigneeId===m.id && t.status==='done').length; + const gave = data.tasks.filter(t=>t.assignerId===m.id).length; + const overdue = data.tasks.filter(t=>t.assigneeId===m.id && t.status==='todo' && t.dueDate && parseDate(t.dueDate) + ${avatarHtml(m,52)} +
+
${escapeHtml(m.name)}
+
поручил другим: ${gave}
+
+
+
${assigned}
активных
+
${done}
готово
+
${overdue}
просроч.
+
+ `; + }).join(''); + $('#main').innerHTML = ` +
+
+

${escapeHtml(data.familyName)}

+

Членов: ${data.members.length} · задач всего: ${data.tasks.length}

+
+ +
+
${cards}
`; + $('#fam-manage').onclick = openFamilyModal; +} + +function emptyHtml(emoji, text, btnLabel, btnId){ + return `
${emoji}

${text}

${btnLabel?``:''}
`; +} + +/* ============================================================ + ДЕЙСТВИЯ С ЗАДАЧАМИ + ============================================================ */ +function setStatus(id, status){ + const t = data.tasks.find(x=>x.id===id); if(!t) return; + const prev = t.status; + t.status = status; + t.updatedAt = Date.now(); + if(status==='done') t.completedAt = Date.now(); + pushHistory(t, status); + save(); render(); + toast(STATUS[status].label+': '+t.title); +} + +function pushHistory(t, action, detail){ + t.history = t.history||[]; + const labels = {done:'✅ Выполнено', todo:'↩ Возвращено в работу', postponed:'📅 Перенесено', cancelled:'✕ Отменено', created:'➕ Создано', edited:'✎ Изменено', assigned:'🔁 Назначено'}; + t.history.push({ at:Date.now(), byId:me, action, detail:detail||labels[action]||action }); +} + +function openPostpone(id){ + postponeId = id; + const t = data.tasks.find(x=>x.id===id); + $('#pp-date').value = t.dueDate || todayStr(); + $('#postpone-modal').classList.remove('hidden'); +} +$('#pp-cancel').onclick = ()=>$('#postpone-modal').classList.add('hidden'); +$('#pp-ok').onclick = ()=>{ + const t = data.tasks.find(x=>x.id===postponeId); if(!t) return; + const newDate = $('#pp-date').value; + t.dueDate = newDate; + t.status = 'postponed'; + t.updatedAt = Date.now(); + pushHistory(t,'postponed','на '+prettyDate(newDate)); + save(); + $('#postpone-modal').classList.add('hidden'); + render(); + toast('Перенесено на '+prettyDate(newDate)); +}; + +/* ============================================================ + МОДАЛКА ЗАДАЧИ (создание/редактирование) + ============================================================ */ +function openTaskModal(id){ + editingId = id; + const t = id ? data.tasks.find(x=>x.id===id) : null; + $('#modal-title').textContent = t ? 'Редактировать задачу' : 'Новая задача'; + $('#f-title').value = t?.title||''; + $('#f-note').value = t?.note||''; + $('#f-date').value = t?.dueDate||''; + $('#f-time').value = t?.time||''; + $('#f-priority').value = t?.priority||'medium'; + $('#f-from').textContent = member(me).name; + // список исполнителей + $('#f-assignee').innerHTML = data.members.map(m=>``).join(''); + // история + if(t && t.history && t.history.length){ + $('#f-history').classList.remove('hidden'); + $('#f-history-list').innerHTML = t.history.slice().reverse().map(h=>{ + const by = member(h.byId); + return `
${new Date(h.at).toLocaleString('ru-RU',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'})} · ${by?escapeHtml(by.name):'?'} — ${escapeHtml(h.detail)}
`; + }).join(''); + } else { $('#f-history').classList.add('hidden'); } + $('#modal-delete').classList.toggle('hidden', !t); + $('#modal').classList.remove('hidden'); + setTimeout(()=>$('#f-title').focus(),50); +} +$('#modal-close').onclick = ()=>$('#modal').classList.add('hidden'); +$('#modal').addEventListener('click', e=>{ if(e.target.id==='modal') $('#modal').classList.add('hidden'); }); + +$('#modal-save').onclick = ()=>{ + const title = $('#f-title').value.trim(); + if(!title){ toast('Введите название задачи'); $('#f-title').focus(); return; } + const assigneeId = $('#f-assignee').value; + const payload = { + title, note: $('#f-note').value.trim(), + assigneeId, + dueDate: $('#f-date').value || null, + time: $('#f-time').value || null, + priority: $('#f-priority').value, + }; + if(editingId){ + const t = data.tasks.find(x=>x.id===editingId); + const reassigned = t.assigneeId !== assigneeId; + Object.assign(t, payload, {updatedAt:Date.now()}); + if(reassigned) pushHistory(t,'assigned','→ '+member(assigneeId).name); + pushHistory(t,'edited'); + toast('Изменено'); + } else { + const t = Object.assign({ id:uid(), assignerId:me, status:'todo', createdAt:Date.now(), updatedAt:Date.now(), history:[] }, payload); + pushHistory(t,'created'); + data.tasks.push(t); + toast('Задача создана ✅'); + } + save(); + $('#modal').classList.add('hidden'); + render(); +}; + +$('#modal-delete').onclick = ()=>{ + if(!editingId) return; + if(!confirm('Удалить задачу безвозвратно?')) return; + data.tasks = data.tasks.filter(t=>t.id!==editingId); + save(); + $('#modal').classList.add('hidden'); + render(); + toast('Задача удалена'); +}; + +/* ============================================================ + УПРАВЛЕНИЕ СЕМЬЁЙ + ============================================================ */ +function openFamilyModal(){ + $('#fm-name').value = data.familyName; + renderFmList(); + $('#family-modal').classList.remove('hidden'); +} +function renderFmList(){ + $('#fm-list').innerHTML = data.members.map((m,i)=>` +
+ ${m.emoji||initials(m.name)} + + + +
`).join(''); + $('#fm-list').children.forEach(row=>{ + const mid = row.dataset.mid; + row.querySelector('.fm-pick').onclick = ()=>cycleAvatar(row, mid); + row.querySelector('.fm-emoji').onclick = ()=>cycleAvatar(row, mid); + row.querySelector('.fm-del').onclick = ()=>{ + const m = member(mid); + if(!confirm(`Удалить «${m.name}»? Его задачи останутся, но без исполнителя.`)) return; + data.members = data.members.filter(x=>x.id!==mid); + data.tasks.forEach(t=>{ if(t.assigneeId===mid) t.assigneeId=null; if(t.assignerId===mid) t.assignerId=null; }); + if(me===mid) me = data.members[0]?.id; + renderFmList(); + }; + }); +} +function cycleAvatar(row, mid){ + const m = member(mid); if(!m) return; + const ci = AVATAR_COLORS.indexOf(m.color); + const ei = AVATAR_EMOJIS.indexOf(m.emoji); + m.color = AVATAR_COLORS[(ci+1)%AVATAR_COLORS.length]; + m.emoji = AVATAR_EMOJIS[(ei+1)%AVATAR_EMOJIS.length]; + const av = row.querySelector('.fm-emoji'); + av.style.background = m.color; av.textContent = m.emoji||initials(m.name); +} +$('#fm-add').onclick = ()=>{ + const i = data.members.length; + data.members.push({ id:uid(), name:'Новый член', color:AVATAR_COLORS[i%AVATAR_COLORS.length], emoji:AVATAR_EMOJIS[i%AVATAR_EMOJIS.length] }); + renderFmList(); +}; +$('#fm-close').onclick = ()=>$('#family-modal').classList.add('hidden'); +$('#family-modal').addEventListener('click', e=>{ if(e.target.id==='family-modal') $('#family-modal').classList.add('hidden'); }); +$('#fm-save').onclick = ()=>{ + data.familyName = $('#fm-name').value.trim() || data.familyName; + $$('#fm-list .fm-name').forEach((inp,i)=>{ if(data.members[i]) data.members[i].name = inp.value.trim() || data.members[i].name; }); + if(!member(me)) me = data.members[0]?.id; + save(); + $('#family-modal').classList.add('hidden'); + renderUserMenu(); setCurrentUser(me); render(); + toast('Семья обновлена'); +}; + +/* ============================================================ + ЗАПУСК + ============================================================ */ +init(); diff --git a/design.md b/design.md new file mode 100644 index 0000000..5c8e829 --- /dev/null +++ b/design.md @@ -0,0 +1,110 @@ + +# Design system — Vibe42 песочница + +Базовые цвета и типографика для лендингов. Можно отклоняться, но начинай с этого. + +## Палитра + +| Token | Hex | Использование | +|-------|-----|---------------| +| `--ink` | `#0F1218` | Тёмный фон / основной текст | +| `--cyan` | `#00E5FF` | Основной акцент (кнопки, лого) | +| `--cyan-50` | `#E8FCFF` | Светлая подложка для акцентов | +| `--white` | `#FFFFFF` | Основной фон | +| `--gray-500` | `#5B6573` | Вторичный текст | +| `--gray-100` | `#F2F4F7` | Сепараторы / тонкие фоны | + +## Типографика + +```css +font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif; +``` + +| Уровень | Размер | Вес | line-height | +|---------|--------|-----|-------------| +| h1 (hero) | 56px | 800 | 1.05 | +| h2 (section) | 36px | 700 | 1.15 | +| h3 | 22px | 700 | 1.3 | +| body | 17px | 400 | 1.6 | +| small | 14px | 400 | 1.5 | + +На мобиле — h1 уменьши до 36px, h2 до 28px. + +## Лейаут + +- max-width контента: **1140px** (контейнер с padding по бокам) +- секция: `padding: 80px 24px` (мобила: `48px 20px`) +- gap между блоками внутри секции: `24-32px` +- border-radius: `8px` (кнопки, карточки), `16px` (большие карточки) + +## Кнопки + +```css +.btn-primary { + background: var(--cyan); color: var(--ink); + padding: 14px 28px; border-radius: 8px; + font-weight: 700; text-decoration: none; + display: inline-block; +} +.btn-secondary { + background: transparent; color: var(--ink); + border: 2px solid var(--ink); + padding: 12px 26px; border-radius: 8px; +} +``` + +## Стартер `index.html` + +```html + + + + + +Мой проект + + + +
+
+

Заголовок проекта

+

Подзаголовок — пара предложений о чём это.

+ Начать +
+
+
+
+

Секция

+
Контент карточки.
+
Контент карточки.
+
+
+ + +``` + +## Чем НЕ пользоваться + +- Bootstrap, Material UI, Chakra, Ant Design — слишком тяжело и не нужно для лендинга +- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG +- jQuery — vanilla JS более чем достаточно + +## Чем МОЖНО (если очень надо) + +- **Tailwind через CDN**: `` — для прототипа OK +- **Lottie animations через CDN** +- **Placeholder картинки**: `https://picsum.photos/800/600`, `https://placehold.co/600x400` +- **Шрифты Google Fonts через ``** в head diff --git a/index.html b/index.html new file mode 100644 index 0000000..d77a4ef --- /dev/null +++ b/index.html @@ -0,0 +1,204 @@ + + + + + +Семейные задачи + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..37d10a4 --- /dev/null +++ b/style.css @@ -0,0 +1,57 @@ +:root{--ink:#0F1218;--accent:#00E5FF;--accent-50:#E8FCFF;--muted:#5B6573;--line:#F2F4F7} + +*{box-sizing:border-box} +.field{width:100%;border:2px solid var(--line);border-radius:8px;padding:10px 12px;font-size:15px;font-family:inherit;background:#fff;color:var(--ink);outline:none;transition:border .15s} +.field:focus{border-color:var(--accent)} +textarea.field{resize:vertical} + +.btn-primary{background:var(--accent);color:var(--ink);padding:11px 20px;border-radius:8px;font-weight:700;transition:filter .15s} +.btn-primary:hover{filter:brightness(1.05)} +.btn-primary:active{filter:brightness(.95)} +.btn-ghost{background:transparent;color:var(--ink);border:2px solid var(--line);padding:9px 18px;border-radius:8px;font-weight:600} +.btn-ghost:hover{background:var(--line)} +.btn-danger{background:#fee2e2;color:#b91c1c;padding:11px 18px;border-radius:8px;font-weight:700} +.btn-danger:hover{background:#fecaca} + +.navtab{white-space:nowrap;padding:10px 14px;font-size:14px;font-weight:600;color:#ffffff80;border-bottom:3px solid transparent;transition:.15s} +.navtab:hover{color:#fff} +.navtab.active{color:var(--accent);border-bottom-color:var(--accent)} + +.avatar{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;font-weight:800;color:#fff;user-select:none;flex-shrink:0} + +/* карточка задачи */ +.task{background:#fff;border:1px solid var(--line);border-radius:14px;padding:14px 16px;transition:box-shadow .15s,border-color .15s} +.task:hover{box-shadow:0 6px 20px rgba(15,18,24,.07)} +.task.done{opacity:.6} +.task.done .t-title{text-decoration:line-through} +.task.cancelled{opacity:.5} +.task.cancelled .t-title{text-decoration:line-through} +.task.overdue:not(.done):not(.cancelled){border-color:#fca5a5;box-shadow:0 0 0 1px #fca5a5 inset} + +.chip{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;padding:2px 8px;border-radius:999px} +.badge{font-size:11px;font-weight:700;padding:2px 8px;border-radius:6px;text-transform:uppercase;letter-spacing:.02em} + +.iconbtn{width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;border:1px solid var(--line);background:#fff;font-size:16px;transition:.15s} +.iconbtn:hover{background:var(--line)} +.iconbtn.primary{background:var(--accent);border-color:var(--accent)} + +/* календарь */ +.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px} +.cal-dow{text-align:center;font-size:12px;font-weight:700;color:var(--muted);padding:6px 0} +.cal-day{min-height:96px;background:#fff;border:1px solid var(--line);border-radius:10px;padding:6px;overflow:hidden} +.cal-day.off{background:#fafbfc;color:#c2c8d2} +.cal-day.today{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-50) inset} +.cal-dnum{font-size:12px;font-weight:700;margin-bottom:4px} +.cal-task{font-size:11px;font-weight:600;border-radius:5px;padding:2px 5px;margin-bottom:3px;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + +.empty{text-align:center;padding:48px 20px;color:var(--muted)} +.empty .big{font-size:48px;margin-bottom:8px} + +.stat-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px} + +.menu-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;cursor:pointer;transition:.12s} +.menu-row:hover{background:var(--line)} +.menu-row.active{background:var(--accent-50)} + +::-webkit-scrollbar{width:10px;height:10px} +::-webkit-scrollbar-thumb{background:#d6dbe2;border-radius:10px}