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,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@repo/packages-ui/button';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { authClient } from '@/lib/auth';
|
|
7
|
+
|
|
8
|
+
export function GoogleButton() {
|
|
9
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
10
|
+
|
|
11
|
+
const handleGoogleSignIn = async () => {
|
|
12
|
+
setIsLoading(true);
|
|
13
|
+
await authClient.signIn.social({
|
|
14
|
+
provider: 'google',
|
|
15
|
+
callbackURL: `${window.location.origin}/dashboard`,
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Button
|
|
21
|
+
type="button"
|
|
22
|
+
variant="outline"
|
|
23
|
+
className="w-full"
|
|
24
|
+
onClick={handleGoogleSignIn}
|
|
25
|
+
isLoading={isLoading}
|
|
26
|
+
>
|
|
27
|
+
{!isLoading && (
|
|
28
|
+
<svg
|
|
29
|
+
className="mr-2 h-5 w-5"
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
>
|
|
33
|
+
<path
|
|
34
|
+
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"
|
|
35
|
+
fill="#4285F4"
|
|
36
|
+
/>
|
|
37
|
+
<path
|
|
38
|
+
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"
|
|
39
|
+
fill="#34A853"
|
|
40
|
+
/>
|
|
41
|
+
<path
|
|
42
|
+
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"
|
|
43
|
+
fill="#FBBC05"
|
|
44
|
+
/>
|
|
45
|
+
<path
|
|
46
|
+
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"
|
|
47
|
+
fill="#EA4335"
|
|
48
|
+
/>
|
|
49
|
+
</svg>
|
|
50
|
+
)}
|
|
51
|
+
Continue with Google
|
|
52
|
+
</Button>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Skeleton } from '@repo/packages-ui/skeleton';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { type ReactNode, useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Forbidden } from '@/components/error/forbidden';
|
|
8
|
+
import { authClient } from '@/lib/auth';
|
|
9
|
+
|
|
10
|
+
interface ProtectedRouteProps {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
redirectTo?: string;
|
|
13
|
+
fallback?: ReactNode;
|
|
14
|
+
requiredRole?: 'admin' | 'super_admin';
|
|
15
|
+
loadingMessage?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ProtectedRoute({
|
|
19
|
+
children,
|
|
20
|
+
redirectTo = '/login',
|
|
21
|
+
fallback,
|
|
22
|
+
requiredRole,
|
|
23
|
+
loadingMessage,
|
|
24
|
+
}: ProtectedRouteProps) {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
const { data: session, isPending } = authClient.useSession();
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!isPending && !session) {
|
|
30
|
+
router.push(redirectTo);
|
|
31
|
+
}
|
|
32
|
+
}, [session, isPending, router, redirectTo]);
|
|
33
|
+
|
|
34
|
+
if (isPending) {
|
|
35
|
+
return (
|
|
36
|
+
fallback || (
|
|
37
|
+
<div className="container mx-auto max-w-4xl p-8">
|
|
38
|
+
<div className="bg-card rounded-lg border p-6 shadow-sm">
|
|
39
|
+
<div className="mb-4 space-y-2">
|
|
40
|
+
<Skeleton className="h-8 w-48" />
|
|
41
|
+
<Skeleton className="h-4 w-64" />
|
|
42
|
+
</div>
|
|
43
|
+
<div className="space-y-4">
|
|
44
|
+
<Skeleton className="h-4 w-full" />
|
|
45
|
+
<Skeleton className="h-4 w-full" />
|
|
46
|
+
<Skeleton className="h-10 w-24" />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!session) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (requiredRole) {
|
|
59
|
+
const userRole = (session.user as { role?: string }).role;
|
|
60
|
+
if (userRole !== requiredRole && userRole !== 'super_admin') {
|
|
61
|
+
return <Forbidden />;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return <>{children}</>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRouter } from 'next/navigation';
|
|
4
|
+
import { type ReactNode, useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
import { authClient } from '@/lib/auth';
|
|
7
|
+
|
|
8
|
+
interface RedirectIfAuthenticatedProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
redirectTo?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function RedirectIfAuthenticated({
|
|
14
|
+
children,
|
|
15
|
+
redirectTo = '/dashboard',
|
|
16
|
+
}: RedirectIfAuthenticatedProps) {
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const { data: session, isPending } = authClient.useSession();
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!isPending && session) {
|
|
22
|
+
router.replace(redirectTo);
|
|
23
|
+
}
|
|
24
|
+
}, [session, isPending, router, redirectTo]);
|
|
25
|
+
|
|
26
|
+
if (isPending || session) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return <>{children}</>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { ProtectedRoute } from '@/components/auth/protected-route';
|
|
6
|
+
|
|
7
|
+
interface WithAuthOptions {
|
|
8
|
+
redirectTo?: string;
|
|
9
|
+
fallback?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function withAuth<P extends object>(
|
|
13
|
+
Component: ComponentType<P>,
|
|
14
|
+
options: WithAuthOptions = {}
|
|
15
|
+
) {
|
|
16
|
+
const WrappedComponent = (props: P) => {
|
|
17
|
+
return (
|
|
18
|
+
<ProtectedRoute
|
|
19
|
+
redirectTo={options.redirectTo}
|
|
20
|
+
fallback={options.fallback}
|
|
21
|
+
>
|
|
22
|
+
<Component {...props} />
|
|
23
|
+
</ProtectedRoute>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
WrappedComponent.displayName = `withAuth(${Component.displayName || Component.name || 'Component'})`;
|
|
28
|
+
|
|
29
|
+
return WrappedComponent;
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import type { LucideIcon } from 'lucide-react';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
interface ErrorCardProps {
|
|
8
|
+
icon: LucideIcon;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ErrorCard({
|
|
15
|
+
icon: Icon,
|
|
16
|
+
title,
|
|
17
|
+
description,
|
|
18
|
+
actions,
|
|
19
|
+
}: ErrorCardProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex min-h-[60vh] items-center justify-center p-8">
|
|
22
|
+
<motion.div
|
|
23
|
+
className="border-border bg-card w-full max-w-md rounded-lg border p-8"
|
|
24
|
+
initial={{ opacity: 0, y: 20 }}
|
|
25
|
+
animate={{ opacity: 1, y: 0 }}
|
|
26
|
+
transition={{ duration: 0.3, ease: 'easeOut' }}
|
|
27
|
+
>
|
|
28
|
+
<div className="flex flex-col items-center text-center">
|
|
29
|
+
<motion.div
|
|
30
|
+
className="bg-muted mb-4 rounded-full p-4"
|
|
31
|
+
whileHover={{ scale: 1.05 }}
|
|
32
|
+
transition={{ duration: 0.2 }}
|
|
33
|
+
>
|
|
34
|
+
<Icon className="text-muted-foreground h-8 w-8" />
|
|
35
|
+
</motion.div>
|
|
36
|
+
<h1 className="text-card-foreground mb-2 text-2xl font-semibold">
|
|
37
|
+
{title}
|
|
38
|
+
</h1>
|
|
39
|
+
<p className="text-muted-foreground mb-6 text-sm leading-relaxed">
|
|
40
|
+
{description}
|
|
41
|
+
</p>
|
|
42
|
+
{actions && <div className="flex flex-wrap gap-3">{actions}</div>}
|
|
43
|
+
</div>
|
|
44
|
+
</motion.div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Button } from '@repo/packages-ui/button';
|
|
2
|
+
import { ShieldAlert } from 'lucide-react';
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
|
|
5
|
+
import { ErrorCard } from '@/components/error/error-card';
|
|
6
|
+
|
|
7
|
+
export function Forbidden() {
|
|
8
|
+
return (
|
|
9
|
+
<ErrorCard
|
|
10
|
+
icon={ShieldAlert}
|
|
11
|
+
title="Access denied"
|
|
12
|
+
description="You don't have permission to access this page. Please contact an administrator if you believe this is an error."
|
|
13
|
+
actions={
|
|
14
|
+
<>
|
|
15
|
+
<Button asChild variant="outline">
|
|
16
|
+
<Link href="/dashboard">Go to dashboard</Link>
|
|
17
|
+
</Button>
|
|
18
|
+
<Button asChild>
|
|
19
|
+
<Link href="/">Go home</Link>
|
|
20
|
+
</Button>
|
|
21
|
+
</>
|
|
22
|
+
}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Tooltip,
|
|
5
|
+
TooltipContent,
|
|
6
|
+
TooltipTrigger,
|
|
7
|
+
} from '@repo/packages-ui/tooltip';
|
|
8
|
+
import { Check, Copy, Info } from 'lucide-react';
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
|
|
12
|
+
interface CommandBlockProps {
|
|
13
|
+
command: string;
|
|
14
|
+
tooltipContent?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CommandBlock({ command, tooltipContent }: CommandBlockProps) {
|
|
18
|
+
const [copied, setCopied] = useState(false);
|
|
19
|
+
|
|
20
|
+
const handleCopy = async () => {
|
|
21
|
+
await navigator.clipboard.writeText(command);
|
|
22
|
+
setCopied(true);
|
|
23
|
+
toast.success('Copied command to clipboard!');
|
|
24
|
+
setTimeout(() => setCopied(false), 5000);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="border-border bg-card text-card-foreground flex items-center justify-between gap-3 rounded-md border px-4 py-3">
|
|
29
|
+
<span className="font-mono text-sm">
|
|
30
|
+
<span className="text-muted-foreground">$</span> {command}
|
|
31
|
+
</span>
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
{tooltipContent && (
|
|
34
|
+
<Tooltip>
|
|
35
|
+
<TooltipTrigger asChild>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
className="text-muted-foreground hover:text-foreground cursor-pointer transition-colors"
|
|
39
|
+
aria-label="Command information"
|
|
40
|
+
>
|
|
41
|
+
<Info className="h-4 w-4" />
|
|
42
|
+
</button>
|
|
43
|
+
</TooltipTrigger>
|
|
44
|
+
<TooltipContent side="right" className="max-w-xs" sideOffset={8}>
|
|
45
|
+
{tooltipContent}
|
|
46
|
+
</TooltipContent>
|
|
47
|
+
</Tooltip>
|
|
48
|
+
)}
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={handleCopy}
|
|
52
|
+
className="text-muted-foreground hover:text-foreground cursor-pointer transition-colors"
|
|
53
|
+
aria-label={copied ? 'Copied!' : 'Copy command'}
|
|
54
|
+
>
|
|
55
|
+
{copied ? (
|
|
56
|
+
<Check className="h-4 w-4 text-emerald-500" />
|
|
57
|
+
) : (
|
|
58
|
+
<Copy className="h-4 w-4" />
|
|
59
|
+
)}
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import {
|
|
5
|
+
Bot,
|
|
6
|
+
Code2,
|
|
7
|
+
GitBranch,
|
|
8
|
+
Lock,
|
|
9
|
+
PackageOpen,
|
|
10
|
+
Rocket,
|
|
11
|
+
Shield,
|
|
12
|
+
TestTube,
|
|
13
|
+
Workflow,
|
|
14
|
+
Zap,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
|
|
17
|
+
const iconMap = {
|
|
18
|
+
Rocket,
|
|
19
|
+
Lock,
|
|
20
|
+
Zap,
|
|
21
|
+
Shield,
|
|
22
|
+
Workflow,
|
|
23
|
+
Code2,
|
|
24
|
+
TestTube,
|
|
25
|
+
PackageOpen,
|
|
26
|
+
GitBranch,
|
|
27
|
+
Bot,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
type IconName = keyof typeof iconMap;
|
|
31
|
+
|
|
32
|
+
interface FeatureCardProps {
|
|
33
|
+
iconName: IconName;
|
|
34
|
+
title: string;
|
|
35
|
+
description: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function FeatureCard({
|
|
39
|
+
iconName,
|
|
40
|
+
title,
|
|
41
|
+
description,
|
|
42
|
+
}: FeatureCardProps) {
|
|
43
|
+
const Icon = iconMap[iconName];
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<motion.div
|
|
47
|
+
className="border-border bg-card rounded-lg border p-6 transition-colors"
|
|
48
|
+
whileHover={{ y: -4 }}
|
|
49
|
+
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
50
|
+
>
|
|
51
|
+
<Icon className="text-foreground mb-3 h-5 w-5" />
|
|
52
|
+
<h3 className="text-card-foreground mb-2 text-base font-medium">
|
|
53
|
+
{title}
|
|
54
|
+
</h3>
|
|
55
|
+
<p className="text-muted-foreground text-sm leading-relaxed">
|
|
56
|
+
{description}
|
|
57
|
+
</p>
|
|
58
|
+
</motion.div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LayoutDashboard,
|
|
3
|
+
Lock,
|
|
4
|
+
Shield,
|
|
5
|
+
ShieldCheck,
|
|
6
|
+
Users,
|
|
7
|
+
} from 'lucide-react';
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
|
|
10
|
+
const iconMap = {
|
|
11
|
+
Lock,
|
|
12
|
+
Shield,
|
|
13
|
+
ShieldCheck,
|
|
14
|
+
LayoutDashboard,
|
|
15
|
+
Users,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
type IconName = keyof typeof iconMap;
|
|
19
|
+
|
|
20
|
+
interface Page {
|
|
21
|
+
name: string;
|
|
22
|
+
href: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface IncludedFeatureCardProps {
|
|
26
|
+
iconName: IconName;
|
|
27
|
+
title: string;
|
|
28
|
+
pages: readonly Page[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function IncludedFeatureCard({
|
|
32
|
+
iconName,
|
|
33
|
+
title,
|
|
34
|
+
pages,
|
|
35
|
+
}: IncludedFeatureCardProps) {
|
|
36
|
+
const Icon = iconMap[iconName];
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="bg-card rounded-lg border p-6">
|
|
40
|
+
<div className="mb-4">
|
|
41
|
+
<Icon className="text-foreground h-5 w-5" />
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<h3 className="text-card-foreground mb-4 text-base font-medium">
|
|
45
|
+
{title}
|
|
46
|
+
</h3>
|
|
47
|
+
|
|
48
|
+
<div className="flex flex-wrap gap-2">
|
|
49
|
+
{pages.map((page) => (
|
|
50
|
+
<Link
|
|
51
|
+
key={page.href}
|
|
52
|
+
href={page.href}
|
|
53
|
+
target="_blank"
|
|
54
|
+
rel="noopener noreferrer"
|
|
55
|
+
className="border-border hover:border-foreground/30 hover:bg-accent bg-background inline-flex items-center rounded-md border px-3 py-1.5 text-xs font-medium transition-colors"
|
|
56
|
+
>
|
|
57
|
+
{page.name}
|
|
58
|
+
</Link>
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from 'next-themes';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
interface LogoProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Logo({ className = 'h-16 w-auto' }: LogoProps) {
|
|
11
|
+
const { resolvedTheme } = useTheme();
|
|
12
|
+
const [mounted, setMounted] = useState(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
setMounted(true);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const fillPrimary =
|
|
19
|
+
mounted && resolvedTheme === 'dark' ? '#FFFFFF' : '#252525';
|
|
20
|
+
const fillSecondary =
|
|
21
|
+
mounted && resolvedTheme === 'dark' ? '#A3A3A3' : '#5A5A5A';
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<svg
|
|
25
|
+
className={className}
|
|
26
|
+
viewBox="0 0 348 225"
|
|
27
|
+
fill="none"
|
|
28
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
29
|
+
aria-label="Blitzpack Logo"
|
|
30
|
+
>
|
|
31
|
+
<path
|
|
32
|
+
d="M132.096 224.64V216.576C143.872 216.576 151.296 215.04 154.368 211.968L132.864 153.216H59.136L39.168 209.664C41.728 214.272 49.664 216.576 62.976 216.576V224.64H0V216.576C14.08 216.576 22.144 213.888 24.192 208.512L98.688 0H129.792L205.056 208.512C206.848 213.888 215.04 216.576 229.632 216.576V224.64H132.096ZM95.616 51.072L63.744 140.544H128.256L95.616 51.072Z"
|
|
33
|
+
fill={fillPrimary}
|
|
34
|
+
/>
|
|
35
|
+
<path
|
|
36
|
+
d="M187.81 80.64H219.76V208.59H235.81C240.26 208.59 244.035 207.015 247.135 203.865C250.285 200.715 251.86 196.99 251.86 192.69V80.64H283.81V208.59H299.86C304.21 208.59 307.935 207.015 311.035 203.865C314.185 200.715 315.76 196.99 315.76 192.69V80.64H347.86V169.965C347.86 184.865 342.485 197.715 331.735 208.515C321.035 219.265 308.185 224.64 293.185 224.64H251.86V219.54C245.71 222.94 238.135 224.64 229.135 224.64H187.81V80.64Z"
|
|
37
|
+
fill={fillSecondary}
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface TechBadgeProps {
|
|
2
|
+
children: React.ReactNode;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function TechBadge({ children }: TechBadgeProps) {
|
|
6
|
+
return (
|
|
7
|
+
<span className="border-border/60 hover:border-foreground/40 hover:bg-accent/50 inline-flex items-center rounded-md border bg-transparent px-3 py-1.5 text-xs font-normal transition-all duration-200 hover:scale-105">
|
|
8
|
+
{children}
|
|
9
|
+
</span>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@repo/packages-ui/button';
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuLabel,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from '@repo/packages-ui/dropdown-menu';
|
|
12
|
+
import { LogOut, User } from 'lucide-react';
|
|
13
|
+
import Link from 'next/link';
|
|
14
|
+
import { useRouter } from 'next/navigation';
|
|
15
|
+
|
|
16
|
+
import { authClient } from '@/lib/auth';
|
|
17
|
+
|
|
18
|
+
export function AuthNav() {
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
const { data: session } = authClient.useSession();
|
|
21
|
+
|
|
22
|
+
const handleSignOut = async () => {
|
|
23
|
+
await authClient.signOut();
|
|
24
|
+
router.push('/');
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (!session?.user) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<DropdownMenu>
|
|
33
|
+
<DropdownMenuTrigger asChild>
|
|
34
|
+
<Button variant="outline" className="flex items-center gap-2">
|
|
35
|
+
<User className="h-4 w-4" />
|
|
36
|
+
<span className="hidden sm:inline">
|
|
37
|
+
{session.user.name || session.user.email}
|
|
38
|
+
</span>
|
|
39
|
+
</Button>
|
|
40
|
+
</DropdownMenuTrigger>
|
|
41
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
42
|
+
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
43
|
+
<DropdownMenuSeparator />
|
|
44
|
+
<DropdownMenuItem asChild>
|
|
45
|
+
<Link href="/dashboard" className="cursor-pointer">
|
|
46
|
+
<User className="mr-2 h-4 w-4" />
|
|
47
|
+
Dashboard
|
|
48
|
+
</Link>
|
|
49
|
+
</DropdownMenuItem>
|
|
50
|
+
<DropdownMenuSeparator />
|
|
51
|
+
<DropdownMenuItem onClick={handleSignOut} className="cursor-pointer">
|
|
52
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
53
|
+
Sign Out
|
|
54
|
+
</DropdownMenuItem>
|
|
55
|
+
</DropdownMenuContent>
|
|
56
|
+
</DropdownMenu>
|
|
57
|
+
);
|
|
58
|
+
}
|