create-blitzpack 0.1.0
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.
- package/dist/index.js +452 -0
- package/package.json +57 -0
- package/template/.dockerignore +59 -0
- package/template/.github/workflows/ci.yml +157 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.husky/pre-push +1 -0
- package/template/.lintstagedrc.cjs +4 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierrc +9 -0
- package/template/.vscode/settings.json +13 -0
- package/template/CLAUDE.md +175 -0
- package/template/CONTRIBUTING.md +32 -0
- package/template/Dockerfile +90 -0
- package/template/GETTING_STARTED.md +35 -0
- package/template/LICENSE +21 -0
- package/template/README.md +116 -0
- package/template/apps/api/.dockerignore +51 -0
- package/template/apps/api/.env.local.example +62 -0
- package/template/apps/api/emails/account-deleted-email.tsx +69 -0
- package/template/apps/api/emails/components/email-layout.tsx +154 -0
- package/template/apps/api/emails/config.ts +22 -0
- package/template/apps/api/emails/password-changed-email.tsx +88 -0
- package/template/apps/api/emails/password-reset-email.tsx +86 -0
- package/template/apps/api/emails/verification-email.tsx +85 -0
- package/template/apps/api/emails/welcome-email.tsx +70 -0
- package/template/apps/api/package.json +84 -0
- package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
- package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
- package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
- package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
- package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
- package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
- package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
- package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +116 -0
- package/template/apps/api/prisma/seed.ts +159 -0
- package/template/apps/api/prisma.config.ts +14 -0
- package/template/apps/api/src/app.ts +377 -0
- package/template/apps/api/src/common/logger.service.ts +227 -0
- package/template/apps/api/src/config/env.ts +60 -0
- package/template/apps/api/src/config/rate-limit.ts +29 -0
- package/template/apps/api/src/hooks/auth.ts +122 -0
- package/template/apps/api/src/plugins/auth.ts +198 -0
- package/template/apps/api/src/plugins/database.ts +45 -0
- package/template/apps/api/src/plugins/logger.ts +33 -0
- package/template/apps/api/src/plugins/multipart.ts +16 -0
- package/template/apps/api/src/plugins/scalar.ts +20 -0
- package/template/apps/api/src/plugins/schedule.ts +52 -0
- package/template/apps/api/src/plugins/services.ts +66 -0
- package/template/apps/api/src/plugins/swagger.ts +56 -0
- package/template/apps/api/src/routes/accounts.ts +91 -0
- package/template/apps/api/src/routes/admin-sessions.ts +92 -0
- package/template/apps/api/src/routes/metrics.ts +71 -0
- package/template/apps/api/src/routes/password.ts +46 -0
- package/template/apps/api/src/routes/sessions.ts +53 -0
- package/template/apps/api/src/routes/stats.ts +38 -0
- package/template/apps/api/src/routes/uploads-serve.ts +27 -0
- package/template/apps/api/src/routes/uploads.ts +154 -0
- package/template/apps/api/src/routes/users.ts +114 -0
- package/template/apps/api/src/routes/verification.ts +90 -0
- package/template/apps/api/src/server.ts +34 -0
- package/template/apps/api/src/services/accounts.service.ts +125 -0
- package/template/apps/api/src/services/authorization.service.ts +162 -0
- package/template/apps/api/src/services/email.service.ts +170 -0
- package/template/apps/api/src/services/file-storage.service.ts +267 -0
- package/template/apps/api/src/services/metrics.service.ts +175 -0
- package/template/apps/api/src/services/password.service.ts +56 -0
- package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
- package/template/apps/api/src/services/sessions.service.ts +276 -0
- package/template/apps/api/src/services/stats.service.ts +273 -0
- package/template/apps/api/src/services/uploads.service.ts +163 -0
- package/template/apps/api/src/services/users.service.spec.ts +249 -0
- package/template/apps/api/src/services/users.service.ts +198 -0
- package/template/apps/api/src/utils/file-validation.ts +108 -0
- package/template/apps/api/start.sh +33 -0
- package/template/apps/api/test/helpers/fastify-app.ts +24 -0
- package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
- package/template/apps/api/test/helpers/mock-logger.ts +28 -0
- package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
- package/template/apps/api/test/helpers/test-db.ts +125 -0
- package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
- package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
- package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
- package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
- package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
- package/template/apps/api/test/setup.ts +31 -0
- package/template/apps/api/tsconfig.json +26 -0
- package/template/apps/api/vitest.config.ts +35 -0
- package/template/apps/web/.env.local.example +11 -0
- package/template/apps/web/components.json +24 -0
- package/template/apps/web/next.config.ts +22 -0
- package/template/apps/web/package.json +56 -0
- package/template/apps/web/postcss.config.js +5 -0
- package/template/apps/web/public/apple-icon.png +0 -0
- package/template/apps/web/public/icon.png +0 -0
- package/template/apps/web/public/robots.txt +3 -0
- package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
- package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
- package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
- package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
- package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
- package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
- package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
- package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
- package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
- package/template/apps/web/src/app/dashboard/page.tsx +296 -0
- package/template/apps/web/src/app/error.tsx +32 -0
- package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/global-error.tsx +96 -0
- package/template/apps/web/src/app/globals.css +22 -0
- package/template/apps/web/src/app/icon.png +0 -0
- package/template/apps/web/src/app/layout.tsx +34 -0
- package/template/apps/web/src/app/not-found.tsx +28 -0
- package/template/apps/web/src/app/page.tsx +192 -0
- package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
- package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
- package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
- package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
- package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
- package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
- package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
- package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
- package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
- package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
- package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
- package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
- package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
- package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
- package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
- package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
- package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
- package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
- package/template/apps/web/src/components/auth/github-button.tsx +40 -0
- package/template/apps/web/src/components/auth/google-button.tsx +54 -0
- package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
- package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
- package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
- package/template/apps/web/src/components/error/error-card.tsx +47 -0
- package/template/apps/web/src/components/error/forbidden.tsx +25 -0
- package/template/apps/web/src/components/landing/command-block.tsx +64 -0
- package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
- package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
- package/template/apps/web/src/components/landing/logo.tsx +41 -0
- package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
- package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
- package/template/apps/web/src/components/layout/footer.tsx +3 -0
- package/template/apps/web/src/config/landing-data.ts +152 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
- package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
- package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
- package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
- package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
- package/template/apps/web/src/hooks/api/use-users.ts +149 -0
- package/template/apps/web/src/hooks/use-mobile.ts +21 -0
- package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
- package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
- package/template/apps/web/src/lib/api.ts +151 -0
- package/template/apps/web/src/lib/auth.ts +13 -0
- package/template/apps/web/src/lib/env.ts +52 -0
- package/template/apps/web/src/lib/form-utils.ts +11 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/providers.tsx +34 -0
- package/template/apps/web/src/store/atoms.ts +15 -0
- package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
- package/template/apps/web/src/test/setup.ts +8 -0
- package/template/apps/web/tailwind.config.ts +5 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/vitest.config.ts +32 -0
- package/template/assets/logo-512.png +0 -0
- package/template/assets/logo.svg +4 -0
- package/template/docker-compose.prod.yml +66 -0
- package/template/docker-compose.yml +36 -0
- package/template/eslint.config.ts +119 -0
- package/template/package.json +77 -0
- package/template/packages/tailwind-config/package.json +9 -0
- package/template/packages/tailwind-config/theme.css +179 -0
- package/template/packages/types/package.json +29 -0
- package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
- package/template/packages/types/src/api-response.ts +53 -0
- package/template/packages/types/src/health-check.ts +11 -0
- package/template/packages/types/src/pagination.ts +41 -0
- package/template/packages/types/src/role.ts +5 -0
- package/template/packages/types/src/session.ts +48 -0
- package/template/packages/types/src/stats.ts +113 -0
- package/template/packages/types/src/upload.ts +51 -0
- package/template/packages/types/src/user.ts +36 -0
- package/template/packages/types/tsconfig.json +5 -0
- package/template/packages/types/vitest.config.ts +21 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +108 -0
- package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
- package/template/packages/ui/src/alert-dialog.tsx +141 -0
- package/template/packages/ui/src/alert.tsx +66 -0
- package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
- package/template/packages/ui/src/avatar.tsx +53 -0
- package/template/packages/ui/src/badge.tsx +36 -0
- package/template/packages/ui/src/button.tsx +84 -0
- package/template/packages/ui/src/card.tsx +92 -0
- package/template/packages/ui/src/checkbox.tsx +32 -0
- package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
- package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
- package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
- package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
- package/template/packages/ui/src/data-table/data-table.tsx +167 -0
- package/template/packages/ui/src/dialog.tsx +143 -0
- package/template/packages/ui/src/dropdown-menu.tsx +257 -0
- package/template/packages/ui/src/empty-state.tsx +52 -0
- package/template/packages/ui/src/file-upload-input.tsx +202 -0
- package/template/packages/ui/src/form.tsx +168 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
- package/template/packages/ui/src/input.tsx +21 -0
- package/template/packages/ui/src/label.tsx +24 -0
- package/template/packages/ui/src/lib/utils.ts +6 -0
- package/template/packages/ui/src/password-input.tsx +102 -0
- package/template/packages/ui/src/popover.tsx +48 -0
- package/template/packages/ui/src/radio-group.tsx +45 -0
- package/template/packages/ui/src/scroll-area.tsx +58 -0
- package/template/packages/ui/src/select.tsx +187 -0
- package/template/packages/ui/src/separator.tsx +28 -0
- package/template/packages/ui/src/sheet.tsx +139 -0
- package/template/packages/ui/src/sidebar.tsx +726 -0
- package/template/packages/ui/src/skeleton-variants.tsx +87 -0
- package/template/packages/ui/src/skeleton.tsx +13 -0
- package/template/packages/ui/src/slider.tsx +63 -0
- package/template/packages/ui/src/sonner.tsx +25 -0
- package/template/packages/ui/src/spinner.tsx +16 -0
- package/template/packages/ui/src/switch.tsx +31 -0
- package/template/packages/ui/src/table.tsx +116 -0
- package/template/packages/ui/src/tabs.tsx +66 -0
- package/template/packages/ui/src/textarea.tsx +18 -0
- package/template/packages/ui/src/tooltip.tsx +61 -0
- package/template/packages/ui/src/user-avatar.tsx +97 -0
- package/template/packages/ui/test-config.js +3 -0
- package/template/packages/ui/tsconfig.json +12 -0
- package/template/packages/ui/turbo.json +18 -0
- package/template/packages/ui/vitest.config.ts +17 -0
- package/template/packages/ui/vitest.setup.ts +1 -0
- package/template/packages/utils/package.json +23 -0
- package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
- package/template/packages/utils/src/array.ts +18 -0
- package/template/packages/utils/src/async.ts +3 -0
- package/template/packages/utils/src/date.ts +77 -0
- package/template/packages/utils/src/errors.ts +73 -0
- package/template/packages/utils/src/number.ts +11 -0
- package/template/packages/utils/src/string.ts +13 -0
- package/template/packages/utils/tsconfig.json +5 -0
- package/template/packages/utils/vitest.config.ts +21 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.base.json +32 -0
- package/template/turbo.json +133 -0
- package/template/vitest.shared.ts +26 -0
- package/template/vitest.workspace.ts +9 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@repo/packages-ui/button';
|
|
4
|
+
import { Skeleton } from '@repo/packages-ui/skeleton';
|
|
5
|
+
import { UserAvatar } from '@repo/packages-ui/user-avatar';
|
|
6
|
+
import {
|
|
7
|
+
formatDateTime,
|
|
8
|
+
formatShortDate,
|
|
9
|
+
getRelativeTime,
|
|
10
|
+
} from '@repo/packages-utils/date';
|
|
11
|
+
import {
|
|
12
|
+
Calendar,
|
|
13
|
+
Clock,
|
|
14
|
+
KeyRound,
|
|
15
|
+
LogOut,
|
|
16
|
+
Mail,
|
|
17
|
+
ShieldCheck,
|
|
18
|
+
ShieldUser,
|
|
19
|
+
User,
|
|
20
|
+
} from 'lucide-react';
|
|
21
|
+
import Link from 'next/link';
|
|
22
|
+
import { useRouter } from 'next/navigation';
|
|
23
|
+
import React from 'react';
|
|
24
|
+
|
|
25
|
+
import { EmailVerificationBanner } from '@/components/auth/email-verification-banner';
|
|
26
|
+
import { ProtectedRoute } from '@/components/auth/protected-route';
|
|
27
|
+
import { authClient } from '@/lib/auth';
|
|
28
|
+
|
|
29
|
+
function DashboardContent() {
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
const { data: session } = authClient.useSession();
|
|
32
|
+
|
|
33
|
+
const handleSignOut = async () => {
|
|
34
|
+
await authClient.signOut();
|
|
35
|
+
router.push('/');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (!session) return null;
|
|
39
|
+
|
|
40
|
+
const accountCreatedDate = session.user.createdAt
|
|
41
|
+
? formatShortDate(session.user.createdAt)
|
|
42
|
+
: 'N/A';
|
|
43
|
+
|
|
44
|
+
const lastLoginDate = session.session.createdAt
|
|
45
|
+
? getRelativeTime(session.session.createdAt, { alwaysShowTime: true })
|
|
46
|
+
: 'N/A';
|
|
47
|
+
|
|
48
|
+
const sessionExpiresDate = session.session.expiresAt
|
|
49
|
+
? formatDateTime(session.session.expiresAt)
|
|
50
|
+
: 'N/A';
|
|
51
|
+
|
|
52
|
+
const isEmailVerified = session.user.emailVerified;
|
|
53
|
+
const isAdmin =
|
|
54
|
+
session.user.role === 'admin' || session.user.role === 'super_admin';
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="container mx-auto max-w-6xl space-y-6 p-8">
|
|
58
|
+
<div className="mb-4">
|
|
59
|
+
<EmailVerificationBanner />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="flex items-center gap-4">
|
|
63
|
+
<UserAvatar
|
|
64
|
+
src={session.user.image}
|
|
65
|
+
name={session.user.name}
|
|
66
|
+
email={session.user.email}
|
|
67
|
+
/>
|
|
68
|
+
<div>
|
|
69
|
+
<h1 className="text-3xl font-bold">
|
|
70
|
+
Welcome back, {session.user.name || 'User'}!
|
|
71
|
+
</h1>
|
|
72
|
+
<p className="text-muted-foreground">
|
|
73
|
+
Here's what's happening with your account
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
79
|
+
<div className="border-border bg-card rounded-lg border p-6">
|
|
80
|
+
<div className="flex items-center gap-3">
|
|
81
|
+
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
82
|
+
<Calendar className="h-5 w-5" />
|
|
83
|
+
</div>
|
|
84
|
+
<div>
|
|
85
|
+
<p className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
|
86
|
+
Account Created
|
|
87
|
+
</p>
|
|
88
|
+
<p className="text-lg font-semibold">{accountCreatedDate}</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="border-border bg-card rounded-lg border p-6">
|
|
94
|
+
<div className="flex items-center gap-3">
|
|
95
|
+
<div className="bg-primary/10 text-primary rounded-full p-3">
|
|
96
|
+
<Clock className="h-5 w-5" />
|
|
97
|
+
</div>
|
|
98
|
+
<div>
|
|
99
|
+
<p className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
|
100
|
+
Last Login
|
|
101
|
+
</p>
|
|
102
|
+
<p className="text-lg font-semibold">{lastLoginDate}</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="border-border bg-card rounded-lg border p-6">
|
|
108
|
+
<div className="flex items-center gap-3">
|
|
109
|
+
<div
|
|
110
|
+
className={`rounded-full p-3 ${
|
|
111
|
+
isEmailVerified
|
|
112
|
+
? 'bg-green-500/10 text-green-600 dark:text-green-500'
|
|
113
|
+
: 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-500'
|
|
114
|
+
}`}
|
|
115
|
+
>
|
|
116
|
+
<ShieldCheck className="h-5 w-5" />
|
|
117
|
+
</div>
|
|
118
|
+
<div>
|
|
119
|
+
<p className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
|
120
|
+
Email Status
|
|
121
|
+
</p>
|
|
122
|
+
<p className="text-lg font-semibold">
|
|
123
|
+
{isEmailVerified ? 'Verified' : 'Not Verified'}
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
131
|
+
<div className="border-border bg-card rounded-lg border p-8">
|
|
132
|
+
<div className="mb-4 flex items-center gap-2">
|
|
133
|
+
<User className="text-primary h-5 w-5" />
|
|
134
|
+
<h2 className="text-lg font-semibold">User Information</h2>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="space-y-4">
|
|
137
|
+
<div className="flex items-start gap-3">
|
|
138
|
+
<Mail className="text-muted-foreground mt-0.5 h-4 w-4" />
|
|
139
|
+
<div className="flex-1">
|
|
140
|
+
<p className="text-muted-foreground text-xs font-medium">
|
|
141
|
+
Email Address
|
|
142
|
+
</p>
|
|
143
|
+
<p className="text-sm font-medium">{session.user.email}</p>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex items-start gap-3">
|
|
147
|
+
<User className="text-muted-foreground mt-0.5 h-4 w-4" />
|
|
148
|
+
<div className="flex-1">
|
|
149
|
+
<p className="text-muted-foreground text-xs font-medium">
|
|
150
|
+
Display Name
|
|
151
|
+
</p>
|
|
152
|
+
<p className="text-sm font-medium">
|
|
153
|
+
{session.user.name || (
|
|
154
|
+
<span className="text-muted-foreground italic">
|
|
155
|
+
Not set
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="flex items-start gap-3">
|
|
162
|
+
<ShieldUser className="text-muted-foreground mt-0.5 h-4 w-4" />
|
|
163
|
+
<div className="flex-1">
|
|
164
|
+
<p className="text-muted-foreground text-xs font-medium">
|
|
165
|
+
Role
|
|
166
|
+
</p>
|
|
167
|
+
<p className="text-sm font-medium capitalize">
|
|
168
|
+
{session.user.role || 'User'}
|
|
169
|
+
</p>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div className="border-border bg-card rounded-lg border p-8">
|
|
176
|
+
<div className="mb-4 flex items-center gap-2">
|
|
177
|
+
<Clock className="text-primary h-5 w-5" />
|
|
178
|
+
<h2 className="text-lg font-semibold">Session Details</h2>
|
|
179
|
+
</div>
|
|
180
|
+
<div className="space-y-4">
|
|
181
|
+
<div className="flex items-start gap-3">
|
|
182
|
+
<Calendar className="text-muted-foreground mt-0.5 h-4 w-4" />
|
|
183
|
+
<div className="flex-1">
|
|
184
|
+
<p className="text-muted-foreground text-xs font-medium">
|
|
185
|
+
Session Created
|
|
186
|
+
</p>
|
|
187
|
+
<p className="text-sm font-medium">
|
|
188
|
+
{formatDateTime(session.session.createdAt)}
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
<div className="flex items-start gap-3">
|
|
193
|
+
<Clock className="text-muted-foreground mt-0.5 h-4 w-4" />
|
|
194
|
+
<div className="flex-1">
|
|
195
|
+
<p className="text-muted-foreground text-xs font-medium">
|
|
196
|
+
Session Expires
|
|
197
|
+
</p>
|
|
198
|
+
<p className="text-sm font-medium">{sessionExpiresDate}</p>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className="border-border bg-card rounded-lg border p-8">
|
|
206
|
+
<h2 className="mb-4 text-lg font-semibold">Quick Actions</h2>
|
|
207
|
+
<div className="flex flex-wrap gap-3">
|
|
208
|
+
{isAdmin && (
|
|
209
|
+
<Link href="/admin">
|
|
210
|
+
<Button variant="default" className="gap-2">
|
|
211
|
+
<ShieldCheck className="h-4 w-4" />
|
|
212
|
+
Admin Dashboard
|
|
213
|
+
</Button>
|
|
214
|
+
</Link>
|
|
215
|
+
)}
|
|
216
|
+
<Link href="/dashboard/change-password">
|
|
217
|
+
<Button variant="default" className="gap-2">
|
|
218
|
+
<KeyRound className="h-4 w-4" />
|
|
219
|
+
Change Password
|
|
220
|
+
</Button>
|
|
221
|
+
</Link>
|
|
222
|
+
<Button onClick={handleSignOut} variant="outline" className="gap-2">
|
|
223
|
+
<LogOut className="h-4 w-4" />
|
|
224
|
+
Sign Out
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function DashboardSkeleton() {
|
|
233
|
+
return (
|
|
234
|
+
<div className="container mx-auto max-w-6xl space-y-6 p-8">
|
|
235
|
+
<div className="flex items-center gap-4">
|
|
236
|
+
<Skeleton className="h-16 w-16 rounded-full" />
|
|
237
|
+
<div className="space-y-2">
|
|
238
|
+
<Skeleton className="h-8 w-64" />
|
|
239
|
+
<Skeleton className="h-4 w-80" />
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
244
|
+
{[...Array(3)].map((_, i) => (
|
|
245
|
+
<div key={i} className="border-border bg-card rounded-lg border p-6">
|
|
246
|
+
<div className="flex items-center gap-3">
|
|
247
|
+
<Skeleton className="h-11 w-11 rounded-full" />
|
|
248
|
+
<div className="space-y-2">
|
|
249
|
+
<Skeleton className="h-3 w-24" />
|
|
250
|
+
<Skeleton className="h-5 w-32" />
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
258
|
+
{[...Array(2)].map((_, i) => (
|
|
259
|
+
<div key={i} className="border-border bg-card rounded-lg border p-8">
|
|
260
|
+
<div className="mb-4 flex items-center gap-2">
|
|
261
|
+
<Skeleton className="h-5 w-5 rounded" />
|
|
262
|
+
<Skeleton className="h-6 w-40" />
|
|
263
|
+
</div>
|
|
264
|
+
<div className="space-y-4">
|
|
265
|
+
{[...Array(3)].map((_, j) => (
|
|
266
|
+
<div key={j} className="flex items-start gap-3">
|
|
267
|
+
<Skeleton className="mt-0.5 h-4 w-4 rounded" />
|
|
268
|
+
<div className="flex-1 space-y-2">
|
|
269
|
+
<Skeleton className="h-3 w-24" />
|
|
270
|
+
<Skeleton className="h-4 w-full" />
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="border-border bg-card rounded-lg border p-8">
|
|
280
|
+
<Skeleton className="mb-4 h-6 w-32" />
|
|
281
|
+
<div className="flex flex-wrap gap-3">
|
|
282
|
+
<Skeleton className="h-10 w-40 rounded-md" />
|
|
283
|
+
<Skeleton className="h-10 w-28 rounded-md" />
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export default function DashboardPage() {
|
|
291
|
+
return (
|
|
292
|
+
<ProtectedRoute redirectTo="/login" fallback={<DashboardSkeleton />}>
|
|
293
|
+
<DashboardContent />
|
|
294
|
+
</ProtectedRoute>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@repo/packages-ui/button';
|
|
4
|
+
import { ServerCrash } from 'lucide-react';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
|
|
7
|
+
import { ErrorCard } from '@/components/error/error-card';
|
|
8
|
+
|
|
9
|
+
interface ErrorProps {
|
|
10
|
+
error: Error & { digest?: string };
|
|
11
|
+
reset: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function Error({ reset }: ErrorProps) {
|
|
15
|
+
return (
|
|
16
|
+
<ErrorCard
|
|
17
|
+
icon={ServerCrash}
|
|
18
|
+
title="Something went wrong"
|
|
19
|
+
description="An unexpected error occurred. Please try again or return to the home page."
|
|
20
|
+
actions={
|
|
21
|
+
<>
|
|
22
|
+
<Button variant="outline" onClick={reset}>
|
|
23
|
+
Try again
|
|
24
|
+
</Button>
|
|
25
|
+
<Button asChild>
|
|
26
|
+
<Link href="/">Go home</Link>
|
|
27
|
+
</Button>
|
|
28
|
+
</>
|
|
29
|
+
}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Alert } from '@repo/packages-ui/alert';
|
|
4
|
+
import { Button } from '@repo/packages-ui/button';
|
|
5
|
+
import { FileUploadInput } from '@repo/packages-ui/file-upload-input';
|
|
6
|
+
import { FileText, Image, Loader2, Trash2 } from 'lucide-react';
|
|
7
|
+
import { type Upload } from 'packages/types/src/upload';
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
useDeleteUpload,
|
|
12
|
+
useFetchUploads,
|
|
13
|
+
useFetchUploadStats,
|
|
14
|
+
useUploadFile,
|
|
15
|
+
} from '@/hooks/api/use-uploads';
|
|
16
|
+
|
|
17
|
+
export default function FileUploadExamplePage() {
|
|
18
|
+
const [selectedFile, setSelectedFile] = useState<globalThis.File | null>(
|
|
19
|
+
null
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const { data: uploads, isLoading: uploadsLoading } = useFetchUploads();
|
|
23
|
+
const { data: stats } = useFetchUploadStats();
|
|
24
|
+
const uploadMutation = useUploadFile();
|
|
25
|
+
const deleteMutation = useDeleteUpload();
|
|
26
|
+
|
|
27
|
+
const handleFileSelect = (file: globalThis.File) => {
|
|
28
|
+
setSelectedFile(file);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleUpload = () => {
|
|
32
|
+
if (!selectedFile) return;
|
|
33
|
+
|
|
34
|
+
uploadMutation.mutate(selectedFile, {
|
|
35
|
+
onSuccess: () => {
|
|
36
|
+
setSelectedFile(null);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleDelete = (uploadId: string) => {
|
|
42
|
+
if (globalThis.confirm('Are you sure you want to delete this file?')) {
|
|
43
|
+
deleteMutation.mutate({ id: uploadId });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getFileIcon = (mimeType: string) => {
|
|
48
|
+
if (mimeType.startsWith('image/')) {
|
|
49
|
+
return <Image className="text-primary h-8 w-8" />;
|
|
50
|
+
}
|
|
51
|
+
return <FileText className="text-primary h-8 w-8" />;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const formatFileSize = (bytes: number) => {
|
|
55
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
56
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
57
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const formatDate = (date: Date | string) => {
|
|
61
|
+
return new Date(date).toLocaleDateString('en-US', {
|
|
62
|
+
year: 'numeric',
|
|
63
|
+
month: 'short',
|
|
64
|
+
day: 'numeric',
|
|
65
|
+
hour: '2-digit',
|
|
66
|
+
minute: '2-digit',
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="container mx-auto max-w-5xl px-4 py-12">
|
|
72
|
+
<div className="space-y-8">
|
|
73
|
+
{/* Header */}
|
|
74
|
+
<div>
|
|
75
|
+
<h1 className="text-3xl font-bold tracking-tight">File Upload</h1>
|
|
76
|
+
<p className="text-muted-foreground mt-2">
|
|
77
|
+
Upload images, PDFs, and documents up to 10MB
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Upload Stats */}
|
|
82
|
+
{stats && (
|
|
83
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
84
|
+
<div className="bg-card rounded-lg border p-6">
|
|
85
|
+
<div className="text-muted-foreground text-sm font-medium">
|
|
86
|
+
Total Files
|
|
87
|
+
</div>
|
|
88
|
+
<div className="mt-2 text-3xl font-bold">{stats.totalFiles}</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="bg-card rounded-lg border p-6">
|
|
91
|
+
<div className="text-muted-foreground text-sm font-medium">
|
|
92
|
+
Total Size
|
|
93
|
+
</div>
|
|
94
|
+
<div className="mt-2 text-3xl font-bold">
|
|
95
|
+
{formatFileSize(stats.totalSize)}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{/* Upload Section */}
|
|
102
|
+
<div className="bg-card space-y-4 rounded-lg border p-6">
|
|
103
|
+
<h2 className="text-lg font-semibold">Upload File</h2>
|
|
104
|
+
|
|
105
|
+
<FileUploadInput
|
|
106
|
+
onFileSelect={handleFileSelect}
|
|
107
|
+
disabled={uploadMutation.isPending}
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
{selectedFile && (
|
|
111
|
+
<div className="flex justify-end gap-2">
|
|
112
|
+
<Button
|
|
113
|
+
variant="outline"
|
|
114
|
+
onClick={() => setSelectedFile(null)}
|
|
115
|
+
disabled={uploadMutation.isPending}
|
|
116
|
+
>
|
|
117
|
+
Cancel
|
|
118
|
+
</Button>
|
|
119
|
+
<Button
|
|
120
|
+
onClick={handleUpload}
|
|
121
|
+
disabled={uploadMutation.isPending}
|
|
122
|
+
>
|
|
123
|
+
{uploadMutation.isPending && (
|
|
124
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
125
|
+
)}
|
|
126
|
+
Upload
|
|
127
|
+
</Button>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{uploadMutation.isError && (
|
|
132
|
+
<Alert variant="destructive">
|
|
133
|
+
{uploadMutation.error?.message || 'Upload failed'}
|
|
134
|
+
</Alert>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{uploadMutation.isSuccess && (
|
|
138
|
+
<Alert>File uploaded successfully!</Alert>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Uploads List */}
|
|
143
|
+
<div className="bg-card space-y-4 rounded-lg border p-6">
|
|
144
|
+
<h2 className="text-lg font-semibold">Your Uploads</h2>
|
|
145
|
+
|
|
146
|
+
{uploadsLoading ? (
|
|
147
|
+
<div className="flex justify-center py-8">
|
|
148
|
+
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
|
|
149
|
+
</div>
|
|
150
|
+
) : uploads && uploads.length > 0 ? (
|
|
151
|
+
<div className="space-y-3">
|
|
152
|
+
{uploads.map((upload: Upload) => (
|
|
153
|
+
<div
|
|
154
|
+
key={upload.id}
|
|
155
|
+
className="hover:bg-accent/50 flex items-center justify-between rounded-lg border p-4 transition-colors"
|
|
156
|
+
>
|
|
157
|
+
<div className="flex min-w-0 flex-1 items-center gap-4">
|
|
158
|
+
{getFileIcon(upload.mimeType)}
|
|
159
|
+
<div className="min-w-0 flex-1">
|
|
160
|
+
<p className="truncate font-medium">
|
|
161
|
+
{upload.originalName}
|
|
162
|
+
</p>
|
|
163
|
+
<div className="text-muted-foreground mt-1 flex items-center gap-3 text-sm">
|
|
164
|
+
<span>{formatFileSize(upload.size)}</span>
|
|
165
|
+
<span>•</span>
|
|
166
|
+
<span>{formatDate(upload.createdAt)}</span>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="flex items-center gap-2">
|
|
171
|
+
<Button
|
|
172
|
+
variant="outline"
|
|
173
|
+
size="sm"
|
|
174
|
+
onClick={() => window.open(upload.url, '_blank')}
|
|
175
|
+
>
|
|
176
|
+
View
|
|
177
|
+
</Button>
|
|
178
|
+
<Button
|
|
179
|
+
variant="ghost"
|
|
180
|
+
size="icon"
|
|
181
|
+
onClick={() => handleDelete(upload.id)}
|
|
182
|
+
disabled={deleteMutation.isPending}
|
|
183
|
+
>
|
|
184
|
+
<Trash2 className="text-destructive h-4 w-4" />
|
|
185
|
+
</Button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
))}
|
|
189
|
+
</div>
|
|
190
|
+
) : (
|
|
191
|
+
<div className="text-muted-foreground py-12 text-center">
|
|
192
|
+
<FileText className="mx-auto mb-4 h-12 w-12 opacity-50" />
|
|
193
|
+
<p>No files uploaded yet</p>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface GlobalErrorProps {
|
|
6
|
+
error: Error & { digest?: string };
|
|
7
|
+
reset: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function GlobalError({ reset }: GlobalErrorProps) {
|
|
11
|
+
return (
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<body>
|
|
14
|
+
<div
|
|
15
|
+
style={{
|
|
16
|
+
display: 'flex',
|
|
17
|
+
minHeight: '100vh',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
justifyContent: 'center',
|
|
20
|
+
padding: '2rem',
|
|
21
|
+
fontFamily: 'system-ui, sans-serif',
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<div
|
|
25
|
+
style={{
|
|
26
|
+
maxWidth: '28rem',
|
|
27
|
+
width: '100%',
|
|
28
|
+
padding: '2rem',
|
|
29
|
+
border: '1px solid #e5e5e5',
|
|
30
|
+
borderRadius: '0.5rem',
|
|
31
|
+
backgroundColor: '#ffffff',
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<div
|
|
35
|
+
style={{
|
|
36
|
+
display: 'flex',
|
|
37
|
+
flexDirection: 'column',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
textAlign: 'center',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
marginBottom: '1rem',
|
|
45
|
+
padding: '1rem',
|
|
46
|
+
borderRadius: '9999px',
|
|
47
|
+
backgroundColor: '#f5f5f5',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<AlertTriangle
|
|
51
|
+
style={{ width: '2rem', height: '2rem', color: '#737373' }}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
<h1
|
|
55
|
+
style={{
|
|
56
|
+
marginBottom: '0.5rem',
|
|
57
|
+
fontSize: '1.5rem',
|
|
58
|
+
fontWeight: '600',
|
|
59
|
+
color: '#171717',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
Critical error
|
|
63
|
+
</h1>
|
|
64
|
+
<p
|
|
65
|
+
style={{
|
|
66
|
+
marginBottom: '1.5rem',
|
|
67
|
+
fontSize: '0.875rem',
|
|
68
|
+
lineHeight: '1.5',
|
|
69
|
+
color: '#737373',
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
A critical error occurred. Please refresh the page or contact
|
|
73
|
+
support if the problem persists.
|
|
74
|
+
</p>
|
|
75
|
+
<button
|
|
76
|
+
onClick={reset}
|
|
77
|
+
style={{
|
|
78
|
+
padding: '0.5rem 1rem',
|
|
79
|
+
fontSize: '0.875rem',
|
|
80
|
+
fontWeight: '500',
|
|
81
|
+
color: '#ffffff',
|
|
82
|
+
backgroundColor: '#171717',
|
|
83
|
+
border: 'none',
|
|
84
|
+
borderRadius: '0.375rem',
|
|
85
|
+
cursor: 'pointer',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
Try again
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import 'tw-animate-css';
|
|
3
|
+
@import '@repo/tailwind-config/theme.css';
|
|
4
|
+
|
|
5
|
+
@config '../../tailwind.config.ts';
|
|
6
|
+
|
|
7
|
+
@source '../../../../packages/ui/src/**/*.{ts,tsx}';
|
|
8
|
+
|
|
9
|
+
@plugin "tailwindcss-debug-screens" {
|
|
10
|
+
classname: 'debug-screens';
|
|
11
|
+
position: 'bottom, right';
|
|
12
|
+
prefix: 'screen: ';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
::view-transition-old(root) {
|
|
16
|
+
animation: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
::view-transition-new(root) {
|
|
20
|
+
animation: none;
|
|
21
|
+
mix-blend-mode: normal;
|
|
22
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import './globals.css';
|
|
2
|
+
|
|
3
|
+
import { Toaster } from '@repo/packages-ui/sonner';
|
|
4
|
+
import type { Metadata } from 'next';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { Footer } from '@/components/layout/footer';
|
|
8
|
+
import { isDevelopment } from '@/lib/env';
|
|
9
|
+
import { Providers } from '@/providers';
|
|
10
|
+
|
|
11
|
+
export const metadata: Metadata = {
|
|
12
|
+
title: 'Blitzpack',
|
|
13
|
+
description: 'Production-ready TypeScript monorepo',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function RootLayout({
|
|
17
|
+
children,
|
|
18
|
+
}: Readonly<{
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}>) {
|
|
21
|
+
return (
|
|
22
|
+
<html lang="en" suppressHydrationWarning>
|
|
23
|
+
<body
|
|
24
|
+
className={`flex min-h-screen flex-col ${isDevelopment() ? 'debug-screens' : ''}`}
|
|
25
|
+
>
|
|
26
|
+
<Providers>
|
|
27
|
+
{children}
|
|
28
|
+
<Footer />
|
|
29
|
+
<Toaster />
|
|
30
|
+
</Providers>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
);
|
|
34
|
+
}
|