shipd 0.1.3 → 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.
- 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/features/ai-chat/README.md +258 -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 +308 -0
- package/features/analytics/feature.config.json +20 -0
- package/features/analytics/lib/posthog.ts +36 -0
- package/features/auth/README.md +336 -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 +256 -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 +282 -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 +271 -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 +266 -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 +306 -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 +244 -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,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
|
+
|