fastapi-fullstack 0.1.7__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.
- fastapi_fullstack-0.1.7.dist-info/METADATA +739 -0
- fastapi_fullstack-0.1.7.dist-info/RECORD +241 -0
- fastapi_fullstack-0.1.7.dist-info/WHEEL +4 -0
- fastapi_fullstack-0.1.7.dist-info/entry_points.txt +2 -0
- fastapi_fullstack-0.1.7.dist-info/licenses/LICENSE +21 -0
- fastapi_gen/__init__.py +3 -0
- fastapi_gen/cli.py +442 -0
- fastapi_gen/config.py +356 -0
- fastapi_gen/generator.py +207 -0
- fastapi_gen/prompts.py +874 -0
- fastapi_gen/template/VARIABLES.md +276 -0
- fastapi_gen/template/cookiecutter.json +93 -0
- fastapi_gen/template/hooks/post_gen_project.py +355 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +150 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/.gitignore +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/AGENTS.md +55 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/Makefile +315 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +768 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.dockerignore +60 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +155 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.pre-commit-config.yaml +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/Dockerfile +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/script.py.mako +30 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/versions/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic.ini +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/__init__.py +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +447 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +226 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/prompts.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/__init__.py +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/tools/datetime_tool.py +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/deps.py +541 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/exception_handlers.py +98 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/router.py +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/__init__.py +87 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +902 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/auth.py +395 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/conversations.py +498 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/health.py +227 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/items.py +275 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +205 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/sessions.py +168 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/users.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/webhooks.py +477 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/ws.py +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/versioning.py +221 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/__init__.py +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/clients/redis.py +88 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/__init__.py +117 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/example.py +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +266 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/cache.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +267 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/csrf.py +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/exceptions.py +122 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/logfire_setup.py +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/middleware.py +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/oauth.py +23 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/rate_limit.py +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/sanitize.py +271 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/security.py +102 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/__init__.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +319 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +96 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +126 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +218 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +244 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/session.py +130 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +334 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/__init__.py +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/pipelines/base.py +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/__init__.py +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +154 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/conversation.py +838 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/item.py +222 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +318 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/user.py +322 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/webhook.py +358 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/__init__.py +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/base.py +57 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/conversation.py +192 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/item.py +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/session.py +42 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/token.py +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/user.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/schemas/webhook.py +89 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/__init__.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +850 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/item.py +246 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +333 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/user.py +432 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +561 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/celery_app.py +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/taskiq_app.py +38 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +25 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/examples.py +106 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/schedules.py +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/taskiq_examples.py +92 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/cli/commands.py +438 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +180 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/scripts/.gitkeep +0 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/__init__.py +1 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_auth.py +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_exceptions.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_health.py +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_items.py +310 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_users.py +253 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/conftest.py +151 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_admin.py +890 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +261 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_clients.py +183 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_commands.py +173 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_core.py +143 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_pipelines.py +118 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_repositories.py +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_security.py +124 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_services.py +363 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_worker.py +85 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +242 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.frontend.yml +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +435 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +241 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/adding_features.md +132 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/architecture.md +63 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/patterns.md +161 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/docs/testing.md +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.dockerignore +40 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +12 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.gitignore +45 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierignore +19 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.prettierrc +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/Dockerfile +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/README.md +648 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.setup.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/auth.spec.ts +134 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/chat.spec.ts +207 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/e2e/home.spec.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/instrumentation.ts +14 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/en.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/messages/pl.json +84 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/next.config.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/package.json +69 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/playwright.config.ts +101 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/postcss.config.mjs +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/layout.tsx +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/login/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(auth)/register/page.tsx +5 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/chat/page.tsx +48 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/dashboard/page.tsx +99 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/layout.tsx +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/(dashboard)/profile/page.tsx +152 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/auth/callback/page.tsx +113 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/layout.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/[locale]/page.tsx +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/login/route.ts +58 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/logout/route.ts +24 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/me/route.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/oauth-callback/route.ts +50 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/refresh/route.ts +54 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/auth/register/route.ts +26 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/messages/route.ts +41 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/[id]/route.ts +108 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/conversations/route.ts +73 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/api/health/route.ts +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/globals.css +323 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/layout.tsx +22 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/app/providers.tsx +29 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/login-form.tsx +120 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/auth/register-form.tsx +153 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +234 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-input.tsx +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/conversation-sidebar.tsx +328 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/copy-button.tsx +46 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +11 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/local-conversation-sidebar.tsx +295 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/markdown-content.tsx +167 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +18 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-call-card.tsx +79 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/google-icon.tsx +32 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/icons/index.ts +3 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/header.tsx +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/index.ts +2 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/layout/sidebar.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx +53 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/badge.tsx +35 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.test.tsx +75 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/button.tsx +56 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/card.tsx +82 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/index.ts +13 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/input.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/label.tsx +21 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/ui/sheet.tsx +109 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/index.ts +7 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-auth.ts +97 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +203 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-conversations.ts +181 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +165 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-websocket.ts +105 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/i18n.ts +37 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/api-client.ts +90 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +39 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/server-api.ts +78 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.test.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/utils.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/middleware.ts +31 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.test.ts +72 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/auth-store.ts +64 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/chat-store.ts +65 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/conversation-store.ts +76 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/index.ts +9 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/local-chat-store.ts +255 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/sidebar-store.ts +17 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/stores/theme-store.ts +44 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/api.ts +27 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/auth.ts +52 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +83 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/conversation.ts +49 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/index.ts +10 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/tsconfig.json +28 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.config.ts +36 -0
- fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/vitest.setup.ts +56 -0
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/language-switcher.tsx
ADDED
|
@@ -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,65 @@
|
|
|
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
|
+
{%- if cookiecutter.enable_i18n %}
|
|
8
|
+
import { LanguageSwitcherCompact } from "@/components/language-switcher";
|
|
9
|
+
{%- endif %}
|
|
10
|
+
import { ROUTES } from "@/lib/constants";
|
|
11
|
+
import { LogOut, User, Menu } from "lucide-react";
|
|
12
|
+
import { useSidebarStore } from "@/stores";
|
|
13
|
+
|
|
14
|
+
export function Header() {
|
|
15
|
+
const { user, isAuthenticated, logout } = useAuth();
|
|
16
|
+
const { toggle } = useSidebarStore();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<header className="sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
20
|
+
<div className="flex h-14 items-center justify-between px-3 sm:px-6">
|
|
21
|
+
<Button
|
|
22
|
+
variant="ghost"
|
|
23
|
+
size="sm"
|
|
24
|
+
className="h-10 w-10 p-0 md:hidden"
|
|
25
|
+
onClick={toggle}
|
|
26
|
+
>
|
|
27
|
+
<Menu className="h-5 w-5" />
|
|
28
|
+
<span className="sr-only">Toggle menu</span>
|
|
29
|
+
</Button>
|
|
30
|
+
|
|
31
|
+
<div className="hidden md:block" />
|
|
32
|
+
|
|
33
|
+
<div className="flex items-center gap-2 sm:gap-3">
|
|
34
|
+
{%- if cookiecutter.enable_i18n %}
|
|
35
|
+
<LanguageSwitcherCompact />
|
|
36
|
+
{%- endif %}
|
|
37
|
+
<ThemeToggle />
|
|
38
|
+
{isAuthenticated ? (
|
|
39
|
+
<>
|
|
40
|
+
<Button variant="ghost" size="sm" asChild className="h-10 px-2 sm:px-3">
|
|
41
|
+
<Link href={ROUTES.PROFILE} className="flex items-center gap-2">
|
|
42
|
+
<User className="h-4 w-4" />
|
|
43
|
+
<span className="hidden max-w-32 truncate sm:inline">{user?.email}</span>
|
|
44
|
+
</Link>
|
|
45
|
+
</Button>
|
|
46
|
+
<Button variant="ghost" size="sm" onClick={logout} className="h-10 w-10 p-0 sm:w-auto sm:px-3">
|
|
47
|
+
<LogOut className="h-4 w-4" />
|
|
48
|
+
<span className="sr-only sm:not-sr-only sm:ml-2">Logout</span>
|
|
49
|
+
</Button>
|
|
50
|
+
</>
|
|
51
|
+
) : (
|
|
52
|
+
<>
|
|
53
|
+
<Button variant="ghost" size="sm" asChild className="h-10">
|
|
54
|
+
<Link href={ROUTES.LOGIN}>Login</Link>
|
|
55
|
+
</Button>
|
|
56
|
+
<Button size="sm" asChild className="h-10">
|
|
57
|
+
<Link href={ROUTES.REGISTER}>Register</Link>
|
|
58
|
+
</Button>
|
|
59
|
+
</>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
import { useSidebarStore } from "@/stores";
|
|
9
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetClose } from "@/components/ui";
|
|
10
|
+
|
|
11
|
+
const navigation = [
|
|
12
|
+
{ name: "Dashboard", href: ROUTES.DASHBOARD, icon: LayoutDashboard },
|
|
13
|
+
{ name: "Chat", href: ROUTES.CHAT, icon: MessageSquare },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
|
|
17
|
+
const pathname = usePathname();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<nav className="flex-1 space-y-1 p-4">
|
|
21
|
+
{navigation.map((item) => {
|
|
22
|
+
const isActive = pathname === item.href;
|
|
23
|
+
return (
|
|
24
|
+
<Link
|
|
25
|
+
key={item.name}
|
|
26
|
+
href={item.href}
|
|
27
|
+
onClick={onNavigate}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex items-center gap-3 rounded-lg px-3 py-3 text-sm font-medium transition-colors",
|
|
30
|
+
"min-h-[44px]",
|
|
31
|
+
isActive
|
|
32
|
+
? "bg-secondary text-secondary-foreground"
|
|
33
|
+
: "text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground"
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<item.icon className="h-5 w-5" />
|
|
37
|
+
{item.name}
|
|
38
|
+
</Link>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</nav>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function SidebarContent({ onNavigate }: { onNavigate?: () => void }) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex h-full flex-col">
|
|
48
|
+
<div className="flex h-14 items-center border-b px-4">
|
|
49
|
+
<Link
|
|
50
|
+
href={ROUTES.HOME}
|
|
51
|
+
className="flex items-center gap-2 font-semibold"
|
|
52
|
+
onClick={onNavigate}
|
|
53
|
+
>
|
|
54
|
+
<span>{"{{ cookiecutter.project_name }}"}</span>
|
|
55
|
+
</Link>
|
|
56
|
+
</div>
|
|
57
|
+
<NavLinks onNavigate={onNavigate} />
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function Sidebar() {
|
|
63
|
+
const { isOpen, close } = useSidebarStore();
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<aside className="hidden w-64 shrink-0 border-r bg-background md:block">
|
|
68
|
+
<SidebarContent />
|
|
69
|
+
</aside>
|
|
70
|
+
|
|
71
|
+
<Sheet open={isOpen} onOpenChange={close}>
|
|
72
|
+
<SheetContent side="left" className="w-72 p-0">
|
|
73
|
+
<SheetHeader className="h-14 px-4">
|
|
74
|
+
<SheetTitle>{"{{ cookiecutter.project_name }}"}</SheetTitle>
|
|
75
|
+
<SheetClose onClick={close} />
|
|
76
|
+
</SheetHeader>
|
|
77
|
+
<NavLinks onNavigate={close} />
|
|
78
|
+
</SheetContent>
|
|
79
|
+
</Sheet>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-provider.tsx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{%- if cookiecutter.use_frontend %}
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { useThemeStore, getResolvedTheme } from "@/stores/theme-store";
|
|
6
|
+
|
|
7
|
+
interface ThemeProviderProps {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ThemeProvider({ children }: ThemeProviderProps) {
|
|
12
|
+
const { theme } = useThemeStore();
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const root = document.documentElement;
|
|
16
|
+
const resolvedTheme = getResolvedTheme(theme);
|
|
17
|
+
|
|
18
|
+
// Remove both classes first
|
|
19
|
+
root.classList.remove("light", "dark");
|
|
20
|
+
// Add the current theme class
|
|
21
|
+
root.classList.add(resolvedTheme);
|
|
22
|
+
|
|
23
|
+
// Update color-scheme for native elements
|
|
24
|
+
root.style.colorScheme = resolvedTheme;
|
|
25
|
+
}, [theme]);
|
|
26
|
+
|
|
27
|
+
// Listen for system theme changes when using "system" theme
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (theme !== "system") return;
|
|
30
|
+
|
|
31
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
32
|
+
|
|
33
|
+
const handleChange = () => {
|
|
34
|
+
const root = document.documentElement;
|
|
35
|
+
const resolvedTheme = mediaQuery.matches ? "dark" : "light";
|
|
36
|
+
|
|
37
|
+
root.classList.remove("light", "dark");
|
|
38
|
+
root.classList.add(resolvedTheme);
|
|
39
|
+
root.style.colorScheme = resolvedTheme;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
43
|
+
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
44
|
+
}, [theme]);
|
|
45
|
+
|
|
46
|
+
return <>{children}</>;
|
|
47
|
+
}
|
|
48
|
+
{%- else %}
|
|
49
|
+
/* Theme provider - frontend not configured */
|
|
50
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
51
|
+
return <>{children}</>;
|
|
52
|
+
}
|
|
53
|
+
{%- endif %}
|
fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/theme/theme-toggle.tsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{%- if cookiecutter.use_frontend %}
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { Moon, Sun, Monitor } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { useThemeStore, Theme, getResolvedTheme } from "@/stores/theme-store";
|
|
8
|
+
|
|
9
|
+
interface ThemeToggleProps {
|
|
10
|
+
variant?: "icon" | "dropdown";
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ThemeToggle({ variant = "icon", className }: ThemeToggleProps) {
|
|
15
|
+
const { theme, setTheme } = useThemeStore();
|
|
16
|
+
const [mounted, setMounted] = useState(false);
|
|
17
|
+
|
|
18
|
+
// Prevent hydration mismatch by only rendering after mount
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setMounted(true);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const resolvedTheme = getResolvedTheme(theme);
|
|
24
|
+
|
|
25
|
+
const cycleTheme = () => {
|
|
26
|
+
const themes: Theme[] = ["light", "dark", "system"];
|
|
27
|
+
const currentIndex = themes.indexOf(theme);
|
|
28
|
+
const nextIndex = (currentIndex + 1) % themes.length;
|
|
29
|
+
setTheme(themes[nextIndex]);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Render placeholder during SSR to prevent hydration mismatch
|
|
33
|
+
if (!mounted) {
|
|
34
|
+
return (
|
|
35
|
+
<Button
|
|
36
|
+
variant="ghost"
|
|
37
|
+
size="icon"
|
|
38
|
+
className={className}
|
|
39
|
+
aria-label="Toggle theme"
|
|
40
|
+
>
|
|
41
|
+
<Sun className="h-5 w-5" />
|
|
42
|
+
</Button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (variant === "icon") {
|
|
47
|
+
return (
|
|
48
|
+
<Button
|
|
49
|
+
variant="ghost"
|
|
50
|
+
size="icon"
|
|
51
|
+
onClick={cycleTheme}
|
|
52
|
+
className={className}
|
|
53
|
+
aria-label={`Switch theme (current: ${theme})`}
|
|
54
|
+
title={`Theme: ${theme}`}
|
|
55
|
+
>
|
|
56
|
+
{resolvedTheme === "dark" ? (
|
|
57
|
+
<Moon className="h-5 w-5" />
|
|
58
|
+
) : (
|
|
59
|
+
<Sun className="h-5 w-5" />
|
|
60
|
+
)}
|
|
61
|
+
{theme === "system" && (
|
|
62
|
+
<span className="sr-only">(following system)</span>
|
|
63
|
+
)}
|
|
64
|
+
</Button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={`flex gap-1 ${className}`}>
|
|
70
|
+
<Button
|
|
71
|
+
variant={theme === "light" ? "default" : "ghost"}
|
|
72
|
+
size="icon"
|
|
73
|
+
onClick={() => setTheme("light")}
|
|
74
|
+
aria-label="Light mode"
|
|
75
|
+
title="Light mode"
|
|
76
|
+
>
|
|
77
|
+
<Sun className="h-4 w-4" />
|
|
78
|
+
</Button>
|
|
79
|
+
<Button
|
|
80
|
+
variant={theme === "dark" ? "default" : "ghost"}
|
|
81
|
+
size="icon"
|
|
82
|
+
onClick={() => setTheme("dark")}
|
|
83
|
+
aria-label="Dark mode"
|
|
84
|
+
title="Dark mode"
|
|
85
|
+
>
|
|
86
|
+
<Moon className="h-4 w-4" />
|
|
87
|
+
</Button>
|
|
88
|
+
<Button
|
|
89
|
+
variant={theme === "system" ? "default" : "ghost"}
|
|
90
|
+
size="icon"
|
|
91
|
+
onClick={() => setTheme("system")}
|
|
92
|
+
aria-label="System theme"
|
|
93
|
+
title="System theme"
|
|
94
|
+
>
|
|
95
|
+
<Monitor className="h-4 w-4" />
|
|
96
|
+
</Button>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
{%- else %}
|
|
101
|
+
/* Theme toggle - frontend not configured */
|
|
102
|
+
export function ThemeToggle() {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
{%- endif %}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default:
|
|
11
|
+
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
12
|
+
secondary:
|
|
13
|
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
14
|
+
destructive:
|
|
15
|
+
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
|
16
|
+
outline: "text-foreground",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
variant: "default",
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export interface BadgeProps
|
|
26
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
27
|
+
VariantProps<typeof badgeVariants> {}
|
|
28
|
+
|
|
29
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
30
|
+
return (
|
|
31
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{%- if cookiecutter.use_frontend %}
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
|
|
6
|
+
describe("Button component", () => {
|
|
7
|
+
it("should render with children", () => {
|
|
8
|
+
render(<Button>Click me</Button>);
|
|
9
|
+
expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should handle click events", () => {
|
|
13
|
+
const handleClick = vi.fn();
|
|
14
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
15
|
+
|
|
16
|
+
fireEvent.click(screen.getByRole("button"));
|
|
17
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should apply variant classes", () => {
|
|
21
|
+
render(<Button variant="destructive">Delete</Button>);
|
|
22
|
+
const button = screen.getByRole("button");
|
|
23
|
+
expect(button.className).toMatch(/destructive|red|danger/i);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should apply size classes", () => {
|
|
27
|
+
render(<Button size="lg">Large Button</Button>);
|
|
28
|
+
const button = screen.getByRole("button");
|
|
29
|
+
// Check that the button has appropriate size styling
|
|
30
|
+
expect(button).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should be disabled when disabled prop is true", () => {
|
|
34
|
+
render(<Button disabled>Disabled</Button>);
|
|
35
|
+
const button = screen.getByRole("button");
|
|
36
|
+
expect(button).toBeDisabled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should not trigger click when disabled", () => {
|
|
40
|
+
const handleClick = vi.fn();
|
|
41
|
+
render(
|
|
42
|
+
<Button disabled onClick={handleClick}>
|
|
43
|
+
Disabled
|
|
44
|
+
</Button>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
fireEvent.click(screen.getByRole("button"));
|
|
48
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should support custom className", () => {
|
|
52
|
+
render(<Button className="custom-class">Custom</Button>);
|
|
53
|
+
const button = screen.getByRole("button");
|
|
54
|
+
expect(button.className).toContain("custom-class");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should render as a link when asChild is used", () => {
|
|
58
|
+
render(
|
|
59
|
+
<Button asChild>
|
|
60
|
+
<a href="/test">Link Button</a>
|
|
61
|
+
</Button>
|
|
62
|
+
);
|
|
63
|
+
expect(screen.getByRole("link")).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should support different button types", () => {
|
|
67
|
+
render(<Button type="submit">Submit</Button>);
|
|
68
|
+
const button = screen.getByRole("button");
|
|
69
|
+
expect(button).toHaveAttribute("type", "submit");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
{%- else %}
|
|
73
|
+
/* Button tests - frontend not configured */
|
|
74
|
+
export {};
|
|
75
|
+
{%- endif %}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default:
|
|
11
|
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
14
|
+
outline:
|
|
15
|
+
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
18
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
19
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
default: "h-9 px-4 py-2",
|
|
23
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
24
|
+
lg: "h-10 rounded-md px-8",
|
|
25
|
+
icon: "h-9 w-9",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "default",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export interface ButtonProps
|
|
36
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
37
|
+
VariantProps<typeof buttonVariants> {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
42
|
+
({ className, variant, size, asChild: _asChild, ...props }, ref) => {
|
|
43
|
+
// Note: asChild is extracted but not used - this component doesn't support Slot rendering
|
|
44
|
+
// If you need asChild support, install @radix-ui/react-slot and use Slot component
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
48
|
+
ref={ref}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
Button.displayName = "Button";
|
|
55
|
+
|
|
56
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
const Card = React.forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<div
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
"rounded-xl border bg-card text-card-foreground shadow",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
Card.displayName = "Card";
|
|
18
|
+
|
|
19
|
+
const CardHeader = React.forwardRef<
|
|
20
|
+
HTMLDivElement,
|
|
21
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
));
|
|
29
|
+
CardHeader.displayName = "CardHeader";
|
|
30
|
+
|
|
31
|
+
const CardTitle = React.forwardRef<
|
|
32
|
+
HTMLDivElement,
|
|
33
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
34
|
+
>(({ className, ...props }, ref) => (
|
|
35
|
+
<div
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
));
|
|
41
|
+
CardTitle.displayName = "CardTitle";
|
|
42
|
+
|
|
43
|
+
const CardDescription = React.forwardRef<
|
|
44
|
+
HTMLDivElement,
|
|
45
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
46
|
+
>(({ className, ...props }, ref) => (
|
|
47
|
+
<div
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
));
|
|
53
|
+
CardDescription.displayName = "CardDescription";
|
|
54
|
+
|
|
55
|
+
const CardContent = React.forwardRef<
|
|
56
|
+
HTMLDivElement,
|
|
57
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
58
|
+
>(({ className, ...props }, ref) => (
|
|
59
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
60
|
+
));
|
|
61
|
+
CardContent.displayName = "CardContent";
|
|
62
|
+
|
|
63
|
+
const CardFooter = React.forwardRef<
|
|
64
|
+
HTMLDivElement,
|
|
65
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
66
|
+
>(({ className, ...props }, ref) => (
|
|
67
|
+
<div
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
));
|
|
73
|
+
CardFooter.displayName = "CardFooter";
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
Card,
|
|
77
|
+
CardHeader,
|
|
78
|
+
CardFooter,
|
|
79
|
+
CardTitle,
|
|
80
|
+
CardDescription,
|
|
81
|
+
CardContent,
|
|
82
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { Button, buttonVariants } from "./button";
|
|
2
|
+
export { Input } from "./input";
|
|
3
|
+
export { Label } from "./label";
|
|
4
|
+
export {
|
|
5
|
+
Card,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardFooter,
|
|
8
|
+
CardTitle,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardContent,
|
|
11
|
+
} from "./card";
|
|
12
|
+
export { Badge, badgeVariants } from "./badge";
|
|
13
|
+
export { Sheet, SheetContent, SheetHeader, SheetTitle, SheetClose } from "./sheet";
|