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,228 @@
|
|
|
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 { Label } from "@/components/ui/label";
|
|
13
|
+
import { authClient } from "@/lib/auth-client";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
import Link from "next/link";
|
|
16
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
17
|
+
import { Suspense, useState } from "react";
|
|
18
|
+
import { toast } from "sonner";
|
|
19
|
+
|
|
20
|
+
function SignInContent() {
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [email, setEmail] = useState("");
|
|
23
|
+
const [password, setPassword] = useState("");
|
|
24
|
+
const searchParams = useSearchParams();
|
|
25
|
+
const returnTo = searchParams.get("returnTo");
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
|
|
28
|
+
const handleEmailSignIn = async (e: React.FormEvent) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
setLoading(true);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { error } = await authClient.signIn.email({
|
|
34
|
+
email,
|
|
35
|
+
password,
|
|
36
|
+
callbackURL: returnTo || "/dashboard",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (error) {
|
|
40
|
+
console.error("Sign-in error:", error);
|
|
41
|
+
|
|
42
|
+
// Provide helpful error messages
|
|
43
|
+
if (error.message?.includes("database") || error.message?.includes("connect")) {
|
|
44
|
+
toast.error("Database connection failed. Please configure DATABASE_URL in your .env.local file.", {
|
|
45
|
+
duration: 7000,
|
|
46
|
+
});
|
|
47
|
+
} else if (error.message?.includes("Invalid") || error.message?.includes("credentials")) {
|
|
48
|
+
toast.error("Invalid email or password. Please try again.");
|
|
49
|
+
} else {
|
|
50
|
+
toast.error(error.message || "Sign-in failed. Check your environment configuration.", {
|
|
51
|
+
duration: 5000,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
toast.success("Signed in successfully!");
|
|
56
|
+
router.push(returnTo || "/dashboard");
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error("Authentication error:", err);
|
|
60
|
+
toast.error("An unexpected error occurred. Check console for details.", {
|
|
61
|
+
duration: 5000,
|
|
62
|
+
});
|
|
63
|
+
} finally {
|
|
64
|
+
setLoading(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex flex-col justify-center items-center w-full h-screen bg-black">
|
|
70
|
+
<Card className="max-w-md w-full bg-[#0a0a0a] border-[#2a2a2a]">
|
|
71
|
+
<CardHeader>
|
|
72
|
+
<CardTitle className="text-lg md:text-xl text-white">
|
|
73
|
+
Welcome Back
|
|
74
|
+
</CardTitle>
|
|
75
|
+
<CardDescription className="text-xs md:text-sm text-gray-400">
|
|
76
|
+
Sign in to your account to continue
|
|
77
|
+
</CardDescription>
|
|
78
|
+
</CardHeader>
|
|
79
|
+
<CardContent>
|
|
80
|
+
<form onSubmit={handleEmailSignIn} className="grid gap-4">
|
|
81
|
+
<div className="grid gap-2">
|
|
82
|
+
<Label htmlFor="email" className="text-gray-300">Email</Label>
|
|
83
|
+
<Input
|
|
84
|
+
id="email"
|
|
85
|
+
type="email"
|
|
86
|
+
placeholder="you@example.com"
|
|
87
|
+
value={email}
|
|
88
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
89
|
+
required
|
|
90
|
+
disabled={loading}
|
|
91
|
+
className="bg-[#1a1a1a] border-[#2a2a2a] text-white placeholder:text-gray-500"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
<div className="grid gap-2">
|
|
95
|
+
<Label htmlFor="password" className="text-gray-300">Password</Label>
|
|
96
|
+
<Input
|
|
97
|
+
id="password"
|
|
98
|
+
type="password"
|
|
99
|
+
placeholder="Enter your password"
|
|
100
|
+
value={password}
|
|
101
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
102
|
+
required
|
|
103
|
+
disabled={loading}
|
|
104
|
+
className="bg-[#1a1a1a] border-[#2a2a2a] text-white placeholder:text-gray-500"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
108
|
+
{loading ? "Signing in..." : "Sign In"}
|
|
109
|
+
</Button>
|
|
110
|
+
</form>
|
|
111
|
+
|
|
112
|
+
<div className="relative my-4">
|
|
113
|
+
<div className="absolute inset-0 flex items-center">
|
|
114
|
+
<span className="w-full border-t border-[#2a2a2a]" />
|
|
115
|
+
</div>
|
|
116
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
117
|
+
<span className="bg-[#0a0a0a] px-2 text-gray-500">Or continue with</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<Button
|
|
122
|
+
variant="outline"
|
|
123
|
+
className={cn("w-full gap-2 bg-[#1a1a1a] border-[#2a2a2a] text-white hover:bg-[#2a2a2a]")}
|
|
124
|
+
disabled={loading}
|
|
125
|
+
onClick={async () => {
|
|
126
|
+
try {
|
|
127
|
+
setLoading(true);
|
|
128
|
+
await authClient.signIn.social(
|
|
129
|
+
{
|
|
130
|
+
provider: "google",
|
|
131
|
+
callbackURL: returnTo || "/dashboard",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
onResponse: () => {
|
|
135
|
+
setLoading(false);
|
|
136
|
+
},
|
|
137
|
+
onError: (ctx) => {
|
|
138
|
+
setLoading(false);
|
|
139
|
+
console.error("Sign-in failed:", ctx.error);
|
|
140
|
+
|
|
141
|
+
if (ctx.error?.message?.includes("database") || ctx.error?.message?.includes("connect")) {
|
|
142
|
+
toast.error("Database connection failed. Please configure DATABASE_URL in your .env.local file.", {
|
|
143
|
+
duration: 7000,
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
toast.error("Google sign-in failed. Check your Google OAuth configuration.", {
|
|
147
|
+
duration: 5000,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
setLoading(false);
|
|
155
|
+
console.error("Authentication error:", error);
|
|
156
|
+
toast.error("Authentication error. Check console for details.", {
|
|
157
|
+
duration: 5000,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
<svg
|
|
163
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
164
|
+
width="0.98em"
|
|
165
|
+
height="1em"
|
|
166
|
+
viewBox="0 0 256 262"
|
|
167
|
+
>
|
|
168
|
+
<path
|
|
169
|
+
fill="#4285F4"
|
|
170
|
+
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
|
171
|
+
></path>
|
|
172
|
+
<path
|
|
173
|
+
fill="#34A853"
|
|
174
|
+
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
|
175
|
+
></path>
|
|
176
|
+
<path
|
|
177
|
+
fill="#FBBC05"
|
|
178
|
+
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
|
|
179
|
+
></path>
|
|
180
|
+
<path
|
|
181
|
+
fill="#EB4335"
|
|
182
|
+
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
|
183
|
+
></path>
|
|
184
|
+
</svg>
|
|
185
|
+
Continue with Google
|
|
186
|
+
</Button>
|
|
187
|
+
|
|
188
|
+
<p className="mt-4 text-center text-sm text-gray-500">
|
|
189
|
+
Don't have an account?{" "}
|
|
190
|
+
<Link href="/sign-up" className="text-[#ff5722] hover:underline">
|
|
191
|
+
Sign up
|
|
192
|
+
</Link>
|
|
193
|
+
</p>
|
|
194
|
+
</CardContent>
|
|
195
|
+
</Card>
|
|
196
|
+
<p className="mt-6 text-xs text-center text-gray-500 dark:text-gray-400 max-w-md">
|
|
197
|
+
By signing in, you agree to our{" "}
|
|
198
|
+
<Link
|
|
199
|
+
href="/terms-of-service"
|
|
200
|
+
className="underline hover:text-gray-700 dark:hover:text-gray-300"
|
|
201
|
+
>
|
|
202
|
+
Terms of Service
|
|
203
|
+
</Link>{" "}
|
|
204
|
+
and{" "}
|
|
205
|
+
<Link
|
|
206
|
+
href="/privacy-policy"
|
|
207
|
+
className="underline hover:text-gray-700 dark:hover:text-gray-300"
|
|
208
|
+
>
|
|
209
|
+
Privacy Policy
|
|
210
|
+
</Link>
|
|
211
|
+
</p>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default function SignIn() {
|
|
217
|
+
return (
|
|
218
|
+
<Suspense
|
|
219
|
+
fallback={
|
|
220
|
+
<div className="flex flex-col justify-center items-center w-full h-screen">
|
|
221
|
+
<div className="max-w-md w-full bg-gray-200 dark:bg-gray-800 animate-pulse rounded-lg h-96"></div>
|
|
222
|
+
</div>
|
|
223
|
+
}
|
|
224
|
+
>
|
|
225
|
+
<SignInContent />
|
|
226
|
+
</Suspense>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
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 { Label } from "@/components/ui/label";
|
|
13
|
+
import { authClient } from "@/lib/auth-client";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
import Link from "next/link";
|
|
16
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
17
|
+
import { Suspense, useState } from "react";
|
|
18
|
+
import { toast } from "sonner";
|
|
19
|
+
|
|
20
|
+
function SignUpContent() {
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [name, setName] = useState("");
|
|
23
|
+
const [email, setEmail] = useState("");
|
|
24
|
+
const [password, setPassword] = useState("");
|
|
25
|
+
const searchParams = useSearchParams();
|
|
26
|
+
const returnTo = searchParams.get("returnTo");
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
|
|
29
|
+
const handleEmailSignUp = async (e: React.FormEvent) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
setLoading(true);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { error } = await authClient.signUp.email({
|
|
35
|
+
email,
|
|
36
|
+
password,
|
|
37
|
+
name,
|
|
38
|
+
callbackURL: returnTo || "/dashboard",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (error) {
|
|
42
|
+
console.error("Sign-up error:", error);
|
|
43
|
+
|
|
44
|
+
if (error.message?.includes("database") || error.message?.includes("connect")) {
|
|
45
|
+
toast.error("Database connection failed. Please configure DATABASE_URL in your .env.local file.", {
|
|
46
|
+
duration: 7000,
|
|
47
|
+
});
|
|
48
|
+
} else if (error.message?.includes("already exists") || error.message?.includes("duplicate")) {
|
|
49
|
+
toast.error("An account with this email already exists. Please sign in instead.");
|
|
50
|
+
} else {
|
|
51
|
+
toast.error(error.message || "Sign-up failed. Check your environment configuration.", {
|
|
52
|
+
duration: 5000,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
toast.success("Account created successfully!");
|
|
57
|
+
router.push(returnTo || "/dashboard");
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error("Authentication error:", err);
|
|
61
|
+
toast.error("An unexpected error occurred. Check console for details.", {
|
|
62
|
+
duration: 5000,
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex flex-col justify-center items-center w-full h-screen bg-black">
|
|
71
|
+
<Card className="max-w-md w-full bg-[#0a0a0a] border-[#2a2a2a]">
|
|
72
|
+
<CardHeader>
|
|
73
|
+
<CardTitle className="text-lg md:text-xl text-white">
|
|
74
|
+
Create Your Account
|
|
75
|
+
</CardTitle>
|
|
76
|
+
<CardDescription className="text-xs md:text-sm text-gray-400">
|
|
77
|
+
Get started in seconds
|
|
78
|
+
</CardDescription>
|
|
79
|
+
</CardHeader>
|
|
80
|
+
<CardContent>
|
|
81
|
+
<form onSubmit={handleEmailSignUp} className="grid gap-4">
|
|
82
|
+
<div className="grid gap-2">
|
|
83
|
+
<Label htmlFor="name" className="text-gray-300">Name</Label>
|
|
84
|
+
<Input
|
|
85
|
+
id="name"
|
|
86
|
+
type="text"
|
|
87
|
+
placeholder="John Doe"
|
|
88
|
+
value={name}
|
|
89
|
+
onChange={(e) => setName(e.target.value)}
|
|
90
|
+
required
|
|
91
|
+
disabled={loading}
|
|
92
|
+
className="bg-[#1a1a1a] border-[#2a2a2a] text-white placeholder:text-gray-500"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="grid gap-2">
|
|
96
|
+
<Label htmlFor="email" className="text-gray-300">Email</Label>
|
|
97
|
+
<Input
|
|
98
|
+
id="email"
|
|
99
|
+
type="email"
|
|
100
|
+
placeholder="you@example.com"
|
|
101
|
+
value={email}
|
|
102
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
103
|
+
required
|
|
104
|
+
disabled={loading}
|
|
105
|
+
className="bg-[#1a1a1a] border-[#2a2a2a] text-white placeholder:text-gray-500"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="grid gap-2">
|
|
109
|
+
<Label htmlFor="password" className="text-gray-300">Password</Label>
|
|
110
|
+
<Input
|
|
111
|
+
id="password"
|
|
112
|
+
type="password"
|
|
113
|
+
placeholder="Create a strong password"
|
|
114
|
+
value={password}
|
|
115
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
116
|
+
required
|
|
117
|
+
disabled={loading}
|
|
118
|
+
minLength={8}
|
|
119
|
+
className="bg-[#1a1a1a] border-[#2a2a2a] text-white placeholder:text-gray-500"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
123
|
+
{loading ? "Creating account..." : "Create Account"}
|
|
124
|
+
</Button>
|
|
125
|
+
</form>
|
|
126
|
+
|
|
127
|
+
<div className="relative my-4">
|
|
128
|
+
<div className="absolute inset-0 flex items-center">
|
|
129
|
+
<span className="w-full border-t border-[#2a2a2a]" />
|
|
130
|
+
</div>
|
|
131
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
132
|
+
<span className="bg-[#0a0a0a] px-2 text-gray-500">Or continue with</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<Button
|
|
137
|
+
variant="outline"
|
|
138
|
+
className={cn("w-full gap-2 bg-[#1a1a1a] border-[#2a2a2a] text-white hover:bg-[#2a2a2a]")}
|
|
139
|
+
disabled={loading}
|
|
140
|
+
onClick={async () => {
|
|
141
|
+
try {
|
|
142
|
+
setLoading(true);
|
|
143
|
+
await authClient.signIn.social(
|
|
144
|
+
{
|
|
145
|
+
provider: "google",
|
|
146
|
+
callbackURL: returnTo || "/dashboard",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
onResponse: () => {
|
|
150
|
+
setLoading(false);
|
|
151
|
+
},
|
|
152
|
+
onError: (ctx) => {
|
|
153
|
+
setLoading(false);
|
|
154
|
+
console.error("Sign-up failed:", ctx.error);
|
|
155
|
+
|
|
156
|
+
if (ctx.error?.message?.includes("database") || ctx.error?.message?.includes("connect")) {
|
|
157
|
+
toast.error("Database connection failed. Please configure DATABASE_URL in your .env.local file.", {
|
|
158
|
+
duration: 7000,
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
toast.error("Google sign-up failed. Check your Google OAuth configuration.", {
|
|
162
|
+
duration: 5000,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
setLoading(false);
|
|
170
|
+
console.error("Authentication error:", error);
|
|
171
|
+
toast.error("Authentication error. Check console for details.", {
|
|
172
|
+
duration: 5000,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
<svg
|
|
178
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
179
|
+
width="0.98em"
|
|
180
|
+
height="1em"
|
|
181
|
+
viewBox="0 0 256 262"
|
|
182
|
+
>
|
|
183
|
+
<path
|
|
184
|
+
fill="#4285F4"
|
|
185
|
+
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
|
186
|
+
></path>
|
|
187
|
+
<path
|
|
188
|
+
fill="#34A853"
|
|
189
|
+
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
|
190
|
+
></path>
|
|
191
|
+
<path
|
|
192
|
+
fill="#FBBC05"
|
|
193
|
+
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
|
|
194
|
+
></path>
|
|
195
|
+
<path
|
|
196
|
+
fill="#EB4335"
|
|
197
|
+
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
|
198
|
+
></path>
|
|
199
|
+
</svg>
|
|
200
|
+
Continue with Google
|
|
201
|
+
</Button>
|
|
202
|
+
|
|
203
|
+
<p className="mt-4 text-center text-sm text-gray-500">
|
|
204
|
+
Already have an account?{" "}
|
|
205
|
+
<Link href="/sign-in" className="text-[#ff5722] hover:underline">
|
|
206
|
+
Sign in
|
|
207
|
+
</Link>
|
|
208
|
+
</p>
|
|
209
|
+
</CardContent>
|
|
210
|
+
</Card>
|
|
211
|
+
<p className="mt-6 text-xs text-center text-gray-500 max-w-md">
|
|
212
|
+
By signing up, you agree to our{" "}
|
|
213
|
+
<Link
|
|
214
|
+
href="/terms-of-service"
|
|
215
|
+
className="underline hover:text-gray-400"
|
|
216
|
+
>
|
|
217
|
+
Terms of Service
|
|
218
|
+
</Link>{" "}
|
|
219
|
+
and{" "}
|
|
220
|
+
<Link
|
|
221
|
+
href="/privacy-policy"
|
|
222
|
+
className="underline hover:text-gray-400"
|
|
223
|
+
>
|
|
224
|
+
Privacy Policy
|
|
225
|
+
</Link>
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default function SignUp() {
|
|
232
|
+
return (
|
|
233
|
+
<Suspense
|
|
234
|
+
fallback={
|
|
235
|
+
<div className="flex flex-col justify-center items-center w-full h-screen bg-black">
|
|
236
|
+
<div className="max-w-md w-full bg-[#0a0a0a] animate-pulse rounded-lg h-96"></div>
|
|
237
|
+
</div>
|
|
238
|
+
}
|
|
239
|
+
>
|
|
240
|
+
<SignUpContent />
|
|
241
|
+
</Suspense>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, boolean, integer } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
export const user = pgTable("user", {
|
|
4
|
+
id: text('id').primaryKey(),
|
|
5
|
+
name: text('name').notNull(),
|
|
6
|
+
email: text('email').notNull().unique(),
|
|
7
|
+
emailVerified: boolean('email_verified').$defaultFn(() => false).notNull(),
|
|
8
|
+
image: text('image'),
|
|
9
|
+
createdAt: timestamp('created_at').$defaultFn(() => /* @__PURE__ */ new Date()).notNull(),
|
|
10
|
+
updatedAt: timestamp('updated_at').$defaultFn(() => /* @__PURE__ */ new Date()).notNull()
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const session = pgTable("session", {
|
|
14
|
+
id: text('id').primaryKey(),
|
|
15
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
16
|
+
token: text('token').notNull().unique(),
|
|
17
|
+
createdAt: timestamp('created_at').notNull(),
|
|
18
|
+
updatedAt: timestamp('updated_at').notNull(),
|
|
19
|
+
ipAddress: text('ip_address'),
|
|
20
|
+
userAgent: text('user_agent'),
|
|
21
|
+
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' })
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const account = pgTable("account", {
|
|
25
|
+
id: text('id').primaryKey(),
|
|
26
|
+
accountId: text('account_id').notNull(),
|
|
27
|
+
providerId: text('provider_id').notNull(),
|
|
28
|
+
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
|
|
29
|
+
accessToken: text('access_token'),
|
|
30
|
+
refreshToken: text('refresh_token'),
|
|
31
|
+
idToken: text('id_token'),
|
|
32
|
+
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
|
33
|
+
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
|
34
|
+
scope: text('scope'),
|
|
35
|
+
password: text('password'),
|
|
36
|
+
createdAt: timestamp('created_at').notNull(),
|
|
37
|
+
updatedAt: timestamp('updated_at').notNull()
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const verification = pgTable("verification", {
|
|
41
|
+
id: text('id').primaryKey(),
|
|
42
|
+
identifier: text('identifier').notNull(),
|
|
43
|
+
value: text('value').notNull(),
|
|
44
|
+
expiresAt: timestamp('expires_at').notNull(),
|
|
45
|
+
createdAt: timestamp('created_at').$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
46
|
+
updatedAt: timestamp('updated_at').$defaultFn(() => /* @__PURE__ */ new Date())
|
|
47
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from "@/components/ui/card";
|
|
10
|
+
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
|
11
|
+
import { useState, useEffect } from "react";
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Card className="max-w-2xl w-full bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
|
|
15
|
+
<CardHeader>
|
|
16
|
+
<CardTitle className="flex items-center gap-2 text-yellow-900 dark:text-yellow-100">
|
|
17
|
+
<AlertCircle className="w-5 h-5" />
|
|
18
|
+
Setup Required
|
|
19
|
+
</CardTitle>
|
|
20
|
+
<CardDescription className="text-yellow-800 dark:text-yellow-200">
|
|
21
|
+
Configure your environment variables to enable authentication
|
|
22
|
+
</CardDescription>
|
|
23
|
+
</CardHeader>
|
|
24
|
+
<CardContent>
|
|
25
|
+
<div className="space-y-4">
|
|
26
|
+
<div className="space-y-2">
|
|
27
|
+
<h4 className="font-semibold text-yellow-900 dark:text-yellow-100">
|
|
28
|
+
Required Configuration:
|
|
29
|
+
</h4>
|
|
30
|
+
<ul className="space-y-2 text-sm text-yellow-800 dark:text-yellow-200">
|
|
31
|
+
<li className="flex items-start gap-2">
|
|
32
|
+
<CheckCircle2 className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
33
|
+
<div>
|
|
34
|
+
<strong>DATABASE_URL</strong> - PostgreSQL connection string
|
|
35
|
+
<br />
|
|
36
|
+
<span className="text-xs opacity-75">
|
|
37
|
+
Get one from{" "}
|
|
38
|
+
<a
|
|
39
|
+
href="https://neon.tech"
|
|
40
|
+
target="_blank"
|
|
41
|
+
rel="noopener noreferrer"
|
|
42
|
+
className="underline"
|
|
43
|
+
>
|
|
44
|
+
Neon
|
|
45
|
+
</a>{" "}
|
|
46
|
+
or{" "}
|
|
47
|
+
<a
|
|
48
|
+
href="https://supabase.com"
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noopener noreferrer"
|
|
51
|
+
className="underline"
|
|
52
|
+
>
|
|
53
|
+
Supabase
|
|
54
|
+
</a>
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
</li>
|
|
58
|
+
<li className="flex items-start gap-2">
|
|
59
|
+
<CheckCircle2 className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
60
|
+
<div>
|
|
61
|
+
<strong>BETTER_AUTH_SECRET</strong> - Secret key for token signing
|
|
62
|
+
<br />
|
|
63
|
+
<span className="text-xs opacity-75">
|
|
64
|
+
Generate with: <code className="bg-yellow-100 dark:bg-yellow-900 px-1 rounded">openssl rand -base64 32</code>
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
</li>
|
|
68
|
+
<li className="flex items-start gap-2">
|
|
69
|
+
<CheckCircle2 className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
70
|
+
<div>
|
|
71
|
+
<strong>NEXT_PUBLIC_APP_URL</strong> - Your application URL
|
|
72
|
+
<br />
|
|
73
|
+
<span className="text-xs opacity-75">
|
|
74
|
+
Example: <code className="bg-yellow-100 dark:bg-yellow-900 px-1 rounded">http://localhost:3000</code>
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
</li>
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="space-y-2">
|
|
82
|
+
<h4 className="font-semibold text-yellow-900 dark:text-yellow-100">
|
|
83
|
+
Optional (Recommended):
|
|
84
|
+
</h4>
|
|
85
|
+
<ul className="space-y-2 text-sm text-yellow-800 dark:text-yellow-200">
|
|
86
|
+
<li className="flex items-start gap-2">
|
|
87
|
+
<CheckCircle2 className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
88
|
+
<div>
|
|
89
|
+
<strong>GOOGLE_CLIENT_ID</strong> & <strong>GOOGLE_CLIENT_SECRET</strong>
|
|
90
|
+
<br />
|
|
91
|
+
<span className="text-xs opacity-75">
|
|
92
|
+
Get from{" "}
|
|
93
|
+
<a
|
|
94
|
+
href="https://console.cloud.google.com"
|
|
95
|
+
target="_blank"
|
|
96
|
+
rel="noopener noreferrer"
|
|
97
|
+
className="underline"
|
|
98
|
+
>
|
|
99
|
+
Google Cloud Console
|
|
100
|
+
</a>
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
</li>
|
|
104
|
+
</ul>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="pt-4 border-t border-yellow-200 dark:border-yellow-800">
|
|
108
|
+
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
|
109
|
+
<strong>Steps:</strong>
|
|
110
|
+
</p>
|
|
111
|
+
<ol className="list-decimal list-inside space-y-1 text-sm text-yellow-800 dark:text-yellow-200 mt-2">
|
|
112
|
+
<li>Create a <code className="bg-yellow-100 dark:bg-yellow-900 px-1 rounded">.env.local</code> file in your project root</li>
|
|
113
|
+
<li>Add the required environment variables</li>
|
|
114
|
+
<li>Run <code className="bg-yellow-100 dark:bg-yellow-900 px-1 rounded">npm run db:push</code> to create database tables</li>
|
|
115
|
+
<li>Restart your dev server</li>
|
|
116
|
+
</ol>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Better Auth authentication with email/password and Google OAuth",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"better-auth": "^1.0.0",
|
|
7
|
+
"@polar-sh/better-auth": "^1.0.0"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {},
|
|
10
|
+
"envVars": [
|
|
11
|
+
"BETTER_AUTH_SECRET",
|
|
12
|
+
"GOOGLE_CLIENT_ID",
|
|
13
|
+
"GOOGLE_CLIENT_SECRET",
|
|
14
|
+
"NEXT_PUBLIC_APP_URL"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
"app/sign-in/**/*",
|
|
18
|
+
"app/sign-up/**/*",
|
|
19
|
+
"app/dashboard/**/*",
|
|
20
|
+
"app/api/auth/**/*",
|
|
21
|
+
"lib/auth.ts",
|
|
22
|
+
"lib/auth-client.ts",
|
|
23
|
+
"auth-schema.ts"
|
|
24
|
+
],
|
|
25
|
+
"middlewarePatches": [
|
|
26
|
+
"middleware.patch.ts"
|
|
27
|
+
],
|
|
28
|
+
"requires": [
|
|
29
|
+
"database"
|
|
30
|
+
],
|
|
31
|
+
"conflicts": []
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
import { organizationClient } from "better-auth/client/plugins";
|
|
3
|
+
import { polarClient } from "@polar-sh/better-auth";
|
|
4
|
+
|
|
5
|
+
export const authClient = createAuthClient({
|
|
6
|
+
baseURL: process.env.NEXT_PUBLIC_APP_URL,
|
|
7
|
+
plugins: [organizationClient(), polarClient()],
|
|
8
|
+
});
|