Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92ee087e5d | ||
|
|
37f2b39bd2 | ||
|
|
c1fab8feea | ||
|
|
0a5e8a62eb | ||
|
|
4e2cee6625 | ||
|
|
222f528b5e |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,7 +1,4 @@
|
|||||||
/.venv/
|
/.venv/
|
||||||
/.idea
|
/.idea
|
||||||
__pycache__/
|
/src/db/*.db
|
||||||
*.db
|
.env
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
.vscode/
|
|
||||||
17
README.md
17
README.md
@@ -23,7 +23,6 @@ ___
|
|||||||
- 🔥 Установка приоритетов и дедлайнов
|
- 🔥 Установка приоритетов и дедлайнов
|
||||||
- 🔔 Напоминания и уведомления
|
- 🔔 Напоминания и уведомления
|
||||||
- ⚙️ Асинхронная обработка задач
|
- ⚙️ Асинхронная обработка задач
|
||||||
- 📄 Пагинация и фильтрация задач с поддержкой limit/offset
|
|
||||||
- 💡 Современный и интуитивно понятный интерфейс
|
- 💡 Современный и интуитивно понятный интерфейс
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -50,22 +49,6 @@ ___
|
|||||||
### Frontend
|
### Frontend
|
||||||
- **React.js**
|
- **React.js**
|
||||||
- **Vite**
|
- **Vite**
|
||||||
- **Tailwind CSS**
|
|
||||||
- **Material Design 3**
|
|
||||||
- **Custom Design System** (Mirage Palette + Neon)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Design System
|
|
||||||
|
|
||||||
Проект использует собственную систему дизайна:
|
|
||||||
- **Mirage Color Palette** - профессиональная темная сине-синяя тема
|
|
||||||
- **Material Design 3** - современные UI паттерны
|
|
||||||
- **Neon Cyberpunk** - уникальные неоновые акценты
|
|
||||||
|
|
||||||
**Документация:**
|
|
||||||
- [Frontend Guide](taskncoffee-app/README.md) - документация frontend
|
|
||||||
- [Styles Cheatsheet](taskncoffee-app/docs/STYLES_CHEATSHEET.md) - быстрая справка
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,6 @@ ___
|
|||||||
- `GET /users` — Получить список всех пользователей
|
- `GET /users` — Получить список всех пользователей
|
||||||
- `GET /users/{user_id}` — Получить конкретного пользователя
|
- `GET /users/{user_id}` — Получить конкретного пользователя
|
||||||
- `GET /users/{user_id}/tasks` — Получить задачи пользователя
|
- `GET /users/{user_id}/tasks` — Получить задачи пользователя
|
||||||
- **Query параметры:**
|
|
||||||
- `limit` (int, опционально) — Максимальное количество задач для возврата
|
|
||||||
- `offset` (int, опционально, по умолчанию 0) — Количество задач для пропуска
|
|
||||||
- **Пример:** `GET /users/1/tasks?limit=10&offset=20`
|
|
||||||
- `POST /users` — Создать нового пользователя
|
- `POST /users` — Создать нового пользователя
|
||||||
- `PUT /users/{user_id}` — Обновить данные пользователя
|
- `PUT /users/{user_id}` — Обновить данные пользователя
|
||||||
- `PATCH /users/{user_id}` — Частично обновить данные пользователя
|
- `PATCH /users/{user_id}` — Частично обновить данные пользователя
|
||||||
|
|||||||
463
poetry.lock
generated
463
poetry.lock
generated
@@ -74,40 +74,209 @@ test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "except
|
|||||||
trio = ["trio (>=0.26.1)"]
|
trio = ["trio (>=0.26.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "argon2-cffi"
|
||||||
version = "4.0.1"
|
version = "23.1.0"
|
||||||
description = "Modern password hashing for your software and your servers"
|
description = "Argon2 for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"},
|
||||||
|
{file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
argon2-cffi-bindings = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["argon2-cffi[tests,typing]", "tox (>4)"]
|
||||||
|
docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"]
|
||||||
|
tests = ["hypothesis", "pytest"]
|
||||||
|
typing = ["mypy"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi-bindings"
|
||||||
|
version = "21.2.0"
|
||||||
|
description = "Low-level CFFI bindings for Argon2"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"},
|
{file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"},
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"},
|
||||||
{file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"},
|
{file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"},
|
||||||
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"},
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"},
|
||||||
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"},
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"},
|
||||||
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"},
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"},
|
||||||
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"},
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"},
|
||||||
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"},
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"},
|
||||||
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"},
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"},
|
||||||
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"},
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"},
|
||||||
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"},
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"},
|
||||||
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"},
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"},
|
||||||
{file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"},
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = ">=1.0.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["cogapp", "pre-commit", "pytest", "wheel"]
|
||||||
|
tests = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt"
|
||||||
|
version = "4.3.0"
|
||||||
|
description = "Modern password hashing for your software and your servers"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"},
|
||||||
|
{file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"},
|
||||||
|
{file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"},
|
||||||
|
{file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"},
|
||||||
|
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"},
|
||||||
|
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"},
|
||||||
|
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"},
|
||||||
|
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"},
|
||||||
|
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"},
|
||||||
|
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"},
|
||||||
|
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"},
|
||||||
|
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"},
|
||||||
|
{file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
tests = ["pytest (>=3.2.1,!=3.3.0)"]
|
tests = ["pytest (>=3.2.1,!=3.3.0)"]
|
||||||
typecheck = ["mypy"]
|
typecheck = ["mypy"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "1.17.1"
|
||||||
|
description = "Foreign Function Interface for Python calling C code."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
|
||||||
|
{file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
|
||||||
|
{file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
|
||||||
|
{file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
|
||||||
|
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
|
||||||
|
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pycparser = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.2.1"
|
version = "8.2.1"
|
||||||
@@ -129,12 +298,72 @@ version = "0.4.6"
|
|||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
groups = ["main", "test"]
|
groups = ["main"]
|
||||||
|
markers = "platform_system == \"Windows\""
|
||||||
files = [
|
files = [
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""}
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "45.0.3"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710"},
|
||||||
|
{file = "cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab"},
|
||||||
|
{file = "cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e"},
|
||||||
|
{file = "cryptography-45.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c"},
|
||||||
|
{file = "cryptography-45.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19"},
|
||||||
|
{file = "cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""]
|
||||||
|
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||||
|
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""]
|
||||||
|
pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||||
|
sdist = ["build (>=1.0.0)"]
|
||||||
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
|
test = ["certifi (>=2024)", "cryptography-vectors (==45.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||||
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dnspython"
|
name = "dnspython"
|
||||||
@@ -175,14 +404,14 @@ idna = ">=2.0.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.115.14"
|
version = "0.115.12"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"},
|
{file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"},
|
||||||
{file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"},
|
{file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -194,6 +423,49 @@ typing-extensions = ">=4.8.0"
|
|||||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi-users"
|
||||||
|
version = "14.0.1"
|
||||||
|
description = "Ready-to-use and customizable users management for FastAPI"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "fastapi_users-14.0.1-py3-none-any.whl", hash = "sha256:074df59676dccf79412d2880bdcb661ab1fabc2ecec1f043b4e6a23be97ed9e1"},
|
||||||
|
{file = "fastapi_users-14.0.1.tar.gz", hash = "sha256:8c032b3a75c6fb2b1f5eab8ffce5321176e9916efe1fe93e7c15ee55f0b02236"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
email-validator = ">=1.1.0,<2.3"
|
||||||
|
fastapi = ">=0.65.2"
|
||||||
|
fastapi-users-db-sqlalchemy = {version = ">=7.0.0", optional = true, markers = "extra == \"sqlalchemy\""}
|
||||||
|
makefun = ">=1.11.2,<2.0.0"
|
||||||
|
pwdlib = {version = "0.2.1", extras = ["argon2", "bcrypt"]}
|
||||||
|
pyjwt = {version = "2.10.1", extras = ["crypto"]}
|
||||||
|
python-multipart = "0.0.20"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
beanie = ["fastapi-users-db-beanie (>=4.0.0)"]
|
||||||
|
oauth = ["httpx-oauth (>=0.13)"]
|
||||||
|
redis = ["redis (>=4.3.3,<6.0.0)"]
|
||||||
|
sqlalchemy = ["fastapi-users-db-sqlalchemy (>=7.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi-users-db-sqlalchemy"
|
||||||
|
version = "7.0.0"
|
||||||
|
description = "FastAPI Users database adapter for SQLAlchemy"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "fastapi_users_db_sqlalchemy-7.0.0-py3-none-any.whl", hash = "sha256:5fceac018e7cfa69efc70834dd3035b3de7988eb4274154a0dbe8b14f5aa001e"},
|
||||||
|
{file = "fastapi_users_db_sqlalchemy-7.0.0.tar.gz", hash = "sha256:6823eeedf8a92f819276a2b2210ef1dcfd71fe8b6e37f7b4da8d1c60e3dfd595"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
fastapi-users = ">=10.0.0"
|
||||||
|
sqlalchemy = {version = ">=2.0.0,<2.1.0", extras = ["asyncio"]}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.2.2"
|
version = "3.2.2"
|
||||||
@@ -291,15 +563,15 @@ files = [
|
|||||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "makefun"
|
||||||
version = "2.1.0"
|
version = "1.16.0"
|
||||||
description = "brain-dead simple config-ini parsing"
|
description = "Small library to dynamically create python functions."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = "*"
|
||||||
groups = ["test"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
|
{file = "makefun-1.16.0-py2.py3-none-any.whl", hash = "sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4"},
|
||||||
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
{file = "makefun-1.16.0.tar.gz", hash = "sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -394,61 +666,47 @@ files = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "pwdlib"
|
||||||
version = "25.0"
|
version = "0.2.1"
|
||||||
description = "Core utilities for Python packages"
|
description = "Modern password hashing for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["test"]
|
|
||||||
files = [
|
|
||||||
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
|
|
||||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "passlib"
|
|
||||||
version = "1.7.4"
|
|
||||||
description = "comprehensive password hashing framework supporting over 30 schemes"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
|
{file = "pwdlib-0.2.1-py3-none-any.whl", hash = "sha256:1823dc6f22eae472b540e889ecf57fd424051d6a4023ec0bcf7f0de2d9d7ef8c"},
|
||||||
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
|
{file = "pwdlib-0.2.1.tar.gz", hash = "sha256:9a1d8a8fa09a2f7ebf208265e55d7d008103cbdc82b9e4902ffdd1ade91add5e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
argon2-cffi = {version = ">=23.1.0,<24", optional = true, markers = "extra == \"argon2\""}
|
||||||
|
bcrypt = {version = ">=4.1.2,<5", optional = true, markers = "extra == \"bcrypt\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
argon2 = ["argon2-cffi (>=18.2.0)"]
|
argon2 = ["argon2-cffi (>=23.1.0,<24)"]
|
||||||
bcrypt = ["bcrypt (>=3.1.0)"]
|
bcrypt = ["bcrypt (>=4.1.2,<5)"]
|
||||||
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
|
||||||
totp = ["cryptography"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pycparser"
|
||||||
version = "1.6.0"
|
version = "2.22"
|
||||||
description = "plugin and hook calling mechanisms for python"
|
description = "C parser in Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.8"
|
||||||
groups = ["test"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
|
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||||
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
|
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
dev = ["pre-commit", "tox"]
|
|
||||||
testing = ["coverage", "pytest", "pytest-benchmark"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.11.7"
|
version = "2.11.5"
|
||||||
description = "Data validation using Python type hints"
|
description = "Data validation using Python type hints"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"},
|
{file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"},
|
||||||
{file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"},
|
{file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -597,21 +855,6 @@ gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
|
|||||||
toml = ["tomli (>=2.0.1)"]
|
toml = ["tomli (>=2.0.1)"]
|
||||||
yaml = ["pyyaml (>=6.0.1)"]
|
yaml = ["pyyaml (>=6.0.1)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pygments"
|
|
||||||
version = "2.19.2"
|
|
||||||
description = "Pygments is a syntax highlighting package written in Python."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
groups = ["test"]
|
|
||||||
files = [
|
|
||||||
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
|
|
||||||
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
windows-terminal = ["colorama (>=0.4.6)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyjwt"
|
name = "pyjwt"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
@@ -624,53 +867,15 @@ files = [
|
|||||||
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
|
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
crypto = ["cryptography (>=3.4.0)"]
|
crypto = ["cryptography (>=3.4.0)"]
|
||||||
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||||
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||||
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytest"
|
|
||||||
version = "8.4.1"
|
|
||||||
description = "pytest: simple powerful testing with Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.9"
|
|
||||||
groups = ["test"]
|
|
||||||
files = [
|
|
||||||
{file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"},
|
|
||||||
{file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
|
|
||||||
iniconfig = ">=1"
|
|
||||||
packaging = ">=20"
|
|
||||||
pluggy = ">=1.5,<2"
|
|
||||||
pygments = ">=2.7.2"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytest-asyncio"
|
|
||||||
version = "1.1.0"
|
|
||||||
description = "Pytest support for asyncio"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.9"
|
|
||||||
groups = ["test"]
|
|
||||||
files = [
|
|
||||||
{file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"},
|
|
||||||
{file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
pytest = ">=8.2,<9"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
|
|
||||||
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -806,7 +1011,7 @@ files = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
|
greenlet = {version = ">=1", optional = true, markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"asyncio\""}
|
||||||
typing-extensions = ">=4.6.0"
|
typing-extensions = ">=4.6.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@@ -901,4 +1106,4 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.12"
|
python-versions = ">=3.12"
|
||||||
content-hash = "cc2947613c2711ad32ccfa1b8f04a0d8bad6043c6e52bffaff12761ec76cc805"
|
content-hash = "40669246978dc70b09cd8fef22d81e8140d3474feeba1023d8178124914ebcf8"
|
||||||
|
|||||||
@@ -9,89 +9,18 @@ license = {text = "MIT"}
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"fastapi (>=0.115.12,<0.116.0)",
|
||||||
"sqlalchemy (>=2.0.41,<3.0.0)",
|
"sqlalchemy (>=2.0.41,<3.0.0)",
|
||||||
|
"fastapi-users[sqlalchemy] (>=14.0.1,<15.0.0)",
|
||||||
"uvicorn (>=0.34.2,<0.35.0)",
|
"uvicorn (>=0.34.2,<0.35.0)",
|
||||||
"aiosqlite (>=0.21.0,<0.22.0)",
|
"aiosqlite (>=0.21.0,<0.22.0)",
|
||||||
"pydantic-settings (>=2.9.1,<3.0.0)",
|
"pydantic-settings (>=2.9.1,<3.0.0)",
|
||||||
"alembic (>=1.16.1,<2.0.0)",
|
"alembic (>=1.16.1,<2.0.0)",
|
||||||
"ruff (>=0.11.12,<0.12.0)",
|
"ruff (>=0.11.12,<0.12.0)",
|
||||||
"greenlet (>=3.2.2,<4.0.0)",
|
"greenlet (>=3.2.2,<4.0.0)"
|
||||||
"pydantic (>=2.11.7,<3.0.0)",
|
|
||||||
"idna (>=3.10,<4.0)",
|
|
||||||
"fastapi (>=0.115.14,<0.116.0)",
|
|
||||||
"pyjwt (>=2.10.1,<3.0.0)",
|
|
||||||
"passlib (>=1.7.4,<2.0.0)",
|
|
||||||
"email-validator (>=2.2.0,<3.0.0)",
|
|
||||||
"bcrypt (==4.0.1)",
|
|
||||||
"python-multipart (>=0.0.20,<0.0.21)",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
# Exclude commonly ignored directories and files.
|
|
||||||
exclude = [
|
|
||||||
".bzr",
|
|
||||||
".direnv",
|
|
||||||
".eggs",
|
|
||||||
".git",
|
|
||||||
".git-rewrite",
|
|
||||||
".hg",
|
|
||||||
".ipynb_checkpoints",
|
|
||||||
".mypy_cache",
|
|
||||||
".nox",
|
|
||||||
".pants.d",
|
|
||||||
".pyenv",
|
|
||||||
".pytest_cache",
|
|
||||||
".pytype",
|
|
||||||
".ruff_cache",
|
|
||||||
".svn",
|
|
||||||
".tox",
|
|
||||||
".venv",
|
|
||||||
".vscode",
|
|
||||||
"__pypackages__",
|
|
||||||
"_build",
|
|
||||||
"buck-out",
|
|
||||||
"build",
|
|
||||||
"dist",
|
|
||||||
"node_modules",
|
|
||||||
"site-packages",
|
|
||||||
"venv",
|
|
||||||
]
|
|
||||||
# Set the maximum line length for both linting and formatting.
|
|
||||||
line-length = 88
|
|
||||||
# Assume Python 3.9 for compatibility checks.
|
|
||||||
target-version = "py312"
|
|
||||||
# Enable preview features for early access to new rules and formatting changes.
|
|
||||||
preview = true
|
|
||||||
|
|
||||||
[tool.ruff.lint]
|
|
||||||
# Select specific rule groups to enable.
|
|
||||||
# 'E' for pycodestyle, 'F' for Pyflakes, 'I' for isort, 'B' for flake8-bugbear.
|
|
||||||
select = [
|
|
||||||
"E",
|
|
||||||
"F",
|
|
||||||
"I",
|
|
||||||
"B",
|
|
||||||
"UP", # pyupgrade
|
|
||||||
"SIM", # flake8-simplify
|
|
||||||
]
|
|
||||||
# Ignore specific rules within the selected groups.
|
|
||||||
ignore = [
|
|
||||||
"UP",
|
|
||||||
"B903",
|
|
||||||
"B904",
|
|
||||||
"E501",
|
|
||||||
"B008",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.ruff.format]
|
|
||||||
# Enable Ruff's formatter.
|
|
||||||
docstring-code-format = true
|
|
||||||
[tool.poetry.group.test.dependencies]
|
|
||||||
pytest = "^8.4.1"
|
|
||||||
pytest-asyncio = "^1.1.0"
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
[pytest]
|
|
||||||
pythonpath = . src
|
|
||||||
asyncio_mode = auto
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from src.api.users import router as users_router
|
||||||
|
from src.api.tasks import router as tasks_router
|
||||||
|
|
||||||
from src.api.v1 import router as v1_router
|
router = APIRouter()
|
||||||
from src.core.settings import settings
|
|
||||||
|
|
||||||
router = APIRouter(prefix=settings.api.prefix)
|
router.include_router(router=users_router)
|
||||||
|
router.include_router(router=tasks_router)
|
||||||
router.include_router(router=v1_router)
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
from typing import Annotated, AsyncGenerator
|
|
||||||
|
|
||||||
from fastapi import Depends
|
|
||||||
|
|
||||||
from src.core.database import async_session_maker
|
|
||||||
from src.core.db_manager import DBManager
|
|
||||||
from src.core.interfaces import IUOWDB
|
|
||||||
|
|
||||||
|
|
||||||
async def get_db() -> AsyncGenerator[IUOWDB, None]:
|
|
||||||
async with DBManager(async_session_maker) as db:
|
|
||||||
yield db
|
|
||||||
|
|
||||||
|
|
||||||
sessionDep = Annotated[IUOWDB, Depends(get_db)]
|
|
||||||
38
src/api/dependacies/dependancies.py
Normal file
38
src/api/dependacies/dependancies.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from typing import Annotated, AsyncGenerator
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi_users.authentication.strategy import AccessTokenDatabase
|
||||||
|
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
|
||||||
|
from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyAccessTokenDatabase
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.database import async_session_maker
|
||||||
|
from src.models import UsersORM, AccessToken
|
||||||
|
from src.utils.user_manager import UserManager
|
||||||
|
|
||||||
|
|
||||||
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
async with async_session_maker as db:
|
||||||
|
yield db
|
||||||
|
|
||||||
|
|
||||||
|
DBDep = Annotated[AsyncSession, Depends(get_db)]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_users_db(session: DBDep):
|
||||||
|
yield SQLAlchemyUserDatabase(session, UsersORM)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_access_token_db(
|
||||||
|
session: DBDep,
|
||||||
|
):
|
||||||
|
yield SQLAlchemyAccessTokenDatabase(session, AccessToken)
|
||||||
|
|
||||||
|
|
||||||
|
ATDep = Annotated[AccessTokenDatabase[AccessToken], Depends(get_access_token_db)]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_manager(
|
||||||
|
users_db: Annotated[SQLAlchemyUserDatabase, Depends(get_users_db)],
|
||||||
|
):
|
||||||
|
yield UserManager(users_db)
|
||||||
21
src/api/dependacies/strategy.py
Normal file
21
src/api/dependacies/strategy.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from fastapi import Depends
|
||||||
|
from fastapi_users.authentication import AuthenticationBackend
|
||||||
|
from fastapi_users.authentication.strategy import DatabaseStrategy, AccessTokenDatabase
|
||||||
|
|
||||||
|
from src.api.dependacies.dependancies import get_access_token_db
|
||||||
|
from src.api.dependacies.transport import bearer_transport
|
||||||
|
from src.models import AccessToken
|
||||||
|
from src.settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_database_strategy(
|
||||||
|
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
|
||||||
|
) -> DatabaseStrategy:
|
||||||
|
return DatabaseStrategy(access_token_db, lifetime_seconds=settings.lifetime)
|
||||||
|
|
||||||
|
|
||||||
|
auth_backend = AuthenticationBackend(
|
||||||
|
name="database",
|
||||||
|
transport=bearer_transport,
|
||||||
|
get_strategy=get_database_strategy,
|
||||||
|
)
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from datetime import date
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import Depends, Query
|
|
||||||
from fastapi.exceptions import HTTPException
|
|
||||||
from pydantic import BaseModel, model_validator
|
|
||||||
|
|
||||||
from src.schemas.tasks import PriorityEnum, StatusEnum
|
|
||||||
|
|
||||||
|
|
||||||
class Date(BaseModel):
|
|
||||||
date_from: date | None = Query(default=None)
|
|
||||||
date_to: date | None = Query(default=None)
|
|
||||||
|
|
||||||
@model_validator(mode="after")
|
|
||||||
def check_dates(self):
|
|
||||||
if self.date_from and self.date_to and self.date_to < self.date_from:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=422, detail="date_to cannot be less than date_from"
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Page(BaseModel):
|
|
||||||
limit: int = Query(default=30, ge=1, le=100)
|
|
||||||
page: int | None = Query(default=1, ge=1)
|
|
||||||
|
|
||||||
|
|
||||||
class Status(BaseModel):
|
|
||||||
status: StatusEnum | None = Query(default=None)
|
|
||||||
|
|
||||||
|
|
||||||
class Priority(BaseModel):
|
|
||||||
priority: PriorityEnum | None = Query(default=None)
|
|
||||||
|
|
||||||
|
|
||||||
class Filters(Date, Status, Priority, Page):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
FilterDep = Annotated[Filters, Depends()]
|
|
||||||
4
src/api/dependacies/transport.py
Normal file
4
src/api/dependacies/transport.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from fastapi_users.authentication import BearerTransport
|
||||||
|
|
||||||
|
|
||||||
|
bearer_transport = BearerTransport(tokenUrl="auth/login")
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException
|
|
||||||
from fastapi.security import (
|
|
||||||
HTTPAuthorizationCredentials,
|
|
||||||
HTTPBearer,
|
|
||||||
OAuth2PasswordBearer,
|
|
||||||
)
|
|
||||||
from jwt import InvalidTokenError
|
|
||||||
|
|
||||||
from src.core.auth_manager import AuthManager
|
|
||||||
from src.core.settings import settings
|
|
||||||
from src.schemas.auth import TokenData
|
|
||||||
|
|
||||||
http_bearer = HTTPBearer(auto_error=False)
|
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api.v1_login_url}/login")
|
|
||||||
AccessTokenDep = Annotated[HTTPAuthorizationCredentials, Depends(http_bearer)]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(
|
|
||||||
token: AccessTokenDep, verify_exp: bool = True, check_active: bool = False
|
|
||||||
):
|
|
||||||
credentials_exception = HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail="Could not validate credentials",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
payload = AuthManager.decode_access_token(token.credentials, verify_exp)
|
|
||||||
if payload is None:
|
|
||||||
raise credentials_exception
|
|
||||||
user = TokenData(**payload)
|
|
||||||
if check_active and not user.is_active:
|
|
||||||
raise HTTPException(status_code=400, detail="Inactive user")
|
|
||||||
except (InvalidTokenError, AttributeError):
|
|
||||||
raise credentials_exception
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user_basic(token: AccessTokenDep):
|
|
||||||
return await get_current_user(token, verify_exp=True, check_active=False)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_active_user(token: AccessTokenDep):
|
|
||||||
return await get_current_user(token, verify_exp=True, check_active=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user_for_refresh(token: AccessTokenDep):
|
|
||||||
return await get_current_user(token, verify_exp=False, check_active=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user_for_admin(token: AccessTokenDep):
|
|
||||||
admin = await get_current_user(token, verify_exp=True, check_active=True)
|
|
||||||
if not admin.is_superuser:
|
|
||||||
raise HTTPException(status_code=403, detail="Admin access required")
|
|
||||||
return admin
|
|
||||||
|
|
||||||
|
|
||||||
CurrentUser = Annotated[TokenData, Depends(get_current_user_basic)]
|
|
||||||
ActiveUser = Annotated[TokenData, Depends(get_current_active_user)]
|
|
||||||
RefreshUser = Annotated[TokenData, Depends(get_current_user_for_refresh)]
|
|
||||||
AdminUser = Annotated[TokenData, Depends(get_current_user_for_admin)]
|
|
||||||
27
src/api/tasks.py
Normal file
27
src/api/tasks.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/tasks", tags=["Tasks"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def get_tasks(): ...
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}")
|
||||||
|
async def get_task_id(id: int): ...
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/")
|
||||||
|
async def post_task(): ...
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}")
|
||||||
|
async def put_task(id: int): ...
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{id}")
|
||||||
|
async def patch_task(id: int): ...
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}")
|
||||||
|
async def delete_task(id: int): ...
|
||||||
3
src/api/users.py
Normal file
3
src/api/users.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/users", tags=["Users"])
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
from src.api.v1.auth import router as auth_router
|
|
||||||
from src.api.v1.tasks import router as tasks_router
|
|
||||||
from src.api.v1.users import router as users_router
|
|
||||||
from src.core.settings import settings
|
|
||||||
|
|
||||||
router = APIRouter(prefix=settings.api.v1.prefix)
|
|
||||||
|
|
||||||
router.include_router(router=auth_router)
|
|
||||||
router.include_router(router=users_router)
|
|
||||||
router.include_router(router=tasks_router)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Cookie, Depends, Form, HTTPException, Response
|
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
|
||||||
|
|
||||||
from src.api.dependacies.db_dep import sessionDep
|
|
||||||
from src.api.dependacies.user_dep import ActiveUser, RefreshUser, http_bearer
|
|
||||||
from src.core.settings import settings
|
|
||||||
from src.schemas.auth import Token
|
|
||||||
from src.schemas.users import UserRequestADD
|
|
||||||
from src.services.auth import AuthService
|
|
||||||
from src.services.users import UserService
|
|
||||||
|
|
||||||
router = APIRouter(
|
|
||||||
prefix=settings.api.v1.auth, tags=["Auth"], dependencies=[Depends(http_bearer)]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(path="/signup")
|
|
||||||
async def registration(session: sessionDep, credential: UserRequestADD):
|
|
||||||
auth = await AuthService(session).registration(credential)
|
|
||||||
return auth
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(path="/login", response_model=Token)
|
|
||||||
async def login(
|
|
||||||
session: sessionDep,
|
|
||||||
credential: Annotated[OAuth2PasswordRequestForm, Depends()],
|
|
||||||
response: Response,
|
|
||||||
fingerprint: str = Form(min_length=5),
|
|
||||||
):
|
|
||||||
result = await AuthService(session).login(
|
|
||||||
credential.username, credential.password, fingerprint=fingerprint
|
|
||||||
)
|
|
||||||
response.set_cookie(
|
|
||||||
key="refresh_token",
|
|
||||||
value=result["refresh_token"],
|
|
||||||
httponly=True,
|
|
||||||
samesite="lax",
|
|
||||||
path=settings.api.v1_login_url,
|
|
||||||
max_age=60 * 60 * 24 * settings.refresh_token.expire_days,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(path="/refresh", response_model=Token)
|
|
||||||
async def refresh(
|
|
||||||
session: sessionDep,
|
|
||||||
current_user: RefreshUser,
|
|
||||||
response: Response,
|
|
||||||
fingerprint: Annotated[str, Body(embed=True)],
|
|
||||||
refresh_token: Annotated[str | None, Cookie(name="refresh_token")] = None,
|
|
||||||
):
|
|
||||||
if refresh_token is None:
|
|
||||||
raise HTTPException(status_code=401, detail="No refresh token")
|
|
||||||
result = await AuthService(session).refresh_tokens(
|
|
||||||
refresh_token, current_user, fingerprint
|
|
||||||
)
|
|
||||||
response.set_cookie(
|
|
||||||
key="refresh_token",
|
|
||||||
value=result["refresh_token"],
|
|
||||||
httponly=True,
|
|
||||||
samesite="lax",
|
|
||||||
path=settings.api.v1_login_url,
|
|
||||||
max_age=60 * 60 * 24 * settings.refresh_token.expire_days,
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me")
|
|
||||||
async def get_me(session: sessionDep, user: ActiveUser):
|
|
||||||
cur_user = await UserService(session).get_user_by_filter_or_raise(id=user.id)
|
|
||||||
return cur_user
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(path="/logout")
|
|
||||||
async def logout(
|
|
||||||
session: sessionDep,
|
|
||||||
response: Response,
|
|
||||||
refresh_token: Annotated[str | None, Cookie(name="refresh_token")] = None,
|
|
||||||
):
|
|
||||||
if refresh_token is None:
|
|
||||||
raise HTTPException(status_code=401, detail="No refresh token")
|
|
||||||
await AuthService(session).delete_token(token=refresh_token)
|
|
||||||
response.delete_cookie(key="refresh_token")
|
|
||||||
return {"status": "ok"}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
|
||||||
|
|
||||||
from src.api.dependacies.db_dep import sessionDep
|
|
||||||
from src.api.dependacies.user_dep import ActiveUser
|
|
||||||
from src.schemas.tasks import TaskADDRequest, TaskPATCHRequest
|
|
||||||
from src.services.tasks import TaskService
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/tasks", tags=["Tasks"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}")
|
|
||||||
async def get_task_id(session: sessionDep, id: int, user: ActiveUser):
|
|
||||||
task = await TaskService(session).get_task(id)
|
|
||||||
if task.user_id != user.id and user.is_superuser is False:
|
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
|
||||||
return task
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
|
||||||
async def post_task(
|
|
||||||
task_data: Annotated[TaskADDRequest, Depends()],
|
|
||||||
session: sessionDep,
|
|
||||||
user: ActiveUser,
|
|
||||||
):
|
|
||||||
result = await TaskService(session).create_task(
|
|
||||||
user_id=user.id, task_data=task_data
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{id}")
|
|
||||||
async def patch_task(
|
|
||||||
session: sessionDep,
|
|
||||||
id: int,
|
|
||||||
user: ActiveUser,
|
|
||||||
task_data: TaskPATCHRequest = Body(),
|
|
||||||
):
|
|
||||||
if user.is_superuser is False:
|
|
||||||
task = await TaskService(session).get_task(id)
|
|
||||||
if task.user_id != user.id:
|
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
|
||||||
updated_task = await TaskService(session).update_task(id, task_data)
|
|
||||||
return updated_task
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}")
|
|
||||||
async def delete_task(
|
|
||||||
session: sessionDep,
|
|
||||||
id: int,
|
|
||||||
user: ActiveUser,
|
|
||||||
):
|
|
||||||
if user.is_superuser is False:
|
|
||||||
task = await TaskService(session).get_task(id)
|
|
||||||
if task.user_id != user.id:
|
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
|
||||||
await TaskService(session).delete_task(id)
|
|
||||||
return {"message": "Task deleted successfully"}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from fastapi import APIRouter, Body, HTTPException
|
|
||||||
|
|
||||||
from src.api.dependacies.db_dep import sessionDep
|
|
||||||
from src.api.dependacies.task_dep import FilterDep
|
|
||||||
from src.api.dependacies.user_dep import (
|
|
||||||
ActiveUser,
|
|
||||||
AdminUser,
|
|
||||||
)
|
|
||||||
from src.core.settings import settings
|
|
||||||
from src.schemas.users import UserUpdate
|
|
||||||
from src.services.users import UserService
|
|
||||||
|
|
||||||
router = APIRouter(prefix=settings.api.v1.users, tags=["Users"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
|
||||||
async def get_all_users(session: sessionDep, _: AdminUser):
|
|
||||||
users = await UserService(session).get_all_users()
|
|
||||||
return users
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}")
|
|
||||||
async def get_user_by_id(session: sessionDep, id: int, _: AdminUser):
|
|
||||||
user = await UserService(session).get_user_by_filter_or_raise(id=id)
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}/tasks")
|
|
||||||
async def get_user_tasks(
|
|
||||||
session: sessionDep, id: int, user: ActiveUser, filters: FilterDep
|
|
||||||
):
|
|
||||||
if user.id != id and user.is_superuser is False:
|
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
|
||||||
tasks = await UserService(session).get_user_with_tasks(id, **filters.model_dump())
|
|
||||||
return tasks.tasks
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{id}")
|
|
||||||
async def patch_user(
|
|
||||||
session: sessionDep,
|
|
||||||
id: int,
|
|
||||||
user: ActiveUser,
|
|
||||||
user_update: UserUpdate = Body(),
|
|
||||||
):
|
|
||||||
if user.id != id and user.is_superuser is False:
|
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
|
||||||
updated_user = await UserService(session).update_user(
|
|
||||||
id=id, update_data=user_update
|
|
||||||
)
|
|
||||||
return updated_user
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{id}")
|
|
||||||
async def delete_user(session: sessionDep, id: int, user: AdminUser):
|
|
||||||
await UserService(session).delete_user(id)
|
|
||||||
return {"message": "User deleted successfully"}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import secrets
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
|
|
||||||
import jwt
|
|
||||||
from passlib.context import CryptContext
|
|
||||||
|
|
||||||
from src.core.settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
class AuthManager:
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def verify_password(cls, plain_password, hashed_password):
|
|
||||||
return cls.pwd_context.verify(plain_password, hashed_password)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_password_hash(cls, password):
|
|
||||||
return cls.pwd_context.hash(password)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_access_token(cls, data: dict, expires_delta: timedelta | None = None):
|
|
||||||
to_encode = data.copy()
|
|
||||||
if expires_delta:
|
|
||||||
expire = datetime.now(timezone.utc) + expires_delta
|
|
||||||
else:
|
|
||||||
expire = datetime.now(timezone.utc) + timedelta(
|
|
||||||
minutes=settings.access_token.expire_minutes
|
|
||||||
)
|
|
||||||
to_encode.update({"exp": expire})
|
|
||||||
encoded_jwt = jwt.encode(
|
|
||||||
to_encode,
|
|
||||||
settings.access_token.secret_key,
|
|
||||||
algorithm=settings.access_token.algorithm,
|
|
||||||
)
|
|
||||||
return encoded_jwt
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_refresh_token(cls) -> str:
|
|
||||||
token_hash = secrets.token_urlsafe(32)
|
|
||||||
return token_hash
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def decode_access_token(cls, token: str, verify_exp: bool = True) -> dict:
|
|
||||||
return jwt.decode(
|
|
||||||
token,
|
|
||||||
settings.access_token.secret_key,
|
|
||||||
algorithms=[settings.access_token.algorithm],
|
|
||||||
options={"verify_exp": verify_exp},
|
|
||||||
)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import TIMESTAMP, event, func
|
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
||||||
|
|
||||||
from src.core.settings import settings
|
|
||||||
|
|
||||||
engine = create_async_engine(settings.db.url, echo=settings.db.echo)
|
|
||||||
|
|
||||||
|
|
||||||
@event.listens_for(engine.sync_engine, "connect")
|
|
||||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
|
||||||
if "sqlite" in settings.db.url:
|
|
||||||
cursor = dbapi_connection.cursor()
|
|
||||||
cursor.execute("PRAGMA foreign_keys=ON")
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
|
|
||||||
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
|
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
|
||||||
TIMESTAMP(timezone=True), server_default=func.now()
|
|
||||||
)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
||||||
|
|
||||||
from src.repository.auth import AuthRepo
|
|
||||||
from src.repository.tasks import TasksRepo
|
|
||||||
from src.repository.users import UsersRepo
|
|
||||||
|
|
||||||
|
|
||||||
class DBManager:
|
|
||||||
def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
|
|
||||||
self.session_factory = session_factory
|
|
||||||
|
|
||||||
async def __aenter__(self) -> "DBManager":
|
|
||||||
self.session: AsyncSession = self.session_factory()
|
|
||||||
self.user = UsersRepo(self.session)
|
|
||||||
self.task = TasksRepo(self.session)
|
|
||||||
self.auth = AuthRepo(self.session)
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
||||||
await self.session.rollback()
|
|
||||||
await self.session.close()
|
|
||||||
|
|
||||||
async def commit(self) -> None:
|
|
||||||
await self.session.commit()
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from typing import TYPE_CHECKING, Any, Protocol
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from src.repository.auth import AuthRepo
|
|
||||||
from src.repository.tasks import TasksRepo
|
|
||||||
from src.repository.users import UsersRepo
|
|
||||||
|
|
||||||
|
|
||||||
class IUOWDB(Protocol):
|
|
||||||
session: AsyncSession
|
|
||||||
user: "UsersRepo"
|
|
||||||
task: "TasksRepo"
|
|
||||||
auth: "AuthRepo"
|
|
||||||
|
|
||||||
async def __aenter__(self) -> "IUOWDB": ...
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ...
|
|
||||||
|
|
||||||
async def commit(self) -> None: ...
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).parent.parent
|
|
||||||
DB_PATH = BASE_DIR / "db/taskncoffee.db"
|
|
||||||
|
|
||||||
|
|
||||||
class ApiV1Prefix(BaseModel):
|
|
||||||
prefix: str = "/v1"
|
|
||||||
auth: str = "/auth"
|
|
||||||
users: str = "/users"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def login_url(self) -> str:
|
|
||||||
return f"{self.prefix}{self.auth}"
|
|
||||||
|
|
||||||
|
|
||||||
class ApiPrefix(BaseModel):
|
|
||||||
prefix: str = "/api"
|
|
||||||
v1: ApiV1Prefix = ApiV1Prefix()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def v1_login_url(self) -> str:
|
|
||||||
return f"{self.prefix}{self.v1.login_url}"
|
|
||||||
|
|
||||||
|
|
||||||
class DbSettings(BaseModel):
|
|
||||||
url: str = f"sqlite+aiosqlite:///{DB_PATH}"
|
|
||||||
echo: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(BaseModel):
|
|
||||||
expire_minutes: int
|
|
||||||
secret_key: str
|
|
||||||
algorithm: str = "HS256"
|
|
||||||
token_type: str = "bearer" # noqa: S105
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshToken(BaseModel):
|
|
||||||
expire_days: int
|
|
||||||
|
|
||||||
|
|
||||||
class CorsSettings(BaseModel):
|
|
||||||
production: str
|
|
||||||
local: list[str] = ["http://localhost:5173", "http://localhost:5174"]
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
api: ApiPrefix = ApiPrefix()
|
|
||||||
db: DbSettings = DbSettings()
|
|
||||||
access_token: AccessToken
|
|
||||||
refresh_token: RefreshToken
|
|
||||||
cors_settings: CorsSettings
|
|
||||||
model_config = SettingsConfigDict(
|
|
||||||
env_file=".env", env_file_encoding="utf-8", env_nested_delimiter="__"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
settings = Settings() # type: ignore
|
|
||||||
15
src/db/database.py
Normal file
15
src/db/database.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import TIMESTAMP, func
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||||
|
|
||||||
|
engine = create_async_engine("sqlite+aiosqlite:///src/db/taskncoffee.db", echo=True)
|
||||||
|
|
||||||
|
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
TIMESTAMP(timezone=True), server_default=func.now()
|
||||||
|
)
|
||||||
19
src/main.py
19
src/main.py
@@ -1,25 +1,10 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).parent.parent))
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
from src.api import router
|
from src.api import router
|
||||||
from src.core.settings import settings
|
|
||||||
|
|
||||||
app = FastAPI(title="Task&Coffee")
|
app = FastAPI()
|
||||||
app.include_router(router=router)
|
app.include_router(router=router)
|
||||||
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.cors_settings.local + [settings.cors_settings.production],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("src.main:app", port=8000, log_level="info", reload=True)
|
uvicorn.run("src.main:app", port=5000, log_level="info", reload=True)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
|
|
||||||
from alembic import context
|
from sqlalchemy import engine_from_config
|
||||||
from sqlalchemy import engine_from_config, event, pool
|
from sqlalchemy import pool
|
||||||
|
|
||||||
from src.core.database import Base
|
from alembic import context
|
||||||
|
|
||||||
|
from src.db.database import Base
|
||||||
from src.models import * # noqa
|
from src.models import * # noqa
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
@@ -65,14 +67,6 @@ def run_migrations_online() -> None:
|
|||||||
poolclass=pool.NullPool,
|
poolclass=pool.NullPool,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enable foreign keys for SQLite in migrations
|
|
||||||
@event.listens_for(connectable, "connect")
|
|
||||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
|
||||||
print("⚙️ Enabling PRAGMA foreign_keys=ON for Alembic")
|
|
||||||
cursor = dbapi_connection.cursor()
|
|
||||||
cursor.execute("PRAGMA foreign_keys=ON")
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
context.configure(connection=connection, target_metadata=target_metadata)
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
"""init
|
"""init
|
||||||
|
|
||||||
Revision ID: a2fdd0ec4a96
|
Revision ID: 932121e6b220
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2025-07-06 00:02:09.254907
|
Create Date: 2025-06-22 11:52:49.691545
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "a2fdd0ec4a96"
|
revision: str = "932121e6b220"
|
||||||
down_revision: Union[str, None] = None
|
down_revision: Union[str, None] = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
@@ -20,17 +20,17 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"users",
|
"users",
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column("username", sa.String(length=30), nullable=False),
|
sa.Column("username", sa.String(length=30), nullable=False),
|
||||||
sa.Column("hashed_password", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("email", sa.String(length=255), nullable=True),
|
|
||||||
sa.Column("telegram_id", sa.BigInteger(), nullable=True),
|
sa.Column("telegram_id", sa.BigInteger(), nullable=True),
|
||||||
sa.Column("avatar_path", sa.String(length=255), nullable=True),
|
sa.Column("avatar_path", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("email", sa.String(length=320), nullable=False),
|
||||||
|
sa.Column("hashed_password", sa.String(length=1024), nullable=False),
|
||||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||||
sa.Column("is_superuser", sa.Boolean(), nullable=False),
|
sa.Column("is_superuser", sa.Boolean(), nullable=False),
|
||||||
|
sa.Column("is_verified", sa.Boolean(), nullable=False),
|
||||||
sa.Column(
|
sa.Column(
|
||||||
"created_at",
|
"created_at",
|
||||||
sa.TIMESTAMP(timezone=True),
|
sa.TIMESTAMP(timezone=True),
|
||||||
@@ -38,8 +38,8 @@ def upgrade() -> None:
|
|||||||
nullable=False,
|
nullable=False,
|
||||||
),
|
),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.UniqueConstraint("email"),
|
|
||||||
)
|
)
|
||||||
|
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
|
||||||
op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
|
op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"tasks",
|
"tasks",
|
||||||
@@ -77,13 +77,11 @@ def upgrade() -> None:
|
|||||||
),
|
),
|
||||||
sa.PrimaryKeyConstraint("id"),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema."""
|
"""Downgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table("tasks")
|
op.drop_table("tasks")
|
||||||
op.drop_index(op.f("ix_users_username"), table_name="users")
|
op.drop_index(op.f("ix_users_username"), table_name="users")
|
||||||
|
op.drop_index(op.f("ix_users_email"), table_name="users")
|
||||||
op.drop_table("users")
|
op.drop_table("users")
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""access token
|
||||||
|
|
||||||
|
Revision ID: bc0bdd74718c
|
||||||
|
Revises: 932121e6b220
|
||||||
|
Create Date: 2025-06-22 12:11:19.223212
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import fastapi_users_db_sqlalchemy
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "bc0bdd74718c"
|
||||||
|
down_revision: Union[str, None] = "932121e6b220"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
op.create_table(
|
||||||
|
"accesstoken",
|
||||||
|
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("token", sa.String(length=43), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
fastapi_users_db_sqlalchemy.generics.TIMESTAMPAware(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="cascade"),
|
||||||
|
sa.PrimaryKeyConstraint("token"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
op.f("ix_accesstoken_created_at"), "accesstoken", ["created_at"], unique=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
op.drop_index(op.f("ix_accesstoken_created_at"), table_name="accesstoken")
|
||||||
|
op.drop_table("accesstoken")
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
"""add_cascade_delete_to_tasks
|
|
||||||
|
|
||||||
Revision ID: 197b195208e8
|
|
||||||
Revises: a2fdd0ec4a96
|
|
||||||
Create Date: 2025-08-06 23:41:56.778423
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "197b195208e8"
|
|
||||||
down_revision: Union[str, None] = "a2fdd0ec4a96"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
"""Upgrade schema."""
|
|
||||||
op.execute("PRAGMA foreign_keys=ON")
|
|
||||||
|
|
||||||
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
|
||||||
connection = op.get_bind()
|
|
||||||
inspector = sa.inspect(connection)
|
|
||||||
|
|
||||||
foreign_keys = inspector.get_foreign_keys("tasks")
|
|
||||||
constraint_name = None
|
|
||||||
|
|
||||||
for fk in foreign_keys:
|
|
||||||
if "user_id" in fk["constrained_columns"]:
|
|
||||||
constraint_name = fk["name"]
|
|
||||||
break
|
|
||||||
|
|
||||||
if constraint_name:
|
|
||||||
try: # noqa: SIM105
|
|
||||||
batch_op.drop_constraint(constraint_name, type_="foreignkey")
|
|
||||||
except: # noqa E722
|
|
||||||
pass
|
|
||||||
|
|
||||||
batch_op.create_foreign_key(
|
|
||||||
"fk_tasks_user_id_users", "users", ["user_id"], ["id"], ondelete="CASCADE"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
"""Downgrade schema."""
|
|
||||||
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
|
||||||
try: # noqa: SIM105
|
|
||||||
batch_op.drop_constraint("fk_tasks_user_id_users", type_="foreignkey")
|
|
||||||
except: # noqa E722
|
|
||||||
pass
|
|
||||||
|
|
||||||
batch_op.create_foreign_key(
|
|
||||||
"fk_tasks_user_id_users", "users", ["user_id"], ["id"]
|
|
||||||
)
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
"""fix_duplicate_foreign_keys
|
|
||||||
|
|
||||||
Revision ID: 4b0f3ea2fd26
|
|
||||||
Revises: 197b195208e8
|
|
||||||
Create Date: 2025-08-06 23:54:24.308488
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union # noqa: UP035
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "4b0f3ea2fd26"
|
|
||||||
down_revision: Union[str, None] = "197b195208e8"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
"""Upgrade schema."""
|
|
||||||
op.execute("PRAGMA foreign_keys=ON")
|
|
||||||
|
|
||||||
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
|
||||||
connection = op.get_bind()
|
|
||||||
inspector = sa.inspect(connection)
|
|
||||||
|
|
||||||
foreign_keys = inspector.get_foreign_keys("tasks")
|
|
||||||
|
|
||||||
for fk in foreign_keys:
|
|
||||||
if "user_id" in fk["constrained_columns"]:
|
|
||||||
try: # noqa: SIM105
|
|
||||||
batch_op.drop_constraint(fk["name"], type_="foreignkey")
|
|
||||||
except: # noqa E722
|
|
||||||
pass
|
|
||||||
|
|
||||||
batch_op.create_foreign_key(
|
|
||||||
"fk_tasks_user_id_users", "users", ["user_id"], ["id"], ondelete="CASCADE"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
"""Downgrade schema."""
|
|
||||||
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
|
||||||
try: # noqa: SIM105
|
|
||||||
batch_op.drop_constraint("fk_tasks_user_id_users", type_="foreignkey")
|
|
||||||
except: # noqa E722
|
|
||||||
pass
|
|
||||||
batch_op.create_foreign_key(
|
|
||||||
"fk_tasks_user_id_users", "users", ["user_id"], ["id"]
|
|
||||||
)
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"""add refresh token
|
|
||||||
|
|
||||||
Revision ID: b879d3502c37
|
|
||||||
Revises: 4b0f3ea2fd26
|
|
||||||
Create Date: 2025-09-08 14:56:01.439089
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "b879d3502c37"
|
|
||||||
down_revision: Union[str, None] = "4b0f3ea2fd26"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table(
|
|
||||||
"refresh_tokens",
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("token", sa.String(length=255), nullable=False),
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.TIMESTAMP(timezone=True),
|
|
||||||
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table("refresh_tokens")
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
"""expire and fingerprintjs token
|
|
||||||
|
|
||||||
Revision ID: 5821f37941a8
|
|
||||||
Revises: b879d3502c37
|
|
||||||
Create Date: 2025-09-21 20:16:48.289050
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "5821f37941a8"
|
|
||||||
down_revision: Union[str, None] = "b879d3502c37"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column(
|
|
||||||
"refresh_tokens",
|
|
||||||
sa.Column("fingerprint", sa.String(length=255), nullable=False),
|
|
||||||
)
|
|
||||||
op.add_column(
|
|
||||||
"refresh_tokens",
|
|
||||||
sa.Column(
|
|
||||||
"expired_at",
|
|
||||||
sa.TIMESTAMP(timezone=True),
|
|
||||||
server_default=sa.text("(datetime('now', '+7 days'))"),
|
|
||||||
nullable=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_column("refresh_tokens", "expired_at")
|
|
||||||
op.drop_column("refresh_tokens", "fingerprint")
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
from src.models.tasks import TasksORM
|
from src.models.token import AccessToken
|
||||||
from src.models.tokens import RefreshTokensORM
|
|
||||||
from src.models.users import UsersORM
|
from src.models.users import UsersORM
|
||||||
|
from src.models.tasks import TasksORM
|
||||||
|
|
||||||
__all__ = ["UsersORM", "TasksORM", "RefreshTokensORM"]
|
__all__ = [
|
||||||
|
"UsersORM",
|
||||||
|
"TasksORM",
|
||||||
|
"AccessToken",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import Date, Enum, ForeignKey, String, Text
|
from sqlalchemy import ForeignKey, Text, Date, Enum, String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from src.core.database import Base
|
from src.db.database import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from src.models.users import UsersORM
|
from src.models.users import UsersORM
|
||||||
@@ -16,7 +16,7 @@ priority_enum = Enum("low", "medium", "high", "critical", name="priority_enum")
|
|||||||
class TasksORM(Base):
|
class TasksORM(Base):
|
||||||
__tablename__ = "tasks"
|
__tablename__ = "tasks"
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
||||||
title: Mapped[str] = mapped_column(String(100))
|
title: Mapped[str] = mapped_column(String(100))
|
||||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||||
due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||||
|
|||||||
13
src/models/token.py
Normal file
13
src/models/token.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyBaseAccessTokenTable
|
||||||
|
from sqlalchemy import Integer, ForeignKey
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, declared_attr
|
||||||
|
|
||||||
|
from src.db.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class AccessToken(SQLAlchemyBaseAccessTokenTable[int], Base):
|
||||||
|
@declared_attr
|
||||||
|
def user_id(cls) -> Mapped[int]:
|
||||||
|
return mapped_column(
|
||||||
|
Integer, ForeignKey("users.id", ondelete="cascade"), nullable=False
|
||||||
|
)
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlalchemy import TIMESTAMP, ForeignKey, Integer, String, text
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
from src.core.database import Base
|
|
||||||
from src.core.settings import settings
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokensORM(Base):
|
|
||||||
__tablename__ = "refresh_tokens"
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
||||||
token: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
|
|
||||||
fingerprint: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
||||||
expired_at: Mapped[datetime] = mapped_column(
|
|
||||||
TIMESTAMP(timezone=True),
|
|
||||||
server_default=text(
|
|
||||||
f"datetime('now', '+{settings.refresh_token.expire_days} days')"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
@@ -1,28 +1,22 @@
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import BigInteger, Boolean, Integer, String
|
from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTable
|
||||||
|
from sqlalchemy import String, BigInteger, Integer
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from src.core.database import Base
|
from src.db.database import Base
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from src.models.tasks import TasksORM
|
from src.models.tasks import TasksORM
|
||||||
|
|
||||||
|
|
||||||
class UsersORM(Base):
|
class UsersORM(SQLAlchemyBaseUserTable[int], Base):
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
username: Mapped[str] = mapped_column(
|
username: Mapped[Optional[str]] = mapped_column(
|
||||||
String(30), nullable=False, unique=True, index=True
|
String(30), nullable=False, unique=True, index=True
|
||||||
)
|
)
|
||||||
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
||||||
email: Mapped[Optional[str]] = mapped_column(
|
|
||||||
String(255), unique=True, nullable=True
|
|
||||||
)
|
|
||||||
telegram_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
|
telegram_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
|
||||||
avatar_path: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
avatar_path: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
||||||
is_superuser: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
tasks: Mapped[list["TasksORM"]] = relationship(back_populates="user")
|
||||||
tasks: Mapped[list["TasksORM"]] = relationship(
|
|
||||||
back_populates="user", cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
from src.models.tokens import RefreshTokensORM
|
|
||||||
from src.repository.base import BaseRepo
|
|
||||||
|
|
||||||
|
|
||||||
class AuthRepo(BaseRepo[RefreshTokensORM]):
|
|
||||||
model: type[RefreshTokensORM] = RefreshTokensORM
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
from typing import Any, Generic, Mapping, Sequence, Type, TypeVar
|
|
||||||
|
|
||||||
from sqlalchemy import delete, insert, select, update
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
ModelType = TypeVar("ModelType")
|
|
||||||
|
|
||||||
|
|
||||||
class BaseRepo(Generic[ModelType]):
|
|
||||||
model: Type[ModelType]
|
|
||||||
|
|
||||||
def __init__(self, session: AsyncSession) -> None:
|
|
||||||
self.session: AsyncSession = session
|
|
||||||
|
|
||||||
async def get_filtered(
|
|
||||||
self, *filters: Any, **filter_by: Any
|
|
||||||
) -> Sequence[ModelType]:
|
|
||||||
query = select(self.model).filter(*filters).filter_by(**filter_by)
|
|
||||||
result = await self.session.execute(query)
|
|
||||||
models = result.scalars().all()
|
|
||||||
return models
|
|
||||||
|
|
||||||
async def create_one(self, data: Mapping[str, Any]) -> ModelType:
|
|
||||||
statement = insert(self.model).values(data).returning(self.model)
|
|
||||||
result = await self.session.execute(statement)
|
|
||||||
obj: ModelType = result.scalar_one()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
async def create_bulk(self, data: Sequence[Mapping[str, Any]]) -> list[ModelType]:
|
|
||||||
result = [await self.create_one(item) for item in data]
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def get_one_or_none(self, **filter_by: Any) -> ModelType | None:
|
|
||||||
query = select(self.model).filter_by(**filter_by)
|
|
||||||
result = await self.session.execute(query)
|
|
||||||
model_obj: ModelType | None = result.scalars().one_or_none()
|
|
||||||
return model_obj
|
|
||||||
|
|
||||||
async def get_all(self, *args: Any, **kwargs: Any) -> Sequence[ModelType]:
|
|
||||||
result: Sequence[ModelType] = await self.get_filtered(*args, **kwargs)
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def delete_one(self, **filter_by) -> None:
|
|
||||||
await self.session.execute(delete(self.model).filter_by(**filter_by))
|
|
||||||
|
|
||||||
async def update_one(self, data: dict[str, Any], **filter_by: Any) -> ModelType:
|
|
||||||
stmt = (
|
|
||||||
update(self.model).filter_by(**filter_by).values(data).returning(self.model)
|
|
||||||
)
|
|
||||||
result = await self.session.execute(stmt)
|
|
||||||
model = result.scalar_one()
|
|
||||||
return model
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from src.models.tasks import TasksORM
|
|
||||||
from src.repository.base import BaseRepo
|
|
||||||
|
|
||||||
|
|
||||||
class TasksRepo(BaseRepo[TasksORM]):
|
|
||||||
model: type[TasksORM] = TasksORM
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
from datetime import date
|
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from src.models import UsersORM
|
|
||||||
from src.models.tasks import TasksORM
|
|
||||||
from src.repository.base import BaseRepo
|
|
||||||
|
|
||||||
|
|
||||||
class UsersRepo(BaseRepo[UsersORM]):
|
|
||||||
model: type[UsersORM] = UsersORM
|
|
||||||
|
|
||||||
async def get_one_with_load(
|
|
||||||
self,
|
|
||||||
user_id: int,
|
|
||||||
status: str | None = None,
|
|
||||||
priority: str | None = None,
|
|
||||||
tasks_limit: int | None = None,
|
|
||||||
tasks_offset: int | None = 0,
|
|
||||||
date_to: date | None = None,
|
|
||||||
date_from: date | None = None,
|
|
||||||
) -> UsersORM | None:
|
|
||||||
filters_sq: dict = {"user_id": user_id}
|
|
||||||
if status:
|
|
||||||
filters_sq["status"] = status
|
|
||||||
if priority:
|
|
||||||
filters_sq["priority"] = priority
|
|
||||||
tasks_subquery = self._tasks_subquary(
|
|
||||||
date_from=date_from, date_to=date_to, **filters_sq
|
|
||||||
)
|
|
||||||
|
|
||||||
if tasks_limit is not None:
|
|
||||||
tasks_subquery = tasks_subquery.limit(tasks_limit)
|
|
||||||
if tasks_offset is not None and tasks_offset > 0:
|
|
||||||
tasks_subquery = tasks_subquery.offset(tasks_offset)
|
|
||||||
query = (
|
|
||||||
select(self.model)
|
|
||||||
.where(self.model.id == user_id)
|
|
||||||
.options(
|
|
||||||
selectinload(
|
|
||||||
self.model.tasks.and_(TasksORM.id.in_(tasks_subquery))
|
|
||||||
).load_only(
|
|
||||||
TasksORM.id,
|
|
||||||
TasksORM.title,
|
|
||||||
TasksORM.due_date,
|
|
||||||
TasksORM.priority,
|
|
||||||
TasksORM.status,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result = await self.session.execute(query)
|
|
||||||
obj = result.scalar_one_or_none()
|
|
||||||
if obj and obj.tasks:
|
|
||||||
obj.tasks.sort(
|
|
||||||
key=lambda task: (
|
|
||||||
task.due_date or date.max,
|
|
||||||
-self._priority(task.priority),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
async def get_tasks_count(
|
|
||||||
self, date_from: date | None, date_to: date | None, **filter_by
|
|
||||||
) -> int:
|
|
||||||
subq = self._tasks_subquary(date_from, date_to, **filter_by).subquery()
|
|
||||||
stmt = select(func.count()).select_from(subq)
|
|
||||||
result = await self.session.execute(stmt)
|
|
||||||
return result.scalar_one()
|
|
||||||
|
|
||||||
def _priority(self, priority: str):
|
|
||||||
priority_map = {"low": 1, "medium": 2, "high": 3, "critical": 4}
|
|
||||||
return priority_map.get(priority, 0)
|
|
||||||
|
|
||||||
def _tasks_subquary(
|
|
||||||
self, date_from: date | None, date_to: date | None, **filter_by
|
|
||||||
):
|
|
||||||
subq = select(TasksORM.id).filter_by(**filter_by)
|
|
||||||
if date_from is not None:
|
|
||||||
subq = subq.filter(TasksORM.due_date >= date_from)
|
|
||||||
if date_to is not None:
|
|
||||||
subq = subq.filter(TasksORM.due_date <= date_to)
|
|
||||||
return subq
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from pydantic import BaseModel, ConfigDict, Field
|
|
||||||
|
|
||||||
|
|
||||||
class Token(BaseModel):
|
|
||||||
access_token: str
|
|
||||||
token_type: str
|
|
||||||
model_config = ConfigDict(extra="ignore")
|
|
||||||
|
|
||||||
|
|
||||||
class TokenData(BaseModel):
|
|
||||||
id: int
|
|
||||||
sub: str = Field(alias="username")
|
|
||||||
is_superuser: bool
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
from datetime import date
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
|
||||||
|
|
||||||
|
|
||||||
class PriorityEnum(str, Enum):
|
|
||||||
low = "low"
|
|
||||||
medium = "medium"
|
|
||||||
high = "high"
|
|
||||||
critical = "critical"
|
|
||||||
|
|
||||||
|
|
||||||
class StatusEnum(str, Enum):
|
|
||||||
open = "open"
|
|
||||||
closed = "closed"
|
|
||||||
in_progress = "in_progress"
|
|
||||||
todo = "todo"
|
|
||||||
|
|
||||||
|
|
||||||
class TaskShort(BaseModel):
|
|
||||||
title: str
|
|
||||||
due_date: date | None = None
|
|
||||||
priority: PriorityEnum = PriorityEnum.medium
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|
||||||
|
|
||||||
class TaskWithId(TaskShort):
|
|
||||||
id: int
|
|
||||||
status: str
|
|
||||||
|
|
||||||
|
|
||||||
class TaskADDRequest(TaskShort):
|
|
||||||
description: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TaskPATCHRequest(BaseModel):
|
|
||||||
title: str | None = None
|
|
||||||
description: str | None = None
|
|
||||||
due_date: date | None = None
|
|
||||||
status: StatusEnum | None = None
|
|
||||||
priority: PriorityEnum | None = None
|
|
||||||
time_spent: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class Task(TaskADDRequest):
|
|
||||||
id: int
|
|
||||||
user_id: int
|
|
||||||
status: StatusEnum
|
|
||||||
time_spent: int
|
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
|
||||||
|
|||||||
@@ -1,49 +1,15 @@
|
|||||||
from typing import Annotated
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, BeforeValidator, ConfigDict, EmailStr
|
from fastapi_users import schemas
|
||||||
|
|
||||||
from src.schemas.tasks import TaskWithId
|
|
||||||
from src.schemas.validators import ensure_password, ensure_username
|
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(BaseModel):
|
class UserRead(schemas.BaseUser[int]):
|
||||||
email: EmailStr | None = None
|
|
||||||
username: str | None = None
|
|
||||||
is_active: bool | None = None
|
|
||||||
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
|
||||||
id: int
|
|
||||||
email: EmailStr | None
|
|
||||||
username: str
|
username: str
|
||||||
is_active: bool
|
|
||||||
is_superuser: bool
|
|
||||||
telegram_id: int | None
|
|
||||||
avatar_path: str | None
|
|
||||||
model_config = ConfigDict(from_attributes=True, extra="ignore")
|
|
||||||
|
|
||||||
|
|
||||||
class UserWithHashedPass(User):
|
class UserCreate(schemas.BaseUserCreate):
|
||||||
hashed_password: str
|
|
||||||
|
|
||||||
|
|
||||||
class UserWithTasks(User):
|
|
||||||
tasks: list[TaskWithId]
|
|
||||||
|
|
||||||
|
|
||||||
class UserRequest(BaseModel):
|
|
||||||
username: str
|
username: str
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
class UserRequestADD(BaseModel):
|
class UserUpdate(schemas.BaseUserUpdate):
|
||||||
username: Annotated[str, BeforeValidator(ensure_username)]
|
username: Optional[str] = None
|
||||||
email: EmailStr | None = None
|
|
||||||
password: Annotated[str, BeforeValidator(ensure_password)]
|
|
||||||
|
|
||||||
|
|
||||||
class UserAdd(BaseModel):
|
|
||||||
email: EmailStr | None
|
|
||||||
username: str
|
|
||||||
hashed_password: str
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_password(value: Any) -> str:
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise TypeError("Password must be a string")
|
|
||||||
value = value.strip()
|
|
||||||
if len(value) < 8:
|
|
||||||
raise ValueError("Password must be at least 8 characters")
|
|
||||||
elif value.strip() == "":
|
|
||||||
raise ValueError("Password cannot be empty")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_username(value: str) -> str:
|
|
||||||
value = value.strip()
|
|
||||||
if len(value) < 3:
|
|
||||||
raise ValueError("Username must be at least 3 characters")
|
|
||||||
elif value.lower() in ["admin", "moderator", "админ", "модератор"]:
|
|
||||||
raise ValueError("Login is already taken")
|
|
||||||
return value
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
from src.core.auth_manager import AuthManager
|
|
||||||
from src.core.settings import settings
|
|
||||||
from src.schemas.auth import TokenData
|
|
||||||
from src.schemas.users import User, UserAdd, UserRequestADD, UserWithHashedPass
|
|
||||||
from src.services.base import BaseService
|
|
||||||
|
|
||||||
|
|
||||||
class AuthService(BaseService):
|
|
||||||
async def registration(self, cred: UserRequestADD) -> User:
|
|
||||||
hashed_pass = AuthManager.get_password_hash(cred.password)
|
|
||||||
user_to_insert = UserAdd(
|
|
||||||
username=cred.username,
|
|
||||||
email=cred.email,
|
|
||||||
hashed_password=hashed_pass,
|
|
||||||
)
|
|
||||||
result = await self.session.user.create_one(user_to_insert.model_dump())
|
|
||||||
await self.session.commit()
|
|
||||||
return User.model_validate(result)
|
|
||||||
|
|
||||||
async def _tokens_create(self, user_data: TokenData, fingerprint: str):
|
|
||||||
access_token = AuthManager.create_access_token(user_data.model_dump())
|
|
||||||
refresh_token = AuthManager.create_refresh_token()
|
|
||||||
await self.session.auth.create_one({
|
|
||||||
"token": refresh_token,
|
|
||||||
"user_id": user_data.id,
|
|
||||||
"fingerprint": fingerprint,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
"access_token": access_token,
|
|
||||||
"token_type": settings.access_token.token_type,
|
|
||||||
"refresh_token": refresh_token,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def login(self, username: str, password: str, fingerprint: str) -> dict:
|
|
||||||
login_exception = HTTPException(
|
|
||||||
status_code=401,
|
|
||||||
detail="Incorrect username or password",
|
|
||||||
)
|
|
||||||
result = await self.session.user.get_one_or_none(username=username)
|
|
||||||
if result is None:
|
|
||||||
raise login_exception
|
|
||||||
user = UserWithHashedPass.model_validate(result)
|
|
||||||
token_data = TokenData.model_validate(user.model_dump())
|
|
||||||
verify = AuthManager.verify_password(
|
|
||||||
plain_password=password, hashed_password=user.hashed_password
|
|
||||||
)
|
|
||||||
if not verify or user.is_active is False:
|
|
||||||
raise login_exception
|
|
||||||
tokens = await self._tokens_create(token_data, fingerprint)
|
|
||||||
await self.session.commit()
|
|
||||||
return tokens
|
|
||||||
|
|
||||||
async def refresh_tokens(
|
|
||||||
self, refresh_token: str, user_data: TokenData, fingerprint: str
|
|
||||||
) -> dict:
|
|
||||||
token_record = await self.session.auth.get_one_or_none(token=refresh_token)
|
|
||||||
if not token_record or token_record.user_id != user_data.id:
|
|
||||||
raise HTTPException(status_code=401, detail="Invalid refresh token")
|
|
||||||
token = await self._tokens_create(user_data, fingerprint)
|
|
||||||
await self.session.auth.delete_one(token=refresh_token)
|
|
||||||
await self.session.commit()
|
|
||||||
return token
|
|
||||||
|
|
||||||
async def delete_token(self, token: str) -> None:
|
|
||||||
await self.session.auth.delete_one(token=token)
|
|
||||||
await self.session.commit()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from src.core.interfaces import IUOWDB
|
|
||||||
|
|
||||||
|
|
||||||
class BaseService:
|
|
||||||
session: IUOWDB
|
|
||||||
|
|
||||||
def __init__(self, session: "IUOWDB"):
|
|
||||||
self.session = session
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
from src.schemas.tasks import Task, TaskADDRequest, TaskPATCHRequest
|
|
||||||
from src.services.base import BaseService
|
|
||||||
|
|
||||||
|
|
||||||
class TaskService(BaseService):
|
|
||||||
async def create_task(self, user_id: int, task_data: TaskADDRequest) -> Task:
|
|
||||||
user = await self.session.user.get_one_or_none(id=user_id)
|
|
||||||
if user is None:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found.")
|
|
||||||
data_to_insert = task_data.model_dump(exclude_none=True)
|
|
||||||
data_to_insert["user_id"] = user.id
|
|
||||||
created_task_orm = await self.session.task.create_one(data_to_insert)
|
|
||||||
await self.session.commit()
|
|
||||||
return Task.model_validate(created_task_orm)
|
|
||||||
|
|
||||||
async def get_task(self, task_id: int):
|
|
||||||
task = await self.session.task.get_one_or_none(id=task_id)
|
|
||||||
if task is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Task not found.")
|
|
||||||
return Task.model_validate(task)
|
|
||||||
|
|
||||||
async def delete_task(self, task_id: int):
|
|
||||||
await self.session.task.delete_one(id=task_id)
|
|
||||||
await self.session.commit()
|
|
||||||
|
|
||||||
async def update_task(
|
|
||||||
self,
|
|
||||||
task_id: int,
|
|
||||||
task_data: TaskPATCHRequest,
|
|
||||||
exclude_unset: bool = True,
|
|
||||||
):
|
|
||||||
task = await self.session.task.update_one(
|
|
||||||
data=task_data.model_dump(exclude_unset=exclude_unset), id=task_id
|
|
||||||
)
|
|
||||||
await self.session.commit()
|
|
||||||
return Task.model_validate(task)
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
from src.schemas.users import User, UserUpdate, UserWithTasks
|
|
||||||
from src.services.base import BaseService
|
|
||||||
|
|
||||||
|
|
||||||
class UserService(BaseService):
|
|
||||||
async def get_user_by_filter(self, **filter_by) -> User | None:
|
|
||||||
result = await self.session.user.get_one_or_none(**filter_by)
|
|
||||||
if result is None:
|
|
||||||
return None
|
|
||||||
return User.model_validate(result)
|
|
||||||
|
|
||||||
async def get_user_by_filter_or_raise(self, **filter_by) -> User:
|
|
||||||
user = await self.get_user_by_filter(**filter_by)
|
|
||||||
if user is None:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
return user
|
|
||||||
|
|
||||||
async def validate_admin_user(self, username: str) -> User:
|
|
||||||
user = await self.get_user_by_filter_or_raise(username=username)
|
|
||||||
if not user.is_superuser:
|
|
||||||
raise HTTPException(status_code=403, detail="Admin access required")
|
|
||||||
return user
|
|
||||||
|
|
||||||
async def get_all_users(self) -> list[User]:
|
|
||||||
users = await self.session.user.get_all()
|
|
||||||
return [User.model_validate(user) for user in users]
|
|
||||||
|
|
||||||
async def delete_user(self, id: int) -> None:
|
|
||||||
await self.session.user.delete_one(id=id)
|
|
||||||
await self.session.commit()
|
|
||||||
|
|
||||||
async def update_user(self, id: int, update_data: UserUpdate) -> User:
|
|
||||||
await self.get_user_by_filter_or_raise(id=id)
|
|
||||||
user = await self.session.user.update_one(
|
|
||||||
data=update_data.model_dump(exclude_unset=True), id=id
|
|
||||||
)
|
|
||||||
await self.session.commit()
|
|
||||||
return User.model_validate(user)
|
|
||||||
|
|
||||||
async def get_user_with_tasks(self, user_id: int, **attrs):
|
|
||||||
if attrs.get("page") and attrs.get("limit"):
|
|
||||||
tasks_offset = (attrs.get("page", 0) - 1) * attrs.get("limit")
|
|
||||||
else:
|
|
||||||
tasks_offset = None
|
|
||||||
user = await self.session.user.get_one_with_load(
|
|
||||||
user_id=user_id,
|
|
||||||
status=attrs.get("status"),
|
|
||||||
priority=attrs.get("priority"),
|
|
||||||
tasks_limit=attrs.get("limit"),
|
|
||||||
tasks_offset=tasks_offset,
|
|
||||||
date_from=attrs.get("date_from"),
|
|
||||||
date_to=attrs.get("date_to"),
|
|
||||||
)
|
|
||||||
if user is None:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found.")
|
|
||||||
return UserWithTasks.model_validate(user)
|
|
||||||
10
src/settings.py
Normal file
10
src/settings.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||||
|
LIFETIME: int
|
||||||
|
SECRET: str
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
34
src/utils/user_manager.py
Normal file
34
src/utils/user_manager.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi_users import BaseUserManager, IntegerIDMixin
|
||||||
|
|
||||||
|
from src.models import UsersORM
|
||||||
|
from src.settings import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(IntegerIDMixin, BaseUserManager[UsersORM, int]):
|
||||||
|
reset_password_token_secret = settings.SECRET
|
||||||
|
verification_token_secret = settings.SECRET
|
||||||
|
|
||||||
|
async def on_after_register(
|
||||||
|
self, user: UsersORM, request: Optional[Request] = None
|
||||||
|
):
|
||||||
|
logger.warning("User %r has registered.", user.id)
|
||||||
|
|
||||||
|
async def on_after_forgot_password(
|
||||||
|
self, user: UsersORM, token: str, request: Optional[Request] = None
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"User %r has forgot their password. Reset token: %r", user.id, token
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_after_request_verify(
|
||||||
|
self, user: UsersORM, token: str, request: Optional[Request] = None
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"Verification requested for user %r. Verification token: %r", user.id, token
|
||||||
|
)
|
||||||
24
taskncoffee-app/.gitignore
vendored
24
taskncoffee-app/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Task&Coffee Frontend
|
|
||||||
|
|
||||||
> Modern task management app with Material Design 3 + Neon aesthetic
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Styling System
|
|
||||||
|
|
||||||
We use a custom design system combining:
|
|
||||||
- **Mirage Color Palette** - Professional dark blue theme
|
|
||||||
- **Material Design 3** - Modern UI patterns
|
|
||||||
- **Neon Cyberpunk** - Unique glowing accents
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import styles from '@/lib/styles';
|
|
||||||
|
|
||||||
<div className={styles.card.filled}>
|
|
||||||
<h2 className={styles.text.h2}>Hello</h2>
|
|
||||||
<button className={styles.button.primaryNeon}>Click</button>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📖 Documentation
|
|
||||||
|
|
||||||
| Document | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| **[Cheatsheet](./docs/STYLES_CHEATSHEET.md)** | 📋 Quick reference - copy/paste examples |
|
|
||||||
| **[Style Guide](./docs/STYLE_GUIDE.md)** | 📚 Complete guide with examples |
|
|
||||||
| **[Color Palette](./docs/COLOR_PALETTE.md)** | 🎨 Color meanings & usage |
|
|
||||||
|
|
||||||
**👉 Start with [Cheatsheet](./docs/STYLES_CHEATSHEET.md)** for quick examples!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Tech Stack
|
|
||||||
|
|
||||||
- React 18 + Vite
|
|
||||||
- Tailwind CSS + Custom Design System
|
|
||||||
- Radix UI (accessibility)
|
|
||||||
- Lucide React (icons)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── api/ API services
|
|
||||||
├── components/ Reusable components
|
|
||||||
├── lib/
|
|
||||||
│ ├── styles.js ⭐ Style presets (60+ ready-to-use)
|
|
||||||
│ └── utils.js Helper functions
|
|
||||||
├── pages/ Page components
|
|
||||||
├── index.css ⭐ Color variables (Mirage palette)
|
|
||||||
└── neon.css ⭐ Neon glow effects
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Learn More
|
|
||||||
|
|
||||||
- [Design System Overview](../DESIGN_SYSTEM_SUMMARY.md)
|
|
||||||
- [Vite](https://vitejs.dev/) | [React](https://react.dev/) | [Tailwind](https://tailwindcss.com/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Made with 💙 and neon ✨**
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "new-york",
|
|
||||||
"rsc": false,
|
|
||||||
"tsx": false,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "src/index.css",
|
|
||||||
"baseColor": "gray",
|
|
||||||
"cssVariables": true,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide",
|
|
||||||
"aliases": {
|
|
||||||
"components": "@/components",
|
|
||||||
"utils": "@/lib/utils",
|
|
||||||
"ui": "@/components/ui",
|
|
||||||
"lib": "@/lib",
|
|
||||||
"hooks": "@/hooks"
|
|
||||||
},
|
|
||||||
"registries": {}
|
|
||||||
}
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
# 🎨 Task&Coffee Color Palette
|
|
||||||
|
|
||||||
**Usage examples?** See [STYLE_GUIDE.md](./STYLE_GUIDE.md)
|
|
||||||
**Quick reference?** See [STYLES_CHEATSHEET.md](./STYLES_CHEATSHEET.md)
|
|
||||||
**Back to main?** See [README.md](../README.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mirage Palette - Visual Guide
|
|
||||||
|
|
||||||
```
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
MIRAGE COLOR SCALE
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
50 ░░░░░░░░░ #f4f6fb Lightest - heading text on dark bg
|
|
||||||
100 ▒▒▒▒▒▒▒▒▒ #e7edf7 Card foreground text
|
|
||||||
200 ▓▓▓▓▓▓▓▓▓ #cbd9ec Main foreground text
|
|
||||||
300 ▓▓▓▓▓▓▓▓▓ #9db8dc Muted foreground text
|
|
||||||
────────────────────────────────────────────────────────────
|
|
||||||
400 ████████ #6893c8 PRIMARY ACCENT ⭐
|
|
||||||
500 ████████ #4474b3 Accent hover state
|
|
||||||
────────────────────────────────────────────────────────────
|
|
||||||
600 ████████ #335b96 Secondary, elevated cards
|
|
||||||
700 ████████ #2a4a7a Dark cards alternative
|
|
||||||
800 ████████ #264066 Muted elements, popovers
|
|
||||||
900 ████████ #243756 CARDS ⭐
|
|
||||||
950 ████████ #111928 BACKGROUND (darkest) ⭐
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌟 Semantic Color Mapping
|
|
||||||
|
|
||||||
### Background & Surfaces
|
|
||||||
```
|
|
||||||
--background #111928 (mirage-950) Main app background
|
|
||||||
--card #243756 (mirage-900) Card surfaces
|
|
||||||
--popover #264066 (mirage-800) Floating elements
|
|
||||||
--muted #264066 (mirage-800) Subtle backgrounds
|
|
||||||
--sidebar #111928 (mirage-950) Navigation rail
|
|
||||||
```
|
|
||||||
|
|
||||||
### Foreground & Text
|
|
||||||
```
|
|
||||||
--foreground #cbd9ec (mirage-200) Primary text
|
|
||||||
--card-foreground #e7edf7 (mirage-100) Text on cards
|
|
||||||
--muted-foreground #9db8dc (mirage-300) Secondary text
|
|
||||||
--popover-foreground #e7edf7 (mirage-100) Text in popovers
|
|
||||||
```
|
|
||||||
|
|
||||||
### Interactive Elements
|
|
||||||
```
|
|
||||||
--primary #6893c8 (mirage-400) Primary actions, links
|
|
||||||
--primary-foreground #111928 (mirage-950) Text on primary
|
|
||||||
--secondary #2a4a7a (mirage-700) Secondary actions
|
|
||||||
--accent #4474b3 (mirage-500) Hover states
|
|
||||||
```
|
|
||||||
|
|
||||||
### Borders & Inputs
|
|
||||||
```
|
|
||||||
--border rgba(157, 184, 220, 0.15) Subtle borders
|
|
||||||
--input rgba(157, 184, 220, 0.2) Input borders
|
|
||||||
--ring #6893c8 (mirage-400) Focus ring
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💫 Neon Accent Colors
|
|
||||||
|
|
||||||
```
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
NEON ACCENTS
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
|
|
||||||
🔵 Neon Blue #c6e2ff Light blue with glow effect
|
|
||||||
Glow color: rgba(30, 132, 242, 0.6)
|
|
||||||
|
|
||||||
💗 Neon Pink #ffc5ec Light pink with glow effect
|
|
||||||
Glow color: rgba(255, 20, 147, 0.6)
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Usage by Element Type
|
|
||||||
|
|
||||||
### Text Colors
|
|
||||||
```
|
|
||||||
Headings: mirage-200 (#cbd9ec)
|
|
||||||
Body text: mirage-200 (#cbd9ec)
|
|
||||||
Secondary text: mirage-300 (#9db8dc)
|
|
||||||
Disabled text: mirage-300 (#9db8dc) with opacity
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backgrounds
|
|
||||||
```
|
|
||||||
Page background: mirage-950 (#111928)
|
|
||||||
Card background: mirage-900 (#243756)
|
|
||||||
Hover background: mirage-800 (#264066)
|
|
||||||
Active background: mirage-700 (#2a4a7a)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Buttons
|
|
||||||
```
|
|
||||||
Primary: mirage-400 (#6893c8) bg
|
|
||||||
Primary hover: mirage-500 (#4474b3) bg
|
|
||||||
Secondary: mirage-700 (#2a4a7a) bg
|
|
||||||
Secondary hover: mirage-600 (#335b96) bg
|
|
||||||
Ghost hover: mirage-800 (#264066) bg
|
|
||||||
```
|
|
||||||
|
|
||||||
### Borders
|
|
||||||
```
|
|
||||||
Default: mirage-300 with 15% opacity
|
|
||||||
Input: mirage-300 with 20% opacity
|
|
||||||
Focus: mirage-400 (#6893c8) solid
|
|
||||||
Divider: mirage-800 (#264066)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌈 Color Relationships
|
|
||||||
|
|
||||||
```
|
|
||||||
LIGHT ↑
|
|
||||||
|
|
||||||
┌─────────────────────────────────┐
|
|
||||||
│ mirage-50 │ Lightest text │
|
|
||||||
│ mirage-100 │ Card text │
|
|
||||||
│ mirage-200 │ Main text │
|
|
||||||
│ mirage-300 │ Muted text │
|
|
||||||
├─────────────────────────────────┤
|
|
||||||
│ mirage-400 │ PRIMARY ⭐ │ ← Interactive
|
|
||||||
│ mirage-500 │ Hover state │
|
|
||||||
├─────────────────────────────────┤
|
|
||||||
│ mirage-600 │ Secondary │
|
|
||||||
│ mirage-700 │ Alt cards │
|
|
||||||
│ mirage-800 │ Muted bg │
|
|
||||||
│ mirage-900 │ CARDS ⭐ │ ← Surfaces
|
|
||||||
│ mirage-950 │ BACKGROUND ⭐ │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
|
|
||||||
DARK ↓
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Contrast Ratios (WCAG)
|
|
||||||
|
|
||||||
```
|
|
||||||
Background (950) + Text (200): AAA ✓ (14.2:1)
|
|
||||||
Card (900) + Card Text (100): AAA ✓ (12.8:1)
|
|
||||||
Primary (400) + Primary FG (950): AA ✓ (5.2:1)
|
|
||||||
Muted (800) + Muted Text (300): AA ✓ (4.8:1)
|
|
||||||
```
|
|
||||||
|
|
||||||
All color combinations meet WCAG AA standards minimum! 🎉
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Example Compositions
|
|
||||||
|
|
||||||
### Card with Neon Accent
|
|
||||||
```
|
|
||||||
┌────────────────────────────────┐
|
|
||||||
│ mirage-900 background │
|
|
||||||
│ ┌──────────────────────────┐ │
|
|
||||||
│ │ mirage-100 text │ │
|
|
||||||
│ │ mirage-300 muted text │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ [mirage-400 button] │ │
|
|
||||||
│ │ + neon blue glow │ │
|
|
||||||
│ └──────────────────────────┘ │
|
|
||||||
└────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Primary Button
|
|
||||||
```
|
|
||||||
┌──────────────────────┐
|
|
||||||
│ mirage-400 bg │ ← Background
|
|
||||||
│ mirage-950 text │ ← Text
|
|
||||||
│ + subtle shadow │
|
|
||||||
│ hover: neon glow │ ← Neon effect on hover
|
|
||||||
└──────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Today's Task Column
|
|
||||||
```
|
|
||||||
┌────────────────────────────────┐
|
|
||||||
│ mirage-400/10 background │ ← 10% opacity
|
|
||||||
│ + mirage-400 border (2px) │ ← Solid border
|
|
||||||
│ + neon-blue glow │ ← Glow effect
|
|
||||||
│ │
|
|
||||||
│ Tasks: mirage-900 cards │
|
|
||||||
└────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Dark Mode Toggle
|
|
||||||
|
|
||||||
When dark mode is active (`.dark` class):
|
|
||||||
|
|
||||||
```
|
|
||||||
Background: mirage-900 → mirage-950
|
|
||||||
Cards: mirage-700 → mirage-900
|
|
||||||
Primary: mirage-300 → mirage-400
|
|
||||||
Text: mirage-100 → mirage-200
|
|
||||||
```
|
|
||||||
|
|
||||||
Currently configured for dark-first design.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Best Practices
|
|
||||||
|
|
||||||
1. **Depth hierarchy:**
|
|
||||||
- Background: mirage-950
|
|
||||||
- Elevated: mirage-900
|
|
||||||
- More elevated: mirage-800
|
|
||||||
|
|
||||||
2. **Text contrast:**
|
|
||||||
- Always use mirage-100/200 for text on dark backgrounds
|
|
||||||
- Use mirage-300 for less important text
|
|
||||||
|
|
||||||
3. **Neon sparingly:**
|
|
||||||
- Use neon effects only for:
|
|
||||||
- Brand elements (logo, headers)
|
|
||||||
- Important CTAs
|
|
||||||
- Today's tasks/current items
|
|
||||||
- Don't overuse - it should feel special
|
|
||||||
|
|
||||||
4. **Accessible focus states:**
|
|
||||||
- Always use mirage-400 for focus rings
|
|
||||||
- Add 3px ring with 50% opacity
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Quick Reference
|
|
||||||
|
|
||||||
| Element | Light Value | Dark Value | Usage |
|
|
||||||
|---------|-------------|------------|-------|
|
|
||||||
| Page BG | `mirage-950` | `mirage-900` | Main background |
|
|
||||||
| Card BG | `mirage-900` | `mirage-700` | Surface |
|
|
||||||
| Text | `mirage-200` | `mirage-100` | Primary text |
|
|
||||||
| Primary | `mirage-400` | `mirage-300` | CTA buttons |
|
|
||||||
| Border | `mirage-300/15%` | `mirage-300/20%` | Dividers |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**See also:**
|
|
||||||
- `STYLE_GUIDE.md` - Complete style documentation
|
|
||||||
- `STYLES_CHEATSHEET.md` - Quick usage examples
|
|
||||||
- `src/lib/styles.js` - Tailwind class presets
|
|
||||||
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
# Task&Coffee Styles Cheatsheet 🎨
|
|
||||||
|
|
||||||
Быстрая справка по использованию стилей в проекте.
|
|
||||||
|
|
||||||
**Full guide?** See [STYLE_GUIDE.md](./STYLE_GUIDE.md)
|
|
||||||
**Color reference?** See [COLOR_PALETTE.md](./COLOR_PALETTE.md)
|
|
||||||
**Back to main?** See [README.md](../README.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Импорт
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import styles from '@/lib/styles';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Карточки
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<div className={styles.card.filled}>Basic card</div>
|
|
||||||
<div className={styles.card.elevated}>Elevated card</div>
|
|
||||||
<div className={styles.card.task}>Task card</div>
|
|
||||||
<div className={styles.card.taskNeon}>Task card with glow</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔘 Кнопки
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<button className={styles.button.primary}>Primary</button>
|
|
||||||
<button className={styles.button.primaryNeon}>Primary Neon</button>
|
|
||||||
<button className={styles.button.secondary}>Secondary</button>
|
|
||||||
<button className={styles.button.outline}>Outline</button>
|
|
||||||
<button className={styles.button.ghost}>Ghost</button>
|
|
||||||
<button className={styles.button.destructive}>Delete</button>
|
|
||||||
<button className={styles.button.icon}>🔍</button>
|
|
||||||
<button className={styles.button.iconNeon}>✨</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Inputs
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<input className={styles.input.default} />
|
|
||||||
<input className={styles.input.neon} />
|
|
||||||
<input className={styles.input.search} />
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏷️ Badges
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<span className={styles.badge.low}>Low</span>
|
|
||||||
<span className={styles.badge.medium}>Medium</span>
|
|
||||||
<span className={styles.badge.high}>High</span>
|
|
||||||
<span className={styles.badge.critical}>Critical</span>
|
|
||||||
<span className={styles.badge.success}>Success</span>
|
|
||||||
<span className={styles.badge.warning}>Warning</span>
|
|
||||||
<span className={styles.badge.error}>Error</span>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Neon Effects (CSS Classes)
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<div className="neon-glow-blue">Blue glow on hover</div>
|
|
||||||
<div className="neon-glow-pink">Pink glow on hover</div>
|
|
||||||
<div className="neon-glow-soft">Soft glow</div>
|
|
||||||
<div className="neon-border-blue">Blue neon border</div>
|
|
||||||
<div className="neon-border-pink">Pink neon border</div>
|
|
||||||
<span className="neon-text-blue">Blue neon text</span>
|
|
||||||
<span className="neon-text-pink">Pink neon text</span>
|
|
||||||
<Icon className="neon-icon" />
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎭 Neon Animated Text
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<span className="sign-inline">Task&Coffee</span>
|
|
||||||
<span className="sign-pink-inline">Task&Coffee</span>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📐 Layout
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<div className={styles.layout.container}>
|
|
||||||
<aside className={styles.layout.sidebar}>Sidebar</aside>
|
|
||||||
<main className={styles.layout.main}>
|
|
||||||
<div className={styles.layout.content}>Content</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.layout.gridCards}>Cards grid</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔤 Typography
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<h1 className={styles.text.h1}>Heading 1</h1>
|
|
||||||
<h2 className={styles.text.h2}>Heading 2</h2>
|
|
||||||
<p className={styles.text.body}>Body text</p>
|
|
||||||
<p className={styles.text.bodyMuted}>Muted text</p>
|
|
||||||
<small className={styles.text.small}>Small text</small>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Utilities
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Glow effects
|
|
||||||
<div className={styles.utility.glowBlue}>Blue glow</div>
|
|
||||||
<div className={styles.utility.glowPink}>Pink glow</div>
|
|
||||||
<div className={styles.utility.glowSoft}>Soft glow</div>
|
|
||||||
|
|
||||||
// Glass effect
|
|
||||||
<div className={styles.utility.glass}>Glassmorphism</div>
|
|
||||||
|
|
||||||
// Transitions
|
|
||||||
<div className={styles.utility.transition}>Smooth</div>
|
|
||||||
<div className={styles.utility.transitionSlow}>Slow</div>
|
|
||||||
|
|
||||||
// Focus ring
|
|
||||||
<button className={styles.utility.focusRing}>Accessible</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Цветовые переменные
|
|
||||||
|
|
||||||
### В CSS
|
|
||||||
```css
|
|
||||||
background: var(--color-background);
|
|
||||||
color: var(--color-foreground);
|
|
||||||
background: var(--color-card);
|
|
||||||
color: var(--color-primary);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
```
|
|
||||||
|
|
||||||
### В Tailwind
|
|
||||||
```jsx
|
|
||||||
<div className="bg-background text-foreground">
|
|
||||||
<div className="bg-card text-card-foreground">
|
|
||||||
<div className="bg-primary text-primary-foreground">
|
|
||||||
<div className="border border-border">
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Комбинирование
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
<div className={cn(
|
|
||||||
styles.card.task,
|
|
||||||
"neon-glow-soft",
|
|
||||||
isActive && "neon-border-blue"
|
|
||||||
)}>
|
|
||||||
Combined styles
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📏 Радиусы
|
|
||||||
|
|
||||||
- `rounded-lg` = **0.5rem** ← используем для кнопок и карточек
|
|
||||||
- `rounded-xl` = 0.75rem
|
|
||||||
- `rounded-2xl` = 1rem
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Полный пример компонента
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import styles from '@/lib/styles';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function TaskCard({ task, isToday }) {
|
|
||||||
return (
|
|
||||||
<div className={cn(
|
|
||||||
styles.card.task,
|
|
||||||
"neon-glow-soft",
|
|
||||||
isToday && "neon-border-blue"
|
|
||||||
)}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className={styles.badge[task.priority]}>
|
|
||||||
{task.priority}
|
|
||||||
</span>
|
|
||||||
<h3 className={styles.text.small}>{task.title}</h3>
|
|
||||||
</div>
|
|
||||||
<p className={styles.text.smallMuted}>{task.description}</p>
|
|
||||||
<button className={styles.button.primaryNeon}>
|
|
||||||
Complete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌈 Mirage Colors
|
|
||||||
|
|
||||||
| Name | Hex | Usage |
|
|
||||||
|------|-----|-------|
|
|
||||||
| mirage-950 | `#111928` | Background (darkest) |
|
|
||||||
| mirage-900 | `#243756` | Cards |
|
|
||||||
| mirage-800 | `#264066` | Muted elements |
|
|
||||||
| mirage-700 | `#2a4a7a` | Secondary |
|
|
||||||
| mirage-600 | `#335b96` | Elevated cards |
|
|
||||||
| mirage-500 | `#4474b3` | Accent hover |
|
|
||||||
| mirage-400 | `#6893c8` | Primary accent |
|
|
||||||
| mirage-300 | `#9db8dc` | Muted text |
|
|
||||||
| mirage-200 | `#cbd9ec` | Main text |
|
|
||||||
| mirage-100 | `#e7edf7` | Card text |
|
|
||||||
| mirage-50 | `#f4f6fb` | Lightest text |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Tips
|
|
||||||
|
|
||||||
1. Используйте готовые стили из `styles.js`
|
|
||||||
2. Комбинируйте с `cn()` для кастомизации
|
|
||||||
3. Неон - только для акцентов!
|
|
||||||
4. Всегда используйте CSS переменные
|
|
||||||
5. `rounded-lg` для консистентности
|
|
||||||
|
|
||||||
**Полная документация:** См. `STYLE_GUIDE.md`
|
|
||||||
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
# Task&Coffee Style Guide
|
|
||||||
|
|
||||||
> Complete guide with examples and best practices
|
|
||||||
|
|
||||||
**Quick reference?** See [STYLES_CHEATSHEET.md](./STYLES_CHEATSHEET.md)
|
|
||||||
**Color details?** See [COLOR_PALETTE.md](./COLOR_PALETTE.md)
|
|
||||||
**Back to main?** See [README.md](../README.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Using Styles
|
|
||||||
|
|
||||||
### Import
|
|
||||||
```javascript
|
|
||||||
import styles from '@/lib/styles';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Примеры использования
|
|
||||||
|
|
||||||
#### 1. Карточки (Cards)
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Filled card - основной вариант
|
|
||||||
<div className={cardStyles.filled}>
|
|
||||||
<h3>Card Title</h3>
|
|
||||||
<p>Card content...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Elevated card с тенью
|
|
||||||
<div className={cardStyles.elevated}>
|
|
||||||
<h3>Important Card</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Task card с неоновым эффектом
|
|
||||||
<div className={cardStyles.taskNeon}>
|
|
||||||
<p>Task description</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Или используя cn helper
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
<div className={cn(cardStyles.filled, "additional-class")}>
|
|
||||||
Content
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Кнопки (Buttons)
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Primary button
|
|
||||||
<button className={buttonStyles.primary}>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
|
|
||||||
// Primary с неоновым эффектом
|
|
||||||
<button className={buttonStyles.primaryNeon}>
|
|
||||||
Create Task
|
|
||||||
</button>
|
|
||||||
|
|
||||||
// Secondary button
|
|
||||||
<button className={buttonStyles.secondary}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
|
|
||||||
// Ghost button
|
|
||||||
<button className={buttonStyles.ghost}>
|
|
||||||
Learn More
|
|
||||||
</button>
|
|
||||||
|
|
||||||
// Icon button с неоновым эффектом
|
|
||||||
<button className={buttonStyles.iconNeon}>
|
|
||||||
<Icon />
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Layouts
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Dashboard layout
|
|
||||||
<div className={layoutStyles.container}>
|
|
||||||
<aside className={layoutStyles.sidebar}>
|
|
||||||
{/* Sidebar content */}
|
|
||||||
</aside>
|
|
||||||
<main className={layoutStyles.main}>
|
|
||||||
<div className={layoutStyles.content}>
|
|
||||||
{/* Main content */}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Grid для карточек
|
|
||||||
<div className={layoutStyles.gridCards}>
|
|
||||||
<Card />
|
|
||||||
<Card />
|
|
||||||
<Card />
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Inputs
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Стандартный input
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={inputStyles.default}
|
|
||||||
placeholder="Enter text..."
|
|
||||||
/>
|
|
||||||
|
|
||||||
// Input с неоновым фокусом
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={inputStyles.neon}
|
|
||||||
placeholder="Task name..."
|
|
||||||
/>
|
|
||||||
|
|
||||||
// Search input
|
|
||||||
<div className="relative">
|
|
||||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2" />
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className={inputStyles.search}
|
|
||||||
placeholder="Search tasks..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. Typography
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Headings
|
|
||||||
<h1 className={textStyles.h1}>Main Title</h1>
|
|
||||||
<h2 className={textStyles.h2}>Section Title</h2>
|
|
||||||
|
|
||||||
// Body text
|
|
||||||
<p className={textStyles.body}>Regular text</p>
|
|
||||||
<p className={textStyles.bodyMuted}>Muted text</p>
|
|
||||||
|
|
||||||
// Неоновый текст (используется с neon.css)
|
|
||||||
<span className={cn(textStyles.neonBlue, "sign-inline")}>
|
|
||||||
Task&
|
|
||||||
</span>
|
|
||||||
<span className={cn(textStyles.neonPink, "sign-pink-inline")}>
|
|
||||||
Coffee
|
|
||||||
</span>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 6. Badges
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Priority badges
|
|
||||||
<span className={badgeStyles.low}>Low</span>
|
|
||||||
<span className={badgeStyles.medium}>Medium</span>
|
|
||||||
<span className={badgeStyles.high}>High</span>
|
|
||||||
<span className={badgeStyles.critical}>Critical</span>
|
|
||||||
|
|
||||||
// Status badges
|
|
||||||
<span className={badgeStyles.success}>Completed</span>
|
|
||||||
<span className={badgeStyles.warning}>In Progress</span>
|
|
||||||
<span className={badgeStyles.error}>Failed</span>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Неоновые эффекты
|
|
||||||
|
|
||||||
### CSS классы из neon.css
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Неоновое свечение при hover - голубое
|
|
||||||
<div className="neon-glow-blue bg-card p-4 rounded-lg">
|
|
||||||
Card with blue glow on hover
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Неоновое свечение - розовое
|
|
||||||
<div className="neon-glow-pink bg-card p-4 rounded-lg">
|
|
||||||
Card with pink glow on hover
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Мягкое свечение
|
|
||||||
<button className="neon-glow-soft bg-primary px-4 py-2 rounded-lg">
|
|
||||||
Subtle glow
|
|
||||||
</button>
|
|
||||||
|
|
||||||
// Неоновая граница
|
|
||||||
<div className="neon-border-blue p-4 rounded-lg">
|
|
||||||
Border with neon effect
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Неоновый текст (без анимации)
|
|
||||||
<span className="neon-text-blue">
|
|
||||||
Static neon text
|
|
||||||
</span>
|
|
||||||
|
|
||||||
// Иконка с неоновым эффектом
|
|
||||||
<Icon className="neon-icon" />
|
|
||||||
```
|
|
||||||
|
|
||||||
### Анимированный неоновый текст
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// Для заголовков и брендинга
|
|
||||||
<div className="sign-inline">Coffee</div>
|
|
||||||
<div className="sign-pink-inline">Task&</div>
|
|
||||||
|
|
||||||
// Для больших заголовков (fullscreen)
|
|
||||||
<div className="sign">
|
|
||||||
<span>Task&Coffee</span>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎭 Комбинирование стилей
|
|
||||||
|
|
||||||
### Пример: Карточка задачи с неоновым эффектом
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { cardStyles, badgeStyles, textStyles } from '@/lib/styles';
|
|
||||||
|
|
||||||
function TaskCard({ task }) {
|
|
||||||
return (
|
|
||||||
<div className={cn(
|
|
||||||
cardStyles.task,
|
|
||||||
"neon-glow-soft",
|
|
||||||
task.isToday && "neon-border-blue"
|
|
||||||
)}>
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<span className={badgeStyles[task.priority]}>
|
|
||||||
{task.priority}
|
|
||||||
</span>
|
|
||||||
<h3 className={textStyles.small}>
|
|
||||||
{task.title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p className={textStyles.smallMuted}>
|
|
||||||
{task.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Пример: Кнопка с неоновым эффектом и иконкой
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { PlusIcon } from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { buttonStyles } from '@/lib/styles';
|
|
||||||
|
|
||||||
function CreateButton() {
|
|
||||||
return (
|
|
||||||
<button className={cn(
|
|
||||||
buttonStyles.primaryNeon,
|
|
||||||
"gap-2"
|
|
||||||
)}>
|
|
||||||
<PlusIcon className="w-4 h-4" />
|
|
||||||
Create Task
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎨 Миграция с dashboard.css на Tailwind
|
|
||||||
|
|
||||||
### До (dashboard.css):
|
|
||||||
```css
|
|
||||||
.dashboard-task-card {
|
|
||||||
background: var(--color-card);
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### После (с использованием стилей):
|
|
||||||
```jsx
|
|
||||||
<div className={cardStyles.task}>
|
|
||||||
{/* content */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Или чистый Tailwind:
|
|
||||||
<div className="bg-card p-3 rounded-lg shadow-sm">
|
|
||||||
{/* content */}
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📐 Радиусы округления
|
|
||||||
|
|
||||||
Теперь все радиусы синхронизированы:
|
|
||||||
- `rounded-sm` = `0.25rem` (4px)
|
|
||||||
- `rounded-md` = `0.375rem` (6px)
|
|
||||||
- `rounded-lg` = `0.5rem` (8px) ✅ **Используем для карточек и кнопок**
|
|
||||||
- `rounded-xl` = `0.75rem` (12px)
|
|
||||||
- `rounded-2xl` = `1rem` (16px)
|
|
||||||
|
|
||||||
Базовый радиус установлен в `--radius: 0.5rem` для консистентности с Material Design 3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Утилиты
|
|
||||||
|
|
||||||
### Neon Glow Utilities
|
|
||||||
```javascript
|
|
||||||
import { utilityStyles } from '@/lib/styles';
|
|
||||||
|
|
||||||
<div className={cn(cardStyles.filled, utilityStyles.glowBlue)}>
|
|
||||||
Card with blue glow
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Glassmorphism
|
|
||||||
```jsx
|
|
||||||
<div className={cn(utilityStyles.glass, "p-6 rounded-lg")}>
|
|
||||||
Glass effect card
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Focus Ring
|
|
||||||
```jsx
|
|
||||||
<button className={cn(
|
|
||||||
buttonStyles.primary,
|
|
||||||
utilityStyles.focusRing
|
|
||||||
)}>
|
|
||||||
Accessible button
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Best Practices
|
|
||||||
|
|
||||||
1. **Используйте готовые стили** из `styles.js` для консистентности
|
|
||||||
2. **Комбинируйте с cn()** для добавления кастомных классов
|
|
||||||
3. **Неоновые эффекты** используйте умеренно - только для акцентов
|
|
||||||
4. **Радиусы** - используйте `rounded-lg` для основных элементов
|
|
||||||
5. **Цвета** - всегда используйте CSS переменные, не хардкодьте цвета
|
|
||||||
6. **Accessibility** - не забывайте про `focusRing` для интерактивных элементов
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Быстрый старт
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
// 1. Импортируйте стили
|
|
||||||
import styles from '@/lib/styles';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
// 2. Используйте в компонентах
|
|
||||||
function MyComponent() {
|
|
||||||
return (
|
|
||||||
<div className={styles.layout.container}>
|
|
||||||
<div className={styles.card.filled}>
|
|
||||||
<h2 className={styles.text.h2}>Title</h2>
|
|
||||||
<p className={styles.text.bodyMuted}>Description</p>
|
|
||||||
<button className={styles.button.primaryNeon}>
|
|
||||||
Action
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Теперь у вас есть полная система стилей, объединяющая Mirage palette, Material Design 3 и неоновую эстетику! 🎉
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{js,jsx}'],
|
|
||||||
extends: [
|
|
||||||
// js.configs.recommended,
|
|
||||||
reactHooks.configs['recommended-latest'],
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
ecmaFeatures: { jsx: true },
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"react/jsx-uses-react": "error",
|
|
||||||
|
|
||||||
'no-unused-vars': ['error', {
|
|
||||||
varsIgnorePattern: '^[A-Z_]',
|
|
||||||
argsIgnorePattern: '^_',
|
|
||||||
ignoreRestSiblings: true
|
|
||||||
}],
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>taskncoffee-app</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5776
taskncoffee-app/package-lock.json
generated
5776
taskncoffee-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "taskncoffee-app",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
|
||||||
"axios": "^1.12.2",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"jwt-decode": "^4.0.0",
|
|
||||||
"lucide-react": "^0.544.0",
|
|
||||||
"motion": "^12.23.22",
|
|
||||||
"react": "^19.1.1",
|
|
||||||
"react-dom": "^19.1.1",
|
|
||||||
"react-router": "^7.9.4",
|
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"tailwindcss": "^4.1.14"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^9.36.0",
|
|
||||||
"@types/react": "^19.1.16",
|
|
||||||
"@types/react-dom": "^19.1.9",
|
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
|
||||||
"eslint": "^9.36.0",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.22",
|
|
||||||
"globals": "^16.4.0",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
|
||||||
"vite": "^7.1.7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 399 KiB |
@@ -1,35 +0,0 @@
|
|||||||
import './App.css'
|
|
||||||
import { LoginPage } from './pages/Login'
|
|
||||||
import { SignUp } from './pages/SignUp'
|
|
||||||
import { AuthLayout } from './layouts/AuthLayout'
|
|
||||||
import { Routes, Route, Navigate } from 'react-router'
|
|
||||||
import Dashboard from './pages/Dashboard'
|
|
||||||
import RootRedirect from './pages/RootRedirect'
|
|
||||||
import { TaskButton } from './components/Card'
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<RootRedirect />} />
|
|
||||||
|
|
||||||
<Route path="/auth" element={
|
|
||||||
<AuthLayout className="flex min-h-svh flex-col items-center justify-center bg-muted text-foreground" />
|
|
||||||
}>
|
|
||||||
<Route path="login" element={<LoginPage />} />
|
|
||||||
<Route path="signup" element={<SignUp />} />
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path="/dashboard" element={
|
|
||||||
<Dashboard />
|
|
||||||
} />
|
|
||||||
<Route path="/card" element={<TaskButton />} />
|
|
||||||
<Route path="/card/:id" element={
|
|
||||||
<TaskButton />
|
|
||||||
} />
|
|
||||||
<Route path="*" element={<Navigate to="/auth/login" replace />} />
|
|
||||||
</Routes>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
|
|
||||||
|
|
||||||
const API_V1_PREFIX = '/api/v1';
|
|
||||||
|
|
||||||
const API_SOURCES = {
|
|
||||||
AUTH: `${API_BASE_URL}${API_V1_PREFIX}/auth`,
|
|
||||||
USERS: `${API_BASE_URL}${API_V1_PREFIX}/users`,
|
|
||||||
TASKS: `${API_BASE_URL}${API_V1_PREFIX}/tasks`,
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_ENDPOINTS = {
|
|
||||||
AUTH: {
|
|
||||||
LOGIN: `${API_SOURCES.AUTH}/login`,
|
|
||||||
REFRESH: `${API_SOURCES.AUTH}/refresh`,
|
|
||||||
LOGOUT: `${API_SOURCES.AUTH}/logout`,
|
|
||||||
ME: `${API_SOURCES.AUTH}/me`,
|
|
||||||
REGISTER: `${API_SOURCES.AUTH}/signup`,
|
|
||||||
},
|
|
||||||
|
|
||||||
USERS: {
|
|
||||||
LIST: API_SOURCES.USERS,
|
|
||||||
BY_ID: (id) => `${API_SOURCES.USERS}/${id}`,
|
|
||||||
TASKS: (id) => `${API_SOURCES.USERS}/${id}/tasks`,
|
|
||||||
UPDATE: (id) => `${API_SOURCES.USERS}/${id}`,
|
|
||||||
DELETE: (id) => `${API_SOURCES.USERS}/${id}`,
|
|
||||||
},
|
|
||||||
|
|
||||||
TASKS: {
|
|
||||||
BY_ID: (id) => `${API_SOURCES.TASKS}/${id}`,
|
|
||||||
CREATE: `${API_SOURCES.TASKS}`,
|
|
||||||
UPDATE: (id) => `${API_SOURCES.TASKS}/${id}`,
|
|
||||||
DELETE: (id) => `${API_SOURCES.TASKS}/${id}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export { API_BASE_URL, API_V1_PREFIX, API_SOURCES, API_ENDPOINTS };
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import client from './client';
|
|
||||||
import { API_ENDPOINTS } from './ApiSources';
|
|
||||||
import { getFingerprint } from './fingerprint';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* New user registration
|
|
||||||
* @param {Object} userData - User data
|
|
||||||
* @param {string} userData.username - Username
|
|
||||||
* @param {string} userData.password - Password
|
|
||||||
* @param {string} userData.email - Email (optional)
|
|
||||||
* @returns {Promise<Object>} Registered user data
|
|
||||||
*/
|
|
||||||
export const signup = async (userData) => {
|
|
||||||
try {
|
|
||||||
const response = await client.post(API_ENDPOINTS.AUTH.REGISTER, userData);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current user information
|
|
||||||
* @returns {Promise<Object>} Current user information
|
|
||||||
*/
|
|
||||||
export const getMe = async () => {
|
|
||||||
try {
|
|
||||||
const response = await client.get(API_ENDPOINTS.AUTH.ME);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User logout
|
|
||||||
* Backend automatically removes refresh token from httpOnly cookie
|
|
||||||
* @returns {Promise<Object>} Logout result
|
|
||||||
*/
|
|
||||||
export const logout = async () => {
|
|
||||||
try {
|
|
||||||
const response = await client.post(API_ENDPOINTS.AUTH.LOGOUT);
|
|
||||||
if (response.data.status_code === 200) {
|
|
||||||
localStorage.removeItem('access_token');
|
|
||||||
localStorage.removeItem('fingerprint');
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
localStorage.removeItem('access_token');
|
|
||||||
localStorage.removeItem('fingerprint');
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User login
|
|
||||||
* @param {string} username - Username
|
|
||||||
* @param {string} password - Password
|
|
||||||
* @returns {Promise<Object>} Login result
|
|
||||||
*/
|
|
||||||
export const login = async (username, password) => {
|
|
||||||
try {
|
|
||||||
const fingerprint = await getFingerprint();
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('username', username);
|
|
||||||
formData.append('password', password);
|
|
||||||
formData.append('fingerprint', fingerprint);
|
|
||||||
const response = await client.post(API_ENDPOINTS.AUTH.LOGIN, formData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(response.data);
|
|
||||||
if (response.data.access_token && response.data.token_type === 'bearer') {
|
|
||||||
localStorage.setItem('access_token', response.data.access_token);
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const refreshToken = async () => {
|
|
||||||
try {
|
|
||||||
const fingerprint = localStorage.getItem('fingerprint');
|
|
||||||
const response = await client.post(API_ENDPOINTS.AUTH.REFRESH, {
|
|
||||||
fingerprint: fingerprint,
|
|
||||||
});
|
|
||||||
if (response.data.access_token && response.data.token_type === 'bearer') {
|
|
||||||
localStorage.setItem('access_token', response.data.access_token);
|
|
||||||
}
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
localStorage.removeItem('access_token');
|
|
||||||
localStorage.removeItem('fingerprint');
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { API_BASE_URL, API_V1_PREFIX } from './ApiSources';
|
|
||||||
|
|
||||||
|
|
||||||
const client = axios.create({
|
|
||||||
baseURL: `${API_BASE_URL}${API_V1_PREFIX}`,
|
|
||||||
timeout: 10000,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
client.interceptors.request.use(
|
|
||||||
(config) => {
|
|
||||||
const token = localStorage.getItem('access_token');
|
|
||||||
if (token) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
export default client;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
|
||||||
|
|
||||||
let fpPromise = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise} Promise с экземпляром FingerprintJS
|
|
||||||
*/
|
|
||||||
const initFingerprint = () => {
|
|
||||||
if (!fpPromise) {
|
|
||||||
fpPromise = FingerprintJS.load();
|
|
||||||
}
|
|
||||||
return fpPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise<string>} Fingerprint ID
|
|
||||||
*/
|
|
||||||
export const getFingerprint = async () => {
|
|
||||||
try {
|
|
||||||
const savedFingerprint = localStorage.getItem('fingerprint');
|
|
||||||
if (savedFingerprint) {
|
|
||||||
return savedFingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fp = await initFingerprint();
|
|
||||||
const result = await fp.get();
|
|
||||||
const fingerprint = result.visitorId;
|
|
||||||
|
|
||||||
localStorage.setItem('fingerprint', fingerprint);
|
|
||||||
return fingerprint;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting fingerprint:', error);
|
|
||||||
const fallbackFingerprint = `fallback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
localStorage.setItem('fingerprint', fallbackFingerprint);
|
|
||||||
return fallbackFingerprint;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearFingerprint = () => {
|
|
||||||
localStorage.removeItem('fingerprint');
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import client from './client';
|
|
||||||
import { API_ENDPOINTS } from './ApiSources';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new task
|
|
||||||
* @param {Object} taskData - Task data
|
|
||||||
* @param {string} taskData.title - Task title
|
|
||||||
* @param {string} taskData.description - Task description
|
|
||||||
* @param {string} taskData.priority - Task priority
|
|
||||||
* @param {string} taskData.status - Task status
|
|
||||||
* @param {string} taskData.due_date - Task due date
|
|
||||||
* @returns {Promise<Object>} Created task data
|
|
||||||
*/
|
|
||||||
export const createTask = async (taskData) => {
|
|
||||||
try {
|
|
||||||
const response = await client.post(API_ENDPOINTS.TASKS.CREATE, taskData);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import client from './client';
|
|
||||||
import { API_ENDPOINTS } from './ApiSources';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all users
|
|
||||||
* @returns {Promise<Object>} All users data
|
|
||||||
*/
|
|
||||||
export const getUsers = async () => {
|
|
||||||
try {
|
|
||||||
responce = await client.get(API_ENDPOINTS.USERS.LIST);
|
|
||||||
return responce.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user by id
|
|
||||||
* @param {number} id - User id
|
|
||||||
* @returns {Promise<Object>} User data
|
|
||||||
*/
|
|
||||||
export const getUserById = async (id) => {
|
|
||||||
try {
|
|
||||||
const response = await client.get(API_ENDPOINTS.USERS.BY_ID(id));
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all user tasks
|
|
||||||
* @param {number} id - User id
|
|
||||||
* @returns {Promise<Object>} All user tasks data
|
|
||||||
*/
|
|
||||||
export const getUserTasks = async (id) => {
|
|
||||||
try {
|
|
||||||
const response = await client.get(API_ENDPOINTS.USERS.TASKS(id));
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error.response?.data || error.message;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,178 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { getUserTasks } from '@/api/users.service';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import styles from '@/lib/styles';
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function Calendar() {
|
|
||||||
const [tasksFromBackend, setTasksFromBackend] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// Fetch tasks when component mounts
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchTasks = async () => {
|
|
||||||
try {
|
|
||||||
console.log('Calendar: Fetching user tasks...');
|
|
||||||
const response = await getUserTasks(1);
|
|
||||||
console.log('Calendar: Tasks from backend:', response);
|
|
||||||
setTasksFromBackend(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Calendar: Failed to fetch tasks:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchTasks();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
const currentDay = today.getDay();
|
|
||||||
|
|
||||||
|
|
||||||
const currentDate = new Date(today);
|
|
||||||
const dayOffset = currentDay === 0 ? -6 : 1 - currentDay;
|
|
||||||
currentDate.setDate(today.getDate() + dayOffset);
|
|
||||||
|
|
||||||
|
|
||||||
const groupTasksByDay = (tasks) => {
|
|
||||||
const grouped = {};
|
|
||||||
|
|
||||||
tasks.forEach(task => {
|
|
||||||
const taskDate = new Date(task.due_date);
|
|
||||||
const dateKey = taskDate.toDateString();
|
|
||||||
|
|
||||||
if (!grouped[dateKey]) {
|
|
||||||
grouped[dateKey] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
grouped[dateKey].push({
|
|
||||||
id: task.id,
|
|
||||||
title: task.title,
|
|
||||||
priority: task.priority,
|
|
||||||
completed: task.status === 'closed' || task.status === 'completed',
|
|
||||||
dueDate: task.due_date
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return grouped;
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupedTasks = groupTasksByDay(tasksFromBackend);
|
|
||||||
|
|
||||||
|
|
||||||
const daysOfWeek = [
|
|
||||||
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
|
|
||||||
].map((name, index) => {
|
|
||||||
const date = new Date(currentDate);
|
|
||||||
date.setDate(currentDate.getDate() + index);
|
|
||||||
const dateKey = date.toDateString();
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
date: date.getDate(),
|
|
||||||
month: date.toLocaleDateString('en-US', { month: 'short' }),
|
|
||||||
fullDate: date,
|
|
||||||
tasks: groupedTasks[dateKey] || []
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className={cn(styles.card.elevated, "flex-[3]")}>
|
|
||||||
<div className="flex items-center justify-center p-8">
|
|
||||||
<p className={styles.text.body}>Loading tasks...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn(
|
|
||||||
styles.utility.glass,
|
|
||||||
"p-6 rounded-2xl flex-3",
|
|
||||||
styles.utility.transitionSlow
|
|
||||||
)}>
|
|
||||||
<div className="flex gap-3 h-full overflow-x-auto px-3">
|
|
||||||
{daysOfWeek.map((day) => {
|
|
||||||
const isToday = day.fullDate.toDateString() === today.toDateString();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={day.name}
|
|
||||||
className={cn(
|
|
||||||
"flex-1 min-w-[180px] max-w-[200px] flex flex-col rounded-2xl p-4 transition-all duration-300",
|
|
||||||
isToday
|
|
||||||
? "bg-primary/10 border-2 border-primary neon-glow-soft"
|
|
||||||
: "bg-background border border-border"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center pb-3 border-b border-border mb-3">
|
|
||||||
<h3 className={cn(
|
|
||||||
styles.text.small,
|
|
||||||
"font-semibold mb-1",
|
|
||||||
isToday && "text-primary font-bold"
|
|
||||||
)}>
|
|
||||||
{day.name}
|
|
||||||
</h3>
|
|
||||||
<p className={cn(
|
|
||||||
styles.text.smallMuted,
|
|
||||||
|
|
||||||
isToday && "text-primary font-semibold"
|
|
||||||
)}>
|
|
||||||
{day.month} {day.date}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tasks */}
|
|
||||||
<div className="flex-1 flex flex-col gap-2 overflow-y-auto pt-1">
|
|
||||||
{day.tasks.map((task) => (
|
|
||||||
<div
|
|
||||||
key={task.id}
|
|
||||||
className={cn(
|
|
||||||
styles.card.task,
|
|
||||||
task.completed && "opacity-60 line-through"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* Priority indicator */}
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"w-1.5 h-1.5 rounded-full flex-shrink-0",
|
|
||||||
task.priority === 'low' && "bg-green-500",
|
|
||||||
task.priority === 'medium' && "bg-yellow-500",
|
|
||||||
task.priority === 'high' && "bg-orange-500",
|
|
||||||
task.priority === 'critical' && "bg-red-500"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<p className={cn(styles.text.small, "flex-1")}>
|
|
||||||
{task.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
|
|
||||||
{/* Add button */}
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
styles.button.iconNeon,
|
|
||||||
"mt-2 w-full flex items-center justify-center gap-2",
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
console.log('Add task clicked for', day.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
<span className="text-xs">Add Task</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { useNavigate, useLocation } from 'react-router'
|
|
||||||
import styles from '@/lib/styles'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { X } from 'lucide-react'
|
|
||||||
|
|
||||||
|
|
||||||
export function TaskButton({ taskData }) {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
||||||
|
|
||||||
const taskId = taskData?.id || 1
|
|
||||||
|
|
||||||
// Проверяем, открыта ли карточка по URL
|
|
||||||
useEffect(() => {
|
|
||||||
const isCardRoute = location.pathname === `/card/${taskId}`
|
|
||||||
setIsModalOpen(isCardRoute)
|
|
||||||
}, [location.pathname, taskId])
|
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
navigate(`/card/${taskId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
navigate(-1) // Возвращаемся на предыдущую страницу
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
key={taskData?.id || 1}
|
|
||||||
className={cn(
|
|
||||||
styles.card.task,
|
|
||||||
"w-[110px] cursor-pointer hover:opacity-80 transition-opacity"
|
|
||||||
)}
|
|
||||||
onClick={openModal}
|
|
||||||
>
|
|
||||||
{taskData?.title || "Sample task card"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isModalOpen && (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
|
||||||
onClick={closeModal}
|
|
||||||
>
|
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
|
||||||
<CardComponent
|
|
||||||
status={taskData?.status}
|
|
||||||
time_spent={taskData?.time_spent}
|
|
||||||
description={taskData?.description}
|
|
||||||
title={taskData?.title}
|
|
||||||
priority={taskData?.priority}
|
|
||||||
due_date={taskData?.due_date}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CardComponent({
|
|
||||||
status = "todo",
|
|
||||||
time_spent = 0,
|
|
||||||
description = "No description",
|
|
||||||
title = "Task",
|
|
||||||
priority = "medium",
|
|
||||||
due_date = null
|
|
||||||
}) {
|
|
||||||
const formatTimeSpent = (minutes) => {
|
|
||||||
if (!minutes || minutes === 0) return "0 min";
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
const mins = minutes % 60;
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${mins > 0 ? `${mins}m` : ''}`;
|
|
||||||
}
|
|
||||||
return `${mins}m`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
if (!dateString) return "No due date";
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const priorityBadgeStyle = {
|
|
||||||
low: styles.badge.low,
|
|
||||||
medium: styles.badge.medium,
|
|
||||||
high: styles.badge.high,
|
|
||||||
critical: styles.badge.critical,
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusLabels = {
|
|
||||||
open: "Open",
|
|
||||||
closed: "Closed",
|
|
||||||
in_progress: "In Progress",
|
|
||||||
todo: "To Do"
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn(styles.card.filled, "flex flex-col w-full min-w-[400px] max-w-[600px] h-[400px] overflow-hidden")}>
|
|
||||||
<div className="flex items-end flex-1 -mt-6 -mx-6 bg-center bg-cover bg-[url('/images/a.jpg')]">
|
|
||||||
<h1 className={cn(styles.text.h1, "pl-6")}>{title}</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 flex flex-col gap-3 z-10 mt-6">
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className={cn(priorityBadgeStyle[priority])}>
|
|
||||||
{priority.charAt(0).toUpperCase() + priority.slice(1)}
|
|
||||||
</span>
|
|
||||||
<span className={cn(styles.text.smallMuted)}>
|
|
||||||
Status: {statusLabels[status]}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-sm">
|
|
||||||
<span className={cn(styles.text.smallMuted)}>
|
|
||||||
⏱️ {formatTimeSpent(time_spent)}
|
|
||||||
</span>
|
|
||||||
<span className={cn(styles.text.smallMuted)}>
|
|
||||||
📅 {formatDate(due_date)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className={cn(styles.text.smallMuted, "flex-1 line-clamp-3 overflow-auto")}>
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button className={cn(styles.button.primaryNeon, "w-[110px]")}>
|
|
||||||
<span className={styles.text.neonBlue}>Complete</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
import { motion, AnimatePresence } from "motion/react"
|
|
||||||
|
|
||||||
export function useRipple() {
|
|
||||||
const [ripples, setRipples] = useState([])
|
|
||||||
|
|
||||||
const addRipple = (e) => {
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
|
||||||
const x = e.clientX - rect.left
|
|
||||||
const y = e.clientY - rect.top
|
|
||||||
const id = Date.now()
|
|
||||||
|
|
||||||
setRipples((prev) => [...prev, { x, y, id }])
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setRipples((prev) => prev.filter((r) => r.id !== id))
|
|
||||||
}, 600)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Ripples = (
|
|
||||||
<AnimatePresence>
|
|
||||||
{ripples.map((ripple) => (
|
|
||||||
<motion.span
|
|
||||||
key={ripple.id}
|
|
||||||
initial={{ scale: 0, opacity: 0.6 }}
|
|
||||||
animate={{ scale: 4, opacity: 0 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
|
||||||
style={{
|
|
||||||
left: ripple.x,
|
|
||||||
top: ripple.y,
|
|
||||||
}}
|
|
||||||
className="absolute h-8 w-8 rounded-full bg-white/70 pointer-events-none -translate-x-1/2 -translate-y-1/2"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
)
|
|
||||||
|
|
||||||
return { addRipple, Ripples }
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import React, { useState } from "react"
|
|
||||||
import { motion, AnimatePresence } from "motion/react"
|
|
||||||
|
|
||||||
export default function RippleButton({ children }) {
|
|
||||||
const [ripples, setRipples] = useState([])
|
|
||||||
|
|
||||||
const addRipple = (e) => {
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
|
||||||
const x = e.clientX - rect.left
|
|
||||||
const y = e.clientY - rect.top
|
|
||||||
const id = Date.now()
|
|
||||||
|
|
||||||
setRipples([...ripples, { x, y, id }])
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setRipples((r) => r.filter((ripple) => ripple.id !== id))
|
|
||||||
}, 600) // ripple длится ~600ms
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={addRipple}
|
|
||||||
className="relative overflow-hidden rounded-lg bg-blue-600 px-4 py-2 text-white font-medium"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<AnimatePresence>
|
|
||||||
{ripples.map((ripple) => (
|
|
||||||
<motion.span
|
|
||||||
key={ripple.id}
|
|
||||||
initial={{ scale: 0, opacity: 0.6 }}
|
|
||||||
animate={{ scale: 4, opacity: 0 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
|
||||||
style={{
|
|
||||||
left: ripple.x,
|
|
||||||
top: ripple.y,
|
|
||||||
}}
|
|
||||||
className="absolute h-8 w-8 rounded-full bg-white/70 pointer-events-none -translate-x-1/2 -translate-y-1/2"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</AnimatePresence>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Avatar({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
data-slot="avatar"
|
|
||||||
className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarImage({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
data-slot="avatar-image"
|
|
||||||
className={cn("aspect-square size-full", className)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarFallback({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
data-slot="avatar-fallback"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
ghost:
|
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
elevated:
|
|
||||||
"bg-card p-3 rounded-lg shadow-sm text-sm transition-transform hover:-translate-y-0.5 hover:shadow-md",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
||||||
icon: "size-9",
|
|
||||||
"icon-sm": "size-8",
|
|
||||||
"icon-lg": "size-10",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Card({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card"
|
|
||||||
className={cn(
|
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardHeader({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-header"
|
|
||||||
className={cn(
|
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-title"
|
|
||||||
className={cn("leading-none font-semibold", className)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardAction({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-action"
|
|
||||||
className={cn(
|
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (<div data-slot="card-content" className={cn("px-6", className)} {...props} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardFooter({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Input({
|
|
||||||
className,
|
|
||||||
type,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input }
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label }
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
'use client';;
|
|
||||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
|
||||||
import { Home, Briefcase, Calendar, Shield, Settings } from 'lucide-react';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
const defaultItems = [
|
|
||||||
{ label: 'home', icon: Home },
|
|
||||||
{ label: 'work', icon: Briefcase },
|
|
||||||
{ label: 'calendar', icon: Calendar },
|
|
||||||
{ label: 'security', icon: Shield },
|
|
||||||
{ label: 'settings', icon: Settings },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const MenuDock = ({
|
|
||||||
items,
|
|
||||||
className,
|
|
||||||
variant = 'default',
|
|
||||||
orientation = 'horizontal',
|
|
||||||
showLabels = true,
|
|
||||||
animated = true,
|
|
||||||
showIndicator = true
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const finalItems = useMemo(() => {
|
|
||||||
const isValid = items && Array.isArray(items) && items.length >= 2 && items.length <= 8;
|
|
||||||
if (!isValid) {
|
|
||||||
console.warn(
|
|
||||||
"MenuDock: 'items' prop is invalid or missing. Using default items.",
|
|
||||||
items
|
|
||||||
);
|
|
||||||
return defaultItems;
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}, [items]);
|
|
||||||
|
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
|
||||||
const [underlineWidth, setUnderlineWidth] = useState(0);
|
|
||||||
const [underlineLeft, setUnderlineLeft] = useState(0);
|
|
||||||
|
|
||||||
const textRefs = useRef([]);
|
|
||||||
const itemRefs = useRef([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeIndex >= finalItems.length) {
|
|
||||||
setActiveIndex(0);
|
|
||||||
}
|
|
||||||
}, [finalItems, activeIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateUnderline = () => {
|
|
||||||
const activeButton = itemRefs.current[activeIndex];
|
|
||||||
const activeText = textRefs.current[activeIndex];
|
|
||||||
|
|
||||||
if (activeButton && activeText && showLabels && orientation === 'horizontal') {
|
|
||||||
const buttonRect = activeButton.getBoundingClientRect();
|
|
||||||
const textRect = activeText.getBoundingClientRect();
|
|
||||||
const containerRect = activeButton.parentElement?.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (containerRect) {
|
|
||||||
setUnderlineWidth(textRect.width);
|
|
||||||
setUnderlineLeft(
|
|
||||||
buttonRect.left - containerRect.left + (buttonRect.width - textRect.width) / 2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateUnderline();
|
|
||||||
window.addEventListener('resize', updateUnderline);
|
|
||||||
return () => window.removeEventListener('resize', updateUnderline);
|
|
||||||
}, [activeIndex, finalItems, showLabels, orientation]);
|
|
||||||
|
|
||||||
const handleItemClick = (index, item) => {
|
|
||||||
setActiveIndex(index);
|
|
||||||
item.onClick?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVariantStyles = () => {
|
|
||||||
switch (variant) {
|
|
||||||
case 'compact':
|
|
||||||
return {
|
|
||||||
container: 'p-1 gap-3',
|
|
||||||
item: 'px-5 py-2',
|
|
||||||
icon: 'h-4 w-4',
|
|
||||||
text: 'text-xs'
|
|
||||||
};
|
|
||||||
case 'large':
|
|
||||||
return {
|
|
||||||
container: 'p-3 gap-4',
|
|
||||||
item: 'px-6 py-4',
|
|
||||||
icon: 'h-6 w-6',
|
|
||||||
text: 'text-base'
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
container: 'p-2 gap-3',
|
|
||||||
item: 'px-5 py-3.5',
|
|
||||||
icon: 'h-5 w-5',
|
|
||||||
text: 'text-sm'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = getVariantStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav
|
|
||||||
className={cn(
|
|
||||||
'relative inline-flex items-center rounded-3xl bg-transparent',
|
|
||||||
orientation === 'horizontal' ? 'flex-row' : 'flex-col',
|
|
||||||
styles.container,
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
role="navigation">
|
|
||||||
{finalItems.map((item, index) => {
|
|
||||||
const isActive = index === activeIndex;
|
|
||||||
const IconComponent = item.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`${item.label}-${index}`}
|
|
||||||
className="flex flex-col items-center gap-1">
|
|
||||||
<button
|
|
||||||
ref={(el) => { itemRefs.current[index] = el; }}
|
|
||||||
className={cn(
|
|
||||||
'relative flex items-center justify-center rounded-full transition-all duration-300',
|
|
||||||
'hover:bg-primary/10 focus-visible:outline-none',
|
|
||||||
'select-none active:scale-95',
|
|
||||||
styles.item,
|
|
||||||
isActive && 'text-primary bg-primary/15',
|
|
||||||
!isActive && 'text-muted-foreground hover:text-foreground'
|
|
||||||
)}
|
|
||||||
onClick={() => handleItemClick(index, item)}
|
|
||||||
aria-label={item.label}
|
|
||||||
type="button">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex items-center justify-center transition-all duration-200',
|
|
||||||
animated && isActive && 'animate-bounce'
|
|
||||||
)}>
|
|
||||||
<IconComponent className={cn(styles.icon, 'transition-colors duration-200')} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{showLabels && (
|
|
||||||
<span
|
|
||||||
ref={(el) => { textRefs.current[index] = el; }}
|
|
||||||
className={cn(
|
|
||||||
'font-medium transition-colors duration-200 capitalize',
|
|
||||||
styles.text,
|
|
||||||
'whitespace-nowrap',
|
|
||||||
isActive ? 'text-primary' : 'text-muted-foreground'
|
|
||||||
)}>
|
|
||||||
{item.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{/* Animated underline for horizontal orientation with labels */}
|
|
||||||
{showIndicator && showLabels && orientation === 'horizontal' && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute bottom-2 h-0.5 bg-primary rounded-full transition-all duration-300 ease-out',
|
|
||||||
animated ? 'transition-all duration-300' : ''
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
width: `${underlineWidth}px`,
|
|
||||||
left: `${underlineLeft}px`,
|
|
||||||
}} />
|
|
||||||
)}
|
|
||||||
{/* Active indicator for vertical orientation or no labels */}
|
|
||||||
{showIndicator && (!showLabels || orientation === 'vertical') && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute bg-primary rounded-full transition-all duration-300',
|
|
||||||
orientation === 'vertical'
|
|
||||||
? 'left-1 w-1 h-6'
|
|
||||||
: 'bottom-0.5 h-0.5 w-6'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
[orientation === 'vertical' ? 'top' : 'left']:
|
|
||||||
orientation === 'vertical'
|
|
||||||
? `${(activeIndex * (variant === 'large' ? 64 : variant === 'compact' ? 56 : 60)) + (variant === 'large' ? 19 : variant === 'compact' ? 16 : 18)}px`
|
|
||||||
: `${(activeIndex * (variant === 'large' ? 64 : variant === 'compact' ? 56 : 60)) + (variant === 'large' ? 19 : variant === 'compact' ? 16 : 18)}px`
|
|
||||||
}} />
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@import "tw-animate-css";
|
|
||||||
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
:root {
|
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #111928;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
|
|
||||||
/* Mirage Palette Colors */
|
|
||||||
--mirage-50: #f4f6fb;
|
|
||||||
--mirage-100: #e7edf7;
|
|
||||||
--mirage-200: #cbd9ec;
|
|
||||||
--mirage-300: #9db8dc;
|
|
||||||
--mirage-400: #6893c8;
|
|
||||||
--mirage-500: #4474b3;
|
|
||||||
--mirage-600: #335b96;
|
|
||||||
--mirage-700: #2a4a7a;
|
|
||||||
--mirage-800: #264066;
|
|
||||||
--mirage-900: #243756;
|
|
||||||
--mirage-950: #111928;
|
|
||||||
|
|
||||||
/* Neon Accent Colors */
|
|
||||||
--neon-blue: #c6e2ff;
|
|
||||||
--neon-blue-glow: rgba(30, 132, 242, 0.6);
|
|
||||||
--neon-pink: #ffc5ec;
|
|
||||||
--neon-pink-glow: rgba(255, 20, 147, 0.6);
|
|
||||||
|
|
||||||
/* Material Design 3 Semantic Colors with Mirage Palette */
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--background: #111928; /* mirage-950 - самый темный для фона */
|
|
||||||
--foreground: #cbd9ec; /* mirage-200 - светлый текст */
|
|
||||||
|
|
||||||
--card: #243756; /* mirage-900 - карточки темнее фона */
|
|
||||||
--card-foreground: #e7edf7; /* mirage-100 - текст на карточках */
|
|
||||||
|
|
||||||
--popover: #264066; /* mirage-800 - всплывающие элементы */
|
|
||||||
--popover-foreground: #e7edf7; /* mirage-100 */
|
|
||||||
|
|
||||||
--primary: #6893c8; /* mirage-400 - основной акцент (средний синий) */
|
|
||||||
--primary-foreground: #111928; /* mirage-950 - темный текст на акценте */
|
|
||||||
|
|
||||||
--secondary: #2a4a7a; /* mirage-700 - вторичный цвет */
|
|
||||||
--secondary-foreground: #e7edf7; /* mirage-100 */
|
|
||||||
|
|
||||||
--muted: #264066; /* mirage-800 - приглушенные элементы */
|
|
||||||
--muted-foreground: #9db8dc; /* mirage-300 - приглушенный текст */
|
|
||||||
|
|
||||||
--accent: #4474b3; /* mirage-500 - акцент при наведении */
|
|
||||||
--accent-foreground: #f4f6fb; /* mirage-50 */
|
|
||||||
|
|
||||||
--destructive: #ef4444; /* красный для удаления */
|
|
||||||
--destructive-foreground: #f4f6fb;
|
|
||||||
|
|
||||||
--border: rgba(157, 184, 220, 0.15); /* mirage-300 с прозрачностью */
|
|
||||||
--input: rgba(157, 184, 220, 0.2); /* чуть плотнее для полей ввода */
|
|
||||||
--ring: #6893c8; /* mirage-400 - фокус как primary */
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
--sidebar: #111928; /* mirage-950 - как фон */
|
|
||||||
--sidebar-foreground: #cbd9ec; /* mirage-200 */
|
|
||||||
--sidebar-primary: #6893c8; /* mirage-400 */
|
|
||||||
--sidebar-primary-foreground: #111928;
|
|
||||||
--sidebar-accent: #335b96; /* mirage-600 */
|
|
||||||
--sidebar-accent-foreground: #e7edf7;
|
|
||||||
--sidebar-border: rgba(157, 184, 220, 0.1);
|
|
||||||
--sidebar-ring: #6893c8;
|
|
||||||
|
|
||||||
/* Chart Colors - используем палитру */
|
|
||||||
--chart-1: #6893c8; /* mirage-400 */
|
|
||||||
--chart-2: #4474b3; /* mirage-500 */
|
|
||||||
--chart-3: #335b96; /* mirage-600 */
|
|
||||||
--chart-4: #9db8dc; /* mirage-300 */
|
|
||||||
--chart-5: #2a4a7a; /* mirage-700 */
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-chart-1: var(--chart-1);
|
|
||||||
--color-chart-2: var(--chart-2);
|
|
||||||
--color-chart-3: var(--chart-3);
|
|
||||||
--color-chart-4: var(--chart-4);
|
|
||||||
--color-chart-5: var(--chart-5);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
/* Темная тема немного светлее основной (для toggle режима) */
|
|
||||||
--background: #243756; /* mirage-900 */
|
|
||||||
--foreground: #e7edf7; /* mirage-100 */
|
|
||||||
--card: #2a4a7a; /* mirage-700 */
|
|
||||||
--card-foreground: #f4f6fb; /* mirage-50 */
|
|
||||||
--popover: #264066; /* mirage-800 */
|
|
||||||
--popover-foreground: #e7edf7;
|
|
||||||
--primary: #9db8dc; /* mirage-300 - светлее для контраста */
|
|
||||||
--primary-foreground: #111928;
|
|
||||||
--secondary: #335b96; /* mirage-600 */
|
|
||||||
--secondary-foreground: #f4f6fb;
|
|
||||||
--muted: #264066;
|
|
||||||
--muted-foreground: #9db8dc;
|
|
||||||
--accent: #6893c8; /* mirage-400 */
|
|
||||||
--accent-foreground: #111928;
|
|
||||||
--destructive: #ef4444;
|
|
||||||
--destructive-foreground: #f4f6fb;
|
|
||||||
--border: rgba(157, 184, 220, 0.2);
|
|
||||||
--input: rgba(157, 184, 220, 0.25);
|
|
||||||
--ring: #9db8dc;
|
|
||||||
--chart-1: #9db8dc;
|
|
||||||
--chart-2: #6893c8;
|
|
||||||
--chart-3: #4474b3;
|
|
||||||
--chart-4: #cbd9ec;
|
|
||||||
--chart-5: #335b96;
|
|
||||||
--sidebar: #243756;
|
|
||||||
--sidebar-foreground: #e7edf7;
|
|
||||||
--sidebar-primary: #9db8dc;
|
|
||||||
--sidebar-primary-foreground: #111928;
|
|
||||||
--sidebar-accent: #4474b3;
|
|
||||||
--sidebar-accent-foreground: #f4f6fb;
|
|
||||||
--sidebar-border: rgba(157, 184, 220, 0.15);
|
|
||||||
--sidebar-ring: #9db8dc;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border outline-ring/50;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Outlet } from 'react-router'
|
|
||||||
|
|
||||||
const AuthLayout = ({ className }) => {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className="w-full max-w-sm">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AuthLayout }
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tailwind CSS Class Presets for Task&Coffee
|
|
||||||
* Material Design 3 + Neon Aesthetic
|
|
||||||
* Using Mirage Color Palette
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Card Styles - Material Design 3 Variants
|
|
||||||
export const cardStyles = {
|
|
||||||
// Filled card - основной вариант для большинства карточек
|
|
||||||
filled: "bg-card text-card-foreground p-6 rounded-lg shadow-sm",
|
|
||||||
|
|
||||||
// Elevated card - карточки с эффектом поднятия
|
|
||||||
elevated: "bg-card text-card-foreground p-6 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300",
|
|
||||||
|
|
||||||
// Outlined card - карточки с границей
|
|
||||||
outlined: "bg-background border-2 border-border text-foreground p-6 rounded-lg",
|
|
||||||
|
|
||||||
// Task card - компактные карточки для задач
|
|
||||||
task: "bg-card text-card-foreground p-3 rounded-lg shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-200",
|
|
||||||
|
|
||||||
// Task card with neon accent
|
|
||||||
taskNeon: "bg-card text-card-foreground p-3 rounded-lg shadow-sm hover:shadow-[0_0_15px_rgba(104,147,200,0.3)] hover:-translate-y-0.5 transition-all duration-200",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Button Styles
|
|
||||||
export const buttonStyles = {
|
|
||||||
// Primary button - основные действия
|
|
||||||
primary: "bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-lg font-medium transition-all duration-200 shadow-sm hover:shadow-md",
|
|
||||||
|
|
||||||
// Primary with neon glow
|
|
||||||
primaryNeon: "bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-lg font-medium transition-all duration-200 hover:shadow-[0_0_20px_rgba(104,147,200,0.5)]",
|
|
||||||
|
|
||||||
// Secondary button - вторичные действия
|
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 px-4 py-2 rounded-lg font-medium transition-all duration-200",
|
|
||||||
|
|
||||||
// Outline button - менее важные действия
|
|
||||||
outline: "border-2 border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground px-4 py-2 rounded-lg font-medium transition-all duration-200",
|
|
||||||
|
|
||||||
// Ghost button - минималистичные кнопки
|
|
||||||
ghost: "text-foreground hover:bg-accent hover:text-accent-foreground px-4 py-2 rounded-lg font-medium transition-all duration-200",
|
|
||||||
|
|
||||||
// Destructive button - опасные действия
|
|
||||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 px-4 py-2 rounded-lg font-medium transition-all duration-200 shadow-sm hover:shadow-md",
|
|
||||||
|
|
||||||
// Icon button
|
|
||||||
icon: "p-2 rounded-lg hover:bg-accent transition-all duration-200 hover:-translate-y-0.5",
|
|
||||||
|
|
||||||
// Icon button with neon effect
|
|
||||||
iconNeon: "p-2 rounded-lg hover:bg-accent transition-all duration-200 hover:shadow-[0_0_15px_rgba(104,147,200,0.4)] hover:-translate-y-0.5",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Layout Styles
|
|
||||||
export const layoutStyles = {
|
|
||||||
// Main container
|
|
||||||
container: "flex min-h-screen bg-background text-foreground",
|
|
||||||
|
|
||||||
// Sidebar/Navigation rail
|
|
||||||
sidebar: "w-20 bg-sidebar border-r border-sidebar-border flex flex-col items-center py-4 gap-4",
|
|
||||||
|
|
||||||
// Main content area
|
|
||||||
main: "flex-1 p-6 bg-background flex flex-col",
|
|
||||||
|
|
||||||
// Content wrapper with max width
|
|
||||||
content: "max-w-7xl mx-auto w-full flex flex-1 flex-col gap-6",
|
|
||||||
|
|
||||||
// Grid layouts
|
|
||||||
gridCards: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",
|
|
||||||
gridTasks: "grid grid-cols-1 gap-3",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Input Styles
|
|
||||||
export const inputStyles = {
|
|
||||||
// Standard input
|
|
||||||
default: "w-full px-4 py-2 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all duration-200",
|
|
||||||
|
|
||||||
// Input with neon focus effect
|
|
||||||
neon: "w-full px-4 py-2 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:shadow-[0_0_15px_rgba(104,147,200,0.3)] transition-all duration-200",
|
|
||||||
|
|
||||||
// Search input
|
|
||||||
search: "w-full px-4 py-2 pl-10 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all duration-200",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Text Styles
|
|
||||||
export const textStyles = {
|
|
||||||
// Headings
|
|
||||||
h1: "text-4xl font-bold text-foreground",
|
|
||||||
h2: "text-3xl font-semibold text-foreground",
|
|
||||||
h3: "text-2xl font-semibold text-foreground",
|
|
||||||
h4: "text-xl font-semibold text-foreground",
|
|
||||||
|
|
||||||
// Body text
|
|
||||||
body: "text-base text-foreground",
|
|
||||||
bodyMuted: "text-base text-muted-foreground",
|
|
||||||
|
|
||||||
// Small text
|
|
||||||
small: "text-sm text-foreground",
|
|
||||||
smallMuted: "text-sm text-muted-foreground",
|
|
||||||
|
|
||||||
// Neon text (to be combined with neon.css animations)
|
|
||||||
neonBlue: "text-[#c6e2ff]",
|
|
||||||
neonPink: "text-[#ffc5ec]",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Badge/Chip Styles
|
|
||||||
export const badgeStyles = {
|
|
||||||
// Default badge
|
|
||||||
default: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-primary/20 text-primary border border-primary/30 cursor-default select-none",
|
|
||||||
|
|
||||||
// Priority badges
|
|
||||||
low: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30 cursor-default select-none",
|
|
||||||
medium: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 cursor-default select-none",
|
|
||||||
high: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-orange-500/20 text-orange-400 border border-orange-500/30 cursor-default select-none",
|
|
||||||
critical: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-red-500/20 text-red-400 border border-red-500/30 cursor-default select-none",
|
|
||||||
|
|
||||||
// Status badges
|
|
||||||
success: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30",
|
|
||||||
warning: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30",
|
|
||||||
error: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-red-500/20 text-red-400 border border-red-500/30",
|
|
||||||
info: "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium bg-blue-500/20 text-blue-400 border border-blue-500/30",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Utility Styles
|
|
||||||
export const utilityStyles = {
|
|
||||||
// Neon glow effects (for hover states)
|
|
||||||
glowBlue: "hover:shadow-[0_0_20px_rgba(104,147,200,0.6)]",
|
|
||||||
glowPink: "hover:shadow-[0_0_20px_rgba(255,20,147,0.6)]",
|
|
||||||
glowSoft: "hover:shadow-[0_0_15px_rgba(104,147,200,0.3)]",
|
|
||||||
|
|
||||||
// Glassmorphism effect
|
|
||||||
glass: "bg-card/50 backdrop-blur-md border border-border/50",
|
|
||||||
|
|
||||||
// Smooth transitions
|
|
||||||
transition: "transition-all duration-200 ease-in-out",
|
|
||||||
transitionSlow: "transition-all duration-300 ease-in-out",
|
|
||||||
|
|
||||||
// Focus visible states
|
|
||||||
focusRing: "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Complete component compositions
|
|
||||||
export const componentStyles = {
|
|
||||||
// Day column in calendar
|
|
||||||
dayColumn: "flex-1 min-w-[180px] flex flex-col bg-background rounded-lg p-4 transition-all duration-300",
|
|
||||||
dayColumnToday: "flex-1 min-w-[180px] flex flex-col bg-primary/10 border-2 border-primary rounded-lg p-4 transition-all duration-300",
|
|
||||||
|
|
||||||
// Avatar
|
|
||||||
avatar: "w-14 h-14 rounded-lg overflow-hidden",
|
|
||||||
avatarLarge: "w-20 h-20 rounded-xl overflow-hidden",
|
|
||||||
|
|
||||||
// Divider
|
|
||||||
divider: "h-px bg-border w-full",
|
|
||||||
dividerVertical: "w-px bg-border h-full",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Export all styles as a single object for convenience
|
|
||||||
export const styles = {
|
|
||||||
card: cardStyles,
|
|
||||||
button: buttonStyles,
|
|
||||||
layout: layoutStyles,
|
|
||||||
input: inputStyles,
|
|
||||||
text: textStyles,
|
|
||||||
badge: badgeStyles,
|
|
||||||
utility: utilityStyles,
|
|
||||||
component: componentStyles,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default styles;
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { clsx } from "clsx";
|
|
||||||
import { twMerge } from "tailwind-merge"
|
|
||||||
import { jwtDecode as decode } from "jwt-decode";
|
|
||||||
import { refreshToken } from "../api/auth.service";
|
|
||||||
|
|
||||||
export function cn(...inputs) {
|
|
||||||
return twMerge(clsx(inputs));
|
|
||||||
}
|
|
||||||
|
|
||||||
let refreshPromise = null;
|
|
||||||
|
|
||||||
export async function jwtexp(token) {
|
|
||||||
const decoded = decode(token);
|
|
||||||
const currentTime = Date.now() / 1000;
|
|
||||||
const tokenExp = decoded.exp;
|
|
||||||
|
|
||||||
if (tokenExp < currentTime || tokenExp - currentTime < 120) {
|
|
||||||
if (refreshPromise) {
|
|
||||||
console.log("Refresh already in progress, waiting...");
|
|
||||||
return await refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshPromise = (async () => {
|
|
||||||
try {
|
|
||||||
await refreshToken();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to refresh token:", error);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
refreshPromise = null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return await refreshPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import './index.css'
|
|
||||||
import App from './App.jsx'
|
|
||||||
import { BrowserRouter } from 'react-router'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
)
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Vibur';
|
|
||||||
src: url('./assets/fonts/vibur.woff2') format('woff2');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Fabulous';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('./assets/fonts/Fabulous.ttf') format('truetype');
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Carry-You';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('./assets/fonts/Carry-You.ttf') format('truetype');
|
|
||||||
}
|
|
||||||
/*-- Sign Styles --*/
|
|
||||||
|
|
||||||
.sign {
|
|
||||||
font-family: "Carry-You", cursive;
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 5.6rem;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1;
|
|
||||||
color: #c6e2ff;
|
|
||||||
animation: neon .08s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sign-pink {
|
|
||||||
font-family: "Carry-You", cursive;
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 5.6rem;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1;
|
|
||||||
color: #ffc5ec;
|
|
||||||
animation: neon-pink .08s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*-- Inline Sign Styles for Headers --*/
|
|
||||||
.sign-inline {
|
|
||||||
font-family: "Carry-You", cursive;
|
|
||||||
font-size: 3rem;
|
|
||||||
line-height: 1;
|
|
||||||
color: #c6e2ff;
|
|
||||||
animation: neon .08s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sign-pink-inline {
|
|
||||||
font-family: "Carry-You", cursive;
|
|
||||||
font-size: 3rem;
|
|
||||||
line-height: 1;
|
|
||||||
color: #ffc5ec;
|
|
||||||
animation: neon-pink .08s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes neon {
|
|
||||||
from {
|
|
||||||
text-shadow:
|
|
||||||
0 0 6px rgba(202,228,225,0.92),
|
|
||||||
0 0 30px rgba(202,228,225,0.34),
|
|
||||||
0 0 12px rgba(30,132,242,0.52),
|
|
||||||
0 0 21px rgba(30,132,242,0.92),
|
|
||||||
0 0 34px rgba(30,132,242,0.78),
|
|
||||||
0 0 54px rgba(30,132,242,0.92);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
text-shadow:
|
|
||||||
0 0 6px rgba(202,228,225,0.98),
|
|
||||||
0 0 30px rgba(202,228,225,0.42),
|
|
||||||
0 0 12px rgba(30,132,242,0.58),
|
|
||||||
0 0 22px rgba(30,132,242,0.84),
|
|
||||||
0 0 38px rgba(30,132,242,0.88),
|
|
||||||
0 0 60px rgba(30,132,242,1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes neon-pink {
|
|
||||||
from {
|
|
||||||
text-shadow:
|
|
||||||
0 0 6px rgba(255,182,193,0.92),
|
|
||||||
0 0 30px rgba(255,182,193,0.34),
|
|
||||||
0 0 12px rgba(255,20,147,0.52),
|
|
||||||
0 0 21px rgba(255,20,147,0.92),
|
|
||||||
0 0 34px rgba(255,20,147,0.78),
|
|
||||||
0 0 54px rgba(255,20,147,0.92);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
text-shadow:
|
|
||||||
0 0 6px rgba(255,182,193,0.98),
|
|
||||||
0 0 30px rgba(255,182,193,0.42),
|
|
||||||
0 0 12px rgba(255,20,147,0.58),
|
|
||||||
0 0 22px rgba(255,20,147,0.84),
|
|
||||||
0 0 38px rgba(255,20,147,0.88),
|
|
||||||
0 0 60px rgba(255,20,147,1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px; left: 10px;
|
|
||||||
color: #828282;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
color: #c6e2ff;
|
|
||||||
text-shadow:
|
|
||||||
0 0 2px rgba(202,228,225,0.92),
|
|
||||||
0 0 10px rgba(202,228,225,0.34),
|
|
||||||
0 0 4px rgba(30,132,242,0.52),
|
|
||||||
0 0 7px rgba(30,132,242,0.92),
|
|
||||||
0 0 11px rgba(30,132,242,0.78),
|
|
||||||
0 0 16px rgba(30,132,242,0.92);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*-- Utility Neon Classes for Cards and Buttons --*/
|
|
||||||
|
|
||||||
/* Neon glow on hover - blue */
|
|
||||||
.neon-glow-blue {
|
|
||||||
transition: box-shadow 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neon-glow-blue:hover {
|
|
||||||
box-shadow: 0 0 20px rgba(104, 147, 200, 0.6),
|
|
||||||
0 0 30px rgba(104, 147, 200, 0.4),
|
|
||||||
0 0 40px rgba(104, 147, 200, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Neon glow on hover - pink */
|
|
||||||
.neon-glow-pink {
|
|
||||||
transition: box-shadow 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neon-glow-pink:hover {
|
|
||||||
box-shadow: 0 0 20px rgba(255, 20, 147, 0.6),
|
|
||||||
0 0 30px rgba(255, 20, 147, 0.4),
|
|
||||||
0 0 40px rgba(255, 20, 147, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Soft neon glow - for subtle effects */
|
|
||||||
.neon-glow-soft {
|
|
||||||
transition: box-shadow 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neon-glow-soft:hover {
|
|
||||||
box-shadow: 0 0 15px rgba(104, 147, 200, 0.3),
|
|
||||||
0 0 25px rgba(104, 147, 200, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Neon border - blue */
|
|
||||||
.neon-border-blue {
|
|
||||||
border: 1px solid rgba(104, 147, 200, 0.4);
|
|
||||||
box-shadow: 0 0 10px rgba(104, 147, 200, 0.2);
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neon-border-blue:hover {
|
|
||||||
border-color: rgba(104, 147, 200, 0.8);
|
|
||||||
box-shadow: 0 0 15px rgba(104, 147, 200, 0.4),
|
|
||||||
0 0 25px rgba(104, 147, 200, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Neon border - pink */
|
|
||||||
.neon-border-pink {
|
|
||||||
border: 1px solid rgba(255, 20, 147, 0.4);
|
|
||||||
box-shadow: 0 0 10px rgba(255, 20, 147, 0.2);
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neon-border-pink:hover {
|
|
||||||
border-color: rgba(255, 20, 147, 0.8);
|
|
||||||
box-shadow: 0 0 15px rgba(255, 20, 147, 0.4),
|
|
||||||
0 0 25px rgba(255, 20, 147, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Text with subtle neon effect (non-animated) */
|
|
||||||
.neon-text-blue {
|
|
||||||
color: #c6e2ff;
|
|
||||||
text-shadow: 0 0 10px rgba(30, 132, 242, 0.5),
|
|
||||||
0 0 20px rgba(30, 132, 242, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.neon-text-pink {
|
|
||||||
color: #ffc5ec;
|
|
||||||
text-shadow: 0 0 10px rgba(255, 20, 147, 0.5),
|
|
||||||
0 0 20px rgba(255, 20, 147, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Icon with neon glow on hover */
|
|
||||||
.neon-icon {
|
|
||||||
transition: filter 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.neon-icon:hover {
|
|
||||||
filter: drop-shadow(0 0 8px rgba(104, 147, 200, 0.8))
|
|
||||||
drop-shadow(0 0 15px rgba(104, 147, 200, 0.5));
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import '../neon.css';
|
|
||||||
import { MenuDock } from '@/components/ui/shadcn-io/menu-dock';
|
|
||||||
import { Home, Settings, Bell } from 'lucide-react';
|
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
|
||||||
import { jwtexp, cn } from '@/lib/utils';
|
|
||||||
import Calendar from '@/components/Calendar';
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import styles from '@/lib/styles';
|
|
||||||
|
|
||||||
const menuItems = [
|
|
||||||
{ label: 'home', icon: Home, onClick: () => console.log('Home clicked') },
|
|
||||||
{ label: 'notify', icon: Bell, onClick: () => console.log('Notifications clicked') },
|
|
||||||
{ label: 'settings', icon: Settings, onClick: () => console.log('Settings clicked') },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [authError, setAuthError] = useState(false);
|
|
||||||
|
|
||||||
// Validate token on page load
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('Dashboard useEffect triggered');
|
|
||||||
const validateAuth = async () => {
|
|
||||||
try {
|
|
||||||
console.log('Validating authentication...');
|
|
||||||
const token = localStorage.getItem('access_token');
|
|
||||||
if (token) {
|
|
||||||
console.log('Token found, validating...');
|
|
||||||
const isTokenValid = await jwtexp(token);
|
|
||||||
if (!isTokenValid) {
|
|
||||||
console.error('Token validation failed');
|
|
||||||
setAuthError(true);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('No access token found');
|
|
||||||
setAuthError(true);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('Authentication validated successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Authentication validation error:', error);
|
|
||||||
setAuthError(true);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
validateAuth();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Redirect to login if authentication fails
|
|
||||||
useEffect(() => {
|
|
||||||
if (authError) {
|
|
||||||
console.log('Authentication error detected, redirecting to login...');
|
|
||||||
navigate('/auth/login', { replace: true });
|
|
||||||
}
|
|
||||||
}, [authError, navigate]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (loading || authError) {
|
|
||||||
return (
|
|
||||||
<div className={styles.layout.container}>
|
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
|
||||||
<p className={styles.text.h3}>
|
|
||||||
{loading ? 'Loading dashboard...' : 'Redirecting...'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.layout.container}>
|
|
||||||
{/* Navigation Rail - Material Design 3 */}
|
|
||||||
<aside className={styles.layout.sidebar}>
|
|
||||||
{/* Avatar */}
|
|
||||||
<div className="mb-2">
|
|
||||||
<Avatar className={styles.component.avatar}>
|
|
||||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MenuDock
|
|
||||||
items={menuItems}
|
|
||||||
orientation="vertical"
|
|
||||||
animated={false}
|
|
||||||
showIndicator={false}
|
|
||||||
showLabels={true}
|
|
||||||
variant="compact"
|
|
||||||
/>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<main className={styles.layout.main}>
|
|
||||||
<div className={styles.layout.content}>
|
|
||||||
{/* Header with neon text */}
|
|
||||||
<h1 className={cn(styles.text.h1, "mb-6")}>
|
|
||||||
<span className="sign-pink-inline">Task&</span>
|
|
||||||
<span className="sign-inline">Coffee</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Calendar component */}
|
|
||||||
<Calendar />
|
|
||||||
|
|
||||||
{/* Spacer */}
|
|
||||||
<div className="flex-[0.5]"></div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import { useState } from "react"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { login } from "@/api/auth.service"
|
|
||||||
import { Link, useNavigate } from "react-router"
|
|
||||||
|
|
||||||
|
|
||||||
export function LoginPage({ className }) {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const [username, setUsername] = useState("")
|
|
||||||
const [password, setPassword] = useState("")
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [error, setError] = useState("")
|
|
||||||
const [success, setSuccess] = useState("")
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setError("")
|
|
||||||
setSuccess("")
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await login(username, password)
|
|
||||||
setSuccess("Login successful!")
|
|
||||||
console.log("Logged in:", result)
|
|
||||||
|
|
||||||
// Redirect to dashboard after successful login
|
|
||||||
navigate('/dashboard', { replace: true })
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.detail || "Login failed. Please check your credentials.")
|
|
||||||
console.error("Login error:", err)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<Card className="w-full max-w-sm">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Login to your account</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Enter your username below to login to your account
|
|
||||||
</CardDescription>
|
|
||||||
<CardAction>
|
|
||||||
<Button variant="link">
|
|
||||||
<Link to="/auth/signup">Sign Up</Link>
|
|
||||||
</Button>
|
|
||||||
</CardAction>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
{error && (
|
|
||||||
<div className="text-sm text-red-500 bg-red-50 p-2 rounded">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<div className="text-sm text-green-500 bg-green-50 p-2 rounded">
|
|
||||||
{success}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="username">Username</Label>
|
|
||||||
<Input
|
|
||||||
id="username"
|
|
||||||
type="text"
|
|
||||||
placeholder="username"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
required
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Label htmlFor="password">Password</Label>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
Forgot your password?
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
required
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex-col gap-2">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? "Loading..." : "Login"}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
import { jwtexp } from '@/lib/utils';
|
|
||||||
|
|
||||||
export default function RootRedirect() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [checking, setChecking] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkAuthAndRedirect = async () => {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('access_token');
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
const isTokenValid = await jwtexp(token);
|
|
||||||
|
|
||||||
if (isTokenValid) {
|
|
||||||
navigate('/dashboard', { replace: true });
|
|
||||||
} else {
|
|
||||||
console.error('Token is invalid, redirecting...');
|
|
||||||
navigate('/auth/login', { replace: true });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('No token found, redirecting...');
|
|
||||||
navigate('/auth/login', { replace: true });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking auth:', error);
|
|
||||||
navigate('/auth/login', { replace: true });
|
|
||||||
} finally {
|
|
||||||
setChecking(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkAuthAndRedirect();
|
|
||||||
}, [navigate]);
|
|
||||||
|
|
||||||
if (checking) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
|
||||||
<p className="text-xl">Checking authentication...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user