diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..90be1ed
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+data.json
+node_modules/
+.vibe42-run.log
+.vibe42-run.pid
diff --git a/index.html b/index.html
index 1f53f3b..846c6de 100644
--- a/index.html
+++ b/index.html
@@ -3,18 +3,20 @@
-Календарь дней
+Календарь-планер
-
-
+
+
- Нажми на любой день, чтобы увидеть дату
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3b2cf7b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "planner",
+ "private": true,
+ "type": "commonjs",
+ "scripts": { "start": "node server.js" }
+}
diff --git a/script.js b/script.js
index a370b5a..bfc0a46 100644
--- a/script.js
+++ b/script.js
@@ -6,17 +6,30 @@ var MONTHS = [
var now = new Date();
var view = { year: now.getFullYear(), month: now.getMonth() };
-var selected = null;
+var openDate = null;
+var allByDate = {};
var gridEl = document.getElementById("grid");
var monthNameEl = document.getElementById("monthName");
-var hintEl = document.getElementById("hint");
+var panelBody = document.getElementById("panelBody");
+var panelEmpty = document.getElementById("panelEmpty");
+var panelDateEl = document.getElementById("panelDate");
+var notesEl = document.getElementById("notes");
+var countEl = document.getElementById("count");
function pad(n) { return (n < 10 ? "0" : "") + n; }
function fmt(d) {
return pad(d.getDate()) + "." + pad(d.getMonth() + 1) + "." + d.getFullYear();
}
+function iso(date) {
+ return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate());
+}
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, function (c) {
+ return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
+ });
+}
function renderWeekdays() {
document.getElementById("weekdays").innerHTML =
@@ -30,7 +43,6 @@ function render() {
var startPad = (first.getDay() + 6) % 7;
var daysInMonth = new Date(view.year, view.month + 1, 0).getDate();
var prevDays = new Date(view.year, view.month, 0).getDate();
-
var html = "";
for (var i = startPad - 1; i >= 0; i--) {
@@ -38,18 +50,21 @@ function render() {
}
for (var d = 1; d <= daysInMonth; d++) {
- var cls = "day";
var date = new Date(view.year, view.month, d);
+ var key = iso(date);
+ var cls = "day";
var isToday =
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
if (isToday) cls += " today";
+ if (openDate === key) cls += " selected";
- if (selected && fmt(date) === fmt(selected)) cls += " selected";
+ var has = (allByDate[key] || []).length > 0;
+ var dot = has ? '' : '';
- html += '' + d + "
";
+ html += '' + d + dot + "
";
}
var endPad = 7 - ((startPad + daysInMonth) % 7);
@@ -66,28 +81,121 @@ function changeMonth(delta) {
view.month += delta;
if (view.month < 0) { view.month = 11; view.year--; }
if (view.month > 11) { view.month = 0; view.year++; }
- selected = null;
render();
}
function goToday() {
now = new Date();
view = { year: now.getFullYear(), month: now.getMonth() };
- selected = null;
render();
}
+function api(url, opts) {
+ return fetch(url, opts).then(function (r) {
+ if (!r.ok) throw new Error("Ошибка сервера " + r.status);
+ return r.json();
+ });
+}
+
+function loadAll() {
+ return api("/api/notes").then(function (list) {
+ allByDate = {};
+ list.forEach(function (n) {
+ (allByDate[n.date] = allByDate[n.date] || []).push(n);
+ });
+ render();
+ if (openDate) renderPanel();
+ });
+}
+
+function renderPanel() {
+ var list = allByDate[openDate] || [];
+ notesEl.innerHTML = list.map(function (n) {
+ return (
+ '' +
+ '" +
+ '' + escapeHtml(n.text) + "" +
+ '' +
+ ""
+ );
+ }).join("");
+ countEl.textContent = list.length ? "Дела: " + list.length : "";
+}
+
+function openDay(key) {
+ openDate = key;
+ panelEmpty.classList.add("hidden");
+ panelBody.classList.remove("hidden");
+ var parts = key.split("-");
+ var date = new Date(parts[0], +parts[1] - 1, +parts[2]);
+ panelDateEl.textContent = fmt(date);
+ render();
+ renderPanel();
+}
+
+function closePanel() {
+ openDate = null;
+ panelBody.classList.add("hidden");
+ panelEmpty.classList.remove("hidden");
+ render();
+}
+
+function addNote(text) {
+ return api("/api/notes", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ date: openDate, text: text })
+ }).then(loadAll);
+}
+
+function toggleNote(id, done) {
+ return api("/api/notes", {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ id: id, done: done })
+ }).then(loadAll);
+}
+
+function deleteNote(id) {
+ return api("/api/notes/" + id, { method: "DELETE" }).then(loadAll);
+}
+
gridEl.addEventListener("click", function (e) {
var day = e.target.closest(".day");
if (!day || day.classList.contains("empty")) return;
- selected = new Date(view.year, view.month, parseInt(day.textContent, 10));
- hintEl.textContent = "Выбрано: " + fmt(selected);
- render();
+ openDay(day.dataset.key);
+});
+
+notesEl.addEventListener("click", function (e) {
+ var li = e.target.closest(".note");
+ if (!li) return;
+ var id = +li.dataset.id;
+ var item = (allByDate[openDate] || []).find(function (n) { return n.id === id; });
+ if (!item) return;
+
+ if (e.target.classList.contains("del")) {
+ if (confirm("Удалить это дело?")) deleteNote(id);
+ return;
+ }
+ if (e.target.classList.contains("chk")) {
+ toggleNote(id, !item.done);
+ }
+});
+
+document.getElementById("addForm").addEventListener("submit", function (e) {
+ e.preventDefault();
+ var input = document.getElementById("noteText");
+ var text = input.value.trim();
+ if (!text || !openDate) return;
+ addNote(text);
+ input.value = "";
});
document.getElementById("prev").addEventListener("click", function () { changeMonth(-1); });
document.getElementById("next").addEventListener("click", function () { changeMonth(1); });
document.getElementById("today").addEventListener("click", goToday);
+document.getElementById("closePanel").addEventListener("click", closePanel);
renderWeekdays();
render();
+loadAll();
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..4289a16
--- /dev/null
+++ b/server.js
@@ -0,0 +1,81 @@
+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 load() {
+ try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { notes: [] }; }
+}
+function save(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) => {
+ const url = req.url.split("?")[0];
+
+ // Список всех заметок (опционально фильтр по дате)
+ if (req.method === "GET" && url === "/api/notes") {
+ const q = new URL(req.url, "http://x").searchParams;
+ const date = q.get("date");
+ const db = load();
+ const list = date ? db.notes.filter((n) => n.date === date) : db.notes;
+ res.writeHead(200, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify(list));
+ }
+
+ // Добавить заметку
+ if (req.method === "POST" && url === "/api/notes") {
+ const note = await body(req);
+ if (!note.date || !note.text) {
+ res.writeHead(400, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ ok: false, error: "date и text обязательны" }));
+ }
+ const db = load();
+ const item = { id: Date.now(), date: note.date, text: String(note.text).slice(0, 300), done: !!note.done };
+ db.notes.push(item);
+ save(db);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify(item));
+ }
+
+ // Переключить «сделано»
+ if (req.method === "PATCH" && url === "/api/notes") {
+ const patch = await body(req);
+ const db = load();
+ const note = db.notes.find((n) => n.id === patch.id);
+ if (note) note.done = !!patch.done;
+ save(db);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ ok: !!note }));
+ }
+
+ // Удалить заметку
+ if (req.method === "DELETE" && url.startsWith("/api/notes/")) {
+ const id = Number(url.split("/").pop());
+ const db = load();
+ db.notes = db.notes.filter((n) => n.id !== id);
+ save(db);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ ok: true }));
+ }
+
+ // Статика: index.html / style.css / script.js
+ let file = req.url === "/" ? "/index.html" : url;
+ 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" : "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("Сервер на порту " + PORT));
diff --git a/style.css b/style.css
index f85fe28..1f1ae51 100644
--- a/style.css
+++ b/style.css
@@ -1,15 +1,17 @@
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
*{box-sizing:border-box;margin:0;padding:0}
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--gray-100);min-height:100vh}
+.container{max-width:1060px;margin:0 auto;padding:40px 24px}
+.hidden{display:none!important}
-.wrap{max-width:560px;margin:0 auto;padding:48px 20px}
+.top{background:var(--ink);color:#fff}
+.top h1{font-size:40px;font-weight:800;line-height:1.1}
+.top .sub{color:#9aa3b2;margin-top:8px}
-.top{text-align:center;margin-bottom:32px}
-.top h1{font-size:36px;font-weight:800;line-height:1.1;color:var(--ink)}
-.top .sub{color:var(--gray-500);font-size:16px;margin-top:8px}
-
-.cal{background:var(--white);border-radius:16px;box-shadow:0 12px 32px rgba(15,18,24,.08);padding:24px;overflow:hidden}
+.layout{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:28px;align-items:start}
+/* ===== Календарь ===== */
+.cal{background:var(--white);border-radius:16px;box-shadow:0 12px 32px rgba(15,18,24,.08);padding:24px}
.cal-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.month-name{font-size:24px;font-weight:800;text-transform:capitalize}
.arrow{border:none;background:var(--ink);color:#fff;width:42px;height:42px;border-radius:10px;font-size:20px;cursor:pointer;transition:transform .15s ease,opacity .15s ease}
@@ -20,23 +22,51 @@ body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,s
.weekdays span{text-align:center;font-size:13px;font-weight:700;color:var(--gray-500);text-transform:uppercase;letter-spacing:.05em}
.grid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px}
-.day{display:flex;align-items:center;justify-content:center;height:52px;border-radius:10px;cursor:pointer;font-size:16px;font-weight:600;color:var(--ink);transition:background .15s ease,transform .15s ease}
+.day{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;height:56px;border-radius:10px;cursor:pointer;font-size:16px;font-weight:600;color:var(--ink);transition:background .15s ease,transform .15s ease}
.day:hover{background:var(--cyan-50);transform:scale(1.05)}
.day.empty{pointer-events:none}
.day.today{background:var(--ink);color:#fff;box-shadow:0 4px 12px rgba(15,18,24,.25)}
.day.today:hover{background:var(--ink)}
.day.selected{background:var(--cyan);color:var(--ink);box-shadow:0 4px 12px rgba(0,229,255,.4)}
+.day.selected:hover{background:var(--cyan)}
+.dot{width:6px;height:6px;border-radius:50%;background:var(--cyan)}
+.day.today .dot,.day.selected .dot{background:#fff}
.cal-foot{text-align:center;margin-top:18px;padding-top:18px;border-top:1px solid var(--gray-100)}
.today-btn{border:none;background:var(--cyan-50);color:var(--ink);padding:10px 24px;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;transition:background .15s ease}
.today-btn:hover{background:var(--cyan)}
-.hint{text-align:center;color:var(--gray-500);font-size:15px;margin-top:20px}
+/* ===== Панель дел ===== */
+.panel{background:var(--white);border-radius:16px;box-shadow:0 12px 32px rgba(15,18,24,.08);padding:24px;min-height:320px}
+.panel-empty{text-align:center;color:var(--gray-500);padding:70px 10px}
+.pe-emoji{font-size:44px;margin-bottom:12px}
+.panel-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
+.panel-date{font-size:22px;font-weight:800}
+.close-btn{border:none;background:var(--gray-100);width:36px;height:36px;border-radius:10px;font-size:20px;line-height:1;cursor:pointer;color:var(--gray-500)}
+.close-btn:hover{background:var(--cyan-50);color:var(--ink)}
-@media (max-width:480px){
- .wrap{padding:32px 14px}
+.count{font-size:14px;color:var(--gray-500);margin-bottom:14px}
+.notes{list-style:none;margin-bottom:18px;max-height:340px;overflow-y:auto}
+.notes:empty::after{content:"Пока пусто — добавь первое дело ниже";color:#b4bcc7;font-size:15px;display:block;padding:8px 0}
+
+.note{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--gray-100)}
+.note:last-child{border-bottom:none}
+.chk{width:24px;height:24px;border:2px solid #c4ccd6;border-radius:8px;background:var(--white);cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:14px;color:#fff;transition:background .15s ease,border-color .15s ease}
+.chk.on{background:var(--cyan);border-color:var(--cyan)}
+.note-text{flex:1;font-size:16px}
+.note.done .note-text{text-decoration:line-through;color:#aab3bf}
+.del{border:none;background:none;color:#c4ccd6;font-size:20px;cursor:pointer;padding:4px;line-height:1}
+.del:hover{color:#e5484d}
+
+.add-form{display:flex;gap:10px}
+.add-form input{flex:1;border:2px solid var(--gray-100);border-radius:10px;padding:12px 14px;font-size:16px;font-family:inherit;outline:none;transition:border-color .15s ease}
+.add-form input:focus{border-color:var(--cyan)}
+.add-form button{border:none;background:var(--ink);color:#fff;width:50px;border-radius:10px;font-size:26px;cursor:pointer;flex-shrink:0}
+.add-form button:hover{background:#232a35}
+
+@media (max-width:860px){
+ .layout{grid-template-columns:1fr}
+ .container{padding:28px 16px}
.top h1{font-size:30px}
- .cal{padding:16px}
- .day{height:44px;font-size:14px}
- .month-name{font-size:20px}
+ .day{height:48px}
}