From ef225e928045ab92ea4e77f2aa6dbd3074f6e9f8 Mon Sep 17 00:00:00 2001
From: Lychee <2006tka@gmail.com>
Date: Sun, 17 May 2026 19:16:19 +0500
Subject: [PATCH 1/2] Initial tutorial
---
Api/wwwroot/css/tutorial.css | 206 +++++++++++++++++++++++++++++++++++
Api/wwwroot/js/authGuard.js | 8 ++
Api/wwwroot/js/tutorial.js | 125 +++++++++++++++++++++
3 files changed, 339 insertions(+)
create mode 100644 Api/wwwroot/css/tutorial.css
create mode 100644 Api/wwwroot/js/tutorial.js
diff --git a/Api/wwwroot/css/tutorial.css b/Api/wwwroot/css/tutorial.css
new file mode 100644
index 0000000..efa4a43
--- /dev/null
+++ b/Api/wwwroot/css/tutorial.css
@@ -0,0 +1,206 @@
+:root {
+ --tutorial-bg: #1f1f1f;
+ --tutorial-panel: #282828;
+ --tutorial-line: #3a3a3a;
+ --tutorial-text: #f4f4f4;
+ --tutorial-muted: #a7a7a7;
+ --tutorial-orange: #f4864d;
+ --tutorial-green: #407d52;
+ --tutorial-serif: "Yeseva One", Georgia, serif;
+ --tutorial-sans: Inter, Arial, sans-serif;
+ --tutorial-label: Raleway, Inter, Arial, sans-serif;
+}
+
+body.tutorial-locked {
+ overflow: hidden;
+}
+
+.tutorial-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 12000;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+ background: rgba(0, 0, 0, 0.64);
+ backdrop-filter: blur(4px);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.22s ease;
+}
+
+.tutorial-overlay.is-open {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.tutorial-card {
+ position: relative;
+ width: min(720px, calc(100vw - 32px));
+ max-height: calc(100vh - 48px);
+ padding: 34px;
+ border: 1px solid rgba(244, 134, 77, 0.42);
+ border-radius: 24px;
+ background: linear-gradient(180deg, #2b2b2b 0%, var(--tutorial-panel) 100%);
+ color: var(--tutorial-text);
+ box-shadow: 0 30px 90px rgba(0, 0, 0, 0.62);
+ display: grid;
+ gap: 22px;
+ overflow-y: auto;
+ transform: translateY(18px) scale(0.97);
+ transition: transform 0.22s ease;
+ text-align: left;
+}
+
+.tutorial-overlay.is-open .tutorial-card {
+ transform: translateY(0) scale(1);
+}
+
+.tutorial-close {
+ position: absolute;
+ right: 22px;
+ top: 20px;
+ width: 38px;
+ height: 38px;
+ border: 0;
+ background: transparent;
+ cursor: pointer;
+}
+
+.tutorial-close span {
+ position: absolute;
+ left: 6px;
+ top: 17px;
+ width: 28px;
+ height: 4px;
+ border-radius: 999px;
+ background: white;
+}
+
+.tutorial-close span:first-child {
+ transform: rotate(45deg);
+}
+
+.tutorial-close span:last-child {
+ transform: rotate(-45deg);
+}
+
+.tutorial-close:hover span,
+.tutorial-close:focus-visible span {
+ background: var(--tutorial-orange);
+}
+
+.tutorial-eyebrow {
+ margin: 0;
+ color: var(--tutorial-orange);
+ font-family: var(--tutorial-label);
+ font-size: 16px;
+ font-weight: 700;
+ text-align: left;
+}
+
+.tutorial-card h2 {
+ margin: 0;
+ padding-right: 44px;
+ color: white;
+ font-family: var(--tutorial-label);
+ font-size: 36px;
+ font-weight: 500;
+ line-height: 1.1;
+ text-align: left;
+}
+
+.tutorial-steps {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ display: grid;
+ gap: 12px;
+}
+
+.tutorial-steps li {
+ min-height: 58px;
+ padding: 14px 16px;
+ border: 1px solid var(--tutorial-line);
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.04);
+ display: grid;
+ grid-template-columns: 34px minmax(0, 1fr);
+ align-items: center;
+ gap: 12px;
+ color: var(--tutorial-text);
+ font-family: var(--tutorial-label);
+ font-size: 17px;
+ line-height: 1.35;
+ text-align: left;
+}
+
+.tutorial-step-index {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ background: rgba(244, 134, 77, 0.18);
+ color: var(--tutorial-orange);
+ display: grid;
+ place-items: center;
+ font-family: var(--tutorial-sans);
+ font-size: 14px;
+ font-weight: 800;
+}
+
+.tutorial-done {
+ width: max-content;
+ min-height: 44px;
+ margin-left: auto;
+ border: 0;
+ background: transparent;
+ color: var(--tutorial-green);
+ cursor: pointer;
+ font-family: var(--tutorial-label);
+ font-size: 21px;
+ font-weight: 600;
+ text-align: left;
+}
+
+.tutorial-done:hover,
+.tutorial-done:focus-visible {
+ color: #5fb978;
+ outline: none;
+}
+
+@media (max-width: 700px) {
+ .tutorial-overlay {
+ padding: 14px;
+ }
+
+ .tutorial-card {
+ width: calc(100vw - 28px);
+ padding: 26px 20px;
+ gap: 18px;
+ text-align: left;
+ }
+
+ .tutorial-card h2 {
+ font-size: 28px;
+ padding-right: 38px;
+ text-align: left;
+ }
+
+ .tutorial-eyebrow {
+ text-align: left;
+ }
+
+ .tutorial-steps li {
+ grid-template-columns: 30px minmax(0, 1fr);
+ padding: 12px;
+ font-size: 15px;
+ align-items: center;
+ text-align: left;
+ }
+
+ .tutorial-done {
+ width: 100%;
+ margin-left: 0;
+ text-align: left;
+ }
+}
\ No newline at end of file
diff --git a/Api/wwwroot/js/authGuard.js b/Api/wwwroot/js/authGuard.js
index e2cae6a..83243b0 100644
--- a/Api/wwwroot/js/authGuard.js
+++ b/Api/wwwroot/js/authGuard.js
@@ -8,4 +8,12 @@ if (!hasToken) {
authUrl.searchParams.set("next", current);
window.location.replace(authUrl);
+} else {
+ import("./tutorial.js")
+ .then(({ showFirstLoginTutorial }) => {
+ showFirstLoginTutorial();
+ })
+ .catch((error) => {
+ console.error(error);
+ });
}
diff --git a/Api/wwwroot/js/tutorial.js b/Api/wwwroot/js/tutorial.js
new file mode 100644
index 0000000..3eabd4c
--- /dev/null
+++ b/Api/wwwroot/js/tutorial.js
@@ -0,0 +1,125 @@
+const TUTORIAL_STORAGE_KEY = "boardify.tutorialSeen";
+const TUTORIAL_CSS_ID = "boardifyTutorialStyles";
+const TUTORIAL_CSS_HREF = "./css/tutorial.css";
+
+export function showFirstLoginTutorial() {
+ if (wasTutorialSeen() || document.querySelector("[data-tutorial-overlay]")) {
+ return;
+ }
+
+ ensureTutorialStyles();
+
+ const overlay = document.createElement("div");
+ overlay.className = "tutorial-overlay";
+ overlay.dataset.tutorialOverlay = "true";
+ overlay.innerHTML = createTutorialMarkup();
+
+ document.body.appendChild(overlay);
+ document.body.classList.add("tutorial-locked");
+
+ const close = () => closeTutorial(overlay);
+
+ overlay.querySelector("[data-tutorial-close]")?.addEventListener("click", close);
+ overlay.querySelector("[data-tutorial-done]")?.addEventListener("click", close);
+ overlay.addEventListener("click", (event) => {
+ if (event.target === overlay) {
+ close();
+ }
+ });
+
+ const onKeyDown = (event) => {
+ if (event.key === "Escape") {
+ close();
+ }
+ };
+
+ document.addEventListener("keydown", onKeyDown);
+ overlay.cleanupTutorial = () => {
+ document.removeEventListener("keydown", onKeyDown);
+ };
+
+ requestAnimationFrame(() => {
+ overlay.classList.add("is-open");
+ overlay.querySelector("[data-tutorial-done]")?.focus();
+ });
+}
+
+function createTutorialMarkup() {
+ return `
+
+
+
+ Быстрый старт
+ С чего начать?
+
+
+ -
+ 1
+ Откройте меню: нажмите на три полоски слева сверху.
+
+ -
+ 2
+ Перейдите в «Команды» и нажмите «Добавить»: задайте название, цвет и пригласите участников по имени или email.
+
+ -
+ 3
+ Откройте «Проекты» и создайте первый проект для своей команды.
+
+ -
+ 4
+ Внутри проекта добавьте kanban-доску.
+
+ -
+ 5
+ Готово: как админ команды вы можете создавать колонки, задачи и подзадачи.
+
+
+
+
+
+ `;
+}
+
+function closeTutorial(overlay) {
+ markTutorialSeen();
+ overlay.classList.remove("is-open");
+ document.body.classList.remove("tutorial-locked");
+ overlay.cleanupTutorial?.();
+
+ setTimeout(() => {
+ overlay.remove();
+ }, 220);
+}
+
+function ensureTutorialStyles() {
+ if (document.getElementById(TUTORIAL_CSS_ID)) {
+ return;
+ }
+
+ const link = document.createElement("link");
+ link.id = TUTORIAL_CSS_ID;
+ link.rel = "stylesheet";
+ link.href = TUTORIAL_CSS_HREF;
+ document.head.appendChild(link);
+}
+
+function wasTutorialSeen() {
+ try {
+ return localStorage.getItem(TUTORIAL_STORAGE_KEY) === "true";
+ } catch {
+ return false;
+ }
+}
+
+function markTutorialSeen() {
+ try {
+ localStorage.setItem(TUTORIAL_STORAGE_KEY, "true");
+ } catch {
+ // Ignore storage errors: closing the dialog should still work.
+ }
+}
From 9ebf63d0fe23d60ae58a064d6ebd96bd25a13036 Mon Sep 17 00:00:00 2001
From: Lychee <2006tka@gmail.com>
Date: Sun, 17 May 2026 19:44:14 +0500
Subject: [PATCH 2/2] =?UTF-8?q?+=20README=20+=20=D0=BC=D0=B8=D0=BA=D1=80?=
=?UTF-8?q?=D0=BE=20=D1=84=D0=B8=D0=BA=D1=81=D1=8B=20(sidebar,=20=D0=BD?=
=?UTF-8?q?=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC?=
=?UTF-8?q?=D0=B0=D0=BD=D0=B4=D1=8B=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B5?=
=?UTF-8?q?=D0=BA=D1=82=D0=B0=20=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?=
=?UTF-8?q?=D0=BB=D0=B0)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Api/wwwroot/css/project.css | 141 ++++++++--------
Api/wwwroot/css/styles.css | 13 +-
Api/wwwroot/js/project.js | 8 +-
README.md | 315 ++++++++++++++++++++++++++++++++++++
4 files changed, 396 insertions(+), 81 deletions(-)
create mode 100644 README.md
diff --git a/Api/wwwroot/css/project.css b/Api/wwwroot/css/project.css
index 6cfa478..ad30137 100644
--- a/Api/wwwroot/css/project.css
+++ b/Api/wwwroot/css/project.css
@@ -4,12 +4,10 @@
--panel-line: #3a3a3a;
--text: #f4f4f4;
--muted: #838383;
-
--orange: #f4864d;
--blue: #42609f;
--green: #407d52;
--peach: #ffe3d8;
-
--serif: "Yeseva One", Georgia, serif;
--sans: Inter, Arial, sans-serif;
--label: Raleway, Inter, Arial, sans-serif;
@@ -49,11 +47,9 @@ select {
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.28);
border-bottom-right-radius: 14px;
border-bottom-left-radius: 14px;
-
display: flex;
align-items: center;
justify-content: center;
-
position: sticky;
top: 0;
z-index: 40;
@@ -62,7 +58,6 @@ select {
.brand {
text-decoration: none;
display: block;
-
font-family: var(--serif);
font-size: clamp(72px, 7vw, 96px);
line-height: 1;
@@ -76,10 +71,8 @@ select {
.home-link {
position: absolute;
top: 42px;
-
width: 38px;
height: 38px;
-
border: 0;
background: transparent;
color: var(--orange);
@@ -136,7 +129,6 @@ select {
.project-head {
margin-bottom: 38px;
-
display: flex;
align-items: center;
justify-content: space-between;
@@ -145,7 +137,6 @@ select {
.project-head h1 {
margin: 0;
-
color: white;
font-family: var(--label);
font-size: 64px;
@@ -158,16 +149,12 @@ select {
border: 0;
background: transparent;
color: white;
-
display: inline-flex;
align-items: center;
gap: 9px;
-
cursor: pointer;
-
font-family: var(--label);
font-weight: 500;
-
transition: color 0.2s ease;
}
@@ -192,17 +179,14 @@ select {
.project-panel {
min-height: 300px;
-
padding: 24px 26px;
background: var(--panel);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.25);
border: 1px solid transparent;
border-radius: 30px;
-
display: flex;
flex-direction: column;
gap: 18px;
-
cursor: pointer;
transition:
border-color 0.2s ease,
@@ -221,45 +205,57 @@ select {
}
.project-panel-head {
+ position: relative;
+ min-height: 58px;
display: grid;
- grid-template-columns: 22px minmax(0, 1fr) auto;
+ grid-template-columns: 24px minmax(260px, 1fr) auto;
align-items: center;
- gap: 14px;
+ gap: 12px;
}
.project-color-dot {
width: 21px;
height: 21px;
border-radius: 50%;
+ z-index: 3;
}
.project-title-block {
min-width: 0;
+ z-index: 3;
}
.project-team-name {
- margin: 0 0 8px;
+ margin: 0;
+ max-width: none;
color: var(--orange);
font-family: var(--label);
- font-size: 16px;
- font-weight: 700;
- line-height: 1.1;
- overflow: hidden;
- text-overflow: ellipsis;
+ font-size: 21px;
+ font-weight: 800;
+ line-height: 1.15;
+ text-align: left;
+ overflow: visible;
+ text-overflow: unset;
white-space: nowrap;
}
.project-card-name {
- min-width: 0;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ max-width: 360px;
margin: 0;
-
+ transform: translate(-50%, -50%);
color: white;
font-family: var(--label);
font-size: 38px;
font-weight: 700;
line-height: 1;
-
- overflow-wrap: anywhere;
+ text-align: center;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ z-index: 1;
}
.project-board-count {
@@ -267,25 +263,23 @@ select {
padding: 0 14px;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 999px;
-
color: var(--muted);
display: inline-flex;
align-items: center;
justify-content: center;
-
font-family: var(--label);
font-size: 15px;
white-space: nowrap;
+ justify-self: end;
+ z-index: 3;
}
.project-boards {
flex: 1;
min-height: 180px;
-
border: 1px solid white;
border-radius: 20px;
overflow: hidden;
-
display: flex;
flex-direction: column;
}
@@ -298,18 +292,14 @@ select {
.project-board-row {
min-height: 66px;
-
padding: 14px 20px;
-
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(150px, 0.6fr) 32px;
align-items: center;
gap: 24px;
-
border-bottom: 1px solid rgba(255, 255, 255, 0.9);
color: white;
text-decoration: none;
-
transition:
background 0.2s ease,
color 0.2s ease;
@@ -332,12 +322,10 @@ select {
.project-board-name,
.project-board-meta {
min-width: 0;
-
font-family: var(--label);
font-size: 24px;
font-weight: 400;
line-height: 1.25;
-
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -358,10 +346,8 @@ select {
.project-empty {
flex: 1;
-
display: grid;
place-items: center;
-
color: var(--orange);
font-family: var(--serif);
font-size: 24px;
@@ -369,17 +355,13 @@ select {
.project-state {
min-height: 220px;
-
padding: 36px;
border: 1px solid var(--panel-line);
border-radius: 30px;
-
background: var(--panel);
color: var(--muted);
-
display: grid;
place-items: center;
-
text-align: center;
font-family: var(--label);
font-size: 24px;
@@ -395,17 +377,13 @@ body.modal-locked {
position: fixed;
inset: 0;
z-index: 999;
-
display: grid;
place-items: center;
-
padding: 24px;
background: rgba(0, 0, 0, 0.62);
backdrop-filter: blur(3px);
-
opacity: 0;
pointer-events: none;
-
transition: opacity 0.22s ease;
}
@@ -417,19 +395,15 @@ body.modal-locked {
.project-modal {
width: min(720px, calc(100vw - 36px));
max-height: calc(100vh - 48px);
-
padding: 28px;
border: 0;
border-radius: 22px;
-
background: var(--panel);
color: white;
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.62);
-
display: flex;
flex-direction: column;
gap: 26px;
-
overflow-y: auto;
transform: translateY(18px) scale(0.97);
transition: transform 0.22s ease;
@@ -452,7 +426,6 @@ body.modal-locked {
.project-modal-header h2 {
margin: 0;
-
color: white;
font-family: var(--label);
font-size: 30px;
@@ -462,10 +435,8 @@ body.modal-locked {
.project-modal-close {
position: relative;
-
width: 42px;
height: 42px;
-
border: 0;
background: transparent;
cursor: pointer;
@@ -475,10 +446,8 @@ body.modal-locked {
position: absolute;
left: 6px;
top: 19px;
-
width: 32px;
height: 4px;
-
border-radius: 999px;
background: white;
}
@@ -513,15 +482,12 @@ body.modal-locked {
.project-select {
width: 100%;
min-height: 48px;
-
padding: 0 18px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 12px;
outline: none;
-
background: rgba(255, 255, 255, 0.06);
color: white;
-
font-size: 16px;
}
@@ -542,7 +508,6 @@ body.modal-locked {
.project-modal-actions {
margin-top: auto;
-
display: flex;
justify-content: space-between;
align-items: center;
@@ -551,12 +516,9 @@ body.modal-locked {
.project-modal-action {
min-height: 42px;
-
border: 0;
background: transparent;
-
cursor: pointer;
-
font-family: var(--label);
font-size: 18px;
font-weight: 500;
@@ -595,24 +557,18 @@ body.modal-locked {
left: 50%;
bottom: 46px;
z-index: 250;
-
transform: translateX(-50%) translateY(20px);
opacity: 0;
pointer-events: none;
-
min-height: 34px;
padding: 0 14px;
border-radius: 10px;
-
background: white;
color: #111;
-
display: flex;
align-items: center;
gap: 8px;
-
font-size: 14px;
-
transition: opacity 0.25s ease, transform 0.25s ease;
}
@@ -629,6 +585,8 @@ body.modal-locked {
color: #000;
}
+/* MOBILE */
+
@media (max-width: 800px) {
.project-topbar {
height: 82px;
@@ -664,18 +622,53 @@ body.modal-locked {
}
.project-panel-head {
- grid-template-columns: 18px minmax(0, 1fr);
+ min-height: auto;
+ grid-template-columns: 20px minmax(0, 1fr);
+ gap: 10px;
+ align-items: center;
}
- .project-board-count {
- width: max-content;
+ .project-color-dot {
+ width: 18px;
+ height: 18px;
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ .project-title-block {
grid-column: 2;
+ grid-row: 1;
+ min-width: 0;
+ }
+
+ .project-team-name {
+ max-width: 100%;
+ margin: 0 0 8px;
+ font-size: 18px;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.project-card-name {
+ position: static;
+ max-width: 100%;
+ transform: none;
+ text-align: left;
+ white-space: normal;
+ overflow: visible;
+ text-overflow: unset;
font-size: 30px;
}
+ .project-board-count {
+ width: max-content;
+ grid-column: 2;
+ grid-row: 2;
+ justify-self: start;
+ }
+
.project-board-row {
grid-template-columns: minmax(0, 1fr) 32px;
gap: 8px 12px;
@@ -717,4 +710,4 @@ body.modal-locked {
.project-modal-action {
width: 100%;
}
-}
+}
\ No newline at end of file
diff --git a/Api/wwwroot/css/styles.css b/Api/wwwroot/css/styles.css
index fbeb26f..89243bb 100644
--- a/Api/wwwroot/css/styles.css
+++ b/Api/wwwroot/css/styles.css
@@ -628,6 +628,10 @@ button.section-head:hover {
margin 0.22s ease;
}
+.sidebar-section:not(.is-collapsed) .collapsible-content {
+ overflow: visible;
+}
+
.sidebar-section.is-collapsed .collapsible-content {
max-height: 0;
opacity: 0;
@@ -648,7 +652,7 @@ button.section-head:hover {
background: var(--sidebar-hover-bg);
border-color: var(--sidebar-hover-border);
transform: translateX(var(--sidebar-hover-shift));
- box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
+ box-shadow: none;
}
.project-item:hover .project-name {
@@ -669,6 +673,7 @@ button.section-head:hover {
.project-list {
display: grid;
gap: 16px;
+ padding-right: calc(var(--sidebar-hover-shift) + 10px);
}
.sidebar-empty {
@@ -699,7 +704,7 @@ button.section-head:hover {
.project-item.active {
min-height: 72px;
background: #333333;
- box-shadow: 0 12px 20px rgba(0, 0, 0, 0.32);
+ box-shadow: none;
}
.project-color {
@@ -740,6 +745,7 @@ button.section-head:hover {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px 20px;
+ padding-right: calc(var(--sidebar-hover-shift) + 10px);
}
.team-card {
@@ -771,7 +777,7 @@ button.section-head:hover {
background: var(--sidebar-hover-bg);
border-color: var(--sidebar-hover-border);
transform: translateX(var(--sidebar-hover-shift));
- box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
+ box-shadow: none;
}
@@ -806,6 +812,7 @@ button.section-head:hover {
display: flex;
flex-wrap: wrap;
gap: 16px;
+ padding-right: calc(var(--sidebar-hover-shift) + 10px);
}
/* =========================
diff --git a/Api/wwwroot/js/project.js b/Api/wwwroot/js/project.js
index 753c65c..c9de99d 100644
--- a/Api/wwwroot/js/project.js
+++ b/Api/wwwroot/js/project.js
@@ -81,13 +81,13 @@ function createProjectCard(project) {
panel.innerHTML = `
-
-
-
+
+
${escapeHtml(project.team?.name || "Без команды")}
-
${escapeHtml(project.name || "Проект")}
+
${escapeHtml(project.name || "Проект")}
+
${escapeHtml(formatBoardCount(project.kanbans?.length ?? 0))}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e125e9a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,315 @@
+# UnitHack
+
+
+
+
+
+
+
+
+
+
+UnitHack — система для управления задачами с автоматизацией и синхронизацией между пользователями и событиями.
+
+Проект создан для кейса компании Victory Group в рамках хакатона UnitHack 2026. Платформа помогает командам вести проекты, управлять задачами на kanban-досках, назначать исполнителей, использовать теги и получать уведомления о важных изменениях.
+
+# Contents
+
+* [About](#about)
+* [Возможности](#возможности)
+* [Шаги для установки](#шаги-для-установки)
+* [Tech Stack](#tech-stack)
+* [API](#api)
+* [Realtime и автоматизация](#realtime-и-автоматизация)
+* [Архитектура](#архитектура)
+* [Authors](#authors)
+
+# About
+
+UnitHack позволяет:
+
+* регистрироваться и авторизовываться в системе
+* создавать команды и управлять участниками
+* менять роли участников внутри команды
+* создавать проекты внутри команд
+* создавать kanban-доски для проектов
+* добавлять, переименовывать и удалять колонки
+* создавать, обновлять, перемещать и удалять задачи
+* назначать задачи пользователям
+* добавлять дедлайны, приоритеты и описания задач
+* создавать теги для kanban-доски
+* прикреплять теги к задачам
+* получать уведомления о создании, изменении и перемещении задач
+* работать с интерфейсом через статический frontend
+
+# Возможности
+
+UnitHack построен вокруг командной работы с задачами:
+
+* **Teams** — команды, участники и роли
+* **Projects** — проекты, привязанные к командам
+* **Kanban** — доски внутри проектов
+* **Columns** — колонки kanban-досок
+* **Tasks** — задачи с исполнителем, приоритетом, дедлайном и порядком
+* **Tags** — теги для группировки задач
+* **Notifications** — уведомления о событиях задач
+* **Realtime** — доставка уведомлений через SignalR
+* **Automation events** — публикация событий через outbox и RabbitMQ
+
+# Шаги для установки
+
+1. Склонируйте репозиторий.
+
+```bash
+git clone https://github.com/prlzrakk/UnitHack.git
+cd UnitHack
+```
+
+2. Создайте `.env` на основе `.env.example`.
+
+Пример переменных окружения:
+
+```env
+# ASP.NET
+ASPNETCORE_URLS=http://+:8080
+ASPNETCORE_ENVIRONMENT=Development
+
+# JWT
+JWT_SECRET=your_super_secret_key_32_chars_minimum
+
+# Database
+DB_HOST=db
+DB_PORT=5432
+DB_NAME=unithack
+DB_USER=postgres
+DB_PASSWORD=postgres
+
+# RabbitMQ
+RabbitMq__Host=rabbitmq
+RabbitMq__Port=5672
+RabbitMq__Username=guest
+RabbitMq__Password=guest
+RabbitMq__Exchange=kanban.events
+```
+
+3. Запустите проект через Docker Compose.
+
+```bash
+docker compose up -d --build
+```
+
+4. После запуска будут доступны:
+
+* frontend/backend: `http://localhost:8080`
+* nginx: `http://localhost`
+* PostgreSQL: `localhost:5332`
+* RabbitMQ AMQP: `localhost:5672`
+* RabbitMQ Management UI: `http://localhost:15672`
+* Swagger в Development-режиме: `http://localhost:8080/swagger`
+
+Для запуска тестов:
+
+```bash
+dotnet test
+```
+
+# Tech Stack
+
+## Frontend
+
+* HTML
+* CSS
+* JavaScript
+* SignalR client
+
+Статический frontend находится в `Api/wwwroot`.
+
+Основные страницы:
+
+* `auth.html` — авторизация
+* `index.html` — главная страница
+* `teams.html` — команды
+* `project.html` — проекты
+* `kanban.html` — kanban-доска
+* `user.html` — профиль пользователя
+
+## Backend
+
+* C#
+* .NET
+* ASP.NET Core Web API
+* MediatR
+* FluentValidation
+* JWT authentication
+* Swagger / OpenAPI
+* SignalR
+
+Backend реализован в проекте `Api`.
+
+## Database
+
+* PostgreSQL
+* Entity Framework Core
+* Npgsql
+
+В базе данных есть сущности для:
+
+* пользователей
+* команд
+* участников команд
+* проектов
+* kanban-досок
+* колонок
+* задач
+* тегов
+* уведомлений
+* событий задач
+* правил автоматизации
+* outbox-событий
+
+## Messaging and realtime
+
+* RabbitMQ
+* Outbox pattern
+* Background workers
+* SignalR
+
+RabbitMQ используется для публикации и обработки событий задач. SignalR используется для realtime-уведомлений пользователей.
+
+## DevOps
+
+* Docker
+* Docker Compose
+* Nginx
+* GitHub Actions
+
+## Tests
+
+* xUnit
+
+# API
+
+## Auth and users
+
+* `POST /api/users` — регистрация пользователя
+* `GET /api/users/me` — текущий пользователь
+* `GET /api/users?query=&limit=` — поиск пользователей
+* `POST /api/auth/sessions` — создание сессии
+* `POST /api/auth/tokens/refresh` — обновление access/refresh token
+
+## Teams
+
+* `GET /api/teams` — список команд пользователя
+* `GET /api/teams/{teamId}` — команда по id
+* `POST /api/teams` — создание команды
+* `POST /api/teams/{teamId}/members` — добавление участника
+* `DELETE /api/teams/{teamId}/members/{userId}` — удаление участника
+* `PATCH /api/teams/{teamId}/members/{userId}/role` — изменение роли участника
+
+## Projects
+
+* `POST /api/teams/{teamId}/projects` — создание проекта
+* `GET /api/projects/{projectId}` — проект по id
+* `GET /api/teams/{teamId}/projects` — проекты команды
+* `DELETE /api/projects/{projectId}` — удаление проекта
+
+## Kanban
+
+* `POST /api/projects/{projectId}/kanbans` — создание kanban-доски
+* `GET /api/projects/{projectId}/kanbans` — все kanban-доски проекта
+* `GET /api/kanbans/{kanbanId}` — kanban-доска по id
+* `PUT /api/kanbans/{kanbanId}` — переименование kanban-доски
+* `DELETE /api/kanbans/{kanbanId}` — удаление kanban-доски
+
+## Columns
+
+* `POST /api/kanbans/{kanbanId}/columns` — создание колонки
+* `PUT /api/columns/{columnId}` — переименование колонки
+* `DELETE /api/columns/{columnId}` — удаление колонки
+
+## Tasks
+
+* `POST /api/kanbans/{kanbanId}/tasks` — создание задачи
+* `PUT /api/tasks/{taskId}` — обновление задачи
+* `PATCH /api/tasks/{taskId}` — перемещение задачи
+* `DELETE /api/tasks/{taskId}` — удаление задачи
+
+## Tags
+
+* `GET /api/kanbans/{kanbanId}/tags` — теги kanban-доски
+* `POST /api/kanbans/{kanbanId}/tags` — создание тега
+* `PUT /api/tags/{tagId}` — переименование тега
+* `DELETE /api/tags/{tagId}` — удаление тега
+* `GET /api/tasks/{taskId}/tags` — теги задачи
+* `POST /api/tasks/{taskId}/tags/{tagId}` — прикрепить тег к задаче
+* `DELETE /api/tasks/{taskId}/tags/{tagId}` — открепить тег от задачи
+
+## Notifications
+
+* `GET /api/notifications` — все уведомления пользователя
+* `GET /api/notifications/unread` — непрочитанные уведомления
+* `GET /api/notifications/{notificationId}` — уведомление по id
+* `PATCH /api/notifications/{notificationId}/read` — отметить уведомление прочитанным
+* `PUT /api/notifications/read-all` — отметить все уведомления прочитанными
+
+# Realtime и автоматизация
+
+В проекте предусмотрена синхронизация событий задач и realtime-уведомления.
+
+Как это работает:
+
+1. Пользователь создает, обновляет или перемещает задачу.
+2. Backend создает событие задачи.
+3. Событие сохраняется в outbox.
+4. `OutboxWorker` публикует pending-события в RabbitMQ.
+5. `NotificationWorker` читает события из очереди.
+6. Для пользователя создается уведомление.
+7. Уведомление отправляется через SignalR.
+
+SignalR hub:
+
+```text
+/hubs/notifications
+```
+
+RabbitMQ queue:
+
+```text
+notifications:events
+```
+
+# Архитектура
+
+Основные части решения:
+
+* `Api` — ASP.NET Core приложение, API-контроллеры, middleware, SignalR hub и статический frontend
+* `Client.Models` — DTO, enum-ы и конфигурационные модели, общие для API и клиента
+* `Core` — слой для доменной и прикладной логики
+* `Infrastructure` — база данных, Entity Framework Core, репозитории, JWT/security, RabbitMQ, workers
+* `Tests` — тестовый проект
+
+Проект следует feature-based структуре: каждая крупная возможность вынесена в отдельную папку внутри `Api/Application/Features`.
+
+Пример:
+
+```text
+Api/Application/Features/
+├── Auth
+├── Columns
+├── Kanban
+├── Notifications
+├── Projects
+├── Tags
+├── TaskTags
+├── Tasks
+├── Teams
+└── Users
+```
+
+# Authors
+
+* [prlzrakk](https://github.com/prlzrakk)
+* [Jlychee](https://github.com/Jlychee)
+* [Kitiketov](https://github.com/Kitiketov)
+* [FayTim](https://github.com/FayTim)
+* [reqied](https://github.com/reqied)