create-ncf 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/README.md +145 -0
- package/dist/index.js +813 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
- package/templates/auth/src/app/(auth)/sign-in/page.tsx +99 -0
- package/templates/auth/src/app/(auth)/sign-up/page.tsx +118 -0
- package/templates/auth/src/app/api/auth/[...all]/route.ts +11 -0
- package/templates/auth/src/middleware.ts +74 -0
- package/templates/auth/src/server/auth/auth-client.ts +8 -0
- package/templates/auth/src/server/auth/auth.ts +77 -0
- package/templates/auth/src/server/db/schema/auth.ts +115 -0
- package/templates/base/biome.jsonc +68 -0
- package/templates/base/open-next.config.ts +3 -0
- package/templates/base/postcss.config.mjs +5 -0
- package/templates/base/src/app/layout.tsx +30 -0
- package/templates/base/src/app/page.tsx +24 -0
- package/templates/base/src/app/robots.ts +13 -0
- package/templates/base/src/app/sitemap.ts +12 -0
- package/templates/base/src/lib/utils.ts +6 -0
- package/templates/base/src/middleware.ts +9 -0
- package/templates/base/src/styles/globals.css +121 -0
- package/templates/base/tsconfig.json +37 -0
- package/templates/drizzle/drizzle.config.ts +7 -0
- package/templates/drizzle/migrations/.gitkeep +0 -0
- package/templates/drizzle/src/server/db/index.ts +9 -0
- package/templates/drizzle/src/server/db/schema/example.ts +15 -0
- package/templates/drizzle/src/server/db/schema/index.ts +1 -0
- package/templates/image-loader/src/lib/image-loader.ts +34 -0
- package/templates/posthog/instrumentation-client.ts +15 -0
- package/templates/queues/src/server/queues/handler.ts +43 -0
- package/templates/r2/src/server/services/storage.ts +53 -0
- package/templates/shadcn/components.json +22 -0
- package/templates/shadcn/src/components/ui/button.tsx +59 -0
- package/templates/shadcn/src/components/ui/card.tsx +92 -0
- package/templates/shadcn/src/components/ui/input.tsx +21 -0
- package/templates/shadcn/src/components/ui/skeleton.tsx +13 -0
- package/templates/shadcn/src/components/ui/sonner.tsx +28 -0
- package/templates/trpc/src/app/api/trpc/[trpc]/route.ts +29 -0
- package/templates/trpc/src/server/api/root.ts +10 -0
- package/templates/trpc/src/server/api/routes/example.ts +12 -0
- package/templates/trpc/src/server/api/trpc.ts +45 -0
- package/templates/trpc/src/server/api/trpc.with-auth.ts +67 -0
- package/templates/trpc/src/trpc/query-client.ts +23 -0
- package/templates/trpc/src/trpc/react.tsx +65 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
3
|
+
"root": true,
|
|
4
|
+
"vcs": {
|
|
5
|
+
"enabled": true,
|
|
6
|
+
"useIgnoreFile": true,
|
|
7
|
+
"clientKind": "git"
|
|
8
|
+
},
|
|
9
|
+
"assist": {
|
|
10
|
+
"enabled": true,
|
|
11
|
+
"actions": {
|
|
12
|
+
"recommended": true,
|
|
13
|
+
"source": {
|
|
14
|
+
"recommended": true,
|
|
15
|
+
"organizeImports": "on",
|
|
16
|
+
"useSortedAttributes": "on"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"formatter": {
|
|
21
|
+
"enabled": true
|
|
22
|
+
},
|
|
23
|
+
"files": {
|
|
24
|
+
"includes": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "!!**/cloudflare-env.d.ts"]
|
|
25
|
+
},
|
|
26
|
+
"linter": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"rules": {
|
|
29
|
+
"recommended": true,
|
|
30
|
+
"nursery": {
|
|
31
|
+
"useSortedClasses": {
|
|
32
|
+
"level": "warn",
|
|
33
|
+
"fix": "safe",
|
|
34
|
+
"options": {
|
|
35
|
+
"functions": ["clsx", "cva", "cn"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"correctness": {
|
|
40
|
+
"noUnusedImports": {
|
|
41
|
+
"level": "error",
|
|
42
|
+
"fix": "safe"
|
|
43
|
+
},
|
|
44
|
+
"noUnusedVariables": {
|
|
45
|
+
"level": "error"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"javascript": {
|
|
51
|
+
"assist": { "enabled": true },
|
|
52
|
+
"formatter": { "enabled": true },
|
|
53
|
+
"linter": { "enabled": true }
|
|
54
|
+
},
|
|
55
|
+
"css": {
|
|
56
|
+
"assist": { "enabled": true },
|
|
57
|
+
"formatter": { "enabled": true },
|
|
58
|
+
"linter": { "enabled": true },
|
|
59
|
+
"parser": {
|
|
60
|
+
"cssModules": true,
|
|
61
|
+
"tailwindDirectives": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"json": {
|
|
65
|
+
"formatter": { "enabled": true },
|
|
66
|
+
"linter": { "enabled": true }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import "~/styles/globals.css";
|
|
4
|
+
|
|
5
|
+
const inter = Inter({
|
|
6
|
+
variable: "--font-sans",
|
|
7
|
+
subsets: ["latin"],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({
|
|
11
|
+
children,
|
|
12
|
+
}: Readonly<{
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}>) {
|
|
15
|
+
return (
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<body className={`${inter.variable} font-sans antialiased`}>
|
|
18
|
+
{children}
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const metadata: Metadata = {
|
|
25
|
+
title: {
|
|
26
|
+
default: "My App",
|
|
27
|
+
template: "%s | My App",
|
|
28
|
+
},
|
|
29
|
+
description: "Built with create-ncf",
|
|
30
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export default function HomePage() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="flex min-h-screen flex-col items-center justify-center">
|
|
4
|
+
<div className="space-y-4 text-center">
|
|
5
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
6
|
+
Next.js + Cloudflare
|
|
7
|
+
</h1>
|
|
8
|
+
<p className="text-muted-foreground">
|
|
9
|
+
Scaffolded with{" "}
|
|
10
|
+
<code className="rounded bg-muted px-1.5 py-0.5 text-sm">
|
|
11
|
+
create-ncf
|
|
12
|
+
</code>
|
|
13
|
+
</p>
|
|
14
|
+
<p className="text-muted-foreground text-sm">
|
|
15
|
+
Edit{" "}
|
|
16
|
+
<code className="rounded bg-muted px-1.5 py-0.5 text-sm">
|
|
17
|
+
src/app/page.tsx
|
|
18
|
+
</code>{" "}
|
|
19
|
+
to get started.
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
</main>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
|
|
3
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
url: process.env.SITE_URL ?? "http://localhost:3000",
|
|
7
|
+
lastModified: new Date(),
|
|
8
|
+
changeFrequency: "daily",
|
|
9
|
+
priority: 1,
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:is(.dark *));
|
|
5
|
+
|
|
6
|
+
@theme inline {
|
|
7
|
+
--color-background: var(--background);
|
|
8
|
+
--color-foreground: var(--foreground);
|
|
9
|
+
--font-sans: var(--font-sans);
|
|
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.145 0 0);
|
|
84
|
+
--card-foreground: oklch(0.985 0 0);
|
|
85
|
+
--popover: oklch(0.145 0 0);
|
|
86
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
87
|
+
--primary: oklch(0.985 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.577 0.245 27.325);
|
|
96
|
+
--border: oklch(0.269 0 0);
|
|
97
|
+
--input: oklch(0.269 0 0);
|
|
98
|
+
--ring: oklch(0.439 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,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"checkJs": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"module": "esnext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
"incremental": true,
|
|
17
|
+
"plugins": [
|
|
18
|
+
{
|
|
19
|
+
"name": "next"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"paths": {
|
|
23
|
+
"~/*": ["./src/*"],
|
|
24
|
+
"lib.env": ["./lib.env.d.ts"]
|
|
25
|
+
},
|
|
26
|
+
"types": ["./cloudflare-env.d.ts", "node"]
|
|
27
|
+
},
|
|
28
|
+
"include": [
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
"next-env.d.ts",
|
|
31
|
+
"**/*.ts",
|
|
32
|
+
"**/*.tsx",
|
|
33
|
+
"**/*.cjs",
|
|
34
|
+
"**/*.js"
|
|
35
|
+
],
|
|
36
|
+
"exclude": ["node_modules"]
|
|
37
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getCloudflareContext } from "@opennextjs/cloudflare";
|
|
2
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
3
|
+
import * as schema from "./schema";
|
|
4
|
+
|
|
5
|
+
export async function getDB() {
|
|
6
|
+
const { env } = await getCloudflareContext({ async: true });
|
|
7
|
+
|
|
8
|
+
return drizzle(env.DB, { schema });
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
3
|
+
|
|
4
|
+
export const posts = sqliteTable("posts", {
|
|
5
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
6
|
+
title: text("title").notNull(),
|
|
7
|
+
content: text("content"),
|
|
8
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
|
9
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
10
|
+
.notNull(),
|
|
11
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
|
12
|
+
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
|
13
|
+
.$onUpdate(() => new Date())
|
|
14
|
+
.notNull(),
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./example";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default function cloudflareLoader({
|
|
2
|
+
src,
|
|
3
|
+
width,
|
|
4
|
+
quality,
|
|
5
|
+
}: {
|
|
6
|
+
src: string;
|
|
7
|
+
width: number;
|
|
8
|
+
quality?: number;
|
|
9
|
+
}) {
|
|
10
|
+
const params = [`width=${width}`];
|
|
11
|
+
if (quality) {
|
|
12
|
+
params.push(`quality=${quality}`);
|
|
13
|
+
}
|
|
14
|
+
params.push("format=auto");
|
|
15
|
+
|
|
16
|
+
const cdnDomain = process.env.NEXT_PUBLIC_CDN_DOMAIN;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(src);
|
|
20
|
+
|
|
21
|
+
// Only apply Cloudflare Image Resizing to the configured CDN domain
|
|
22
|
+
if (cdnDomain) {
|
|
23
|
+
const cdnHostname = new URL(cdnDomain).hostname;
|
|
24
|
+
if (url.hostname === cdnHostname) {
|
|
25
|
+
return `${url.origin}/cdn-cgi/image/${params.join(",")}${url.pathname}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return src;
|
|
30
|
+
} catch {
|
|
31
|
+
// If src is not a valid URL (e.g., relative path), return as is
|
|
32
|
+
return src;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import posthog from "posthog-js";
|
|
2
|
+
|
|
3
|
+
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
|
4
|
+
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
|
|
5
|
+
|
|
6
|
+
if (posthogKey && posthogHost) {
|
|
7
|
+
posthog.init(posthogKey, {
|
|
8
|
+
api_host: posthogHost,
|
|
9
|
+
ui_host: "https://us.posthog.com",
|
|
10
|
+
capture_exceptions: true,
|
|
11
|
+
disable_surveys: true,
|
|
12
|
+
disable_session_recording: true,
|
|
13
|
+
debug: process.env.NODE_ENV === "development",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Example queue message type — replace with your own
|
|
2
|
+
interface QueueMessage {
|
|
3
|
+
type: string;
|
|
4
|
+
payload: unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function handleBatch(
|
|
8
|
+
batch: MessageBatch<unknown>,
|
|
9
|
+
env: CloudflareEnv,
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
for (const message of batch.messages) {
|
|
12
|
+
const body = message.body as QueueMessage;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
console.log(`Processing message: ${body.type}`, body.payload);
|
|
16
|
+
|
|
17
|
+
// TODO: Add your queue processing logic here
|
|
18
|
+
|
|
19
|
+
message.ack();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`Failed to process message: ${body.type}`, error);
|
|
22
|
+
message.retry();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleDlqBatch(
|
|
28
|
+
batch: MessageBatch<unknown>,
|
|
29
|
+
env: CloudflareEnv,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
for (const message of batch.messages) {
|
|
32
|
+
const body = message.body as QueueMessage;
|
|
33
|
+
|
|
34
|
+
console.error(
|
|
35
|
+
`Dead letter queue message: ${body.type}`,
|
|
36
|
+
JSON.stringify(body.payload),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// TODO: Add dead letter queue handling (alerting, logging, etc.)
|
|
40
|
+
|
|
41
|
+
message.ack();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getCloudflareContext } from "@opennextjs/cloudflare";
|
|
2
|
+
import { env } from "~/env";
|
|
3
|
+
|
|
4
|
+
function normalizeKey(key: string): string {
|
|
5
|
+
return key.replace(/^\/+/, "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function resolveBucket(bucket?: R2Bucket): Promise<R2Bucket> {
|
|
9
|
+
if (bucket) return bucket;
|
|
10
|
+
const { env: cfEnv } = await getCloudflareContext({ async: true });
|
|
11
|
+
return cfEnv.STORAGE;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildR2PublicUrl(key: string): string {
|
|
15
|
+
const normalized = normalizeKey(key);
|
|
16
|
+
const base = (env.NEXT_PUBLIC_R2_DOMAIN ?? "").replace(/\/+$/, "");
|
|
17
|
+
return `${base}/${normalized}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function uploadFile(
|
|
21
|
+
key: string,
|
|
22
|
+
data: ArrayBuffer,
|
|
23
|
+
contentType: string,
|
|
24
|
+
bucket?: R2Bucket,
|
|
25
|
+
): Promise<{ key: string; url: string }> {
|
|
26
|
+
const storage = await resolveBucket(bucket);
|
|
27
|
+
const normalized = normalizeKey(key);
|
|
28
|
+
|
|
29
|
+
await storage.put(normalized, data, {
|
|
30
|
+
httpMetadata: { contentType },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
key: normalized,
|
|
35
|
+
url: buildR2PublicUrl(normalized),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getFile(
|
|
40
|
+
key: string,
|
|
41
|
+
bucket?: R2Bucket,
|
|
42
|
+
): Promise<R2ObjectBody | null> {
|
|
43
|
+
const storage = await resolveBucket(bucket);
|
|
44
|
+
return storage.get(normalizeKey(key));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function deleteFile(
|
|
48
|
+
key: string,
|
|
49
|
+
bucket?: R2Bucket,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const storage = await resolveBucket(bucket);
|
|
52
|
+
await storage.delete(normalizeKey(key));
|
|
53
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/styles/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "~/components",
|
|
16
|
+
"utils": "~/lib/utils",
|
|
17
|
+
"ui": "~/components/ui",
|
|
18
|
+
"lib": "~/lib",
|
|
19
|
+
"hooks": "~/hooks"
|
|
20
|
+
},
|
|
21
|
+
"registries": {}
|
|
22
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { Slot } from "radix-ui"
|
|
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-colors 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 border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
24
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
25
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
26
|
+
icon: "size-9",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: "default",
|
|
31
|
+
size: "default",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
function Button({
|
|
37
|
+
className,
|
|
38
|
+
variant = "default",
|
|
39
|
+
size = "default",
|
|
40
|
+
asChild = false,
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentProps<"button"> &
|
|
43
|
+
VariantProps<typeof buttonVariants> & {
|
|
44
|
+
asChild?: boolean
|
|
45
|
+
}) {
|
|
46
|
+
const Comp = asChild ? Slot.Root : "button"
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Comp
|
|
50
|
+
data-slot="button"
|
|
51
|
+
data-variant={variant}
|
|
52
|
+
data-size={size}
|
|
53
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { Button, buttonVariants }
|