abdellah0l-stack 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +50 -0
  2. package/bin/cli.js +218 -0
  3. package/package.json +30 -0
  4. package/template/README.md +61 -0
  5. package/template/components.json +22 -0
  6. package/template/drizzle.config.ts +13 -0
  7. package/template/eslint.config.mjs +25 -0
  8. package/template/next.config.ts +114 -0
  9. package/template/package.json +62 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/public/file.svg +1 -0
  12. package/template/public/globe.svg +1 -0
  13. package/template/public/next.svg +1 -0
  14. package/template/public/vercel.svg +1 -0
  15. package/template/public/window.svg +1 -0
  16. package/template/src/app/api/v1/auth/[...all]/route.ts +5 -0
  17. package/template/src/app/api/v1/trpc/[trpc]/route.ts +13 -0
  18. package/template/src/app/api/v1/uploadthing/core.ts +50 -0
  19. package/template/src/app/api/v1/uploadthing/route.ts +11 -0
  20. package/template/src/app/favicon.ico +0 -0
  21. package/template/src/app/globals.css +121 -0
  22. package/template/src/app/layout.tsx +50 -0
  23. package/template/src/app/page.tsx +58 -0
  24. package/template/src/components/loading-spinner.tsx +18 -0
  25. package/template/src/components/navigation.tsx +54 -0
  26. package/template/src/components/query-provider.tsx +27 -0
  27. package/template/src/components/ui/badge.tsx +46 -0
  28. package/template/src/components/ui/button.tsx +58 -0
  29. package/template/src/components/ui/card.tsx +92 -0
  30. package/template/src/components/ui/dialog.tsx +143 -0
  31. package/template/src/components/ui/input.tsx +21 -0
  32. package/template/src/components/ui/label.tsx +26 -0
  33. package/template/src/components/ui/select.tsx +185 -0
  34. package/template/src/components/ui/tabs.tsx +55 -0
  35. package/template/src/components/ui/textarea.tsx +23 -0
  36. package/template/src/data/env/client.ts +7 -0
  37. package/template/src/data/env/server.ts +13 -0
  38. package/template/src/drizzle/db.ts +6 -0
  39. package/template/src/drizzle/schema/app-schema.ts +31 -0
  40. package/template/src/drizzle/schema/auth-schema.ts +55 -0
  41. package/template/src/drizzle/schema/index.ts +14 -0
  42. package/template/src/hooks/use-auth.ts +32 -0
  43. package/template/src/hooks/use-debounce.ts +18 -0
  44. package/template/src/lib/arcjet.ts +45 -0
  45. package/template/src/lib/auth-client.ts +7 -0
  46. package/template/src/lib/auth.ts +50 -0
  47. package/template/src/lib/use-mobile.ts +29 -0
  48. package/template/src/lib/utils.ts +6 -0
  49. package/template/src/middleware.ts +14 -0
  50. package/template/src/server/index.ts +13 -0
  51. package/template/src/server/routers/posts.ts +93 -0
  52. package/template/src/server/routers/users.ts +56 -0
  53. package/template/src/server/trpc.ts +38 -0
  54. package/template/src/types/index.ts +10 -0
  55. package/template/src/utils/trpc.ts +5 -0
  56. package/template/src/utils/uploadthing.ts +10 -0
  57. package/template/tsconfig.json +42 -0
@@ -0,0 +1,50 @@
1
+ import { createUploadthing, type FileRouter } from "uploadthing/next";
2
+ import { UploadThingError } from "uploadthing/server";
3
+ import { getSession } from "@/lib/auth";
4
+ import { ProfilePhotoLimiter } from "@/lib/arcjet";
5
+
6
+ const f = createUploadthing();
7
+
8
+ // FileRouter for your app, can contain multiple FileRoutes
9
+ export const ourFileRouter = {
10
+ // Define as many FileRoutes as you like, each with a unique routeSlug
11
+ imageUploader: f({
12
+ image: {
13
+ /**
14
+ * For full list of options and defaults, see the File Route API reference
15
+ * @see https://docs.uploadthing.com/file-routes#route-config
16
+ */
17
+ maxFileSize: "4MB",
18
+ maxFileCount: 1,
19
+ },
20
+ })
21
+ // Set permissions and file types for this FileRoute
22
+ .middleware(async (opts) => {
23
+ // This code runs on your server before upload
24
+ const session = await getSession();
25
+
26
+ // If you throw, the user will not be able to upload
27
+ if (!session || !session.user) {
28
+ throw new UploadThingError("Unauthorized");
29
+ }
30
+
31
+ // Check the rate limit
32
+ const decision = await ProfilePhotoLimiter().protect(opts.req, {
33
+ userId: session.user.id,
34
+ });
35
+
36
+ if (decision.isDenied()) {
37
+ throw new UploadThingError("Rate limit exceeded. You can only upload 3 profile photos per week.");
38
+ }
39
+
40
+ // Whatever is returned here is accessible in onUploadComplete as `metadata`
41
+ return { userId: session.user.id };
42
+ })
43
+ .onUploadComplete(async ({ metadata, file }) => {
44
+ // This code RUNS ON YOUR SERVER after upload
45
+ // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
46
+ return { uploadedBy: metadata.userId };
47
+ }),
48
+ } satisfies FileRouter;
49
+
50
+ export type OurFileRouter = typeof ourFileRouter;
@@ -0,0 +1,11 @@
1
+ import { createRouteHandler } from "uploadthing/next";
2
+
3
+ import { ourFileRouter } from "./core";
4
+
5
+ // Export routes for Next App Router
6
+ export const { GET, POST } = createRouteHandler({
7
+ router: ourFileRouter,
8
+
9
+ // Apply an (optional) custom config:
10
+ // config: { ... },
11
+ });
Binary file
@@ -0,0 +1,121 @@
1
+ @import "tailwindcss";
2
+
3
+ @custom-variant dark (&:is(.dark *));
4
+
5
+ @theme inline {
6
+ --color-background: var(--background);
7
+ --color-foreground: var(--foreground);
8
+ --font-sans: var(--font-geist-sans);
9
+ --font-mono: var(--font-geist-mono);
10
+ --color-sidebar-ring: var(--sidebar-ring);
11
+ --color-sidebar-border: var(--sidebar-border);
12
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
13
+ --color-sidebar-accent: var(--sidebar-accent);
14
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
15
+ --color-sidebar-primary: var(--sidebar-primary);
16
+ --color-sidebar-foreground: var(--sidebar-foreground);
17
+ --color-sidebar: var(--sidebar);
18
+ --color-chart-5: var(--chart-5);
19
+ --color-chart-4: var(--chart-4);
20
+ --color-chart-3: var(--chart-3);
21
+ --color-chart-2: var(--chart-2);
22
+ --color-chart-1: var(--chart-1);
23
+ --color-ring: var(--ring);
24
+ --color-input: var(--input);
25
+ --color-border: var(--border);
26
+ --color-destructive: var(--destructive);
27
+ --color-accent-foreground: var(--accent-foreground);
28
+ --color-accent: var(--accent);
29
+ --color-muted-foreground: var(--muted-foreground);
30
+ --color-muted: var(--muted);
31
+ --color-secondary-foreground: var(--secondary-foreground);
32
+ --color-secondary: var(--secondary);
33
+ --color-primary-foreground: var(--primary-foreground);
34
+ --color-primary: var(--primary);
35
+ --color-popover-foreground: var(--popover-foreground);
36
+ --color-popover: var(--popover);
37
+ --color-card-foreground: var(--card-foreground);
38
+ --color-card: var(--card);
39
+ --radius-sm: calc(var(--radius) - 4px);
40
+ --radius-md: calc(var(--radius) - 2px);
41
+ --radius-lg: var(--radius);
42
+ --radius-xl: calc(var(--radius) + 4px);
43
+ }
44
+
45
+ :root {
46
+ --radius: 0.625rem;
47
+ --background: oklch(1 0 0);
48
+ --foreground: oklch(0.145 0 0);
49
+ --card: oklch(1 0 0);
50
+ --card-foreground: oklch(0.145 0 0);
51
+ --popover: oklch(1 0 0);
52
+ --popover-foreground: oklch(0.145 0 0);
53
+ --primary: oklch(0.205 0 0);
54
+ --primary-foreground: oklch(0.985 0 0);
55
+ --secondary: oklch(0.97 0 0);
56
+ --secondary-foreground: oklch(0.205 0 0);
57
+ --muted: oklch(0.97 0 0);
58
+ --muted-foreground: oklch(0.556 0 0);
59
+ --accent: oklch(0.97 0 0);
60
+ --accent-foreground: oklch(0.205 0 0);
61
+ --destructive: oklch(0.577 0.245 27.325);
62
+ --border: oklch(0.922 0 0);
63
+ --input: oklch(0.922 0 0);
64
+ --ring: oklch(0.708 0 0);
65
+ --chart-1: oklch(0.646 0.222 41.116);
66
+ --chart-2: oklch(0.6 0.118 184.704);
67
+ --chart-3: oklch(0.398 0.07 227.392);
68
+ --chart-4: oklch(0.828 0.189 84.429);
69
+ --chart-5: oklch(0.769 0.188 70.08);
70
+ --sidebar: oklch(0.985 0 0);
71
+ --sidebar-foreground: oklch(0.145 0 0);
72
+ --sidebar-primary: oklch(0.205 0 0);
73
+ --sidebar-primary-foreground: oklch(0.985 0 0);
74
+ --sidebar-accent: oklch(0.97 0 0);
75
+ --sidebar-accent-foreground: oklch(0.205 0 0);
76
+ --sidebar-border: oklch(0.922 0 0);
77
+ --sidebar-ring: oklch(0.708 0 0);
78
+ }
79
+
80
+ .dark {
81
+ --background: oklch(0.145 0 0);
82
+ --foreground: oklch(0.985 0 0);
83
+ --card: oklch(0.205 0 0);
84
+ --card-foreground: oklch(0.985 0 0);
85
+ --popover: oklch(0.205 0 0);
86
+ --popover-foreground: oklch(0.985 0 0);
87
+ --primary: oklch(0.922 0 0);
88
+ --primary-foreground: oklch(0.205 0 0);
89
+ --secondary: oklch(0.269 0 0);
90
+ --secondary-foreground: oklch(0.985 0 0);
91
+ --muted: oklch(0.269 0 0);
92
+ --muted-foreground: oklch(0.708 0 0);
93
+ --accent: oklch(0.269 0 0);
94
+ --accent-foreground: oklch(0.985 0 0);
95
+ --destructive: oklch(0.704 0.191 22.216);
96
+ --border: oklch(1 0 0 / 10%);
97
+ --input: oklch(1 0 0 / 15%);
98
+ --ring: oklch(0.556 0 0);
99
+ --chart-1: oklch(0.488 0.243 264.376);
100
+ --chart-2: oklch(0.696 0.17 162.48);
101
+ --chart-3: oklch(0.769 0.188 70.08);
102
+ --chart-4: oklch(0.627 0.265 303.9);
103
+ --chart-5: oklch(0.645 0.246 16.439);
104
+ --sidebar: oklch(0.205 0 0);
105
+ --sidebar-foreground: oklch(0.985 0 0);
106
+ --sidebar-primary: oklch(0.488 0.243 264.376);
107
+ --sidebar-primary-foreground: oklch(0.985 0 0);
108
+ --sidebar-accent: oklch(0.269 0 0);
109
+ --sidebar-accent-foreground: oklch(0.985 0 0);
110
+ --sidebar-border: oklch(1 0 0 / 10%);
111
+ --sidebar-ring: oklch(0.556 0 0);
112
+ }
113
+
114
+ @layer base {
115
+ * {
116
+ @apply border-border outline-ring/50;
117
+ }
118
+ body {
119
+ @apply bg-background text-foreground;
120
+ }
121
+ }
@@ -0,0 +1,50 @@
1
+ import type { Metadata } from "next";
2
+ import { Toaster } from "react-hot-toast";
3
+ import QueryProvider from "@/components/query-provider";
4
+ import { Navigation } from "@/components/navigation";
5
+ import "./globals.css";
6
+
7
+ // configures here the metadata for your application
8
+ export const metadata: Metadata = {
9
+ title: "My App",
10
+ description: "A modern full-stack application",
11
+ };
12
+
13
+ export default function RootLayout({
14
+ children,
15
+ }: Readonly<{
16
+ children: React.ReactNode;
17
+ }>) {
18
+ return (
19
+ <html lang="en">
20
+ <body className="antialiased bg-black text-white">
21
+ <QueryProvider>
22
+ <Toaster
23
+ position="top-center"
24
+ toastOptions={{
25
+ duration: 4000,
26
+ style: {
27
+ background: '#363636',
28
+ color: '#fff',
29
+ },
30
+ success: {
31
+ iconTheme: {
32
+ primary: '#22c55e',
33
+ secondary: '#fff',
34
+ },
35
+ },
36
+ error: {
37
+ iconTheme: {
38
+ primary: '#ef4444',
39
+ secondary: '#fff',
40
+ },
41
+ },
42
+ }}
43
+ />
44
+ <Navigation />
45
+ {children}
46
+ </QueryProvider>
47
+ </body>
48
+ </html>
49
+ );
50
+ }
@@ -0,0 +1,58 @@
1
+ import Link from "next/link";
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main className="min-h-screen bg-gradient-to-b from-zinc-900 to-black text-white">
6
+ <div className="container mx-auto px-4 py-20">
7
+ <div className="text-center space-y-8">
8
+ <h1 className="text-5xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
9
+ Welcome to Abdellah0l-Stack
10
+ </h1>
11
+ <p className="text-xl text-zinc-400 max-w-2xl mx-auto">
12
+ with Abdellah0l-Stack, you can quickly build modern full-stack
13
+ applications with authentication, type-safe APIs, and a database
14
+ ready to go.
15
+ </p>
16
+
17
+ <div className="flex gap-4 justify-center pt-8">
18
+ <Link
19
+ href="/auth"
20
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium transition-colors"
21
+ >
22
+ Get Started
23
+ </Link>
24
+ <a
25
+ href="https://github.com"
26
+ target="_blank"
27
+ rel="noopener noreferrer"
28
+ className="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-lg font-medium transition-colors"
29
+ >
30
+ GitHub
31
+ </a>
32
+ </div>
33
+
34
+ <div className="grid md:grid-cols-3 gap-6 pt-16 max-w-4xl mx-auto">
35
+ <div className="p-6 bg-zinc-800/50 rounded-xl border border-zinc-700">
36
+ <h3 className="text-lg font-semibold mb-2">Type-Safe APIs</h3>
37
+ <p className="text-zinc-400 text-sm">
38
+ End-to-end type safety with tRPC and TypeScript.
39
+ </p>
40
+ </div>
41
+ <div className="p-6 bg-zinc-800/50 rounded-xl border border-zinc-700">
42
+ <h3 className="text-lg font-semibold mb-2">Authentication</h3>
43
+ <p className="text-zinc-400 text-sm">
44
+ GitHub, Google, and email auth with Better-Auth.
45
+ </p>
46
+ </div>
47
+ <div className="p-6 bg-zinc-800/50 rounded-xl border border-zinc-700">
48
+ <h3 className="text-lg font-semibold mb-2">Database Ready</h3>
49
+ <p className="text-zinc-400 text-sm">
50
+ PostgreSQL with Drizzle ORM for type-safe queries.
51
+ </p>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </main>
57
+ );
58
+ }
@@ -0,0 +1,18 @@
1
+ interface LoadingSpinnerProps {
2
+ size?: 'sm' | 'md' | 'lg'
3
+ className?: string
4
+ }
5
+
6
+ export default function LoadingSpinner({ size = 'md', className = '' }: LoadingSpinnerProps) {
7
+ const sizeClasses = {
8
+ sm: 'h-4 w-4',
9
+ md: 'h-6 w-6',
10
+ lg: 'h-8 w-8'
11
+ }
12
+
13
+ return (
14
+ <div className={`${sizeClasses[size]} ${className}`}>
15
+ <div className="animate-spin rounded-full h-full w-full border-2 border-gray-300 border-t-primary"></div>
16
+ </div>
17
+ )
18
+ }
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useAuth } from "@/hooks/use-auth";
5
+ import { authClient } from "@/lib/auth-client";
6
+ import { useRouter } from "next/navigation";
7
+
8
+ export function Navigation() {
9
+ const { user, isLoading } = useAuth();
10
+ const router = useRouter();
11
+
12
+ const handleSignOut = async () => {
13
+ await authClient.signOut();
14
+ router.push("/");
15
+ router.refresh();
16
+ };
17
+
18
+ return (
19
+ <nav className="border-b border-zinc-800 bg-zinc-900/50 backdrop-blur-sm sticky top-0 z-50">
20
+ <div className="container mx-auto px-4">
21
+ <div className="flex items-center justify-between h-16">
22
+ <Link href="/" className="text-xl font-bold">
23
+ Abdellah0l-Stack
24
+ </Link>
25
+
26
+ <div className="flex items-center gap-4">
27
+ {isLoading ? (
28
+ <div className="w-8 h-8 rounded-full bg-zinc-700 animate-pulse" />
29
+ ) : user ? (
30
+ <>
31
+ <span className="text-sm text-zinc-400">
32
+ {user.name || user.email}
33
+ </span>
34
+ <button
35
+ onClick={handleSignOut}
36
+ className="px-4 py-2 text-sm bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors"
37
+ >
38
+ Sign Out
39
+ </button>
40
+ </>
41
+ ) : (
42
+ <Link
43
+ href="/auth"
44
+ className="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
45
+ >
46
+ Sign In
47
+ </Link>
48
+ )}
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </nav>
53
+ );
54
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { httpBatchLink } from '@trpc/client';
6
+ import { trpc } from "@/utils/trpc";
7
+
8
+ export default function QueryProvider({ children }: { children: React.ReactNode }) {
9
+ const [queryClient] = useState(() => new QueryClient());
10
+ const [trpcClient] = useState(() =>
11
+ trpc.createClient({
12
+ links: [
13
+ httpBatchLink({
14
+ url: '/api/v1/trpc',
15
+ }),
16
+ ],
17
+ })
18
+ );
19
+
20
+ return (
21
+ <trpc.Provider client={trpcClient} queryClient={queryClient}>
22
+ <QueryClientProvider client={queryClient}>
23
+ {children}
24
+ </QueryClientProvider>
25
+ </trpc.Provider>
26
+ );
27
+ }
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }
@@ -0,0 +1,58 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ function Button({
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<"button"> &
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean
46
+ }) {
47
+ const Comp = asChild ? Slot : "button"
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ export { Button, buttonVariants }
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }