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,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@repo/packages-ui/button';
|
|
4
|
+
import { FileQuestion } from 'lucide-react';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { ErrorCard } from '@/components/error/error-card';
|
|
9
|
+
|
|
10
|
+
export default function NotFound() {
|
|
11
|
+
return (
|
|
12
|
+
<ErrorCard
|
|
13
|
+
icon={FileQuestion}
|
|
14
|
+
title="Page not found"
|
|
15
|
+
description="The page you're looking for doesn't exist or has been moved."
|
|
16
|
+
actions={
|
|
17
|
+
<>
|
|
18
|
+
<Button variant="outline" onClick={() => window.history.back()}>
|
|
19
|
+
Go back
|
|
20
|
+
</Button>
|
|
21
|
+
<Button asChild>
|
|
22
|
+
<Link href="/">Go home</Link>
|
|
23
|
+
</Button>
|
|
24
|
+
</>
|
|
25
|
+
}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { AnimatedThemeToggler } from '@repo/packages-ui/animated-theme-toggler';
|
|
2
|
+
import { Button } from '@repo/packages-ui/button';
|
|
3
|
+
import { GitHubIcon } from '@repo/packages-ui/icons/brand-icons';
|
|
4
|
+
import { ArrowRight, Boxes, PackageCheck, Terminal } from 'lucide-react';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { CommandBlock } from '@/components/landing/command-block';
|
|
9
|
+
import { FeatureCard } from '@/components/landing/feature-card';
|
|
10
|
+
import { IncludedFeatureCard } from '@/components/landing/included-feature-card';
|
|
11
|
+
import { Logo } from '@/components/landing/logo';
|
|
12
|
+
import { TechBadge } from '@/components/landing/tech-badge';
|
|
13
|
+
import {
|
|
14
|
+
features,
|
|
15
|
+
includedFeatures,
|
|
16
|
+
quickStartCommands,
|
|
17
|
+
techStack,
|
|
18
|
+
} from '@/config/landing-data';
|
|
19
|
+
import { siteConfig } from '@/config/site';
|
|
20
|
+
|
|
21
|
+
export default async function Home(): Promise<React.ReactElement> {
|
|
22
|
+
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
|
23
|
+
const isLocalDev =
|
|
24
|
+
apiUrl.includes('localhost') || apiUrl.includes('127.0.0.1');
|
|
25
|
+
|
|
26
|
+
const backendPort = new URL(apiUrl).port || '8080';
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<main className="bg-background relative flex flex-1 flex-col items-center justify-center p-8 pb-0">
|
|
30
|
+
<div className="absolute right-8 top-8 flex items-center gap-2">
|
|
31
|
+
<Button asChild variant="default">
|
|
32
|
+
<Link href="/login">Sign In</Link>
|
|
33
|
+
</Button>
|
|
34
|
+
<AnimatedThemeToggler />
|
|
35
|
+
</div>
|
|
36
|
+
<div className="w-full max-w-5xl space-y-12">
|
|
37
|
+
<div className="space-y-6 text-center">
|
|
38
|
+
<div className="flex justify-center">
|
|
39
|
+
<Logo className="h-20 w-auto" />
|
|
40
|
+
</div>
|
|
41
|
+
<div className="space-y-3">
|
|
42
|
+
<h1 className="text-foreground text-5xl font-semibold tracking-tight">
|
|
43
|
+
Ship production apps faster
|
|
44
|
+
</h1>
|
|
45
|
+
<p className="text-muted-foreground mx-auto max-w-2xl text-lg">
|
|
46
|
+
Production-ready full-stack TypeScript monorepo with Next.js,
|
|
47
|
+
Fastify, Turborepo and other modern technologies.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="flex flex-wrap items-center justify-center gap-3 pt-2">
|
|
51
|
+
<Button
|
|
52
|
+
asChild
|
|
53
|
+
size="lg"
|
|
54
|
+
className="group shadow-lg transition-all hover:scale-105 hover:shadow-xl"
|
|
55
|
+
>
|
|
56
|
+
<Link href="#quick-start" className="flex items-center gap-2">
|
|
57
|
+
Quick Start
|
|
58
|
+
<ArrowRight className="size-4 transition-transform group-hover:translate-x-1" />
|
|
59
|
+
</Link>
|
|
60
|
+
</Button>
|
|
61
|
+
<Button
|
|
62
|
+
asChild
|
|
63
|
+
size="lg"
|
|
64
|
+
variant="outline"
|
|
65
|
+
className="group transition-all hover:scale-105"
|
|
66
|
+
>
|
|
67
|
+
<a
|
|
68
|
+
href={siteConfig.github}
|
|
69
|
+
target="_blank"
|
|
70
|
+
rel="noopener noreferrer"
|
|
71
|
+
className="flex items-center gap-2"
|
|
72
|
+
>
|
|
73
|
+
<GitHubIcon className="size-4" />
|
|
74
|
+
GitHub
|
|
75
|
+
</a>
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
81
|
+
{features.map((feature) => (
|
|
82
|
+
<FeatureCard
|
|
83
|
+
key={feature.title}
|
|
84
|
+
iconName={feature.iconName}
|
|
85
|
+
title={feature.title}
|
|
86
|
+
description={feature.description}
|
|
87
|
+
/>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="border-border bg-card rounded-lg border p-8">
|
|
92
|
+
<h2 className="text-card-foreground mb-8 flex items-center gap-2 text-xl font-medium">
|
|
93
|
+
<PackageCheck className="text-foreground h-5 w-5" />
|
|
94
|
+
<span>Out of the Box</span>
|
|
95
|
+
</h2>
|
|
96
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
97
|
+
{includedFeatures.map((feature) => (
|
|
98
|
+
<IncludedFeatureCard
|
|
99
|
+
key={feature.title}
|
|
100
|
+
iconName={feature.iconName}
|
|
101
|
+
title={feature.title}
|
|
102
|
+
pages={feature.pages}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="border-border bg-card rounded-lg border p-8">
|
|
109
|
+
<h2 className="text-card-foreground mb-8 flex items-center gap-2 text-xl font-medium">
|
|
110
|
+
<Boxes className="text-foreground h-5 w-5" />
|
|
111
|
+
<span>Tech Stack</span>
|
|
112
|
+
</h2>
|
|
113
|
+
<div className="space-y-8">
|
|
114
|
+
{Object.values(techStack).map((stack) => (
|
|
115
|
+
<div key={stack.title}>
|
|
116
|
+
<h3 className="text-card-foreground mb-4 text-sm font-medium">
|
|
117
|
+
{stack.title}
|
|
118
|
+
</h3>
|
|
119
|
+
<div className="flex flex-wrap gap-2">
|
|
120
|
+
{stack.items.map((item) => (
|
|
121
|
+
<TechBadge key={item}>{item}</TechBadge>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div
|
|
130
|
+
id="quick-start"
|
|
131
|
+
className="border-border bg-card border-l-primary rounded-lg border border-l-8 p-8"
|
|
132
|
+
>
|
|
133
|
+
<div className="mb-6 flex items-center gap-3">
|
|
134
|
+
<Terminal className="text-foreground h-5 w-5" />
|
|
135
|
+
<h2 className="text-foreground text-xl font-medium">Quick Start</h2>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="space-y-3">
|
|
138
|
+
{quickStartCommands.map((cmd) => (
|
|
139
|
+
<CommandBlock
|
|
140
|
+
key={cmd.command}
|
|
141
|
+
command={cmd.command}
|
|
142
|
+
tooltipContent={cmd.tooltip}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
{isLocalDev && (
|
|
147
|
+
<>
|
|
148
|
+
<div className="mt-6 flex flex-wrap items-center justify-center gap-2">
|
|
149
|
+
<span className="text-muted-foreground text-xs">Services:</span>
|
|
150
|
+
<a
|
|
151
|
+
href="http://localhost:3000"
|
|
152
|
+
target="_blank"
|
|
153
|
+
rel="noopener noreferrer"
|
|
154
|
+
className="border-border hover:border-foreground/30 hover:bg-accent bg-background inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors"
|
|
155
|
+
>
|
|
156
|
+
Web <span className="text-muted-foreground">:3000</span>
|
|
157
|
+
</a>
|
|
158
|
+
<a
|
|
159
|
+
href={`http://localhost:${backendPort}`}
|
|
160
|
+
target="_blank"
|
|
161
|
+
rel="noopener noreferrer"
|
|
162
|
+
className="border-border hover:border-foreground/30 hover:bg-accent bg-background inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors"
|
|
163
|
+
>
|
|
164
|
+
API{' '}
|
|
165
|
+
<span className="text-muted-foreground">:{backendPort}</span>
|
|
166
|
+
</a>
|
|
167
|
+
<a
|
|
168
|
+
href={`http://localhost:${backendPort}/docs`}
|
|
169
|
+
target="_blank"
|
|
170
|
+
rel="noopener noreferrer"
|
|
171
|
+
className="border-border hover:border-foreground/30 hover:bg-accent bg-background inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors"
|
|
172
|
+
>
|
|
173
|
+
API Docs
|
|
174
|
+
<span className="text-muted-foreground">
|
|
175
|
+
:{backendPort}/docs
|
|
176
|
+
</span>
|
|
177
|
+
</a>
|
|
178
|
+
</div>
|
|
179
|
+
<p className="text-muted-foreground mt-4 text-center text-xs">
|
|
180
|
+
See{' '}
|
|
181
|
+
<code className="bg-muted rounded px-1.5 py-0.5 font-mono">
|
|
182
|
+
GETTING_STARTED.md
|
|
183
|
+
</code>
|
|
184
|
+
for further optional customization.
|
|
185
|
+
</p>
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</main>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { RecentSignup } from '@repo/packages-types/stats';
|
|
4
|
+
import { ScrollArea } from '@repo/packages-ui/scroll-area';
|
|
5
|
+
import { Skeleton } from '@repo/packages-ui/skeleton';
|
|
6
|
+
import { UserAvatar } from '@repo/packages-ui/user-avatar';
|
|
7
|
+
import { motion } from 'framer-motion';
|
|
8
|
+
|
|
9
|
+
function formatRelativeTime(dateStr: string) {
|
|
10
|
+
const date = new Date(dateStr);
|
|
11
|
+
const now = new Date();
|
|
12
|
+
const diffMs = now.getTime() - date.getTime();
|
|
13
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
14
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
15
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
16
|
+
|
|
17
|
+
if (diffMins < 1) return 'Just now';
|
|
18
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
19
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
20
|
+
if (diffDays === 1) return 'Yesterday';
|
|
21
|
+
return `${diffDays}d ago`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RecentSignupsFeedProps {
|
|
25
|
+
signups: RecentSignup[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function RecentSignupsFeed({ signups }: RecentSignupsFeedProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="bg-card rounded-xl border p-5">
|
|
31
|
+
<div className="mb-4">
|
|
32
|
+
<h3 className="text-sm font-semibold">Recent Signups</h3>
|
|
33
|
+
<p className="text-muted-foreground text-xs">
|
|
34
|
+
Latest user registrations
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
<ScrollArea className="h-[280px] pr-3">
|
|
38
|
+
<div className="space-y-3">
|
|
39
|
+
{signups.map((signup, index) => (
|
|
40
|
+
<motion.div
|
|
41
|
+
key={signup.id}
|
|
42
|
+
initial={{ opacity: 0, x: -10 }}
|
|
43
|
+
animate={{ opacity: 1, x: 0 }}
|
|
44
|
+
transition={{ delay: index * 0.05 }}
|
|
45
|
+
className="flex items-center gap-3 rounded-lg border p-3"
|
|
46
|
+
>
|
|
47
|
+
<UserAvatar
|
|
48
|
+
src={signup.image}
|
|
49
|
+
name={signup.name}
|
|
50
|
+
email={signup.email}
|
|
51
|
+
size="sm"
|
|
52
|
+
/>
|
|
53
|
+
<div className="min-w-0 flex-1">
|
|
54
|
+
<p className="truncate text-sm font-medium">
|
|
55
|
+
{signup.name || 'No name'}
|
|
56
|
+
</p>
|
|
57
|
+
<p className="text-muted-foreground truncate text-xs">
|
|
58
|
+
{signup.email}
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
<span className="text-muted-foreground shrink-0 text-xs">
|
|
62
|
+
{formatRelativeTime(signup.createdAt)}
|
|
63
|
+
</span>
|
|
64
|
+
</motion.div>
|
|
65
|
+
))}
|
|
66
|
+
{signups.length === 0 && (
|
|
67
|
+
<p className="text-muted-foreground py-8 text-center text-sm">
|
|
68
|
+
No recent signups
|
|
69
|
+
</p>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</ScrollArea>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function ActivityFeedSkeleton() {
|
|
78
|
+
return (
|
|
79
|
+
<div className="bg-card rounded-xl border p-5">
|
|
80
|
+
<div className="mb-4 space-y-1">
|
|
81
|
+
<Skeleton className="h-4 w-28" />
|
|
82
|
+
<Skeleton className="h-3 w-36" />
|
|
83
|
+
</div>
|
|
84
|
+
<div className="space-y-3">
|
|
85
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
86
|
+
<div
|
|
87
|
+
key={i}
|
|
88
|
+
className="flex items-center gap-3 rounded-lg border p-3"
|
|
89
|
+
>
|
|
90
|
+
<Skeleton className="size-8 rounded-full" />
|
|
91
|
+
<div className="flex-1 space-y-1">
|
|
92
|
+
<Skeleton className="h-3 w-24" />
|
|
93
|
+
<Skeleton className="h-2 w-32" />
|
|
94
|
+
</div>
|
|
95
|
+
<Skeleton className="h-3 w-12" />
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { AuthBreakdown } from '@repo/packages-types/stats';
|
|
4
|
+
import { Skeleton } from '@repo/packages-ui/skeleton';
|
|
5
|
+
import {
|
|
6
|
+
Bar,
|
|
7
|
+
BarChart,
|
|
8
|
+
Cell,
|
|
9
|
+
ResponsiveContainer,
|
|
10
|
+
Tooltip,
|
|
11
|
+
XAxis,
|
|
12
|
+
YAxis,
|
|
13
|
+
} from 'recharts';
|
|
14
|
+
|
|
15
|
+
import { SimpleChartTooltip } from './chart-tooltip';
|
|
16
|
+
|
|
17
|
+
interface AuthBreakdownChartProps {
|
|
18
|
+
data: AuthBreakdown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const AUTH_CONFIG = [
|
|
22
|
+
{ key: 'verified', label: 'Verified', color: '#10b981' },
|
|
23
|
+
{ key: 'unverified', label: 'Unverified', color: '#f59e0b' },
|
|
24
|
+
{ key: 'banned', label: 'Banned', color: '#ef4444' },
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export function AuthBreakdownChart({ data }: AuthBreakdownChartProps) {
|
|
28
|
+
const chartData = AUTH_CONFIG.map((config) => ({
|
|
29
|
+
name: config.label,
|
|
30
|
+
count: data[config.key],
|
|
31
|
+
color: config.color,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const total = data.verified + data.unverified + data.banned;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="bg-card rounded-xl border p-5">
|
|
38
|
+
<div className="mb-4">
|
|
39
|
+
<h3 className="text-sm font-semibold">Auth Status</h3>
|
|
40
|
+
<p className="text-muted-foreground text-xs">
|
|
41
|
+
Users by verification and ban status
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="h-[200px]">
|
|
45
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
46
|
+
<BarChart
|
|
47
|
+
data={chartData}
|
|
48
|
+
layout="vertical"
|
|
49
|
+
margin={{ top: 5, right: 30, left: 0, bottom: 5 }}
|
|
50
|
+
>
|
|
51
|
+
<XAxis
|
|
52
|
+
type="number"
|
|
53
|
+
tick={{ fontSize: 11 }}
|
|
54
|
+
tickLine={false}
|
|
55
|
+
axisLine={false}
|
|
56
|
+
allowDecimals={false}
|
|
57
|
+
className="text-zinc-500 dark:text-zinc-400"
|
|
58
|
+
/>
|
|
59
|
+
<YAxis
|
|
60
|
+
type="category"
|
|
61
|
+
dataKey="name"
|
|
62
|
+
tick={{ fontSize: 11 }}
|
|
63
|
+
tickLine={false}
|
|
64
|
+
axisLine={false}
|
|
65
|
+
width={75}
|
|
66
|
+
className="text-zinc-500 dark:text-zinc-400"
|
|
67
|
+
/>
|
|
68
|
+
<Tooltip
|
|
69
|
+
content={
|
|
70
|
+
<SimpleChartTooltip
|
|
71
|
+
valueFormatter={(value: number) => [
|
|
72
|
+
`${value} (${total > 0 ? ((value / total) * 100).toFixed(1) : 0}%)`,
|
|
73
|
+
'Users',
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
}
|
|
77
|
+
cursor={{ fill: 'rgba(99, 102, 241, 0.08)' }}
|
|
78
|
+
/>
|
|
79
|
+
<Bar dataKey="count" radius={[0, 4, 4, 0]} maxBarSize={28}>
|
|
80
|
+
{chartData.map((entry, index) => (
|
|
81
|
+
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
82
|
+
))}
|
|
83
|
+
</Bar>
|
|
84
|
+
</BarChart>
|
|
85
|
+
</ResponsiveContainer>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function AuthBreakdownChartSkeleton() {
|
|
92
|
+
return (
|
|
93
|
+
<div className="bg-card rounded-xl border p-5">
|
|
94
|
+
<div className="mb-4 space-y-1">
|
|
95
|
+
<Skeleton className="h-4 w-20" />
|
|
96
|
+
<Skeleton className="h-3 w-44" />
|
|
97
|
+
</div>
|
|
98
|
+
<div className="space-y-3 pt-4">
|
|
99
|
+
<div className="flex items-center gap-3">
|
|
100
|
+
<Skeleton className="h-3 w-16" />
|
|
101
|
+
<Skeleton className="h-6 flex-1" />
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex items-center gap-3">
|
|
104
|
+
<Skeleton className="h-3 w-16" />
|
|
105
|
+
<Skeleton className="h-6 w-2/3" />
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex items-center gap-3">
|
|
108
|
+
<Skeleton className="h-3 w-16" />
|
|
109
|
+
<Skeleton className="h-6 w-1/4" />
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@repo/packages-ui/lib/utils';
|
|
4
|
+
|
|
5
|
+
interface TooltipPayload {
|
|
6
|
+
value?: number;
|
|
7
|
+
name?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
payload?: { color?: string };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ChartTooltipProps {
|
|
13
|
+
active?: boolean;
|
|
14
|
+
payload?: TooltipPayload[];
|
|
15
|
+
label?: string;
|
|
16
|
+
labelFormatter?: (label: string) => string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ChartTooltip({
|
|
20
|
+
active,
|
|
21
|
+
payload,
|
|
22
|
+
label,
|
|
23
|
+
labelFormatter,
|
|
24
|
+
}: ChartTooltipProps) {
|
|
25
|
+
if (!active || !payload?.length) return null;
|
|
26
|
+
|
|
27
|
+
const formattedLabel =
|
|
28
|
+
labelFormatter && label ? labelFormatter(label) : label;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
'rounded-lg border px-3 py-2 shadow-lg',
|
|
34
|
+
'bg-white/90 dark:bg-zinc-900/90',
|
|
35
|
+
'backdrop-blur-md',
|
|
36
|
+
'border-zinc-200/50 dark:border-zinc-700/50'
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
{formattedLabel && (
|
|
40
|
+
<p className="mb-1.5 text-xs font-semibold text-zinc-900 dark:text-zinc-100">
|
|
41
|
+
{formattedLabel}
|
|
42
|
+
</p>
|
|
43
|
+
)}
|
|
44
|
+
<div className="space-y-1">
|
|
45
|
+
{payload.map((entry: TooltipPayload, index: number) => (
|
|
46
|
+
<div key={index} className="flex items-center gap-2 text-xs">
|
|
47
|
+
<span
|
|
48
|
+
className="size-2 rounded-full"
|
|
49
|
+
style={{ backgroundColor: entry.color }}
|
|
50
|
+
/>
|
|
51
|
+
<span className="text-zinc-600 dark:text-zinc-400">
|
|
52
|
+
{entry.name}:
|
|
53
|
+
</span>
|
|
54
|
+
<span className="font-medium text-zinc-900 dark:text-zinc-100">
|
|
55
|
+
{typeof entry.value === 'number'
|
|
56
|
+
? entry.value.toLocaleString()
|
|
57
|
+
: entry.value}
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface SimpleChartTooltipProps {
|
|
67
|
+
active?: boolean;
|
|
68
|
+
payload?: TooltipPayload[];
|
|
69
|
+
label?: string;
|
|
70
|
+
labelFormatter?: (label: string) => string;
|
|
71
|
+
valueFormatter?: (value: number, name: string) => [string, string];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function SimpleChartTooltip({
|
|
75
|
+
active,
|
|
76
|
+
payload,
|
|
77
|
+
label,
|
|
78
|
+
labelFormatter,
|
|
79
|
+
valueFormatter,
|
|
80
|
+
}: SimpleChartTooltipProps) {
|
|
81
|
+
if (!active || !payload?.length) return null;
|
|
82
|
+
|
|
83
|
+
const formattedLabel =
|
|
84
|
+
labelFormatter && label ? labelFormatter(label) : label;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
className={cn(
|
|
89
|
+
'rounded-lg border px-3 py-2 shadow-lg',
|
|
90
|
+
'bg-white/90 dark:bg-zinc-900/90',
|
|
91
|
+
'backdrop-blur-md',
|
|
92
|
+
'border-zinc-200/50 dark:border-zinc-700/50'
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{formattedLabel && (
|
|
96
|
+
<p className="mb-1.5 text-xs font-semibold text-zinc-900 dark:text-zinc-100">
|
|
97
|
+
{formattedLabel}
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
<div className="space-y-1">
|
|
101
|
+
{payload.map((entry: TooltipPayload, index: number) => {
|
|
102
|
+
const [formattedValue, formattedName] = valueFormatter
|
|
103
|
+
? valueFormatter(entry.value as number, entry.name as string)
|
|
104
|
+
: [entry.value?.toLocaleString(), entry.name];
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div key={index} className="flex items-center gap-2 text-xs">
|
|
108
|
+
<span
|
|
109
|
+
className="size-2 rounded-full"
|
|
110
|
+
style={{ backgroundColor: entry.color || entry.payload?.color }}
|
|
111
|
+
/>
|
|
112
|
+
<span className="text-zinc-600 dark:text-zinc-400">
|
|
113
|
+
{formattedName}:
|
|
114
|
+
</span>
|
|
115
|
+
<span className="font-medium text-zinc-900 dark:text-zinc-100">
|
|
116
|
+
{formattedValue}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|