shipd 0.1.3 → 0.2.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/base-package/app/globals.css +126 -0
- package/base-package/app/layout.tsx +53 -0
- package/base-package/app/page.tsx +15 -0
- package/base-package/base.config.json +57 -0
- package/base-package/components/ui/avatar.tsx +53 -0
- package/base-package/components/ui/badge.tsx +46 -0
- package/base-package/components/ui/button.tsx +59 -0
- package/base-package/components/ui/card.tsx +92 -0
- package/base-package/components/ui/chart.tsx +353 -0
- package/base-package/components/ui/checkbox.tsx +32 -0
- package/base-package/components/ui/dialog.tsx +135 -0
- package/base-package/components/ui/dropdown-menu.tsx +257 -0
- package/base-package/components/ui/form.tsx +167 -0
- package/base-package/components/ui/input.tsx +21 -0
- package/base-package/components/ui/label.tsx +24 -0
- package/base-package/components/ui/progress.tsx +31 -0
- package/base-package/components/ui/resizable.tsx +56 -0
- package/base-package/components/ui/select.tsx +185 -0
- package/base-package/components/ui/separator.tsx +28 -0
- package/base-package/components/ui/sheet.tsx +139 -0
- package/base-package/components/ui/skeleton.tsx +13 -0
- package/base-package/components/ui/sonner.tsx +25 -0
- package/base-package/components/ui/switch.tsx +31 -0
- package/base-package/components/ui/tabs.tsx +66 -0
- package/base-package/components/ui/textarea.tsx +18 -0
- package/base-package/components/ui/toggle-group.tsx +73 -0
- package/base-package/components/ui/toggle.tsx +47 -0
- package/base-package/components/ui/tooltip.tsx +61 -0
- package/base-package/components.json +21 -0
- package/base-package/eslint.config.mjs +16 -0
- package/base-package/lib/utils.ts +6 -0
- package/base-package/middleware.ts +12 -0
- package/base-package/next.config.ts +27 -0
- package/base-package/package.json +49 -0
- package/base-package/postcss.config.mjs +5 -0
- package/base-package/public/favicon.svg +4 -0
- package/base-package/tailwind.config.ts +89 -0
- package/base-package/tsconfig.json +27 -0
- package/dist/index.js +1858 -956
- package/docs-template/README.md +74 -0
- package/features/ai-chat/README.md +316 -0
- package/features/ai-chat/app/api/chat/route.ts +16 -0
- package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
- package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
- package/features/ai-chat/feature.config.json +22 -0
- package/features/analytics/README.md +364 -0
- package/features/analytics/feature.config.json +20 -0
- package/features/analytics/lib/posthog.ts +36 -0
- package/features/auth/README.md +409 -0
- package/features/auth/app/api/auth/[...all]/route.ts +4 -0
- package/features/auth/app/dashboard/layout.tsx +15 -0
- package/features/auth/app/dashboard/page.tsx +140 -0
- package/features/auth/app/sign-in/page.tsx +228 -0
- package/features/auth/app/sign-up/page.tsx +243 -0
- package/features/auth/auth-schema.ts +47 -0
- package/features/auth/components/auth/setup-instructions.tsx +123 -0
- package/features/auth/feature.config.json +33 -0
- package/features/auth/lib/auth-client.ts +8 -0
- package/features/auth/lib/auth.ts +295 -0
- package/features/auth/lib/email-stub.ts +55 -0
- package/features/auth/lib/email.ts +47 -0
- package/features/auth/middleware.patch.ts +43 -0
- package/features/database/README.md +312 -0
- package/features/database/db/drizzle.ts +48 -0
- package/features/database/db/schema.ts +21 -0
- package/features/database/drizzle.config.ts +13 -0
- package/features/database/feature.config.json +30 -0
- package/features/email/README.md +341 -0
- package/features/email/emails/components/layout.tsx +181 -0
- package/features/email/emails/password-reset.tsx +67 -0
- package/features/email/emails/payment-failed.tsx +167 -0
- package/features/email/emails/subscription-confirmation.tsx +129 -0
- package/features/email/emails/welcome.tsx +100 -0
- package/features/email/feature.config.json +22 -0
- package/features/email/lib/email.ts +118 -0
- package/features/file-upload/README.md +329 -0
- package/features/file-upload/app/api/upload-image/route.ts +64 -0
- package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
- package/features/file-upload/feature.config.json +23 -0
- package/features/file-upload/lib/upload-image.ts +28 -0
- package/features/marketing-landing/README.md +333 -0
- package/features/marketing-landing/app/page.tsx +25 -0
- package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
- package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
- package/features/marketing-landing/components/homepage/footer.tsx +53 -0
- package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
- package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
- package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
- package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
- package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
- package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
- package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
- package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
- package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
- package/features/marketing-landing/components/logos/Polar.tsx +7 -0
- package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
- package/features/marketing-landing/components/logos/index.ts +6 -0
- package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
- package/features/marketing-landing/feature.config.json +23 -0
- package/features/payments/README.md +375 -0
- package/features/payments/app/api/subscription/route.ts +25 -0
- package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
- package/features/payments/app/dashboard/payment/page.tsx +126 -0
- package/features/payments/app/success/page.tsx +123 -0
- package/features/payments/feature.config.json +31 -0
- package/features/payments/lib/polar-products.ts +49 -0
- package/features/payments/lib/subscription.ts +148 -0
- package/features/payments/payments-schema.ts +30 -0
- package/features/seo/README.md +302 -0
- package/features/seo/app/blog/[slug]/page.tsx +314 -0
- package/features/seo/app/blog/page.tsx +107 -0
- package/features/seo/app/robots.txt +13 -0
- package/features/seo/app/sitemap.ts +70 -0
- package/features/seo/feature.config.json +19 -0
- package/features/seo/lib/seo-utils.ts +163 -0
- package/package.json +3 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "@/components/ui/card";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import { Progress } from "@/components/ui/progress";
|
|
13
|
+
import { Check, FileImage, Upload, X } from "lucide-react";
|
|
14
|
+
import Image from "next/image";
|
|
15
|
+
import { useCallback, useState } from "react";
|
|
16
|
+
import { toast } from "sonner";
|
|
17
|
+
|
|
18
|
+
interface UploadedFile {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
url: string;
|
|
22
|
+
size: number;
|
|
23
|
+
type: string;
|
|
24
|
+
uploadedAt: Date;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function UploadPage() {
|
|
28
|
+
const [uploading, setUploading] = useState(false);
|
|
29
|
+
const [uploadProgress, setUploadProgress] = useState(0);
|
|
30
|
+
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
|
|
31
|
+
const [dragActive, setDragActive] = useState(false);
|
|
32
|
+
|
|
33
|
+
const handleFileUpload = async (files: FileList | File[]) => {
|
|
34
|
+
const fileArray = Array.from(files);
|
|
35
|
+
|
|
36
|
+
for (const file of fileArray) {
|
|
37
|
+
if (!file.type.startsWith("image/")) {
|
|
38
|
+
toast.error(`${file.name} is not an image file`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
43
|
+
toast.error(`${file.name} is too large (max 5MB)`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setUploading(true);
|
|
48
|
+
setUploadProgress(0);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const formData = new FormData();
|
|
52
|
+
formData.append("file", file);
|
|
53
|
+
|
|
54
|
+
// Simulate progress
|
|
55
|
+
const progressInterval = setInterval(() => {
|
|
56
|
+
setUploadProgress((prev) => {
|
|
57
|
+
if (prev >= 90) {
|
|
58
|
+
clearInterval(progressInterval);
|
|
59
|
+
return prev;
|
|
60
|
+
}
|
|
61
|
+
return prev + Math.random() * 20;
|
|
62
|
+
});
|
|
63
|
+
}, 200);
|
|
64
|
+
|
|
65
|
+
const response = await fetch("/api/upload-image", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: formData,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
clearInterval(progressInterval);
|
|
71
|
+
setUploadProgress(100);
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error("Upload failed");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { url } = await response.json();
|
|
78
|
+
|
|
79
|
+
const uploadedFile: UploadedFile = {
|
|
80
|
+
id: crypto.randomUUID(),
|
|
81
|
+
name: file.name,
|
|
82
|
+
url,
|
|
83
|
+
size: file.size,
|
|
84
|
+
type: file.type,
|
|
85
|
+
uploadedAt: new Date(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
setUploadedFiles((prev) => [uploadedFile, ...prev]);
|
|
89
|
+
toast.success(`${file.name} uploaded successfully`);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error("Upload error:", error);
|
|
92
|
+
toast.error(`Failed to upload ${file.name}`);
|
|
93
|
+
} finally {
|
|
94
|
+
setUploading(false);
|
|
95
|
+
setUploadProgress(0);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleDrag = useCallback((e: React.DragEvent) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
e.stopPropagation();
|
|
103
|
+
if (e.type === "dragenter" || e.type === "dragover") {
|
|
104
|
+
setDragActive(true);
|
|
105
|
+
} else if (e.type === "dragleave") {
|
|
106
|
+
setDragActive(false);
|
|
107
|
+
}
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
e.stopPropagation();
|
|
113
|
+
setDragActive(false);
|
|
114
|
+
|
|
115
|
+
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
|
116
|
+
handleFileUpload(e.dataTransfer.files);
|
|
117
|
+
}
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
121
|
+
if (e.target.files && e.target.files[0]) {
|
|
122
|
+
handleFileUpload(e.target.files);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const removeFile = (id: string) => {
|
|
127
|
+
setUploadedFiles((prev) => prev.filter((file) => file.id !== id));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const formatFileSize = (bytes: number) => {
|
|
131
|
+
if (bytes === 0) return "0 Bytes";
|
|
132
|
+
const k = 1024;
|
|
133
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
134
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
135
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="p-6 space-y-6">
|
|
140
|
+
<div>
|
|
141
|
+
<h1 className="text-3xl font-semibold tracking-tight">File Upload</h1>
|
|
142
|
+
<p className="text-muted-foreground mt-2">
|
|
143
|
+
Upload images to Cloudflare R2 storage with drag and drop support
|
|
144
|
+
</p>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
148
|
+
{/* Upload Area */}
|
|
149
|
+
<Card>
|
|
150
|
+
<CardHeader>
|
|
151
|
+
<CardTitle className="flex items-center gap-2">
|
|
152
|
+
<Upload className="h-5 w-5" />
|
|
153
|
+
Upload Images
|
|
154
|
+
</CardTitle>
|
|
155
|
+
<CardDescription>
|
|
156
|
+
Upload images to Cloudflare R2. Maximum file size is 5MB.
|
|
157
|
+
</CardDescription>
|
|
158
|
+
</CardHeader>
|
|
159
|
+
<CardContent className="space-y-4">
|
|
160
|
+
<div
|
|
161
|
+
className={`relative border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
|
162
|
+
dragActive
|
|
163
|
+
? "border-primary bg-primary/5"
|
|
164
|
+
: "border-muted-foreground/25 hover:border-muted-foreground/50"
|
|
165
|
+
}`}
|
|
166
|
+
onDragEnter={handleDrag}
|
|
167
|
+
onDragLeave={handleDrag}
|
|
168
|
+
onDragOver={handleDrag}
|
|
169
|
+
onDrop={handleDrop}
|
|
170
|
+
>
|
|
171
|
+
<Input
|
|
172
|
+
type="file"
|
|
173
|
+
accept="image/*"
|
|
174
|
+
multiple
|
|
175
|
+
onChange={handleInputChange}
|
|
176
|
+
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
177
|
+
disabled={uploading}
|
|
178
|
+
/>
|
|
179
|
+
<div className="space-y-2">
|
|
180
|
+
<FileImage className="h-10 w-10 mx-auto text-muted-foreground" />
|
|
181
|
+
<div>
|
|
182
|
+
<p className="text-sm font-medium">
|
|
183
|
+
{dragActive
|
|
184
|
+
? "Drop files here"
|
|
185
|
+
: "Click to upload or drag and drop"}
|
|
186
|
+
</p>
|
|
187
|
+
<p className="text-xs text-muted-foreground">
|
|
188
|
+
PNG, JPG, GIF up to 5MB
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{uploading && (
|
|
195
|
+
<div className="space-y-2">
|
|
196
|
+
<div className="flex items-center justify-between text-sm">
|
|
197
|
+
<span>Uploading...</span>
|
|
198
|
+
<span>{Math.round(uploadProgress)}%</span>
|
|
199
|
+
</div>
|
|
200
|
+
<Progress value={uploadProgress} className="h-2" />
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</CardContent>
|
|
204
|
+
</Card>
|
|
205
|
+
|
|
206
|
+
{/* Upload Info */}
|
|
207
|
+
<Card>
|
|
208
|
+
<CardHeader>
|
|
209
|
+
<CardTitle>About R2 Storage</CardTitle>
|
|
210
|
+
<CardDescription>
|
|
211
|
+
Cloudflare R2 provides S3-compatible object storage
|
|
212
|
+
</CardDescription>
|
|
213
|
+
</CardHeader>
|
|
214
|
+
<CardContent className="space-y-4">
|
|
215
|
+
<div className="space-y-3">
|
|
216
|
+
<div className="flex items-start gap-3">
|
|
217
|
+
<Check className="h-4 w-4 text-green-500 mt-0.5" />
|
|
218
|
+
<div className="text-sm">
|
|
219
|
+
<p className="font-medium">Global CDN</p>
|
|
220
|
+
<p className="text-muted-foreground">
|
|
221
|
+
Fast delivery worldwide
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="flex items-start gap-3">
|
|
226
|
+
<Check className="h-4 w-4 text-green-500 mt-0.5" />
|
|
227
|
+
<div className="text-sm">
|
|
228
|
+
<p className="font-medium">Zero Egress Fees</p>
|
|
229
|
+
<p className="text-muted-foreground">No bandwidth charges</p>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div className="flex items-start gap-3">
|
|
233
|
+
<Check className="h-4 w-4 text-green-500 mt-0.5" />
|
|
234
|
+
<div className="text-sm">
|
|
235
|
+
<p className="font-medium">S3 Compatible</p>
|
|
236
|
+
<p className="text-muted-foreground">
|
|
237
|
+
Works with existing tools
|
|
238
|
+
</p>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex items-start gap-3">
|
|
242
|
+
<Check className="h-4 w-4 text-green-500 mt-0.5" />
|
|
243
|
+
<div className="text-sm">
|
|
244
|
+
<p className="font-medium">Auto Scaling</p>
|
|
245
|
+
<p className="text-muted-foreground">Handles any file size</p>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</CardContent>
|
|
250
|
+
</Card>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Uploaded Files */}
|
|
254
|
+
{uploadedFiles.length > 0 && (
|
|
255
|
+
<Card>
|
|
256
|
+
<CardHeader>
|
|
257
|
+
<CardTitle>Uploaded Files ({uploadedFiles.length})</CardTitle>
|
|
258
|
+
<CardDescription>
|
|
259
|
+
Recently uploaded images to R2 storage
|
|
260
|
+
</CardDescription>
|
|
261
|
+
</CardHeader>
|
|
262
|
+
<CardContent>
|
|
263
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
264
|
+
{uploadedFiles.map((file) => (
|
|
265
|
+
<div
|
|
266
|
+
key={file.id}
|
|
267
|
+
className="group relative border rounded-lg overflow-hidden hover:shadow-md transition-shadow"
|
|
268
|
+
>
|
|
269
|
+
<div className="aspect-video relative bg-muted">
|
|
270
|
+
<Image
|
|
271
|
+
src={file.url}
|
|
272
|
+
alt={file.name}
|
|
273
|
+
fill
|
|
274
|
+
className="object-cover"
|
|
275
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="p-3">
|
|
279
|
+
<p
|
|
280
|
+
className="font-medium text-sm truncate"
|
|
281
|
+
title={file.name}
|
|
282
|
+
>
|
|
283
|
+
{file.name}
|
|
284
|
+
</p>
|
|
285
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground mt-1">
|
|
286
|
+
<span>{formatFileSize(file.size)}</span>
|
|
287
|
+
<span>{file.uploadedAt.toLocaleDateString()}</span>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="mt-2 flex items-center gap-2">
|
|
290
|
+
<Button
|
|
291
|
+
size="sm"
|
|
292
|
+
variant="outline"
|
|
293
|
+
onClick={() => navigator.clipboard.writeText(file.url)}
|
|
294
|
+
className="flex-1 text-xs"
|
|
295
|
+
>
|
|
296
|
+
Copy URL
|
|
297
|
+
</Button>
|
|
298
|
+
<Button
|
|
299
|
+
size="sm"
|
|
300
|
+
variant="outline"
|
|
301
|
+
onClick={() => window.open(file.url, "_blank")}
|
|
302
|
+
className="flex-1 text-xs"
|
|
303
|
+
>
|
|
304
|
+
Open
|
|
305
|
+
</Button>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
<Button
|
|
309
|
+
size="sm"
|
|
310
|
+
variant="ghost"
|
|
311
|
+
onClick={() => removeFile(file.id)}
|
|
312
|
+
className="absolute top-2 right-2 h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity bg-background/80 hover:bg-background"
|
|
313
|
+
>
|
|
314
|
+
<X className="h-4 w-4" />
|
|
315
|
+
</Button>
|
|
316
|
+
</div>
|
|
317
|
+
))}
|
|
318
|
+
</div>
|
|
319
|
+
</CardContent>
|
|
320
|
+
</Card>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "file-upload",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cloudflare R2 file upload with drag-and-drop interface",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@aws-sdk/client-s3": "^3.800.0"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {},
|
|
9
|
+
"envVars": [
|
|
10
|
+
"R2_UPLOAD_IMAGE_ACCESS_KEY_ID",
|
|
11
|
+
"R2_UPLOAD_IMAGE_SECRET_ACCESS_KEY",
|
|
12
|
+
"CLOUDFLARE_ACCOUNT_ID",
|
|
13
|
+
"R2_UPLOAD_IMAGE_BUCKET_NAME"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
"app/api/upload-image/**/*",
|
|
17
|
+
"app/dashboard/upload/**/*",
|
|
18
|
+
"lib/upload-image.ts"
|
|
19
|
+
],
|
|
20
|
+
"requires": [],
|
|
21
|
+
"conflicts": []
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
S3Client,
|
|
3
|
+
PutObjectCommand,
|
|
4
|
+
} from "@aws-sdk/client-s3";
|
|
5
|
+
|
|
6
|
+
const r2 = new S3Client({
|
|
7
|
+
region: "auto", // required for R2
|
|
8
|
+
endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
9
|
+
credentials: {
|
|
10
|
+
accessKeyId: process.env.R2_UPLOAD_IMAGE_ACCESS_KEY_ID!,
|
|
11
|
+
secretAccessKey: process.env.R2_UPLOAD_IMAGE_SECRET_ACCESS_KEY!,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const uploadImageAssets = async (buffer: Buffer, key: string) => {
|
|
16
|
+
await r2.send(
|
|
17
|
+
new PutObjectCommand({
|
|
18
|
+
Bucket: process.env.R2_UPLOAD_IMAGE_BUCKET_NAME!,
|
|
19
|
+
Key: key,
|
|
20
|
+
Body: buffer,
|
|
21
|
+
ContentType: "image/*",
|
|
22
|
+
ACL: "public-read", // optional if bucket is public
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const publicUrl = `https://pub-6f0cf05705c7412b93a792350f3b3aa5.r2.dev/${key}`;
|
|
27
|
+
return publicUrl;
|
|
28
|
+
};
|