diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b52deb..75e132a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,30 @@ ## [Unreleased] +### Added + +- Общий shadcn/ui-слой на Radix UI, CVA, Lucide и семантических theme tokens. +- Responsive workspace shell, command palette, task composer dialog и desktop/mobile documentation gallery. +- Актуальный production-логотип в SVG/PNG, favicon и product preview. + +### Changed + +- Главная теперь сфокусирована на личной работе, очередях разбора/review и последних изменениях. +- List и Kanban используют единые фильтры и представления; task rows стали read-first и ведут в карточку. +- Аналитика показывает только измеримые свойства текущей модели: WIP, review, незакреплённые и stale tasks. +- `/pages/my` оставлен как legacy redirect на overview; отдельная маркетинговая workspace-страница удалена. +- Web-контейнер доступен только внутри Compose-сети, публичный browser endpoint обслуживает Nginx. + +### Removed + +- Повторяющиеся KPI/summary blocks, постоянная inline-форма создания и недостоверный project pulse score. + ### Fixed - GitHub Actions и Docker builds больше не зависят от устаревшего Corepack keyring в Node.js 22.13.1; точная версия `pnpm@9.15.4` устанавливается напрямую, а workspace scripts используют активный pnpm. +- Refresh/logout больше не делят строгий edge rate limit с login, что устраняет ложный разлогин при серии + обычных перезагрузок; login и invitation acceptance сохраняют отдельный строгий лимит. ## [1.1.0] - 2026-08-21 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af51d1a..8564a5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,9 @@ docker compose config --quiet Добавляйте тесты для authorization boundaries, межорганизационной изоляции, token lifecycle, миграций, кеш-инвалидации и realtime-подписок. +При заметном изменении UI обновите production-галерею по инструкции +[`docs/screenshots/README.md`](./docs/screenshots/README.md) и визуально проверьте desktop/mobile кадры. + ## Коммиты и pull request Используйте небольшие Conventional Commits: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`, diff --git a/README.md b/README.md index 7906c16..0618d7f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- Tracker logo + Tracker logo

Tracker

Командный task tracker с организациями, ролями, realtime-доской и безопасной browser session.

@@ -14,10 +14,9 @@ --- -Tracker — full-stack монорепозиторий для управления проектами и задачами. Он сочетает быстрый -Next.js-интерфейс, versioned NestJS API, PostgreSQL/Prisma, Redis cache и Socket.IO. Проект рассчитан -на развитие как модульный монолит: границы домена явные, а frontend и backend можно развёртывать -независимо. +Tracker — full-stack монорепозиторий для управления проектами и задачами. Актуальный интерфейс построен +вокруг личного фокуса, очередей разбора, компактного списка, Kanban и измеримой аналитики потока. +Frontend и API развёртываются независимо, а backend сохраняет границы модульного монолита. > [!IMPORTANT] > Репозиторий предоставляет укреплённый baseline, но не заменяет production-платформу. Перед публичным @@ -27,8 +26,12 @@ Next.js-интерфейс, versioned NestJS API, PostgreSQL/Prisma, Redis cache ## Возможности - организации, membership-роли `OWNER` / `ADMIN` / `MEMBER` и проекты; -- задачи с приоритетами, статусами, исполнителями, фильтрами и сохранёнными представлениями; -- list, kanban, overview, analytics и детальная карточка задачи; +- задачи с приоритетами, статусами, исполнителями, фильтрами и локально сохранёнными представлениями; +- рабочая главная с личной очередью, разбором, review и последними изменениями; +- компактный list view, Kanban с drag-and-drop и едиными фильтрами; +- измеримая аналитика WIP, review, незакреплённых и давно не обновлённых задач; +- полноценная карточка задачи с редактированием, комментариями, activity и подборкой похожих задач; +- command palette, горячая клавиша создания, светлая/тёмная тема и responsive mobile UI; - комментарии и история активности; - realtime-инвалидация данных через защищённые project rooms Socket.IO; - приглашения с одноразовым токеном и сроком действия; @@ -37,24 +40,26 @@ Next.js-интерфейс, versioned NestJS API, PostgreSQL/Prisma, Redis cache - Docker Compose и Kubernetes base с restricted security context; - CI: lint, typecheck, 17 API tests, build, dependency audit, CodeQL и Dependency Review. -## Скриншоты +## Интерфейс -| Вход | Рабочее пространство | -| --- | --- | -| ![Экран входа](./docs/screenshots/01-sign-in.png) | ![Рабочее пространство](./docs/screenshots/02-workspace.png) | +![Главная Tracker](./docs/screenshots/02-overview.png) -| Обзор | Задачи | +| Вход | Список задач | | --- | --- | -| ![Обзор проекта](./docs/screenshots/03-overview.png) | ![Список задач](./docs/screenshots/04-tasks.png) | +| ![Экран входа](./docs/screenshots/01-sign-in.png) | ![Компактный список задач](./docs/screenshots/03-tasks.png) | -| Доска | Аналитика | +| Kanban | Аналитика потока | | --- | --- | -| ![Канбан-доска](./docs/screenshots/05-boards.png) | ![Аналитика проекта](./docs/screenshots/06-analytics.png) | +| ![Kanban-доска](./docs/screenshots/04-boards.png) | ![Аналитика потока задач](./docs/screenshots/05-analytics.png) |
-Карточка задачи +Карточка задачи и мобильный интерфейс + +![Карточка задачи](./docs/screenshots/06-task-detail.png) -![Карточка задачи](./docs/screenshots/07-task-detail.png) +| Мобильная главная | Мобильный список | +| --- | --- | +| ![Мобильная главная](./docs/screenshots/07-mobile-overview.png) | ![Мобильный список задач](./docs/screenshots/08-mobile-tasks.png) |
@@ -107,7 +112,7 @@ decisions: при его отказе API продолжает работать - production runtime выполняет `prisma migrate deploy`, а demo seed выключен по умолчанию; - сервисы запускаются без root, с read-only filesystem/capability restrictions; - plaintext Kubernetes Secret не хранится в репозитории; -- high-severity dependency audit на 2026-08-21: 0 известных уязвимостей. +- high-severity dependency audit на 2026-08-27: 0 известных уязвимостей. Не публикуйте уязвимости в issues. Используйте процедуру из [`SECURITY.md`](./SECURITY.md). Полная модель угроз и residual risks: [`docs/THREAT_MODEL.md`](./docs/THREAT_MODEL.md). @@ -116,7 +121,7 @@ decisions: при его отказе API продолжает работать | Область | Стек | | --- | --- | -| Web | Next.js 15, React 19, React Query 5, Zustand 5, dnd-kit | +| Web | Next.js 15, React 19, shadcn/ui, Radix UI, React Query 5, Zustand 5, dnd-kit | | API | NestJS 11, Passport JWT, Socket.IO 4, class-validator | | Data | PostgreSQL 16, Prisma 6, Redis 7 | | Tooling | TypeScript 5.9, ESLint 8, Node test runner, pnpm 9 | @@ -160,8 +165,8 @@ docker compose ps | Swagger (если включён) | `http://localhost:8080/api/docs` | | API readiness | `http://localhost:8080/api/health/ready` | -Прямые порты PostgreSQL, Redis, API и Web привязаны к loopback для локальной диагностики. Публичной -точкой входа считается Nginx на `8080`. +Прямые порты PostgreSQL, Redis и API привязаны к loopback для локальной диагностики. Web-контейнер +доступен только внутри Compose-сети: единственной браузерной точкой входа является Nginx на `8080`. Остановить окружение: @@ -241,6 +246,9 @@ pnpm db:migrate # development migration flow pnpm db:seed # explicit demo seed ``` +Документационные скриншоты снимаются с production-сборки через Nginx на `8080`; SVG/PNG логотип и +product preview находятся в [`apps/web/public`](./apps/web/public). + Полная проверка deployment manifests: ```bash @@ -315,6 +323,7 @@ Access token намеренно не сохраняется в browser storage. - [Threat model](./docs/THREAT_MODEL.md) - [Operations runbook](./docs/operations/runbook.md) - [Frontend audit](./docs/frontend-audit.md) +- [Обновление скриншотов](./docs/screenshots/README.md) - [Security policy](./SECURITY.md) - [Contributing](./CONTRIBUTING.md) - [Changelog](./CHANGELOG.md) diff --git a/apps/api/README.md b/apps/api/README.md index 029d6d8..f924ba8 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -20,6 +20,9 @@ NestJS backend для аутентификации, организаций, пр Полный список routes генерируется Swagger из запущенного приложения. +На edge login и invitation acceptance используют отдельный строгий limit. Refresh/logout проходят через +общий API edge limit; controller policies дополнительно ограничивают refresh/logout до 30 запросов в минуту. + ## Архитектурные границы ```text diff --git a/apps/web/README.md b/apps/web/README.md index 23062be..7cd116f 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,6 +1,6 @@ # Tracker Web -Next.js App Router frontend для overview, списка задач, kanban-доски, аналитики и task details. +Next.js App Router frontend для личного рабочего фокуса, списка задач, Kanban, аналитики потока и task details. ## Слои @@ -12,10 +12,29 @@ Next.js App Router frontend для overview, списка задач, kanban-д | `lib` | API client, React Query keys и realtime hooks | | `store` | in-memory session и persisted UI preferences | | `shared` | icons и общие frontend utilities | -| `@tracker/ui` | переиспользуемые UI primitives | +| `@tracker/ui` | shadcn/ui-примитивы на Radix UI, CVA и семантические theme tokens | Backend DTO импортируются из `@tracker/types`; page components не дублируют transport contracts. +## UI system + +Компоненты интерфейса находятся в `packages/ui` и следуют композиционной модели shadcn/ui. Radix UI +обеспечивает focus management и keyboard behavior для dialog, dropdown и tooltip; визуальные варианты +задаются через CVA, а классы объединяются через общий `cn`. Цвета экранов используют семантические +CSS variables (`background`, `foreground`, `primary`, `muted`, `destructive`), поэтому светлая и тёмная +темы не требуют локальных цветовых исключений. Конфигурация генератора находится в `components.json`. + +Production-знак Tracker хранится в [`public/logo.svg`](./public/logo.svg); PNG fallback, favicon и +product preview лежат рядом. Документационная галерея находится в [`../../docs/screenshots`](../../docs/screenshots). + +## Информационная архитектура + +- overview отвечает на вопрос «что делать дальше»: личная работа, разбор, review и свежие изменения; +- list оптимизирован для сканирования, а изменение полей выполняется в task detail; +- list и Kanban читают один набор фильтров/views из Zustand, но сами задачи остаются server state; +- create task открывается как dialog из shell, route toolbar, command palette или клавиши `C`; +- analytics не выводит health/capacity score без estimates и completion timestamps. + ## Session model - Access token хранится только в памяти Zustand store и исчезает при reload. @@ -39,11 +58,11 @@ Realtime hook подключается к namespace `/tasks`, подписыва ## Маршруты - `/` — overview; -- `/pages/my` — workspace entry; +- `/pages/my` — legacy redirect на overview; - `/tasks` — список и фильтры; - `/tasks/[taskId]` — карточка, комментарии и activity; - `/boards` — kanban; -- `/analytics` — распределение и показатели проекта. +- `/analytics` — измеримые показатели потока задач. Горячие клавиши: `C` для create task, `Ctrl+K` / `⌘K` для command palette. @@ -57,7 +76,8 @@ NEXT_PUBLIC_SOCKET_URL=http://localhost:3001 ``` `NEXT_PUBLIC_*` встраиваются в browser bundle и не должны содержать секреты. Для same-origin deployment -используйте `/api/v1` и публичный origin ingress/Nginx для Socket.IO. +используйте `NEXT_PUBLIC_API_URL=/api/v1` и пустой `NEXT_PUBLIC_SOCKET_URL`: browser подключится к +публичному origin ingress/Nginx. Значения выше предназначены для запуска Web/API без reverse proxy. ## Команды diff --git a/apps/web/components.json b/apps/web/components.json new file mode 100644 index 0000000..68e2181 --- /dev/null +++ b/apps/web/components.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { "config": "tailwind.config.ts", "css": "src/app/globals.css", "baseColor": "slate", "cssVariables": true, "prefix": "" }, + "aliases": { "components": "@tracker/ui", "utils": "@tracker/ui", "ui": "@tracker/ui", "lib": "@/lib", "hooks": "@/hooks" }, + "iconLibrary": "lucide" +} diff --git a/apps/web/package.json b/apps/web/package.json index 48d3393..dc8eefe 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -16,6 +16,7 @@ "@tracker/types": "workspace:*", "@tracker/ui": "workspace:*", "clsx": "^2.1.1", + "lucide-react": "^1.34.0", "next": "^15.5.23", "next-themes": "^0.4.6", "react": "^19.2.8", diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg index b46396b..4607ae5 100644 --- a/apps/web/public/favicon.svg +++ b/apps/web/public/favicon.svg @@ -1,17 +1,14 @@ - - - - - - - - - - - - + diff --git a/apps/web/public/logo.png b/apps/web/public/logo.png index f083ef1..73fede6 100644 Binary files a/apps/web/public/logo.png and b/apps/web/public/logo.png differ diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg new file mode 100644 index 0000000..d0fd7b4 --- /dev/null +++ b/apps/web/public/logo.svg @@ -0,0 +1,15 @@ + + Tracker + + + diff --git a/apps/web/public/preview.png b/apps/web/public/preview.png index 295c9fc..14af967 100644 Binary files a/apps/web/public/preview.png and b/apps/web/public/preview.png differ diff --git a/apps/web/src/app/analytics/page.tsx b/apps/web/src/app/analytics/page.tsx index 3c5c91d..16813fa 100644 --- a/apps/web/src/app/analytics/page.tsx +++ b/apps/web/src/app/analytics/page.tsx @@ -7,7 +7,7 @@ export default function AnalyticsPage() { return ( {(data) => } diff --git a/apps/web/src/app/boards/page.tsx b/apps/web/src/app/boards/page.tsx index 06d3220..fc9bf87 100644 --- a/apps/web/src/app/boards/page.tsx +++ b/apps/web/src/app/boards/page.tsx @@ -3,49 +3,34 @@ import type { Route } from "next"; import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; -import { Badge } from "@tracker/ui"; import { BoardFilter } from "@/features/board-filter/ui/board-filter"; import { TaskCreate } from "@/features/task-create/ui/task-create"; import { TaskViewBar } from "@/features/task-view/ui/task-view-bar"; -import { statusLabels, statusOrder, statusTone } from "@/lib/task-meta"; import { SkeletonBoard } from "@/shared/ui/skeleton-board"; -import { countByStatus } from "@/widgets/workspace-shell/lib/task-utils"; import { WorkspacePage } from "@/widgets/workspace-shell/ui/workspace-shell"; const KanbanBoard = dynamic( () => import("@/widgets/kanban-board/ui/kanban-board").then((module) => module.KanbanBoard), - { - ssr: false, - loading: () => , - }, + { ssr: false, loading: () => }, ); export default function BoardsPage() { const router = useRouter(); - return ( - + {(data) => (
- - - {data.selectedProjectId ? : null} - -
-

Поток проекта

- {statusOrder.map((status) => ( - - {statusLabels[status]} - {countByStatus(data.tasks, status)} - - ))} +
+
+ +
+ {data.selectedProjectId ? : null} +
+
+
- {data.isLoadingTasks ? ( - - ) : ( - router.push(`/tasks/${taskId}` as Route)} /> - )} + {data.isLoadingTasks ? : router.push(`/tasks/${taskId}` as Route)} />}
)}
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index b037afd..8836404 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -2,158 +2,60 @@ @tailwind components; @tailwind utilities; -:root { - color-scheme: light; - --surface: #f7f8fa; - --card: #ffffff; - --border: #dfe3e8; - --muted: #f0f2f5; - --text: #171a1f; - --accent: #5e6ad2; - --accent-soft: rgba(94, 106, 210, 0.12); - --success: #14805e; - --warning: #b86614; - --danger: #c73d52; - --focus-ring: rgba(94, 106, 210, 0.26); - --panel-shadow: 0 1px 2px rgb(16 24 40 / 0.04), 0 1px 5px rgb(16 24 40 / 0.03); -} - -html, -body { - min-height: 100%; - padding: 0; - margin: 0; -} - -html { - scroll-behavior: smooth; -} - -body { - min-height: 100vh; - color: var(--text); - background: var(--surface); - font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - overflow-x: hidden; -} - -* { - box-sizing: border-box; -} - -::selection { - background: rgba(37, 111, 104, 0.24); -} - -a { - color: inherit; - text-decoration: none; -} - -button, -input, -select, -textarea { - font: inherit; -} - -button { - -webkit-tap-highlight-color: transparent; -} - -button:not(:disabled) { - cursor: pointer; -} - -input, -select, -textarea { - color: var(--text); -} - -input::placeholder, -textarea::placeholder { - color: rgba(17, 24, 39, 0.38); -} - -select option { - color: #172033; - background: #ffffff; -} - -.text-muted { - color: color-mix(in srgb, var(--text) 58%, transparent); -} - -.font-mono { - font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; -} - -@keyframes tracker-rise { - from { - opacity: 0; - transform: translateY(14px); +@layer base { + :root { + --background: 220 20% 97%; --foreground: 222 47% 11%; + --card: 0 0% 100%; --card-foreground: 222 47% 11%; + --popover: 0 0% 100%; --popover-foreground: 222 47% 11%; + --primary: 239 84% 67%; --primary-foreground: 0 0% 100%; + --secondary: 220 17% 94%; --secondary-foreground: 222 47% 11%; + --muted: 220 17% 94%; --muted-foreground: 215 16% 47%; + --accent: 239 75% 96%; --accent-foreground: 239 70% 48%; + --destructive: 0 72% 51%; --destructive-foreground: 0 0% 98%; + --border: 220 16% 88%; --input: 220 16% 85%; --ring: 239 84% 67%; + --success: 158 64% 32%; --warning: 32 95% 44%; --radius: 0.75rem; } - to { - opacity: 1; - transform: translateY(0); - } -} - -main { - animation: tracker-rise 420ms ease-out both; -} - -@layer components { - .tracker-panel { - border: 1px solid var(--border); - background: var(--card); - box-shadow: var(--panel-shadow); + .dark { + --background: 222 47% 7%; --foreground: 210 40% 98%; + --card: 222 39% 10%; --card-foreground: 210 40% 98%; + --popover: 222 39% 10%; --popover-foreground: 210 40% 98%; + --primary: 239 84% 72%; --primary-foreground: 222 47% 8%; + --secondary: 217 25% 16%; --secondary-foreground: 210 40% 98%; + --muted: 217 25% 16%; --muted-foreground: 215 20% 65%; + --accent: 239 35% 20%; --accent-foreground: 239 90% 82%; + --destructive: 0 63% 45%; --destructive-foreground: 0 0% 98%; + --border: 217 25% 19%; --input: 217 25% 22%; --ring: 239 84% 72%; + --success: 158 64% 46%; --warning: 38 92% 52%; } - .tracker-subtle-panel { - border: 1px solid var(--border); - background: var(--muted); + * { @apply border-border; } + html { scroll-behavior: smooth; } + body { + @apply min-h-screen bg-background text-foreground antialiased; + font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + text-rendering: optimizeLegibility; } + ::selection { @apply bg-primary/20; } + :focus-visible { outline: none; } } -:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} - -::-webkit-scrollbar { - width: 12px; - height: 12px; -} - -::-webkit-scrollbar-track { - background: transparent; +@layer components { + .tracker-panel { @apply border bg-card text-card-foreground shadow-sm; } + .tracker-subtle-panel { @apply border bg-muted/50; } + .text-muted { @apply text-muted-foreground; } + .font-mono { font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; } } -::-webkit-scrollbar-thumb { - background: rgba(23, 32, 51, 0.16); - border: 3px solid transparent; - border-radius: 999px; - background-clip: content-box; +@keyframes tracker-rise { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } } -::-webkit-scrollbar-thumb:hover { - background: rgba(23, 32, 51, 0.28); - border: 3px solid transparent; - background-clip: content-box; -} +main { animation: tracker-rise 300ms ease-out both; } +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-thumb { background: hsl(var(--muted-foreground) / 0.25); border: 3px solid transparent; border-radius: 999px; background-clip: content-box; } @media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - scroll-behavior: auto !important; - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } + *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 01e74d0..58b9523 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -9,6 +9,7 @@ export const metadata: Metadata = { template: "%s · Tracker", }, description: "Командный трекер задач с досками, очередями и аналитикой рабочего потока.", + icons: { icon: "/favicon.svg" }, }; export default function RootLayout({ children }: { children: ReactNode }) { @@ -17,7 +18,7 @@ export default function RootLayout({ children }: { children: ReactNode }) { Перейти к содержимому diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx index e051d30..611d7f9 100644 --- a/apps/web/src/app/not-found.tsx +++ b/apps/web/src/app/not-found.tsx @@ -9,7 +9,7 @@ export default function NotFound() {

Возможно, адрес изменился или у вас больше нет доступа к этому разделу.

Вернуться на главную diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index c656324..b2aab20 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,13 +1,10 @@ "use client"; -import { useState } from "react"; import { TaskCreate } from "@/features/task-create/ui/task-create"; import { OverviewContent } from "@/widgets/overview/ui/overview-content"; import { WorkspacePage } from "@/widgets/workspace-shell/ui/workspace-shell"; export default function Page() { - const [focusSignal, setFocusSignal] = useState(0); - return ( {(data) => (
- {data.selectedProjectId ? : null} - setFocusSignal((value) => value + 1)} /> + {data.selectedProjectId ? : null} +
)}
diff --git a/apps/web/src/app/pages/my/page.tsx b/apps/web/src/app/pages/my/page.tsx index 9d2539f..96d51da 100644 --- a/apps/web/src/app/pages/my/page.tsx +++ b/apps/web/src/app/pages/my/page.tsx @@ -1,112 +1,5 @@ -"use client"; +import { redirect } from "next/navigation"; -import Link from "next/link"; -import { ActivityIcon, BoardIcon, CheckCircleIcon, ListIcon, ProjectsIcon, QueueIcon } from "@/shared/ui/tracker-icons"; -import { WorkspacePage } from "@/widgets/workspace-shell/ui/workspace-shell"; - -const trackerSections = [ - { - icon: ListIcon, - title: "Задачи", - description: "Личный и командный список для triage, поиска, фильтрации и быстрого перехода в карточку.", - }, - { - icon: ProjectsIcon, - title: "Проекты и портфели", - description: "Контекст работы команды: выбранный проект управляет задачами, досками и отчётами.", - }, - { - icon: QueueIcon, - title: "Очереди", - description: "Фокусные подборки: мои задачи, без исполнителя, ревью и другие рабочие потоки.", - }, - { - icon: BoardIcon, - title: "Доски задач", - description: "Kanban с drag-and-drop сменой статусов и синхронизацией через API.", - }, - { - icon: ActivityIcon, - title: "Дашборды и отчёты", - description: "Сводка прогресса, распределение по статусам и нагрузка команды.", - }, - { - icon: CheckCircleIcon, - title: "История", - description: "Изменения задач и активность проекта помогают быстро восстановить контекст.", - }, -]; - -export default function MyPage() { - return ( - - {(data) => ( -
-
-
-

Tracker Pro

-

- Рабочий контур для задач, досок и решений команды. -

-

- Здесь собраны маршруты трекера, проектный контекст и быстрый вход в рабочие сценарии. После входа вы всегда попадаете сюда, - чтобы выбрать следующий шаг без лишнего шума. -

-
- - Перейти к задачам - - - Открыть доску - -
-
- - -
- -
-

Возможности

-
- {trackerSections.map((section) => { - const Icon = section.icon; - - return ( -
-
- -
-

{section.title}

-

{section.description}

-
- ); - })} -
-
-
- )} -
- ); +export default function LegacyMyPage() { + redirect("/"); } diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx index d148000..f05391e 100644 --- a/apps/web/src/app/providers.tsx +++ b/apps/web/src/app/providers.tsx @@ -55,7 +55,7 @@ export function Providers({ children }: PropsWithChildren) { }, [completeAuthBootstrap, hydrated, setSession]); return ( - + {children} ); diff --git a/apps/web/src/app/tasks/page.tsx b/apps/web/src/app/tasks/page.tsx index daa2a88..65dbc77 100644 --- a/apps/web/src/app/tasks/page.tsx +++ b/apps/web/src/app/tasks/page.tsx @@ -1,9 +1,7 @@ "use client"; import Link from "next/link"; -import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import clsx from "clsx"; import { Badge, Button } from "@tracker/ui"; import { BoardFilter } from "@/features/board-filter/ui/board-filter"; import { TaskCreate } from "@/features/task-create/ui/task-create"; @@ -11,131 +9,63 @@ import { TaskViewBar } from "@/features/task-view/ui/task-view-bar"; import { apiClient } from "@/lib/api-client"; import { BoardIcon, PlusIcon } from "@/shared/ui/tracker-icons"; import { SkeletonBoard } from "@/shared/ui/skeleton-board"; -import { useUiStore } from "@/store/use-ui-store"; import { TasksTable } from "@/widgets/tasks-table/ui/tasks-table"; -import { countByStatus, filterTasksByScope } from "@/widgets/workspace-shell/lib/task-utils"; import { getAttentionCounts } from "@/widgets/workspace-shell/lib/workspace-insights"; -import { type TaskScope, WorkspacePage } from "@/widgets/workspace-shell/ui/workspace-shell"; - -const scopes: Array<{ id: TaskScope; label: string }> = [ - { id: "all", label: "Все" }, - { id: "mine", label: "Мои" }, - { id: "unassigned", label: "Без исполнителя" }, - { id: "review", label: "Ревью" }, -]; +import { WorkspacePage } from "@/widgets/workspace-shell/ui/workspace-shell"; function ProjectStarter({ projectId }: { projectId: string }) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: async () => { const templates = [ - { - title: "Разобрать входящие задачи", - description: "Соберите новые запросы, назначьте владельцев и переведите готовые элементы в работу.", - priority: "HIGH", - }, - { - title: "Описать критерии готовности", - description: "Добавьте ожидаемый результат, ограничения и ссылки, чтобы задачу можно было принять без уточнений.", - priority: "MEDIUM", - }, - { - title: "Проверить задачу на ревью", - description: "Пример review-очереди: задача ждёт решения или подтверждения результата.", - status: "REVIEW", - priority: "MEDIUM", - }, - { - title: "Закрыть первый результат", - description: "Пример закрытой задачи для проверки аналитики и прогресса проекта.", - status: "DONE", - priority: "LOW", - }, + { title: "Разобрать входящие задачи", description: "Назначьте владельцев и уточните следующий шаг.", priority: "HIGH" }, + { title: "Описать критерии готовности", description: "Зафиксируйте ожидаемый результат и ограничения.", priority: "MEDIUM" }, + { title: "Проверить результат", description: "Задача ждёт решения или подтверждения.", status: "REVIEW", priority: "MEDIUM" }, ]; - await Promise.all(templates.map((task) => apiClient.createTask(projectId, task))); }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["tasks", projectId] }); - }, + onSuccess: async () => queryClient.invalidateQueries({ queryKey: ["tasks", projectId] }), }); return ( -
-
-
-

Проект пока пуст

-

Создайте первую задачу или добавьте небольшой стартовый набор.

-
- -
+
+

Проект пока пуст

+

Создайте первую задачу или добавьте три примера рабочего потока.

+
); } export default function TasksPage() { - const [scope, setScope] = useState("all"); - const createTaskSignal = useUiStore((state) => state.createTaskSignal); - return ( - + {(data) => { - const scopedTasks = filterTasksByScope(data.tasks, scope, data.userId); - const attention = getAttentionCounts(scopedTasks); - + const attention = getAttentionCounts(data.tasks); return (
- - - - {data.selectedProjectId ? ( - - ) : null} - {data.selectedProjectId && data.tasks.length === 0 ? : null} -
-
-
- {scopes.map((item) => ( - - ))} - {scopedTasks.length} задач -
- -
- {attention.unassigned > 0 ? {attention.unassigned} без исполнителя : null} - {attention.stale > 0 ? {attention.stale} без движения : null} - {countByStatus(scopedTasks, "DONE")} закрыто - - - Доска - +
+ +
+ + {data.selectedProjectId ? : null}
+
+
- {data.isLoadingTasks ? ( -
- -
- ) : ( - - )} + {data.selectedProjectId && data.tasks.length === 0 ? : null} + +
+
+

{data.tasks.length} задач

+ {attention.unassigned > 0 ? {attention.unassigned} без исполнителя : null} + {attention.stale > 0 ? {attention.stale} без движения : null} +
+ {data.isLoadingTasks ?
: }
); diff --git a/apps/web/src/features/auth/ui/sign-in-form.tsx b/apps/web/src/features/auth/ui/sign-in-form.tsx index 90688c1..bcbffc7 100644 --- a/apps/web/src/features/auth/ui/sign-in-form.tsx +++ b/apps/web/src/features/auth/ui/sign-in-form.tsx @@ -4,8 +4,8 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { useMutation } from "@tanstack/react-query"; import { Button, Card, Input } from "@tracker/ui"; +import { LockKeyhole } from "lucide-react"; import { apiClient } from "@/lib/api-client"; -import { ActivityIcon, BoardIcon, CheckCircleIcon, SparkIcon } from "@/shared/ui/tracker-icons"; import { useUiStore } from "@/store/use-ui-store"; export function SignInForm() { @@ -17,119 +17,88 @@ export function SignInForm() { const mutation = useMutation({ mutationFn: () => apiClient.login(email, password), onSuccess: (session) => { - setSession({ - accessToken: session.tokens.accessToken, - user: session.user, - }); + setSession({ accessToken: session.tokens.accessToken, user: session.user }); router.replace("/"); }, }); return ( -
-
-
-
-
-
-
- - Production tracker workspace -
-

- Управляйте задачами без операционного тумана. -

-

- Доска, фокус, список, аналитика и realtime-обновления в одном рабочем контуре. Без demo-слоя, без локального фейкового хранилища. -

+
+ +
+ + + +
+

Tracker

+

Рабочее пространство

+
-
- {[ - { icon: BoardIcon, label: "Kanban" }, - { icon: ActivityIcon, label: "Realtime" }, - { icon: CheckCircleIcon, label: "API-first" }, - ].map((item) => { - const Icon = item.icon; - - return ( -
- -

{item.label}

-
- ); - })} -
-
+

Вход

+

+ Используйте учётные данные рабочего пространства. +

-
- -

Tracker Pro

-

Вход в рабочее пространство

-

- Демо-доступ уже подставлен, чтобы быстро проверить весь поток от фронтенда до API и базы. -

+
{ + event.preventDefault(); + mutation.mutate(); + }} + > + - { - event.preventDefault(); - mutation.mutate(); + - + {mutation.error ? ( +

+ Не удалось войти. Проверьте email и пароль или повторите попытку через минуту. +

+ ) : null} - {mutation.error ? ( -

- Не удалось войти. Проверьте, что API и seed-данные запущены. -

- ) : null} + +
- - -
-
-
+

+ + Refresh-сессия защищена HttpOnly cookie +

+
); } diff --git a/apps/web/src/features/board-filter/ui/board-filter.tsx b/apps/web/src/features/board-filter/ui/board-filter.tsx index 7680166..7f27493 100644 --- a/apps/web/src/features/board-filter/ui/board-filter.tsx +++ b/apps/web/src/features/board-filter/ui/board-filter.tsx @@ -1,12 +1,14 @@ "use client"; +import { useState } from "react"; import type { TaskPriority, TaskStatus, UserSummaryDto } from "@tracker/types"; -import { Button, Input, Select } from "@tracker/ui"; +import { Badge, Button, Input, Select, cn } from "@tracker/ui"; import { TASK_PRIORITY_OPTIONS, TASK_STATUS_OPTIONS } from "@/shared/config/task-options"; -import { SearchIcon } from "@/shared/ui/tracker-icons"; +import { FilterIcon, SearchIcon } from "@/shared/ui/tracker-icons"; import { useUiStore } from "@/store/use-ui-store"; export function BoardFilter({ users }: { users: UserSummaryDto[] }) { + const [filtersOpen, setFiltersOpen] = useState(false); const search = useUiStore((state) => state.search); const status = useUiStore((state) => state.status); const priority = useUiStore((state) => state.priority); @@ -17,23 +19,31 @@ export function BoardFilter({ users }: { users: UserSummaryDto[] }) { const setAssigneeId = useUiStore((state) => state.setAssigneeId); const hasFilters = search.trim().length > 0 || status !== "ALL" || priority !== "ALL" || assigneeId !== "ALL"; + const activeCount = [status !== "ALL", priority !== "ALL", assigneeId !== "ALL"].filter(Boolean).length; return ( -
+
+ + +
setPriority(event.target.value as TaskPriority | "ALL")} - className="border-black/[0.08] bg-[#f8fafc] py-2.5 text-sm" + className="bg-background text-sm" > {TASK_PRIORITY_OPTIONS.map((option) => (