Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 70 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# DayFlow

Персональный органайзер знаний: воркспейсы, карточки (заметки, ссылки, чеклисты), инструменты, трекинг обучения и прогресса.
Персональный органайзер знаний: воркспейсы, карточки (заметки, ссылки, чеклисты), роадмапы, инструменты, совместное редактирование, трекинг обучения и прогресса.

## Стек

Expand All @@ -10,40 +10,84 @@

## Возможности

- **Воркспейсы** — доски для тем/проектов с колонками и беклогом
- **Карточки** — три типа: заметка, ссылка (с превью), чеклист
- **Хаб (библиотека)** — все карточки пользователя без привязки к воркспейсу
- **Инструменты** — ссылки/ресурсы, привязанные к воркспейсу или хабу; поиск по названию и тегам
### Воркспейсы
- Доски для тем/проектов с колонками и беклогом
- Эмодзи-иконка для каждого воркспейса
- Два режима: **Доска** и **Роадмап** (табы)
- Цвет колонок (хедер + бордер)
- Скрытие выполненных карточек (кнопка-глазик, состояние в БД)
- Пин (закрепление воркспейсов)

### Карточки
- Три типа: **заметка**, **ссылка**, **чеклист**
- Теги, статус обучения, порядок (drag & drop между колонками)
- **Конспект** — markdown-конспект в каждой карточке (все типы), редактор через модалку
- **AI-промпт** — генерация промпта для ИИ по содержимому карточки
- Модалка «Все конспекты» — просмотр и экспорт в `.md`

### Роадмап
- Один роадмап на воркспейс — дерево узлов (до 4 уровней вложенности)
- Создание из вложенного текста (вставка + парсинг отступов)
- Добавление / редактирование / удаление / реордер узлов
- Прогресс-бар (done/total)
- LLM-промпт для генерации роадмапа и промпт по конкретному узлу

### Совместное редактирование
- **Приглашения** — владелец генерирует invite-ссылку (`/invite/:token`), другие принимают
- **Участники** — список членов воркспейса, удаление участника (только владелец)
- **Блокировка редактирования** — один редактирует, остальные в read-only; heartbeat каждые 15с
- **Передача лока** — передать права редактирования другому участнику
- **Индикатор** — замочек + аватар редактирующего в хедере
- **Smart-синхронизация** — фоновый poll каждые 10с с точечным патчем изменившихся полей (без полного ре-рендера)

### Хаб (библиотека)
- Все карточки пользователя без привязки к воркспейсу
- Пагинация, фильтры, сортировка

### Инструменты
- Ссылки/ресурсы, привязанные к воркспейсу или хабу
- Иконка, описание, теги
- Поиск и фильтрация по названию и тегам

### Обучение
- Три статуса: «повторить», «остались вопросы», «углубить»
- Вью с группировкой по воркспейсам, сворачиваемые секции

### Прочее
- **Теги** — фильтрация и навигация по тегам карточек
- **Обучение** — три статуса: «повторить», «остались вопросы», «углубить»; отдельные вью с группировкой по воркспейсам
- **Drag & Drop** — перетаскивание карточек между колонками
- **Поиск** — по заголовкам и тегам (Ctrl+K — быстрое добавление)
- **Скрытие выполненных** — кнопка-глазик в колонке, состояние хранится в БД
- **Сворачивание секций** — в обучении и инструментах секции по воркспейсам сворачиваются
- **Drag & Drop** — перетаскивание карточек между колонками и беклогом
- **Статистика** — публичная страница `/user/:id` с прогрессом по воркспейсам
- **7 тем оформления** — Light, Dark, Old Money, Nord, Solarized Dark, Full Moon, Old Money 2
- **Авторизация** — email + пароль, httpOnly cookie сессия
- **Профиль** — смена аватара (URL)
- **7 тем оформления** — Light, Dark (Obsidian), Old Money, Old Money II, Nord, Solarized Dark, Full Moon; переключение light/dark с запоминанием предпочтений
- **Авторизация** — email + пароль (argon2), httpOnly cookie сессия
- **Rate limiting** — ограничение запросов по IP/пользователю

## Структура

```
├── client/ # Vue 3 SPA
│ ├── src/
│ │ ├── components/ # card/, common/, workspace/, toolbox/
│ │ ├── views/ # Home, Auth, Library, Workspace, Profile,
│ │ │ # UserStats, Tags, Tools, Learning
│ │ ├── composables/ # useCardActions, useCardForm, useInlineEdit
│ │ ├── stores/ # Pinia: auth, workspace, cards, theme
│ │ ├── components/ # card/, common/, workspace/, toolbox/, roadmap/
│ │ ├── views/ # Home, Auth, Library, Workspace (Board + Roadmap),
│ │ │ # Profile, UserStats, Tags, Tools, Learning, Invite
│ │ ├── composables/ # useCardActions, useCardForm, useInlineEdit,
│ │ │ # useCardGrouping
│ │ ├── stores/ # Pinia: auth, workspace, roadmap, cards, theme
│ │ ├── graphql/ # queries, mutations, types
│ │ └── lib/ # apollo, graphql-error, constants, utils
│ │ └── lib/ # apollo, graphql-error, constants, utils,
│ │ # patch-workspace, card-payload
│ └── public/
├── server/ # GraphQL API
│ ├── src/
│ │ ├── resolvers/ # auth, workspace, column, card, tool, user-stats
│ │ ├── resolvers/ # auth, workspace, column, card, tool,
│ │ │ # roadmap, user-stats
│ │ ├── schema/ # schema.graphql
│ │ └── lib/ # prisma, auth, context, errors, constants
│ │ └── lib/ # prisma, auth, context, errors, constants,
│ │ # lock-middleware, workspace-access, rate-limiter,
│ │ # dataloaders
│ └── prisma/ # schema.prisma
└── shared/ # Общие типы и ErrorCodes (dayflow-shared)
└── shared/ # Общие типы, ErrorCodes, лимиты (dayflow-shared)
```

## Модели данных
Expand All @@ -52,10 +96,13 @@
|--------|----------|
| **User** | email, пароль (argon2), аватар |
| **Session** | httpOnly cookie сессии |
| **Workspace** | доска с колонками, карточками и инструментами; пин |
| **Column** | колонка воркспейса; порядок, скрытие выполненных |
| **Card** | заметка / ссылка / чеклист; теги, статус обучения, порядок |
| **Workspace** | доска с колонками, карточками, инструментами; пин, иконка (эмодзи), invite-токен, editingBy (лок) |
| **WorkspaceMember** | участник воркспейса (userId, joinedAt) |
| **Column** | колонка воркспейса; порядок, скрытие выполненных, цвет |
| **Card** | заметка / ссылка / чеклист; теги, статус обучения, порядок, конспект (в payload) |
| **Tool** | инструмент (ссылка + описание + иконка + теги) |
| **Roadmap** | роадмап воркспейса (один на воркспейс); title, sourceText |
| **RoadmapNode** | узел роадмапа; parent/children, order, done |

## Обработка ошибок

Expand Down
1 change: 0 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
- загрузка фото из буфера в виде карточки (яндекс диск)
- AI-саммари — автоматическое резюме по карточке
- AI-саммари конкретно по ссылке пишет в конспект (карточка типа ссылка) кнопка получить краткую выжимку
- совместный доступ к воркспейсам
- Новый вью (пространство как в миро)
- Отображать превью ссылок (если тип ссылка)

Expand Down
16 changes: 16 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 15 additions & 7 deletions client/src/components/card/CardItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ const props = withDefaults(
isBacklog?: boolean;
/** Отключить drag-курсор (для Tags view и т.п.) */
static?: boolean;
/** Режим только чтение (чужой лок) */
readOnly?: boolean;
}>(),
{ isBacklog: false, static: false }
{ isBacklog: false, static: false, readOnly: false }
);

const emit = defineEmits<{
Expand Down Expand Up @@ -173,6 +175,7 @@ function copyAiPrompt() {
<template>
<div class="card-item relative group" :data-card-id="card.id">
<button
v-if="!readOnly"
type="button"
class="absolute -top-2 right-[9px] z-10 icon-btn-edit rounded-full bg-surface border border-border shadow-sm card-action transition-colors duration-150 pointer-events-none group-hover:pointer-events-auto"
title="Редактировать"
Expand All @@ -194,13 +197,14 @@ function copyAiPrompt() {
<div class="flex items-center gap-2 p-3 pb-0">
<button
type="button"
@click="cardActions.toggleDone()"
@click="!readOnly && cardActions.toggleDone()"
class="w-4 h-4 rounded flex-center border transition-colors shrink-0"
:class="
:class="[
readOnly ? 'pointer-events-none' : '',
card.done
? 'bg-success border-success text-on-primary'
: 'border-border hover:border-success'
"
: 'border-border hover:border-success',
]"
>
<span v-if="card.done" class="i-lucide-check text-xs" />
</button>
Expand All @@ -215,9 +219,10 @@ function copyAiPrompt() {

<div
ref="titleContainerRef"
class="flex-1 min-w-0 flex items-center cursor-text overflow-hidden"
class="flex-1 min-w-0 flex items-center overflow-hidden"
:class="readOnly ? 'cursor-default' : 'cursor-text'"
:title="card.title || '(без названия)'"
@dblclick.prevent="startEditTitle()"
@dblclick.prevent="!readOnly && startEditTitle()"
>
<template v-if="isEditingTitle">
<input
Expand Down Expand Up @@ -251,6 +256,7 @@ function copyAiPrompt() {
<CardChecklist
v-else-if="parsed.type === CARD_TYPES.CHECKLIST"
:payload="parsed.payload"
:disabled="readOnly"
@toggle-item="cardActions.toggleChecklistItem"
/>
</template>
Expand All @@ -267,6 +273,7 @@ function copyAiPrompt() {
<p v-else class="text-xs text-muted/50 italic leading-5">Конспект...</p>
</div>
<CardActionBtn
v-if="!readOnly"
icon="i-lucide-pencil"
title="Редактировать конспект"
@click="showSummaryModal = true"
Expand All @@ -289,6 +296,7 @@ function copyAiPrompt() {
</a>
<span class="flex-1" />
<CardActionBtn
v-if="!readOnly"
:icon="promptCopied ? 'i-lucide-check' : 'i-lucide-sparkles'"
title="Промпт для ИИ: разобраться в теме"
:active="promptCopied"
Expand Down
24 changes: 5 additions & 19 deletions client/src/components/card/CardNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { ref, computed } from 'vue';
import type { NotePayload } from '@/lib/card-payload';
import { linkify } from '@/lib/utils';
import { toast } from 'vue-sonner';
import CardActionBtn from './CardActionBtn.vue';
import CopyButton from '@/components/common/CopyButton.vue';
import {
DialogRoot,
DialogPortal,
Expand All @@ -18,18 +18,9 @@ const props = defineProps<{
}>();

const showContentModal = ref(false);
const copied = ref(false);

const contentHtml = computed(() => linkify(props.payload.content ?? ''));
const hasLongContent = computed(() => (props.payload.content?.length ?? 0) > 100);

function copyContent() {
navigator.clipboard.writeText(props.payload.content ?? '').then(() => {
copied.value = true;
toast.success('Текст скопирован');
setTimeout(() => (copied.value = false), 2000);
});
}
</script>

<template>
Expand Down Expand Up @@ -60,16 +51,11 @@ function copyContent() {
<div class="dialog-header">
<DialogTitle class="dialog-title">Текст заметки</DialogTitle>
<div class="flex items-center gap-1">
<button
type="button"
class="icon-btn-ghost transition-colors"
:class="copied && 'text-success!'"
<CopyButton
:text="payload.content ?? ''"
success-message="Текст скопирован"
title="Скопировать текст"
@click="copyContent"
>
<span v-if="copied" class="i-lucide-check" />
<span v-else class="i-lucide-copy" />
</button>
/>
<DialogClose class="icon-btn-close">
<span class="i-lucide-x" />
</DialogClose>
Expand Down
Loading