shipd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1366 -0
- package/docs-template/README.md +255 -0
- package/docs-template/[slug]/[subslug]/page.tsx +1242 -0
- package/docs-template/[slug]/page.tsx +422 -0
- package/docs-template/api/page.tsx +47 -0
- package/docs-template/components/docs/docs-category-page.tsx +162 -0
- package/docs-template/components/docs/docs-code-card.tsx +135 -0
- package/docs-template/components/docs/docs-header.tsx +69 -0
- package/docs-template/components/docs/docs-nav.ts +95 -0
- package/docs-template/components/docs/docs-sidebar.tsx +112 -0
- package/docs-template/components/docs/docs-toc.tsx +38 -0
- package/docs-template/components/ui/badge.tsx +47 -0
- package/docs-template/components/ui/button.tsx +60 -0
- package/docs-template/components/ui/card.tsx +93 -0
- package/docs-template/components/ui/sheet.tsx +140 -0
- package/docs-template/documentation/page.tsx +80 -0
- package/docs-template/layout.tsx +27 -0
- package/docs-template/lib/utils.ts +7 -0
- package/docs-template/page.tsx +360 -0
- package/package.json +66 -0
- package/template/.env.example +45 -0
- package/template/README.md +239 -0
- package/template/app/api/auth/[...all]/route.ts +4 -0
- package/template/app/api/chat/route.ts +16 -0
- package/template/app/api/subscription/route.ts +25 -0
- package/template/app/api/upload-image/route.ts +64 -0
- package/template/app/blog/[slug]/page.tsx +314 -0
- package/template/app/blog/page.tsx +107 -0
- package/template/app/dashboard/_components/chart-interactive.tsx +289 -0
- package/template/app/dashboard/_components/chatbot.tsx +39 -0
- package/template/app/dashboard/_components/mode-toggle.tsx +46 -0
- package/template/app/dashboard/_components/navbar.tsx +84 -0
- package/template/app/dashboard/_components/section-cards.tsx +102 -0
- package/template/app/dashboard/_components/sidebar.tsx +90 -0
- package/template/app/dashboard/_components/subscribe-button.tsx +49 -0
- package/template/app/dashboard/billing/page.tsx +277 -0
- package/template/app/dashboard/chat/page.tsx +73 -0
- package/template/app/dashboard/cli/page.tsx +260 -0
- package/template/app/dashboard/layout.tsx +24 -0
- package/template/app/dashboard/page.tsx +216 -0
- package/template/app/dashboard/payment/_components/manage-subscription.tsx +22 -0
- package/template/app/dashboard/payment/page.tsx +126 -0
- package/template/app/dashboard/settings/page.tsx +613 -0
- package/template/app/dashboard/upload/page.tsx +324 -0
- package/template/app/error.tsx +78 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +126 -0
- package/template/app/layout.tsx +135 -0
- package/template/app/not-found.tsx +45 -0
- package/template/app/page.tsx +28 -0
- package/template/app/pricing/_component/pricing-table.tsx +276 -0
- package/template/app/pricing/page.tsx +23 -0
- package/template/app/privacy-policy/page.tsx +280 -0
- package/template/app/robots.txt +12 -0
- package/template/app/sign-in/page.tsx +228 -0
- package/template/app/sign-up/page.tsx +243 -0
- package/template/app/sitemap.ts +62 -0
- package/template/app/success/page.tsx +123 -0
- package/template/app/terms-of-service/page.tsx +212 -0
- package/template/auth-schema.ts +47 -0
- package/template/components/homepage/cli-workflow-section.tsx +138 -0
- package/template/components/homepage/features-section.tsx +150 -0
- package/template/components/homepage/footer.tsx +53 -0
- package/template/components/homepage/hero-section.tsx +112 -0
- package/template/components/homepage/integrations.tsx +124 -0
- package/template/components/homepage/navigation.tsx +116 -0
- package/template/components/homepage/news-section.tsx +82 -0
- package/template/components/homepage/testimonials-section.tsx +34 -0
- package/template/components/logos/BetterAuth.tsx +21 -0
- package/template/components/logos/NeonPostgres.tsx +41 -0
- package/template/components/logos/Nextjs.tsx +72 -0
- package/template/components/logos/Polar.tsx +7 -0
- package/template/components/logos/TailwindCSS.tsx +27 -0
- package/template/components/logos/index.ts +6 -0
- package/template/components/logos/shadcnui.tsx +8 -0
- package/template/components/provider.tsx +8 -0
- package/template/components/ui/avatar.tsx +53 -0
- package/template/components/ui/badge.tsx +46 -0
- package/template/components/ui/button.tsx +59 -0
- package/template/components/ui/card.tsx +92 -0
- package/template/components/ui/chart.tsx +353 -0
- package/template/components/ui/checkbox.tsx +32 -0
- package/template/components/ui/dialog.tsx +135 -0
- package/template/components/ui/dropdown-menu.tsx +257 -0
- package/template/components/ui/form.tsx +167 -0
- package/template/components/ui/input.tsx +21 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/progress.tsx +31 -0
- package/template/components/ui/resizable.tsx +56 -0
- package/template/components/ui/select.tsx +185 -0
- package/template/components/ui/separator.tsx +28 -0
- package/template/components/ui/sheet.tsx +139 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/sonner.tsx +25 -0
- package/template/components/ui/switch.tsx +31 -0
- package/template/components/ui/tabs.tsx +66 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +73 -0
- package/template/components/ui/toggle.tsx +47 -0
- package/template/components/ui/tooltip.tsx +61 -0
- package/template/components/user-profile.tsx +139 -0
- package/template/components.json +21 -0
- package/template/db/drizzle.ts +14 -0
- package/template/db/migrations/0000_worried_rawhide_kid.sql +77 -0
- package/template/db/migrations/meta/0000_snapshot.json +494 -0
- package/template/db/migrations/meta/_journal.json +13 -0
- package/template/db/schema.ts +85 -0
- package/template/drizzle.config.ts +13 -0
- package/template/emails/components/layout.tsx +181 -0
- package/template/emails/password-reset.tsx +67 -0
- package/template/emails/payment-failed.tsx +167 -0
- package/template/emails/subscription-confirmation.tsx +129 -0
- package/template/emails/welcome.tsx +100 -0
- package/template/eslint.config.mjs +16 -0
- package/template/hooks/use-mobile.ts +21 -0
- package/template/lib/auth-client.ts +8 -0
- package/template/lib/auth.ts +276 -0
- package/template/lib/email.ts +118 -0
- package/template/lib/polar-products.ts +49 -0
- package/template/lib/subscription.ts +148 -0
- package/template/lib/upload-image.ts +28 -0
- package/template/lib/utils.ts +6 -0
- package/template/middleware.ts +30 -0
- package/template/next-env.d.ts +5 -0
- package/template/next.config.ts +27 -0
- package/template/package.json +99 -0
- package/template/postcss.config.mjs +5 -0
- package/template/public/add.png +0 -0
- package/template/public/favicon.svg +4 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/iphone.png +0 -0
- package/template/public/logo.png +0 -0
- package/template/public/next.svg +1 -0
- package/template/public/polar-sh.svg +1 -0
- package/template/public/shadcn-ui.svg +1 -0
- package/template/public/site.webmanifest +21 -0
- package/template/public/vercel.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/tailwind.config.ts +89 -0
- package/template/template.config.json +138 -0
- package/template/tsconfig.json +27 -0
|
@@ -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,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { AlertCircle, Home, RefreshCcw } from "lucide-react";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
|
|
8
|
+
export default function Error({
|
|
9
|
+
error,
|
|
10
|
+
reset,
|
|
11
|
+
}: {
|
|
12
|
+
error: Error & { digest?: string };
|
|
13
|
+
reset: () => void;
|
|
14
|
+
}) {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// Log the error to an error reporting service
|
|
17
|
+
console.error("Application error:", error);
|
|
18
|
+
}, [error]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
|
22
|
+
<div className="text-center space-y-6 max-w-md">
|
|
23
|
+
<div className="flex justify-center">
|
|
24
|
+
<div className="rounded-full bg-destructive/10 p-4">
|
|
25
|
+
<AlertCircle className="h-12 w-12 text-destructive" />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
<h1 className="text-4xl font-bold">Something Went Wrong</h1>
|
|
31
|
+
<p className="text-muted-foreground text-lg">
|
|
32
|
+
We're sorry, but something unexpected happened. Our team has been notified.
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
{process.env.NODE_ENV === "development" && (
|
|
37
|
+
<div className="bg-muted/50 rounded-lg p-4 text-left">
|
|
38
|
+
<p className="text-sm font-mono text-destructive break-all">
|
|
39
|
+
{error.message}
|
|
40
|
+
</p>
|
|
41
|
+
{error.digest && (
|
|
42
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
43
|
+
Error ID: {error.digest}
|
|
44
|
+
</p>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div className="flex flex-col sm:flex-row gap-3 justify-center items-center pt-4">
|
|
50
|
+
<Button onClick={reset} variant="default">
|
|
51
|
+
<RefreshCcw className="mr-2 h-4 w-4" />
|
|
52
|
+
Try Again
|
|
53
|
+
</Button>
|
|
54
|
+
<Button asChild variant="outline">
|
|
55
|
+
<Link href="/">
|
|
56
|
+
<Home className="mr-2 h-4 w-4" />
|
|
57
|
+
Go Home
|
|
58
|
+
</Link>
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="pt-8 text-sm text-muted-foreground">
|
|
63
|
+
<p>
|
|
64
|
+
If this problem persists,{" "}
|
|
65
|
+
<Link
|
|
66
|
+
href="https://github.com/kedbrant/Saas-scaffold/issues"
|
|
67
|
+
className="text-primary hover:underline"
|
|
68
|
+
target="_blank"
|
|
69
|
+
rel="noopener noreferrer"
|
|
70
|
+
>
|
|
71
|
+
please report it
|
|
72
|
+
</Link>
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:is(.dark *));
|
|
5
|
+
|
|
6
|
+
@theme inline {
|
|
7
|
+
--color-background: var(--background);
|
|
8
|
+
--color-foreground: var(--foreground);
|
|
9
|
+
--font-sans: var(--font-apple-system);
|
|
10
|
+
--font-mono: var(--font-sf-mono);
|
|
11
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
12
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
13
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
14
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
15
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
16
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
17
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
18
|
+
--color-sidebar: var(--sidebar);
|
|
19
|
+
--color-chart-5: var(--chart-5);
|
|
20
|
+
--color-chart-4: var(--chart-4);
|
|
21
|
+
--color-chart-3: var(--chart-3);
|
|
22
|
+
--color-chart-2: var(--chart-2);
|
|
23
|
+
--color-chart-1: var(--chart-1);
|
|
24
|
+
--color-ring: var(--ring);
|
|
25
|
+
--color-input: var(--input);
|
|
26
|
+
--color-border: var(--border);
|
|
27
|
+
--color-destructive: var(--destructive);
|
|
28
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
29
|
+
--color-accent: var(--accent);
|
|
30
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
31
|
+
--color-muted: var(--muted);
|
|
32
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
+
--color-secondary: var(--secondary);
|
|
34
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
35
|
+
--color-primary: var(--primary);
|
|
36
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
37
|
+
--color-popover: var(--popover);
|
|
38
|
+
--color-card-foreground: var(--card-foreground);
|
|
39
|
+
--color-card: var(--card);
|
|
40
|
+
--radius-sm: 8px;
|
|
41
|
+
--radius-md: 10px;
|
|
42
|
+
--radius-lg: 12px;
|
|
43
|
+
--radius-xl: 20px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
:root {
|
|
47
|
+
/* Dark Theme with Sunset Orange */
|
|
48
|
+
--background: #000000; /* Pure black */
|
|
49
|
+
--foreground: #ffffff;
|
|
50
|
+
--card: #0a0a0a; /* Slightly lighter than black */
|
|
51
|
+
--card-foreground: #ffffff;
|
|
52
|
+
--popover: #0a0a0a;
|
|
53
|
+
--popover-foreground: #ffffff;
|
|
54
|
+
--primary: #ff5722; /* Deep sunset orange */
|
|
55
|
+
--primary-foreground: #ffffff;
|
|
56
|
+
--secondary: #1a1a1a; /* Dark gray */
|
|
57
|
+
--secondary-foreground: #ffffff;
|
|
58
|
+
--muted: #1a1a1a;
|
|
59
|
+
--muted-foreground: #a1a1a1;
|
|
60
|
+
--accent: #ff5722; /* Sunset orange */
|
|
61
|
+
--accent-foreground: #ffffff;
|
|
62
|
+
--destructive: #ff3b30;
|
|
63
|
+
--destructive-foreground: #ffffff;
|
|
64
|
+
--border: #2a2a2a; /* Subtle dark border */
|
|
65
|
+
--input: #1a1a1a;
|
|
66
|
+
--ring: #ff5722;
|
|
67
|
+
--chart-1: #ff5722; /* Sunset orange */
|
|
68
|
+
--chart-2: #ff7043; /* Lighter orange */
|
|
69
|
+
--chart-3: #ff8a65; /* Even lighter orange */
|
|
70
|
+
--chart-4: #ffab91; /* Peachy orange */
|
|
71
|
+
--chart-5: #d84315; /* Deep burnt orange */
|
|
72
|
+
--sidebar: #0a0a0a;
|
|
73
|
+
--sidebar-foreground: #ffffff;
|
|
74
|
+
--sidebar-primary: #ff5722;
|
|
75
|
+
--sidebar-primary-foreground: #ffffff;
|
|
76
|
+
--sidebar-accent: #1a1a1a;
|
|
77
|
+
--sidebar-accent-foreground: #ffffff;
|
|
78
|
+
--sidebar-border: #2a2a2a;
|
|
79
|
+
--sidebar-ring: #ff5722;
|
|
80
|
+
/* Fonts */
|
|
81
|
+
--font-apple-system:
|
|
82
|
+
-apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue",
|
|
83
|
+
Helvetica, sans-serif;
|
|
84
|
+
--font-sf-mono: "SF Mono", Menlo, monospace;
|
|
85
|
+
--font-sans: var(--font-apple-system);
|
|
86
|
+
--font-serif: "New York", Georgia, serif;
|
|
87
|
+
--font-mono: var(--font-sf-mono);
|
|
88
|
+
--radius: 10px;
|
|
89
|
+
/* Dark theme shadows with orange glow */
|
|
90
|
+
--shadow-2xs: 0px 1px 2px rgba(255, 87, 34, 0.05);
|
|
91
|
+
--shadow-xs: 0px 1px 3px rgba(255, 87, 34, 0.1);
|
|
92
|
+
--shadow-sm: 0px 2px 4px rgba(255, 87, 34, 0.1);
|
|
93
|
+
--shadow: 0px 2px 6px rgba(255, 87, 34, 0.1);
|
|
94
|
+
--shadow-md: 0px 4px 8px rgba(255, 87, 34, 0.15);
|
|
95
|
+
--shadow-lg: 0px 8px 16px rgba(255, 87, 34, 0.2);
|
|
96
|
+
--shadow-xl: 0px 12px 24px rgba(255, 87, 34, 0.25);
|
|
97
|
+
--shadow-2xl: 0px 16px 32px rgba(255, 87, 34, 0.3);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@layer base {
|
|
101
|
+
* {
|
|
102
|
+
@apply border-border outline-ring/50;
|
|
103
|
+
}
|
|
104
|
+
body {
|
|
105
|
+
@apply bg-background text-foreground font-sans;
|
|
106
|
+
-webkit-font-smoothing: antialiased;
|
|
107
|
+
-moz-osx-font-smoothing: grayscale;
|
|
108
|
+
}
|
|
109
|
+
button,
|
|
110
|
+
input,
|
|
111
|
+
select,
|
|
112
|
+
textarea {
|
|
113
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all duration-200;
|
|
114
|
+
}
|
|
115
|
+
button {
|
|
116
|
+
@apply hover:cursor-pointer;
|
|
117
|
+
}
|
|
118
|
+
h1,
|
|
119
|
+
h2,
|
|
120
|
+
h3,
|
|
121
|
+
h4,
|
|
122
|
+
h5,
|
|
123
|
+
h6 {
|
|
124
|
+
@apply font-medium tracking-tight;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Toaster } from "@/components/ui/sonner";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import { ThemeProvider } from "../components/provider";
|
|
4
|
+
import "./globals.css";
|
|
5
|
+
import { Analytics } from "@vercel/analytics/next";
|
|
6
|
+
|
|
7
|
+
const siteConfig = {
|
|
8
|
+
name: "{{PROJECT_NAME}}",
|
|
9
|
+
description: "{{PROJECT_DESCRIPTION}}",
|
|
10
|
+
url: "{{APP_URL}}",
|
|
11
|
+
ogImage: "{{APP_URL}}/og-image.png",
|
|
12
|
+
links: {
|
|
13
|
+
twitter: "https://twitter.com/yourhandle",
|
|
14
|
+
github: "https://github.com/yourusername/{{PROJECT_NAME_KEBAB}}",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const metadata: Metadata = {
|
|
19
|
+
metadataBase: new URL(siteConfig.url),
|
|
20
|
+
title: {
|
|
21
|
+
default: siteConfig.name,
|
|
22
|
+
template: `%s | ${siteConfig.name}`,
|
|
23
|
+
},
|
|
24
|
+
description: siteConfig.description,
|
|
25
|
+
keywords: [
|
|
26
|
+
"SaaS",
|
|
27
|
+
"Next.js",
|
|
28
|
+
"React",
|
|
29
|
+
"TypeScript",
|
|
30
|
+
"Tailwind CSS",
|
|
31
|
+
"shadcn/ui",
|
|
32
|
+
"Authentication",
|
|
33
|
+
"Payments",
|
|
34
|
+
],
|
|
35
|
+
authors: [
|
|
36
|
+
{
|
|
37
|
+
name: siteConfig.name,
|
|
38
|
+
url: siteConfig.url,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
creator: siteConfig.name,
|
|
42
|
+
openGraph: {
|
|
43
|
+
type: "website",
|
|
44
|
+
locale: "en_US",
|
|
45
|
+
url: siteConfig.url,
|
|
46
|
+
title: siteConfig.name,
|
|
47
|
+
description: siteConfig.description,
|
|
48
|
+
siteName: siteConfig.name,
|
|
49
|
+
images: [
|
|
50
|
+
{
|
|
51
|
+
url: siteConfig.ogImage,
|
|
52
|
+
width: 1200,
|
|
53
|
+
height: 630,
|
|
54
|
+
alt: siteConfig.name,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
twitter: {
|
|
59
|
+
card: "summary_large_image",
|
|
60
|
+
title: siteConfig.name,
|
|
61
|
+
description: siteConfig.description,
|
|
62
|
+
images: [siteConfig.ogImage],
|
|
63
|
+
creator: "@yourhandle",
|
|
64
|
+
},
|
|
65
|
+
icons: {
|
|
66
|
+
icon: "/favicon.svg",
|
|
67
|
+
shortcut: "/favicon.svg",
|
|
68
|
+
apple: "/favicon.svg",
|
|
69
|
+
},
|
|
70
|
+
manifest: "/site.webmanifest",
|
|
71
|
+
robots: {
|
|
72
|
+
index: true,
|
|
73
|
+
follow: true,
|
|
74
|
+
googleBot: {
|
|
75
|
+
index: true,
|
|
76
|
+
follow: true,
|
|
77
|
+
"max-video-preview": -1,
|
|
78
|
+
"max-image-preview": "large",
|
|
79
|
+
"max-snippet": -1,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
verification: {
|
|
83
|
+
google: "your-google-verification-code",
|
|
84
|
+
// yandex: "your-yandex-verification-code",
|
|
85
|
+
// yahoo: "your-yahoo-verification-code",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default function RootLayout({
|
|
90
|
+
children,
|
|
91
|
+
}: Readonly<{
|
|
92
|
+
children: React.ReactNode;
|
|
93
|
+
}>) {
|
|
94
|
+
const jsonLd = {
|
|
95
|
+
"@context": "https://schema.org",
|
|
96
|
+
"@type": "WebApplication",
|
|
97
|
+
name: siteConfig.name,
|
|
98
|
+
description: siteConfig.description,
|
|
99
|
+
url: siteConfig.url,
|
|
100
|
+
applicationCategory: "BusinessApplication",
|
|
101
|
+
offers: {
|
|
102
|
+
"@type": "Offer",
|
|
103
|
+
category: "SaaS",
|
|
104
|
+
},
|
|
105
|
+
creator: {
|
|
106
|
+
"@type": "Organization",
|
|
107
|
+
name: siteConfig.name,
|
|
108
|
+
url: siteConfig.url,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<html lang="en" suppressHydrationWarning>
|
|
114
|
+
<head>
|
|
115
|
+
<script
|
|
116
|
+
type="application/ld+json"
|
|
117
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
118
|
+
/>
|
|
119
|
+
</head>
|
|
120
|
+
<body className={`font-[-apple-system,BlinkMacSystemFont]antialiased`}>
|
|
121
|
+
<ThemeProvider
|
|
122
|
+
attribute="class"
|
|
123
|
+
defaultTheme="light"
|
|
124
|
+
enableSystem
|
|
125
|
+
forcedTheme="light"
|
|
126
|
+
disableTransitionOnChange
|
|
127
|
+
>
|
|
128
|
+
{children}
|
|
129
|
+
<Toaster />
|
|
130
|
+
<Analytics />
|
|
131
|
+
</ThemeProvider>
|
|
132
|
+
</body>
|
|
133
|
+
</html>
|
|
134
|
+
);
|
|
135
|
+
}
|