shipd 0.1.2 → 0.1.4

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 (115) hide show
  1. package/base-package/app/globals.css +126 -0
  2. package/base-package/app/layout.tsx +53 -0
  3. package/base-package/app/page.tsx +15 -0
  4. package/base-package/base.config.json +57 -0
  5. package/base-package/components/ui/avatar.tsx +53 -0
  6. package/base-package/components/ui/badge.tsx +46 -0
  7. package/base-package/components/ui/button.tsx +59 -0
  8. package/base-package/components/ui/card.tsx +92 -0
  9. package/base-package/components/ui/chart.tsx +353 -0
  10. package/base-package/components/ui/checkbox.tsx +32 -0
  11. package/base-package/components/ui/dialog.tsx +135 -0
  12. package/base-package/components/ui/dropdown-menu.tsx +257 -0
  13. package/base-package/components/ui/form.tsx +167 -0
  14. package/base-package/components/ui/input.tsx +21 -0
  15. package/base-package/components/ui/label.tsx +24 -0
  16. package/base-package/components/ui/progress.tsx +31 -0
  17. package/base-package/components/ui/resizable.tsx +56 -0
  18. package/base-package/components/ui/select.tsx +185 -0
  19. package/base-package/components/ui/separator.tsx +28 -0
  20. package/base-package/components/ui/sheet.tsx +139 -0
  21. package/base-package/components/ui/skeleton.tsx +13 -0
  22. package/base-package/components/ui/sonner.tsx +25 -0
  23. package/base-package/components/ui/switch.tsx +31 -0
  24. package/base-package/components/ui/tabs.tsx +66 -0
  25. package/base-package/components/ui/textarea.tsx +18 -0
  26. package/base-package/components/ui/toggle-group.tsx +73 -0
  27. package/base-package/components/ui/toggle.tsx +47 -0
  28. package/base-package/components/ui/tooltip.tsx +61 -0
  29. package/base-package/components.json +21 -0
  30. package/base-package/eslint.config.mjs +16 -0
  31. package/base-package/lib/utils.ts +6 -0
  32. package/base-package/middleware.ts +12 -0
  33. package/base-package/next.config.ts +27 -0
  34. package/base-package/package.json +49 -0
  35. package/base-package/postcss.config.mjs +5 -0
  36. package/base-package/public/favicon.svg +4 -0
  37. package/base-package/tailwind.config.ts +89 -0
  38. package/base-package/tsconfig.json +27 -0
  39. package/dist/index.js +1862 -948
  40. package/features/ai-chat/README.md +258 -0
  41. package/features/ai-chat/app/api/chat/route.ts +16 -0
  42. package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
  43. package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
  44. package/features/ai-chat/feature.config.json +22 -0
  45. package/features/analytics/README.md +308 -0
  46. package/features/analytics/feature.config.json +20 -0
  47. package/features/analytics/lib/posthog.ts +36 -0
  48. package/features/auth/README.md +336 -0
  49. package/features/auth/app/api/auth/[...all]/route.ts +4 -0
  50. package/features/auth/app/dashboard/layout.tsx +15 -0
  51. package/features/auth/app/dashboard/page.tsx +140 -0
  52. package/features/auth/app/sign-in/page.tsx +228 -0
  53. package/features/auth/app/sign-up/page.tsx +243 -0
  54. package/features/auth/auth-schema.ts +47 -0
  55. package/features/auth/components/auth/setup-instructions.tsx +123 -0
  56. package/features/auth/feature.config.json +33 -0
  57. package/features/auth/lib/auth-client.ts +8 -0
  58. package/features/auth/lib/auth.ts +295 -0
  59. package/features/auth/lib/email-stub.ts +55 -0
  60. package/features/auth/lib/email.ts +47 -0
  61. package/features/auth/middleware.patch.ts +43 -0
  62. package/features/database/README.md +256 -0
  63. package/features/database/db/drizzle.ts +48 -0
  64. package/features/database/db/schema.ts +21 -0
  65. package/features/database/drizzle.config.ts +13 -0
  66. package/features/database/feature.config.json +30 -0
  67. package/features/email/README.md +282 -0
  68. package/features/email/emails/components/layout.tsx +181 -0
  69. package/features/email/emails/password-reset.tsx +67 -0
  70. package/features/email/emails/payment-failed.tsx +167 -0
  71. package/features/email/emails/subscription-confirmation.tsx +129 -0
  72. package/features/email/emails/welcome.tsx +100 -0
  73. package/features/email/feature.config.json +22 -0
  74. package/features/email/lib/email.ts +118 -0
  75. package/features/file-upload/README.md +271 -0
  76. package/features/file-upload/app/api/upload-image/route.ts +64 -0
  77. package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
  78. package/features/file-upload/feature.config.json +23 -0
  79. package/features/file-upload/lib/upload-image.ts +28 -0
  80. package/features/marketing-landing/README.md +266 -0
  81. package/features/marketing-landing/app/page.tsx +25 -0
  82. package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
  83. package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
  84. package/features/marketing-landing/components/homepage/footer.tsx +53 -0
  85. package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
  86. package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
  87. package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
  88. package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
  89. package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
  90. package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
  91. package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
  92. package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
  93. package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
  94. package/features/marketing-landing/components/logos/Polar.tsx +7 -0
  95. package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
  96. package/features/marketing-landing/components/logos/index.ts +6 -0
  97. package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
  98. package/features/marketing-landing/feature.config.json +23 -0
  99. package/features/payments/README.md +306 -0
  100. package/features/payments/app/api/subscription/route.ts +25 -0
  101. package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
  102. package/features/payments/app/dashboard/payment/page.tsx +126 -0
  103. package/features/payments/app/success/page.tsx +123 -0
  104. package/features/payments/feature.config.json +31 -0
  105. package/features/payments/lib/polar-products.ts +49 -0
  106. package/features/payments/lib/subscription.ts +148 -0
  107. package/features/payments/payments-schema.ts +30 -0
  108. package/features/seo/README.md +244 -0
  109. package/features/seo/app/blog/[slug]/page.tsx +314 -0
  110. package/features/seo/app/blog/page.tsx +107 -0
  111. package/features/seo/app/robots.txt +13 -0
  112. package/features/seo/app/sitemap.ts +70 -0
  113. package/features/seo/feature.config.json +19 -0
  114. package/features/seo/lib/seo-utils.ts +163 -0
  115. package/package.json +3 -1
@@ -0,0 +1,271 @@
1
+ # File Upload Module - Integration Guide
2
+
3
+ **Module Version:** 1.0.0
4
+ **Last Updated:** 2025-12-22
5
+ **Standalone:** ✅ Yes (can work independently)
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ This module adds file upload functionality using Cloudflare R2 (S3-compatible object storage). It includes a drag-and-drop upload interface, progress tracking, and image preview.
12
+
13
+ **Key Features:**
14
+ - Cloudflare R2 integration (S3-compatible)
15
+ - Drag-and-drop file upload
16
+ - Image upload with validation
17
+ - Progress tracking
18
+ - File size validation (5MB max)
19
+ - Image preview gallery
20
+ - Public URL generation
21
+
22
+ ---
23
+
24
+ ## Files Added
25
+
26
+ ### Pages/Routes
27
+ - `app/dashboard/upload/page.tsx` - File upload interface with drag-and-drop
28
+
29
+ ### API Routes
30
+ - `app/api/upload-image/route.ts` - Image upload API endpoint
31
+
32
+ ### Utilities/Libraries
33
+ - `lib/upload-image.ts` - R2 upload utility functions
34
+
35
+ ---
36
+
37
+ ## Dependencies
38
+
39
+ ### Package Dependencies
40
+ The following packages will be added to `package.json`:
41
+ - `@aws-sdk/client-s3@^3.800.0` - AWS SDK for S3-compatible storage (R2)
42
+
43
+ ### Required Modules
44
+ - None - File Upload module is standalone
45
+
46
+ ### Optional Integration
47
+ - **Auth Module** - For protecting upload routes (works without it, but uploads should be protected)
48
+
49
+ ---
50
+
51
+ ## Environment Variables
52
+
53
+ Add these to your `.env.local`:
54
+
55
+ ```env
56
+ # File Upload (Cloudflare R2)
57
+ R2_UPLOAD_IMAGE_ACCESS_KEY_ID="your-r2-access-key-id"
58
+ R2_UPLOAD_IMAGE_SECRET_ACCESS_KEY="your-r2-secret-access-key"
59
+ CLOUDFLARE_ACCOUNT_ID="your-cloudflare-account-id"
60
+ R2_UPLOAD_IMAGE_BUCKET_NAME="your-bucket-name"
61
+ ```
62
+
63
+ **Required Variables:**
64
+ - `R2_UPLOAD_IMAGE_ACCESS_KEY_ID` - R2 access key ID
65
+ - `R2_UPLOAD_IMAGE_SECRET_ACCESS_KEY` - R2 secret access key
66
+ - `CLOUDFLARE_ACCOUNT_ID` - Your Cloudflare account ID
67
+ - `R2_UPLOAD_IMAGE_BUCKET_NAME` - Your R2 bucket name
68
+
69
+ **Where to get them:**
70
+ - **Cloudflare R2**: [dash.cloudflare.com](https://dash.cloudflare.com) - Create R2 bucket and API tokens
71
+ - Create an R2 bucket in Cloudflare dashboard
72
+ - Generate API tokens with read/write permissions
73
+ - Get account ID from Cloudflare dashboard
74
+
75
+ **Note:** You'll also need to configure a public domain for your R2 bucket to generate public URLs. Update `lib/upload-image.ts` with your public R2 domain.
76
+
77
+ ---
78
+
79
+ ## Manual Integration Steps
80
+
81
+ If you're appending this module to an existing project, follow these steps:
82
+
83
+ ### Step 1: Install Dependencies
84
+
85
+ ```bash
86
+ npm install
87
+ # or
88
+ pnpm install
89
+ # or
90
+ yarn install
91
+ ```
92
+
93
+ The append command automatically adds missing dependencies to `package.json`, but you need to install them.
94
+
95
+ ### Step 2: Set Up Cloudflare R2
96
+
97
+ 1. Create a Cloudflare account (if you don't have one)
98
+ 2. Navigate to R2 in the Cloudflare dashboard
99
+ 3. Create a new R2 bucket
100
+ 4. Generate API tokens with read/write permissions
101
+ 5. Get your account ID from the dashboard
102
+
103
+ ### Step 3: Configure Public Domain (Optional)
104
+
105
+ To generate public URLs, you need to configure a custom domain for your R2 bucket:
106
+
107
+ 1. In R2 dashboard, go to your bucket settings
108
+ 2. Configure a custom domain or use R2.dev domain
109
+ 3. Update `lib/upload-image.ts` with your public domain:
110
+
111
+ ```typescript
112
+ const publicUrl = `https://your-custom-domain.com/${key}`;
113
+ // or
114
+ const publicUrl = `https://pub-xxxxx.r2.dev/${key}`;
115
+ ```
116
+
117
+ ### Step 4: Add Environment Variables
118
+
119
+ 1. Copy `.env.example` to `.env.local` (if not exists)
120
+ 2. Add all required R2 environment variables
121
+ 3. Restart dev server after adding env vars
122
+
123
+ ### Step 5: Verify Installation
124
+
125
+ 1. Check that `app/api/upload-image/route.ts` exists
126
+ 2. Check that `app/dashboard/upload/page.tsx` exists
127
+ 3. Check that `lib/upload-image.ts` exists
128
+ 4. Start your dev server: `npm run dev`
129
+ 5. Visit `http://localhost:3000/dashboard/upload` to test uploads
130
+
131
+ ---
132
+
133
+ ## Usage Examples
134
+
135
+ ### Using the Upload Page
136
+
137
+ 1. Navigate to `/dashboard/upload`
138
+ 2. Drag and drop images or click to select
139
+ 3. Watch upload progress
140
+ 4. View uploaded images in the gallery
141
+ 5. Copy image URLs or open in new tab
142
+
143
+ ### Using Upload Function Programmatically
144
+
145
+ ```typescript
146
+ import { uploadImageAssets } from "@/lib/upload-image";
147
+
148
+ // Upload a file buffer
149
+ const buffer = Buffer.from(fileData);
150
+ const filename = "my-image.png";
151
+ const url = await uploadImageAssets(buffer, filename);
152
+ console.log("Uploaded to:", url);
153
+ ```
154
+
155
+ ### Customizing Upload Limits
156
+
157
+ Edit `app/api/upload-image/route.ts` to change limits:
158
+
159
+ ```typescript
160
+ // Change max file size (default: 10MB)
161
+ const maxSizeInBytes = 20 * 1024 * 1024; // 20MB
162
+
163
+ // Change allowed MIME types
164
+ const allowedMimeTypes = [
165
+ "image/jpeg",
166
+ "image/png",
167
+ // Add more types
168
+ ];
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Customization
174
+
175
+ ### Styling
176
+ - Upload UI uses Tailwind CSS classes
177
+ - Customize colors in `tailwind.config.ts`
178
+ - Modify upload area styles in `app/dashboard/upload/page.tsx`
179
+
180
+ ### Configuration
181
+ - File size limits in `app/api/upload-image/route.ts`
182
+ - Allowed file types in `app/api/upload-image/route.ts`
183
+ - Public URL generation in `lib/upload-image.ts`
184
+
185
+ ### Features
186
+ - Drag-and-drop is built-in
187
+ - Progress tracking is automatic
188
+ - Image preview gallery is included
189
+
190
+ ---
191
+
192
+ ## Integration Points
193
+
194
+ ### Files That May Need Manual Updates
195
+
196
+ **lib/upload-image.ts**:
197
+ - Update public URL domain to match your R2 bucket configuration
198
+ - Customize bucket settings if needed
199
+
200
+ **app/api/upload-image/route.ts**:
201
+ - Adjust file size limits
202
+ - Modify allowed file types
203
+ - Add authentication checks if needed
204
+
205
+ **Middleware** (if auth module installed):
206
+ - Upload routes should be protected
207
+ - Add `/dashboard/upload` to protected routes if needed
208
+
209
+ ---
210
+
211
+ ## Troubleshooting
212
+
213
+ ### Common Issues
214
+
215
+ **Issue:** "R2_UPLOAD_IMAGE_ACCESS_KEY_ID is not defined" error
216
+ **Solution:**
217
+ - Add all R2 environment variables to `.env.local`
218
+ - Get credentials from Cloudflare R2 dashboard
219
+ - Restart dev server after adding env vars
220
+
221
+ **Issue:** Upload fails with "Access Denied"
222
+ **Solution:**
223
+ - Verify API tokens have correct permissions
224
+ - Check bucket name is correct
225
+ - Ensure account ID is correct
226
+
227
+ **Issue:** Public URLs not working
228
+ **Solution:**
229
+ - Configure public domain for R2 bucket
230
+ - Update `lib/upload-image.ts` with correct public domain
231
+ - Ensure bucket has public read access
232
+
233
+ **Issue:** "File too large" error
234
+ **Solution:**
235
+ - Check file size limits in `app/api/upload-image/route.ts`
236
+ - Adjust `maxSizeInBytes` if needed
237
+ - Client-side validation also checks 5MB limit
238
+
239
+ ---
240
+
241
+ ## Next Steps
242
+
243
+ After installing this module:
244
+
245
+ 1. Set up Cloudflare R2 bucket
246
+ 2. Generate API tokens
247
+ 3. Add environment variables to `.env.local`
248
+ 4. Configure public domain for bucket
249
+ 5. Update public URL in `lib/upload-image.ts`
250
+ 6. Test upload functionality
251
+ 7. Add authentication if needed
252
+
253
+ ---
254
+
255
+ ## Related Modules
256
+
257
+ This module works well with:
258
+ - **Auth Module** - For protecting upload routes
259
+
260
+ **Note:** File Upload module is optional and standalone. No other modules are required.
261
+
262
+ ---
263
+
264
+ ## Module Status
265
+
266
+ ✅ **Standalone Package** - Can be installed independently
267
+ ✅ **Drag & Drop** - Built-in drag-and-drop interface
268
+ ✅ **Progress Tracking** - Real-time upload progress
269
+ ✅ **Image Preview** - Gallery of uploaded images
270
+ ✅ **Well Documented** - Comprehensive integration guide
271
+
@@ -0,0 +1,64 @@
1
+ import { uploadImageAssets } from "@/lib/upload-image";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+
4
+ export const config = {
5
+ api: { bodyParser: false }, // Disable default body parsing
6
+ };
7
+
8
+ export async function POST(req: NextRequest) {
9
+ try {
10
+ // Parse the form data
11
+ const formData = await req.formData();
12
+ const file = formData.get("file") as File | null;
13
+
14
+ if (!file) {
15
+ return NextResponse.json({ error: "No file provided" }, { status: 400 });
16
+ }
17
+
18
+ // Validate MIME type - only allow image files
19
+ const allowedMimeTypes = [
20
+ "image/jpeg",
21
+ "image/jpg",
22
+ "image/png",
23
+ "image/gif",
24
+ "image/webp",
25
+ "image/svg+xml",
26
+ ];
27
+
28
+ if (!allowedMimeTypes.includes(file.type)) {
29
+ return NextResponse.json(
30
+ { error: "Invalid file type. Only image files are allowed." },
31
+ { status: 400 },
32
+ );
33
+ }
34
+
35
+ // Validate file size - limit to 10MB
36
+ const maxSizeInBytes = 10 * 1024 * 1024; // 10MB
37
+ if (file.size > maxSizeInBytes) {
38
+ return NextResponse.json(
39
+ { error: "File too large. Maximum size allowed is 10MB." },
40
+ { status: 400 },
41
+ );
42
+ }
43
+
44
+ // Convert file to buffer
45
+ const arrayBuffer = await file.arrayBuffer();
46
+ const buffer = Buffer.from(arrayBuffer);
47
+
48
+ // Generate a unique filename with original extension
49
+ const fileExt = file.name.split(".").pop() || "";
50
+ const timestamp = Date.now();
51
+ const filename = `upload-${timestamp}.${fileExt || "png"}`;
52
+
53
+ // Upload the file
54
+ const url = await uploadImageAssets(buffer, filename);
55
+
56
+ return NextResponse.json({ url });
57
+ } catch (error) {
58
+ console.error("Upload error:", error);
59
+ return NextResponse.json(
60
+ { error: "Failed to process upload" },
61
+ { status: 500 },
62
+ );
63
+ }
64
+ }
@@ -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
+