fastapi-fullstack 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. fastapi_fullstack-0.1.2.dist-info/METADATA +545 -0
  2. fastapi_fullstack-0.1.2.dist-info/RECORD +221 -0
  3. fastapi_fullstack-0.1.2.dist-info/WHEEL +4 -0
  4. fastapi_fullstack-0.1.2.dist-info/entry_points.txt +2 -0
  5. fastapi_fullstack-0.1.2.dist-info/licenses/LICENSE +21 -0
  6. fastapi_gen/__init__.py +3 -0
  7. fastapi_gen/cli.py +256 -0
  8. fastapi_gen/config.py +255 -0
  9. fastapi_gen/generator.py +181 -0
  10. fastapi_gen/prompts.py +648 -0
  11. fastapi_gen/template/cookiecutter.json +76 -0
  12. fastapi_gen/template/hooks/post_gen_project.py +111 -0
  13. fastapi_gen/template/{{cookiecutter.project_slug}}/.env.example +136 -0
  14. fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +150 -0
  15. fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +108 -0
  16. fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +357 -0
  17. fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +298 -0
  18. fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +723 -0
  19. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore +60 -0
  20. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +32 -0
  21. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile +56 -0
  22. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +76 -0
  23. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +30 -0
  24. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
  25. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini +48 -0
  26. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +3 -0
  27. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +115 -0
  28. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +13 -0
  29. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +202 -0
  30. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +13 -0
  31. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +17 -0
  32. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +1 -0
  33. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +528 -0
  34. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +85 -0
  35. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +10 -0
  36. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +9 -0
  37. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +87 -0
  38. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +448 -0
  39. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +395 -0
  40. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +490 -0
  41. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +227 -0
  42. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/items.py +275 -0
  43. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +205 -0
  44. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +168 -0
  45. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +333 -0
  46. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +477 -0
  47. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/ws.py +46 -0
  48. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +221 -0
  49. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +14 -0
  50. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +88 -0
  51. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +117 -0
  52. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +75 -0
  53. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +28 -0
  54. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +266 -0
  55. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +5 -0
  56. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +23 -0
  57. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +247 -0
  58. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +153 -0
  59. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +122 -0
  60. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +101 -0
  61. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +99 -0
  62. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +23 -0
  63. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +58 -0
  64. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +271 -0
  65. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +102 -0
  66. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
  67. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +41 -0
  68. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +31 -0
  69. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +319 -0
  70. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +96 -0
  71. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +126 -0
  72. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +218 -0
  73. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +244 -0
  74. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +113 -0
  75. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +326 -0
  76. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/__init__.py +9 -0
  77. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/base.py +73 -0
  78. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +49 -0
  79. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +154 -0
  80. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +760 -0
  81. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/item.py +222 -0
  82. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +318 -0
  83. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +322 -0
  84. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +358 -0
  85. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +50 -0
  86. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +57 -0
  87. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +195 -0
  88. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/item.py +52 -0
  89. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +42 -0
  90. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +31 -0
  91. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +64 -0
  92. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +89 -0
  93. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +38 -0
  94. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +797 -0
  95. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/item.py +246 -0
  96. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +333 -0
  97. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +432 -0
  98. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +561 -0
  99. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +5 -0
  100. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +64 -0
  101. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +38 -0
  102. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +25 -0
  103. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +106 -0
  104. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +29 -0
  105. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +92 -0
  106. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +1 -0
  107. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +438 -0
  108. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +158 -0
  109. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
  110. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +1 -0
  111. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +1 -0
  112. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +242 -0
  113. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +151 -0
  114. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +113 -0
  115. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_items.py +310 -0
  116. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +253 -0
  117. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +151 -0
  118. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +121 -0
  119. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +183 -0
  120. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +173 -0
  121. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +143 -0
  122. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_pipelines.py +118 -0
  123. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +181 -0
  124. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +124 -0
  125. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +363 -0
  126. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +85 -0
  127. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +242 -0
  128. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +31 -0
  129. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +382 -0
  130. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +241 -0
  131. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +12 -0
  132. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore +45 -0
  133. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +19 -0
  134. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +11 -0
  135. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +44 -0
  136. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +693 -0
  137. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +49 -0
  138. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +134 -0
  139. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +207 -0
  140. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +73 -0
  141. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +14 -0
  142. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +84 -0
  143. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +84 -0
  144. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +76 -0
  145. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/package.json +66 -0
  146. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +101 -0
  147. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +7 -0
  148. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/(auth)/layout.tsx +11 -0
  149. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/(auth)/login/page.tsx +5 -0
  150. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/(auth)/register/page.tsx +5 -0
  151. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/(dashboard)/chat/page.tsx +20 -0
  152. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/(dashboard)/dashboard/page.tsx +99 -0
  153. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/(dashboard)/layout.tsx +17 -0
  154. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/(dashboard)/profile/page.tsx +156 -0
  155. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +58 -0
  156. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +24 -0
  157. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +39 -0
  158. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +50 -0
  159. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +54 -0
  160. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +26 -0
  161. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +41 -0
  162. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +108 -0
  163. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +73 -0
  164. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +21 -0
  165. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/auth/callback/page.tsx +96 -0
  166. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +108 -0
  167. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +25 -0
  168. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/page.tsx +73 -0
  169. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +29 -0
  170. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +2 -0
  171. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +120 -0
  172. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +153 -0
  173. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +135 -0
  174. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +73 -0
  175. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +261 -0
  176. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +8 -0
  177. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +63 -0
  178. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +18 -0
  179. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +60 -0
  180. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +32 -0
  181. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +3 -0
  182. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +97 -0
  183. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +45 -0
  184. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +2 -0
  185. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +48 -0
  186. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +7 -0
  187. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +53 -0
  188. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +83 -0
  189. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +35 -0
  190. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +75 -0
  191. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +54 -0
  192. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +82 -0
  193. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +12 -0
  194. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +21 -0
  195. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +21 -0
  196. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +6 -0
  197. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +97 -0
  198. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +203 -0
  199. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +175 -0
  200. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +105 -0
  201. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +32 -0
  202. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +90 -0
  203. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +39 -0
  204. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +78 -0
  205. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +44 -0
  206. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +44 -0
  207. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +33 -0
  208. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +72 -0
  209. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +48 -0
  210. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +65 -0
  211. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +76 -0
  212. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +6 -0
  213. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +44 -0
  214. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +27 -0
  215. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +52 -0
  216. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +81 -0
  217. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +49 -0
  218. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +10 -0
  219. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +28 -0
  220. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +36 -0
  221. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.setup.ts +56 -0
@@ -0,0 +1,261 @@
1
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
2
+ "use client";
3
+
4
+ import { useEffect, useState } from "react";
5
+ import { useConversations } from "@/hooks";
6
+ import { Button } from "@/components/ui";
7
+ import { cn } from "@/lib/utils";
8
+ import {
9
+ MessageSquarePlus,
10
+ MessageSquare,
11
+ Trash2,
12
+ Archive,
13
+ MoreVertical,
14
+ Pencil,
15
+ ChevronLeft,
16
+ ChevronRight,
17
+ } from "lucide-react";
18
+ import type { Conversation } from "@/types";
19
+
20
+ interface ConversationItemProps {
21
+ conversation: Conversation;
22
+ isActive: boolean;
23
+ onSelect: () => void;
24
+ onDelete: () => void;
25
+ onArchive: () => void;
26
+ onRename: (title: string) => void;
27
+ }
28
+
29
+ function ConversationItem({
30
+ conversation,
31
+ isActive,
32
+ onSelect,
33
+ onDelete,
34
+ onArchive,
35
+ onRename,
36
+ }: ConversationItemProps) {
37
+ const [showMenu, setShowMenu] = useState(false);
38
+ const [isEditing, setIsEditing] = useState(false);
39
+ const [editTitle, setEditTitle] = useState(conversation.title || "");
40
+
41
+ const handleRename = () => {
42
+ if (editTitle.trim()) {
43
+ onRename(editTitle.trim());
44
+ }
45
+ setIsEditing(false);
46
+ };
47
+
48
+ const displayTitle =
49
+ conversation.title ||
50
+ `Chat ${new Date(conversation.created_at).toLocaleDateString()}`;
51
+
52
+ return (
53
+ <div
54
+ className={cn(
55
+ "group relative flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors cursor-pointer",
56
+ isActive
57
+ ? "bg-secondary text-secondary-foreground"
58
+ : "text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground"
59
+ )}
60
+ onClick={onSelect}
61
+ >
62
+ <MessageSquare className="h-4 w-4 shrink-0" />
63
+ {isEditing ? (
64
+ <input
65
+ type="text"
66
+ value={editTitle}
67
+ onChange={(e) => setEditTitle(e.target.value)}
68
+ onBlur={handleRename}
69
+ onKeyDown={(e) => {
70
+ if (e.key === "Enter") handleRename();
71
+ if (e.key === "Escape") setIsEditing(false);
72
+ }}
73
+ className="flex-1 bg-transparent outline-none text-foreground"
74
+ autoFocus
75
+ onClick={(e) => e.stopPropagation()}
76
+ />
77
+ ) : (
78
+ <span className="flex-1 truncate">{displayTitle}</span>
79
+ )}
80
+
81
+ <div className="relative">
82
+ <Button
83
+ variant="ghost"
84
+ size="sm"
85
+ className={cn(
86
+ "h-6 w-6 p-0 opacity-0 group-hover:opacity-100",
87
+ showMenu && "opacity-100"
88
+ )}
89
+ onClick={(e) => {
90
+ e.stopPropagation();
91
+ setShowMenu(!showMenu);
92
+ }}
93
+ >
94
+ <MoreVertical className="h-3.5 w-3.5" />
95
+ </Button>
96
+
97
+ {showMenu && (
98
+ <>
99
+ <div
100
+ className="fixed inset-0 z-10"
101
+ onClick={() => setShowMenu(false)}
102
+ />
103
+ <div className="absolute right-0 top-6 z-20 w-36 rounded-md border bg-popover shadow-lg">
104
+ <button
105
+ className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-secondary"
106
+ onClick={(e) => {
107
+ e.stopPropagation();
108
+ setIsEditing(true);
109
+ setShowMenu(false);
110
+ }}
111
+ >
112
+ <Pencil className="h-3.5 w-3.5" />
113
+ Rename
114
+ </button>
115
+ <button
116
+ className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-secondary"
117
+ onClick={(e) => {
118
+ e.stopPropagation();
119
+ onArchive();
120
+ setShowMenu(false);
121
+ }}
122
+ >
123
+ <Archive className="h-3.5 w-3.5" />
124
+ Archive
125
+ </button>
126
+ <button
127
+ className="flex w-full items-center gap-2 px-3 py-2 text-sm text-destructive hover:bg-destructive/10"
128
+ onClick={(e) => {
129
+ e.stopPropagation();
130
+ onDelete();
131
+ setShowMenu(false);
132
+ }}
133
+ >
134
+ <Trash2 className="h-3.5 w-3.5" />
135
+ Delete
136
+ </button>
137
+ </div>
138
+ </>
139
+ )}
140
+ </div>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ interface ConversationSidebarProps {
146
+ className?: string;
147
+ }
148
+
149
+ export function ConversationSidebar({ className }: ConversationSidebarProps) {
150
+ const [isCollapsed, setIsCollapsed] = useState(false);
151
+ const {
152
+ conversations,
153
+ currentConversationId,
154
+ isLoading,
155
+ fetchConversations,
156
+ selectConversation,
157
+ deleteConversation,
158
+ archiveConversation,
159
+ renameConversation,
160
+ startNewChat,
161
+ } = useConversations();
162
+
163
+ useEffect(() => {
164
+ fetchConversations();
165
+ }, [fetchConversations]);
166
+
167
+ const activeConversations = conversations.filter((c) => !c.is_archived);
168
+
169
+ if (isCollapsed) {
170
+ return (
171
+ <div
172
+ className={cn(
173
+ "flex flex-col items-center border-r bg-background py-4 w-12",
174
+ className
175
+ )}
176
+ >
177
+ <Button
178
+ variant="ghost"
179
+ size="sm"
180
+ className="h-8 w-8 p-0 mb-4"
181
+ onClick={() => setIsCollapsed(false)}
182
+ >
183
+ <ChevronRight className="h-4 w-4" />
184
+ </Button>
185
+ <Button
186
+ variant="ghost"
187
+ size="sm"
188
+ className="h-8 w-8 p-0"
189
+ onClick={startNewChat}
190
+ title="New Chat"
191
+ >
192
+ <MessageSquarePlus className="h-4 w-4" />
193
+ </Button>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ return (
199
+ <aside
200
+ className={cn(
201
+ "flex w-64 shrink-0 flex-col border-r bg-background",
202
+ className
203
+ )}
204
+ >
205
+ <div className="flex items-center justify-between border-b px-4 py-3">
206
+ <h2 className="font-semibold text-sm">Conversations</h2>
207
+ <Button
208
+ variant="ghost"
209
+ size="sm"
210
+ className="h-8 w-8 p-0"
211
+ onClick={() => setIsCollapsed(true)}
212
+ >
213
+ <ChevronLeft className="h-4 w-4" />
214
+ </Button>
215
+ </div>
216
+
217
+ <div className="p-3">
218
+ <Button
219
+ variant="outline"
220
+ size="sm"
221
+ className="w-full justify-start gap-2"
222
+ onClick={startNewChat}
223
+ >
224
+ <MessageSquarePlus className="h-4 w-4" />
225
+ New Chat
226
+ </Button>
227
+ </div>
228
+
229
+ <div className="flex-1 overflow-y-auto px-3 pb-3">
230
+ {isLoading && conversations.length === 0 ? (
231
+ <div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
232
+ Loading...
233
+ </div>
234
+ ) : activeConversations.length === 0 ? (
235
+ <div className="flex flex-col items-center justify-center py-8 text-center text-sm text-muted-foreground">
236
+ <MessageSquare className="h-8 w-8 mb-2 opacity-50" />
237
+ <p>No conversations yet</p>
238
+ <p className="text-xs mt-1">Start a new chat to begin</p>
239
+ </div>
240
+ ) : (
241
+ <div className="space-y-1">
242
+ {activeConversations.map((conversation) => (
243
+ <ConversationItem
244
+ key={conversation.id}
245
+ conversation={conversation}
246
+ isActive={conversation.id === currentConversationId}
247
+ onSelect={() => selectConversation(conversation.id)}
248
+ onDelete={() => deleteConversation(conversation.id)}
249
+ onArchive={() => archiveConversation(conversation.id)}
250
+ onRename={(title) => renameConversation(conversation.id, title)}
251
+ />
252
+ ))}
253
+ </div>
254
+ )}
255
+ </div>
256
+ </aside>
257
+ );
258
+ }
259
+ {%- else %}
260
+ // Conversation sidebar - not configured (enable_conversation_persistence is false)
261
+ {%- endif %}
@@ -0,0 +1,8 @@
1
+ export { ChatContainer } from "./chat-container";
2
+ export { MessageList } from "./message-list";
3
+ export { MessageItem } from "./message-item";
4
+ export { ToolCallCard } from "./tool-call-card";
5
+ export { ChatInput } from "./chat-input";
6
+ {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
7
+ export { ConversationSidebar } from "./conversation-sidebar";
8
+ {%- endif %}
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import type { ChatMessage } from "@/types";
5
+ import { ToolCallCard } from "./tool-call-card";
6
+ import { User, Bot } from "lucide-react";
7
+
8
+ interface MessageItemProps {
9
+ message: ChatMessage;
10
+ }
11
+
12
+ export function MessageItem({ message }: MessageItemProps) {
13
+ const isUser = message.role === "user";
14
+
15
+ return (
16
+ <div
17
+ className={cn(
18
+ "flex gap-4 py-4",
19
+ isUser && "flex-row-reverse"
20
+ )}
21
+ >
22
+ {/* Avatar */}
23
+ <div
24
+ className={cn(
25
+ "flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center",
26
+ isUser ? "bg-primary text-primary-foreground" : "bg-orange-500/10 text-orange-500"
27
+ )}
28
+ >
29
+ {isUser ? <User className="h-4 w-4" /> : <Bot className="h-4 w-4" />}
30
+ </div>
31
+
32
+ {/* Content */}
33
+ <div className={cn(
34
+ "flex-1 space-y-2 overflow-hidden max-w-[85%]",
35
+ isUser && "flex flex-col items-end"
36
+ )}>
37
+ {/* Text content */}
38
+ <div className={cn(
39
+ "rounded-2xl px-4 py-2.5",
40
+ isUser
41
+ ? "bg-primary text-primary-foreground rounded-tr-sm"
42
+ : "bg-muted rounded-tl-sm"
43
+ )}>
44
+ <p className="whitespace-pre-wrap break-words text-sm">
45
+ {message.content}
46
+ {message.isStreaming && (
47
+ <span className="inline-block w-1.5 h-4 ml-1 bg-current animate-pulse rounded-full" />
48
+ )}
49
+ </p>
50
+ </div>
51
+
52
+ {/* Tool calls */}
53
+ {message.toolCalls && message.toolCalls.length > 0 && (
54
+ <div className="space-y-2 w-full">
55
+ {message.toolCalls.map((toolCall) => (
56
+ <ToolCallCard key={toolCall.id} toolCall={toolCall} />
57
+ ))}
58
+ </div>
59
+ )}
60
+ </div>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,18 @@
1
+ "use client";
2
+
3
+ import type { ChatMessage } from "@/types";
4
+ import { MessageItem } from "./message-item";
5
+
6
+ interface MessageListProps {
7
+ messages: ChatMessage[];
8
+ }
9
+
10
+ export function MessageList({ messages }: MessageListProps) {
11
+ return (
12
+ <div className="space-y-4">
13
+ {messages.map((message) => (
14
+ <MessageItem key={message.id} message={message} />
15
+ ))}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui";
4
+ import type { ToolCall } from "@/types";
5
+ import { Wrench, CheckCircle, Loader2, AlertCircle } from "lucide-react";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ interface ToolCallCardProps {
9
+ toolCall: ToolCall;
10
+ }
11
+
12
+ export function ToolCallCard({ toolCall }: ToolCallCardProps) {
13
+ const statusConfig = {
14
+ pending: { icon: Loader2, color: "text-muted-foreground", animate: true },
15
+ running: { icon: Loader2, color: "text-blue-500", animate: true },
16
+ completed: { icon: CheckCircle, color: "text-green-500", animate: false },
17
+ error: { icon: AlertCircle, color: "text-red-500", animate: false },
18
+ };
19
+
20
+ const { icon: StatusIcon, color, animate } = statusConfig[toolCall.status];
21
+
22
+ return (
23
+ <Card className="bg-muted/50">
24
+ <CardHeader className="py-2 px-3">
25
+ <div className="flex items-center justify-between">
26
+ <div className="flex items-center gap-2">
27
+ <Wrench className="h-4 w-4 text-muted-foreground" />
28
+ <CardTitle className="text-sm font-medium">
29
+ {toolCall.name}
30
+ </CardTitle>
31
+ </div>
32
+ <StatusIcon
33
+ className={cn("h-4 w-4", color, animate && "animate-spin")}
34
+ />
35
+ </div>
36
+ </CardHeader>
37
+ <CardContent className="py-2 px-3 space-y-2">
38
+ {/* Arguments */}
39
+ <div>
40
+ <p className="text-xs text-muted-foreground mb-1">Arguments:</p>
41
+ <pre className="text-xs bg-background p-2 rounded overflow-x-auto">
42
+ {JSON.stringify(toolCall.args, null, 2)}
43
+ </pre>
44
+ </div>
45
+
46
+ {/* Result */}
47
+ {toolCall.result !== undefined && (
48
+ <div>
49
+ <p className="text-xs text-muted-foreground mb-1">Result:</p>
50
+ <pre className="text-xs bg-background p-2 rounded overflow-x-auto">
51
+ {typeof toolCall.result === "string"
52
+ ? toolCall.result
53
+ : JSON.stringify(toolCall.result, null, 2)}
54
+ </pre>
55
+ </div>
56
+ )}
57
+ </CardContent>
58
+ </Card>
59
+ );
60
+ }
@@ -0,0 +1,32 @@
1
+ {%- if cookiecutter.enable_oauth_google %}
2
+ import { SVGProps } from "react";
3
+
4
+ export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
5
+ return (
6
+ <svg
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ viewBox="0 0 24 24"
9
+ {...props}
10
+ >
11
+ <path
12
+ fill="#4285F4"
13
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
14
+ />
15
+ <path
16
+ fill="#34A853"
17
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
18
+ />
19
+ <path
20
+ fill="#FBBC05"
21
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
22
+ />
23
+ <path
24
+ fill="#EA4335"
25
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
26
+ />
27
+ </svg>
28
+ );
29
+ }
30
+ {%- else %}
31
+ // Google icon component - OAuth not enabled
32
+ {%- endif %}
@@ -0,0 +1,3 @@
1
+ {%- if cookiecutter.enable_oauth_google %}
2
+ export { GoogleIcon } from "./google-icon";
3
+ {%- endif %}
@@ -0,0 +1,97 @@
1
+ {%- if cookiecutter.enable_i18n %}
2
+ 'use client';
3
+
4
+ import { useLocale } from 'next-intl';
5
+ import { useRouter, usePathname } from 'next/navigation';
6
+ import { locales, type Locale, getLocaleLabel } from '@/i18n';
7
+
8
+ /**
9
+ * Language switcher dropdown component.
10
+ * Allows users to switch between available locales.
11
+ */
12
+ export function LanguageSwitcher() {
13
+ const locale = useLocale();
14
+ const router = useRouter();
15
+ const pathname = usePathname();
16
+
17
+ const handleChange = (newLocale: Locale) => {
18
+ // Remove the current locale from pathname and add the new one
19
+ const segments = pathname.split('/');
20
+ segments[1] = newLocale;
21
+ const newPath = segments.join('/');
22
+ router.push(newPath);
23
+ };
24
+
25
+ return (
26
+ <div className="relative">
27
+ <select
28
+ value={locale}
29
+ onChange={(e) => handleChange(e.target.value as Locale)}
30
+ className="appearance-none bg-transparent border border-gray-300 dark:border-gray-600 rounded-md px-3 py-1.5 pr-8 text-sm cursor-pointer hover:border-gray-400 dark:hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
31
+ aria-label="Select language"
32
+ >
33
+ {locales.map((loc) => (
34
+ <option key={loc} value={loc}>
35
+ {getLocaleLabel(loc)}
36
+ </option>
37
+ ))}
38
+ </select>
39
+ <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500">
40
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
41
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
42
+ </svg>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Compact language switcher with flag icons.
50
+ */
51
+ export function LanguageSwitcherCompact() {
52
+ const locale = useLocale();
53
+ const router = useRouter();
54
+ const pathname = usePathname();
55
+
56
+ const flags: Record<Locale, string> = {
57
+ en: '🇬🇧',
58
+ pl: '🇵🇱',
59
+ };
60
+
61
+ const handleChange = (newLocale: Locale) => {
62
+ const segments = pathname.split('/');
63
+ segments[1] = newLocale;
64
+ const newPath = segments.join('/');
65
+ router.push(newPath);
66
+ };
67
+
68
+ return (
69
+ <div className="flex gap-1">
70
+ {locales.map((loc) => (
71
+ <button
72
+ key={loc}
73
+ onClick={() => handleChange(loc)}
74
+ className={`px-2 py-1 rounded-md text-lg transition-opacity ${
75
+ locale === loc
76
+ ? 'opacity-100 bg-gray-100 dark:bg-gray-800'
77
+ : 'opacity-50 hover:opacity-75'
78
+ }`}
79
+ aria-label={getLocaleLabel(loc)}
80
+ aria-pressed={locale === loc}
81
+ >
82
+ {flags[loc]}
83
+ </button>
84
+ ))}
85
+ </div>
86
+ );
87
+ }
88
+ {%- else %}
89
+ // i18n is disabled - no language switcher component
90
+ export function LanguageSwitcher() {
91
+ return null;
92
+ }
93
+
94
+ export function LanguageSwitcherCompact() {
95
+ return null;
96
+ }
97
+ {%- endif %}
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useAuth } from "@/hooks";
5
+ import { Button } from "@/components/ui";
6
+ import { ThemeToggle } from "@/components/theme";
7
+ import { ROUTES } from "@/lib/constants";
8
+ import { LogOut, User } from "lucide-react";
9
+
10
+ export function Header() {
11
+ const { user, isAuthenticated, logout } = useAuth();
12
+
13
+ return (
14
+ <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
15
+ <div className="flex h-14 items-center justify-end px-6">
16
+ <div className="flex items-center gap-3">
17
+ <ThemeToggle />
18
+ {isAuthenticated ? (
19
+ <>
20
+ <Button variant="ghost" size="sm" asChild>
21
+ <Link href={ROUTES.PROFILE} className="flex items-center gap-2">
22
+ <User className="h-4 w-4" />
23
+ <span className="max-w-32 truncate">{user?.email}</span>
24
+ </Link>
25
+ </Button>
26
+ <Button variant="ghost" size="sm" onClick={logout}>
27
+ <LogOut className="h-4 w-4" />
28
+ <span className="sr-only">Logout</span>
29
+ </Button>
30
+ </>
31
+ ) : (
32
+ <>
33
+ <Button variant="ghost" size="sm" asChild>
34
+ <Link href={ROUTES.LOGIN}>Login</Link>
35
+ </Button>
36
+ <Button size="sm" asChild>
37
+ <Link href={ROUTES.REGISTER}>Register</Link>
38
+ </Button>
39
+ </>
40
+ )}
41
+ </div>
42
+ </div>
43
+ </header>
44
+ );
45
+ }
@@ -0,0 +1,2 @@
1
+ export { Header } from "./header";
2
+ export { Sidebar } from "./sidebar";
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import { cn } from "@/lib/utils";
6
+ import { ROUTES } from "@/lib/constants";
7
+ import { LayoutDashboard, MessageSquare } from "lucide-react";
8
+
9
+ const navigation = [
10
+ { name: "Dashboard", href: ROUTES.DASHBOARD, icon: LayoutDashboard },
11
+ { name: "Chat", href: ROUTES.CHAT, icon: MessageSquare },
12
+ ];
13
+
14
+ export function Sidebar() {
15
+ const pathname = usePathname();
16
+
17
+ return (
18
+ <aside className="hidden w-64 shrink-0 border-r bg-background md:block">
19
+ <div className="flex h-full flex-col">
20
+ <div className="flex h-14 items-center border-b px-4">
21
+ <Link href={ROUTES.HOME} className="flex items-center gap-2 font-semibold">
22
+ <span>{{ cookiecutter.project_name }}</span>
23
+ </Link>
24
+ </div>
25
+ <nav className="flex-1 space-y-1 p-4">
26
+ {navigation.map((item) => {
27
+ const isActive = pathname === item.href;
28
+ return (
29
+ <Link
30
+ key={item.name}
31
+ href={item.href}
32
+ className={cn(
33
+ "flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
34
+ isActive
35
+ ? "bg-secondary text-secondary-foreground"
36
+ : "text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground"
37
+ )}
38
+ >
39
+ <item.icon className="h-4 w-4" />
40
+ {item.name}
41
+ </Link>
42
+ );
43
+ })}
44
+ </nav>
45
+ </div>
46
+ </aside>
47
+ );
48
+ }
@@ -0,0 +1,7 @@
1
+ {%- if cookiecutter.use_frontend %}
2
+ export { ThemeProvider } from "./theme-provider";
3
+ export { ThemeToggle } from "./theme-toggle";
4
+ {%- else %}
5
+ /* Theme components - frontend not configured */
6
+ export {};
7
+ {%- endif %}