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.
Files changed (116) 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 +1858 -956
  40. package/docs-template/README.md +74 -0
  41. package/features/ai-chat/README.md +316 -0
  42. package/features/ai-chat/app/api/chat/route.ts +16 -0
  43. package/features/ai-chat/app/dashboard/_components/chatbot.tsx +39 -0
  44. package/features/ai-chat/app/dashboard/chat/page.tsx +73 -0
  45. package/features/ai-chat/feature.config.json +22 -0
  46. package/features/analytics/README.md +364 -0
  47. package/features/analytics/feature.config.json +20 -0
  48. package/features/analytics/lib/posthog.ts +36 -0
  49. package/features/auth/README.md +409 -0
  50. package/features/auth/app/api/auth/[...all]/route.ts +4 -0
  51. package/features/auth/app/dashboard/layout.tsx +15 -0
  52. package/features/auth/app/dashboard/page.tsx +140 -0
  53. package/features/auth/app/sign-in/page.tsx +228 -0
  54. package/features/auth/app/sign-up/page.tsx +243 -0
  55. package/features/auth/auth-schema.ts +47 -0
  56. package/features/auth/components/auth/setup-instructions.tsx +123 -0
  57. package/features/auth/feature.config.json +33 -0
  58. package/features/auth/lib/auth-client.ts +8 -0
  59. package/features/auth/lib/auth.ts +295 -0
  60. package/features/auth/lib/email-stub.ts +55 -0
  61. package/features/auth/lib/email.ts +47 -0
  62. package/features/auth/middleware.patch.ts +43 -0
  63. package/features/database/README.md +312 -0
  64. package/features/database/db/drizzle.ts +48 -0
  65. package/features/database/db/schema.ts +21 -0
  66. package/features/database/drizzle.config.ts +13 -0
  67. package/features/database/feature.config.json +30 -0
  68. package/features/email/README.md +341 -0
  69. package/features/email/emails/components/layout.tsx +181 -0
  70. package/features/email/emails/password-reset.tsx +67 -0
  71. package/features/email/emails/payment-failed.tsx +167 -0
  72. package/features/email/emails/subscription-confirmation.tsx +129 -0
  73. package/features/email/emails/welcome.tsx +100 -0
  74. package/features/email/feature.config.json +22 -0
  75. package/features/email/lib/email.ts +118 -0
  76. package/features/file-upload/README.md +329 -0
  77. package/features/file-upload/app/api/upload-image/route.ts +64 -0
  78. package/features/file-upload/app/dashboard/upload/page.tsx +324 -0
  79. package/features/file-upload/feature.config.json +23 -0
  80. package/features/file-upload/lib/upload-image.ts +28 -0
  81. package/features/marketing-landing/README.md +333 -0
  82. package/features/marketing-landing/app/page.tsx +25 -0
  83. package/features/marketing-landing/components/homepage/cli-workflow-section.tsx +231 -0
  84. package/features/marketing-landing/components/homepage/features-section.tsx +152 -0
  85. package/features/marketing-landing/components/homepage/footer.tsx +53 -0
  86. package/features/marketing-landing/components/homepage/hero-section.tsx +112 -0
  87. package/features/marketing-landing/components/homepage/integrations.tsx +124 -0
  88. package/features/marketing-landing/components/homepage/navigation.tsx +116 -0
  89. package/features/marketing-landing/components/homepage/news-section.tsx +82 -0
  90. package/features/marketing-landing/components/homepage/pricing-section.tsx +98 -0
  91. package/features/marketing-landing/components/homepage/testimonials-section.tsx +34 -0
  92. package/features/marketing-landing/components/logos/BetterAuth.tsx +21 -0
  93. package/features/marketing-landing/components/logos/NeonPostgres.tsx +41 -0
  94. package/features/marketing-landing/components/logos/Nextjs.tsx +72 -0
  95. package/features/marketing-landing/components/logos/Polar.tsx +7 -0
  96. package/features/marketing-landing/components/logos/TailwindCSS.tsx +27 -0
  97. package/features/marketing-landing/components/logos/index.ts +6 -0
  98. package/features/marketing-landing/components/logos/shadcnui.tsx +8 -0
  99. package/features/marketing-landing/feature.config.json +23 -0
  100. package/features/payments/README.md +375 -0
  101. package/features/payments/app/api/subscription/route.ts +25 -0
  102. package/features/payments/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
  103. package/features/payments/app/dashboard/payment/page.tsx +126 -0
  104. package/features/payments/app/success/page.tsx +123 -0
  105. package/features/payments/feature.config.json +31 -0
  106. package/features/payments/lib/polar-products.ts +49 -0
  107. package/features/payments/lib/subscription.ts +148 -0
  108. package/features/payments/payments-schema.ts +30 -0
  109. package/features/seo/README.md +302 -0
  110. package/features/seo/app/blog/[slug]/page.tsx +314 -0
  111. package/features/seo/app/blog/page.tsx +107 -0
  112. package/features/seo/app/robots.txt +13 -0
  113. package/features/seo/app/sitemap.ts +70 -0
  114. package/features/seo/feature.config.json +19 -0
  115. package/features/seo/lib/seo-utils.ts +163 -0
  116. 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
+ };