anhedral 0.1.0 → 0.1.3
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/README.md +82 -20
- package/dist/branding.d.ts +2 -0
- package/dist/branding.js +9 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +86 -0
- package/dist/commands.d.ts +2 -3
- package/dist/commands.js +9 -33
- package/dist/index.js +8 -134
- package/dist/print.d.ts +8 -0
- package/dist/print.js +33 -0
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +770 -381
- package/dist/templates/backend.js +76 -190
- package/dist/templates/default-web.js +819 -237
- package/dist/templates/extension.js +200 -18
- package/dist/templates/frontend.js +12 -11
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +3 -6
- package/dist/util.d.ts +1 -1
- package/dist/util.js +100 -43
- package/favicon.ico +0 -0
- package/package.json +16 -10
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { writeAnhedralWebBranding } from '../branding.js';
|
|
3
3
|
import { writeFile } from '../util.js';
|
|
4
4
|
export function writeDefaultWebEnvExample(root) {
|
|
5
5
|
writeFile(path.join(root, '.env.example'), `# App
|
|
@@ -398,7 +398,7 @@ export async function getDashboardState(input: DashboardStateInput): Promise<Das
|
|
|
398
398
|
writeFile(path.join(root, 'lib/ui/button.ts'), `import { cva, type VariantProps } from 'class-variance-authority';
|
|
399
399
|
|
|
400
400
|
export const buttonVariants = cva(
|
|
401
|
-
'group/button inline-flex shrink-0 items-center justify-center rounded-
|
|
401
|
+
'group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*="size-"])]:size-4',
|
|
402
402
|
{
|
|
403
403
|
variants: {
|
|
404
404
|
variant: {
|
|
@@ -416,14 +416,14 @@ export const buttonVariants = cva(
|
|
|
416
416
|
size: {
|
|
417
417
|
default:
|
|
418
418
|
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
419
|
-
xs: 'h-6 gap-1 rounded-
|
|
420
|
-
sm: 'h-7 gap-1 rounded-
|
|
419
|
+
xs: 'h-6 gap-1 rounded-md px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*="size-"])]:size-3',
|
|
420
|
+
sm: 'h-7 gap-1 rounded-md px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*="size-"])]:size-3.5',
|
|
421
421
|
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
|
|
422
422
|
icon: 'size-8',
|
|
423
423
|
'icon-xs':
|
|
424
|
-
'size-6 rounded-
|
|
424
|
+
'size-6 rounded-md in-data-[slot=button-group]:rounded-md [&_svg:not([class*="size-"])]:size-3',
|
|
425
425
|
'icon-sm':
|
|
426
|
-
'size-7 rounded-
|
|
426
|
+
'size-7 rounded-md in-data-[slot=button-group]:rounded-md',
|
|
427
427
|
'icon-lg': 'size-9',
|
|
428
428
|
},
|
|
429
429
|
},
|
|
@@ -462,7 +462,7 @@ export { Button, buttonVariants };
|
|
|
462
462
|
writeFile(path.join(root, 'components/dashboard/header-user-menu.tsx'), `'use client';
|
|
463
463
|
|
|
464
464
|
import Image from 'next/image';
|
|
465
|
-
import {
|
|
465
|
+
import { useClerk } from '@clerk/nextjs';
|
|
466
466
|
import { Camera, ChevronDown, Coins, CreditCard, Loader2, LogOut } from 'lucide-react';
|
|
467
467
|
import { useRef, useState } from 'react';
|
|
468
468
|
import { Button } from '@/components/ui/button';
|
|
@@ -485,10 +485,12 @@ async function readJsonSafely(response: Response) {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
export function HeaderUserMenu(props: HeaderUserMenuProps) {
|
|
488
|
+
const { signOut } = useClerk();
|
|
488
489
|
const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl);
|
|
489
490
|
const [notice, setNotice] = useState<string | null>(null);
|
|
490
491
|
const [isPortalPending, setIsPortalPending] = useState(false);
|
|
491
492
|
const [isUploadPending, setIsUploadPending] = useState(false);
|
|
493
|
+
const [isSignOutPending, setIsSignOutPending] = useState(false);
|
|
492
494
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
493
495
|
const initials = props.displayName
|
|
494
496
|
.split(' ')
|
|
@@ -518,6 +520,15 @@ export function HeaderUserMenu(props: HeaderUserMenuProps) {
|
|
|
518
520
|
}
|
|
519
521
|
};
|
|
520
522
|
|
|
523
|
+
const handleSignOut = async () => {
|
|
524
|
+
setIsSignOutPending(true);
|
|
525
|
+
try {
|
|
526
|
+
await signOut({ redirectUrl: '/' });
|
|
527
|
+
} finally {
|
|
528
|
+
setIsSignOutPending(false);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
521
532
|
const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
522
533
|
const input = event.currentTarget;
|
|
523
534
|
const file = input.files?.[0];
|
|
@@ -645,12 +656,10 @@ export function HeaderUserMenu(props: HeaderUserMenuProps) {
|
|
|
645
656
|
Upload avatar to R2
|
|
646
657
|
</Button>
|
|
647
658
|
|
|
648
|
-
<
|
|
649
|
-
<
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
</button>
|
|
653
|
-
</SignOutButton>
|
|
659
|
+
<Button disabled={isSignOutPending} onClick={() => void handleSignOut()} type="button" variant="ghost">
|
|
660
|
+
{isSignOutPending ? <Loader2 className="size-4 animate-spin" /> : <LogOut className="size-4" />}
|
|
661
|
+
Sign out
|
|
662
|
+
</Button>
|
|
654
663
|
</div>
|
|
655
664
|
|
|
656
665
|
{notice ? (
|
|
@@ -776,7 +785,7 @@ export async function POST(request: Request) {
|
|
|
776
785
|
return Response.json({ error: 'No email address found for the signed-in user.' }, { status: 400 });
|
|
777
786
|
}
|
|
778
787
|
|
|
779
|
-
await syncUserProfile({
|
|
788
|
+
const user = await syncUserProfile({
|
|
780
789
|
clerkUserId: userId,
|
|
781
790
|
email,
|
|
782
791
|
displayName,
|
|
@@ -784,6 +793,21 @@ export async function POST(request: Request) {
|
|
|
784
793
|
|
|
785
794
|
const origin = new URL(request.url).origin;
|
|
786
795
|
const stripe = getStripe();
|
|
796
|
+
let stripeCustomerId = user?.stripeCustomerId ?? null;
|
|
797
|
+
|
|
798
|
+
if (!stripeCustomerId) {
|
|
799
|
+
const customer = await stripe.customers.create({
|
|
800
|
+
email,
|
|
801
|
+
name: displayName,
|
|
802
|
+
metadata: {
|
|
803
|
+
clerkUserId: userId,
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
stripeCustomerId = customer.id;
|
|
808
|
+
await updateStripeCustomerForUser(userId, stripeCustomerId);
|
|
809
|
+
}
|
|
810
|
+
|
|
787
811
|
const session = await stripe.checkout.sessions.create({
|
|
788
812
|
mode: 'subscription',
|
|
789
813
|
line_items: [
|
|
@@ -793,7 +817,7 @@ export async function POST(request: Request) {
|
|
|
793
817
|
},
|
|
794
818
|
],
|
|
795
819
|
allow_promotion_codes: true,
|
|
796
|
-
|
|
820
|
+
customer: stripeCustomerId,
|
|
797
821
|
client_reference_id: userId,
|
|
798
822
|
success_url: \`\${origin}/dashboard?checkout=success\`,
|
|
799
823
|
cancel_url: \`\${origin}/dashboard?checkout=cancelled\`,
|
|
@@ -802,10 +826,6 @@ export async function POST(request: Request) {
|
|
|
802
826
|
},
|
|
803
827
|
});
|
|
804
828
|
|
|
805
|
-
if (typeof session.customer === 'string' && session.customer.length > 0) {
|
|
806
|
-
await updateStripeCustomerForUser(userId, session.customer);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
829
|
return Response.json({ url: session.url });
|
|
810
830
|
}
|
|
811
831
|
`);
|
|
@@ -841,16 +861,22 @@ export async function POST(request: Request) {
|
|
|
841
861
|
}
|
|
842
862
|
`);
|
|
843
863
|
writeFile(path.join(root, 'app/dashboard/page.tsx'), `import Link from 'next/link';
|
|
844
|
-
import { auth, currentUser } from '@clerk/nextjs/server';
|
|
845
|
-
import { redirect } from 'next/navigation';
|
|
846
|
-
import { ArrowRight, CreditCard, Database, HardDriveUpload
|
|
847
|
-
import {
|
|
848
|
-
import {
|
|
849
|
-
import {
|
|
850
|
-
import {
|
|
851
|
-
import {
|
|
864
|
+
import { auth, currentUser } from '@clerk/nextjs/server';
|
|
865
|
+
import { redirect } from 'next/navigation';
|
|
866
|
+
import { ArrowRight, CreditCard, Database, HardDriveUpload } from 'lucide-react';
|
|
867
|
+
import { ThemeToggle } from '@/components/theme-toggle';
|
|
868
|
+
import { HeaderUserMenu } from '@/components/dashboard/header-user-menu';
|
|
869
|
+
import { getClerkProfile } from '@/lib/auth/clerk';
|
|
870
|
+
import { getDashboardState } from '@/lib/app/dashboard';
|
|
871
|
+
import { clerkEnvReady } from '@/lib/demo-env';
|
|
872
|
+
import { buttonVariants } from '@/lib/ui/button';
|
|
873
|
+
import { cn } from '@/lib/utils';
|
|
852
874
|
|
|
853
875
|
export default async function DashboardPage() {
|
|
876
|
+
if (!clerkEnvReady) {
|
|
877
|
+
redirect('/');
|
|
878
|
+
}
|
|
879
|
+
|
|
854
880
|
const { userId } = await auth();
|
|
855
881
|
|
|
856
882
|
if (!userId) {
|
|
@@ -868,55 +894,59 @@ export default async function DashboardPage() {
|
|
|
868
894
|
});
|
|
869
895
|
|
|
870
896
|
return (
|
|
871
|
-
<main className="min-h-screen bg-
|
|
872
|
-
<
|
|
873
|
-
<
|
|
874
|
-
<div>
|
|
875
|
-
<
|
|
876
|
-
<
|
|
897
|
+
<main className="min-h-screen bg-background">
|
|
898
|
+
<header className="border-b">
|
|
899
|
+
<div className="container mx-auto flex w-full max-w-5xl flex-wrap items-center justify-between gap-3 px-4 py-3 sm:px-6">
|
|
900
|
+
<div className="flex items-center gap-3">
|
|
901
|
+
<img alt="Anhedral logo" className="size-7" src="/anhedral.svg" />
|
|
902
|
+
<div>
|
|
903
|
+
<p className="text-base font-semibold tracking-tight">Anhedral</p>
|
|
904
|
+
<p className="text-sm text-muted-foreground">Dashboard</p>
|
|
905
|
+
</div>
|
|
877
906
|
</div>
|
|
878
|
-
<
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
907
|
+
<div className="flex items-center gap-3">
|
|
908
|
+
<ThemeToggle />
|
|
909
|
+
<HeaderUserMenu
|
|
910
|
+
avatarUrl={dashboard.avatarUrl}
|
|
911
|
+
creditsBalance={dashboard.creditsBalance}
|
|
912
|
+
displayName={dashboard.displayName}
|
|
913
|
+
email={dashboard.email}
|
|
914
|
+
subscriptionStatus={dashboard.subscriptionStatus}
|
|
915
|
+
subscriptionTier={dashboard.subscriptionTier}
|
|
916
|
+
/>
|
|
917
|
+
</div>
|
|
918
|
+
</div>
|
|
919
|
+
</header>
|
|
920
|
+
|
|
921
|
+
<div className="container mx-auto max-w-5xl px-4 py-8 sm:px-6">
|
|
922
|
+
<section className="grid gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,0.9fr)]">
|
|
923
|
+
<div className="min-w-0 space-y-6">
|
|
924
|
+
<section className="min-w-0 rounded-lg border bg-card p-6 text-card-foreground">
|
|
925
|
+
<h2 className="text-3xl font-semibold tracking-tight">
|
|
926
|
+
Dashboard scaffold
|
|
897
927
|
</h2>
|
|
898
|
-
<p className="mt-
|
|
928
|
+
<p className="mt-3 max-w-2xl text-sm text-muted-foreground sm:text-base">
|
|
899
929
|
The scaffold already wires a protected dashboard, a subscription surface, seeded credits, and R2-backed avatar upload.
|
|
900
930
|
Your team can start inside the app shell instead of building this plumbing from scratch.
|
|
901
931
|
</p>
|
|
902
932
|
|
|
903
|
-
<div className="mt-
|
|
933
|
+
<div className="mt-6 flex flex-wrap gap-3">
|
|
904
934
|
<form action="/api/stripe/checkout" method="post">
|
|
905
|
-
<button className={cn(buttonVariants(
|
|
935
|
+
<button className={cn(buttonVariants())} type="submit">
|
|
906
936
|
Start starter checkout
|
|
907
|
-
<ArrowRight className="size-4" />
|
|
937
|
+
<ArrowRight className="ml-2 size-4" />
|
|
908
938
|
</button>
|
|
909
939
|
</form>
|
|
910
940
|
|
|
911
|
-
<Link className={cn(buttonVariants({
|
|
941
|
+
<Link className={cn(buttonVariants({ variant: 'outline' }))} href="/">
|
|
912
942
|
View landing page
|
|
913
943
|
</Link>
|
|
914
944
|
</div>
|
|
915
|
-
</
|
|
945
|
+
</section>
|
|
916
946
|
|
|
917
|
-
<div className="grid gap-4 md:grid-cols-3">
|
|
918
|
-
<article className="rounded-
|
|
919
|
-
<div className="flex items-center gap-2 text-
|
|
947
|
+
<div className="min-w-0 grid gap-4 md:grid-cols-3">
|
|
948
|
+
<article className="min-w-0 rounded-lg border bg-card p-5 text-card-foreground">
|
|
949
|
+
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
|
920
950
|
<CreditCard className="size-3.5" />
|
|
921
951
|
Billing
|
|
922
952
|
</div>
|
|
@@ -926,8 +956,8 @@ export default async function DashboardPage() {
|
|
|
926
956
|
</p>
|
|
927
957
|
</article>
|
|
928
958
|
|
|
929
|
-
<article className="rounded-
|
|
930
|
-
<div className="flex items-center gap-2 text-
|
|
959
|
+
<article className="min-w-0 rounded-lg border bg-card p-5 text-card-foreground">
|
|
960
|
+
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
|
931
961
|
<Database className="size-3.5" />
|
|
932
962
|
Credits
|
|
933
963
|
</div>
|
|
@@ -937,8 +967,8 @@ export default async function DashboardPage() {
|
|
|
937
967
|
</p>
|
|
938
968
|
</article>
|
|
939
969
|
|
|
940
|
-
<article className="rounded-
|
|
941
|
-
<div className="flex items-center gap-2 text-
|
|
970
|
+
<article className="min-w-0 rounded-lg border bg-card p-5 text-card-foreground">
|
|
971
|
+
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
|
942
972
|
<HardDriveUpload className="size-3.5" />
|
|
943
973
|
Storage
|
|
944
974
|
</div>
|
|
@@ -950,26 +980,26 @@ export default async function DashboardPage() {
|
|
|
950
980
|
</div>
|
|
951
981
|
</div>
|
|
952
982
|
|
|
953
|
-
<aside className="space-y-4 rounded-
|
|
983
|
+
<aside className="min-w-0 space-y-4 rounded-lg border bg-card p-6 text-card-foreground">
|
|
954
984
|
<div>
|
|
955
|
-
<p className="text-
|
|
956
|
-
<h3 className="mt-
|
|
985
|
+
<p className="text-sm text-muted-foreground">Template notes</p>
|
|
986
|
+
<h3 className="mt-2 text-2xl font-semibold tracking-tight">Replace these first</h3>
|
|
957
987
|
</div>
|
|
958
988
|
|
|
959
989
|
<ul className="space-y-3 text-sm text-muted-foreground">
|
|
960
|
-
<li className="rounded-
|
|
990
|
+
<li className="rounded-md border bg-muted/50 px-4 py-3">
|
|
961
991
|
Swap the starter Stripe price id with your real recurring product and add webhook handlers.
|
|
962
992
|
</li>
|
|
963
|
-
<li className="rounded-
|
|
993
|
+
<li className="rounded-md border bg-muted/50 px-4 py-3">
|
|
964
994
|
Replace the seeded credits model with your real usage accounting rules.
|
|
965
995
|
</li>
|
|
966
|
-
<li className="rounded-
|
|
996
|
+
<li className="rounded-md border bg-muted/50 px-4 py-3">
|
|
967
997
|
Expand the dashboard once your primary signed-in workflow is defined.
|
|
968
998
|
</li>
|
|
969
999
|
</ul>
|
|
970
1000
|
|
|
971
1001
|
{!dashboard.databaseReady ? (
|
|
972
|
-
<div className="rounded-
|
|
1002
|
+
<div className="rounded-md border bg-muted/50 px-4 py-3 text-sm text-muted-foreground">
|
|
973
1003
|
Database connectivity is not ready yet. The starter falls back to placeholder subscription and credits data until your Neon env and Drizzle migrations are in place.
|
|
974
1004
|
</div>
|
|
975
1005
|
) : null}
|
|
@@ -980,28 +1010,484 @@ export default async function DashboardPage() {
|
|
|
980
1010
|
);
|
|
981
1011
|
}
|
|
982
1012
|
`);
|
|
983
|
-
writeFile(path.join(root, '
|
|
984
|
-
|
|
1013
|
+
writeFile(path.join(root, 'components/auth/sign-in-panel.tsx'), `'use client';
|
|
1014
|
+
|
|
1015
|
+
import Link from 'next/link';
|
|
1016
|
+
import { useRouter } from 'next/navigation';
|
|
1017
|
+
import * as React from 'react';
|
|
1018
|
+
import { useSignIn } from '@clerk/nextjs';
|
|
1019
|
+
import { AlertCircle, ArrowRight, Loader2 } from 'lucide-react';
|
|
1020
|
+
import { Button } from '@/components/ui/button';
|
|
1021
|
+
import { buttonVariants } from '@/lib/ui/button';
|
|
1022
|
+
import { cn } from '@/lib/utils';
|
|
1023
|
+
|
|
1024
|
+
type ClerkHookError = {
|
|
1025
|
+
longMessage?: string;
|
|
1026
|
+
message?: string;
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
type ClerkHookErrorState = {
|
|
1030
|
+
fields?: Record<string, ClerkHookError | undefined>;
|
|
1031
|
+
global?: ClerkHookError[] | null;
|
|
1032
|
+
raw?: ClerkHookError[] | null;
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
const inputClassName = 'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none transition placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50';
|
|
1036
|
+
const labelClassName = 'text-sm font-medium';
|
|
1037
|
+
|
|
1038
|
+
function getMessage(error?: ClerkHookError | null) {
|
|
1039
|
+
return error?.longMessage ?? error?.message ?? null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function getGlobalError(errors?: ClerkHookErrorState | null, fallback?: string | null) {
|
|
1043
|
+
return (
|
|
1044
|
+
fallback ??
|
|
1045
|
+
getMessage(errors?.global?.[0]) ??
|
|
1046
|
+
getMessage(errors?.raw?.[0]) ??
|
|
1047
|
+
null
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async function finalizeSignIn(
|
|
1052
|
+
signIn: Awaited<ReturnType<typeof useSignIn>>['signIn'],
|
|
1053
|
+
router: ReturnType<typeof useRouter>,
|
|
1054
|
+
) {
|
|
1055
|
+
await signIn.finalize({
|
|
1056
|
+
navigate: async ({ decorateUrl }) => {
|
|
1057
|
+
const url = decorateUrl('/dashboard');
|
|
1058
|
+
|
|
1059
|
+
if (url.startsWith('http')) {
|
|
1060
|
+
window.location.href = url;
|
|
1061
|
+
} else {
|
|
1062
|
+
router.replace(url);
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export function SignInPanel() {
|
|
1069
|
+
const router = useRouter();
|
|
1070
|
+
const { signIn, errors, fetchStatus } = useSignIn();
|
|
1071
|
+
const [email, setEmail] = React.useState('');
|
|
1072
|
+
const [password, setPassword] = React.useState('');
|
|
1073
|
+
const [unexpectedError, setUnexpectedError] = React.useState<string | null>(null);
|
|
1074
|
+
const isSubmitting = fetchStatus === 'fetching';
|
|
1075
|
+
const typedErrors = errors as unknown as ClerkHookErrorState | null | undefined;
|
|
1076
|
+
const emailError = getMessage(typedErrors?.fields?.identifier);
|
|
1077
|
+
const passwordError = getMessage(typedErrors?.fields?.password);
|
|
1078
|
+
const globalError = getGlobalError(typedErrors, unexpectedError);
|
|
1079
|
+
|
|
1080
|
+
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
1081
|
+
event.preventDefault();
|
|
1082
|
+
if (isSubmitting) return;
|
|
1083
|
+
|
|
1084
|
+
setUnexpectedError(null);
|
|
1085
|
+
|
|
1086
|
+
try {
|
|
1087
|
+
const { error } = await signIn.password({
|
|
1088
|
+
identifier: email.trim(),
|
|
1089
|
+
password,
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
if (error) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (signIn.status === 'complete') {
|
|
1097
|
+
await finalizeSignIn(signIn, router);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
setUnexpectedError('This starter currently supports email-and-password sign-in only.');
|
|
1102
|
+
} catch (submissionError) {
|
|
1103
|
+
setUnexpectedError(submissionError instanceof Error ? submissionError.message : 'Something went wrong. Please try again.');
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return (
|
|
1108
|
+
<div className="w-full max-w-md rounded-lg border bg-card p-6 text-card-foreground">
|
|
1109
|
+
<div className="mb-6">
|
|
1110
|
+
<h2 className="text-2xl font-semibold tracking-tight">Sign in</h2>
|
|
1111
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
1112
|
+
This route is already wired to Clerk hooks and redirects to the dashboard after authentication.
|
|
1113
|
+
</p>
|
|
1114
|
+
</div>
|
|
1115
|
+
|
|
1116
|
+
<form className="space-y-4" onSubmit={onSubmit}>
|
|
1117
|
+
<div className="space-y-2">
|
|
1118
|
+
<label className={labelClassName} htmlFor="email">Email</label>
|
|
1119
|
+
<input
|
|
1120
|
+
autoComplete="email"
|
|
1121
|
+
className={inputClassName}
|
|
1122
|
+
id="email"
|
|
1123
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
1124
|
+
placeholder="you@example.com"
|
|
1125
|
+
type="email"
|
|
1126
|
+
value={email}
|
|
1127
|
+
/>
|
|
1128
|
+
{emailError ? <p className="text-sm text-destructive">{emailError}</p> : null}
|
|
1129
|
+
</div>
|
|
1130
|
+
|
|
1131
|
+
<div className="space-y-2">
|
|
1132
|
+
<div className="flex items-center justify-between gap-3">
|
|
1133
|
+
<label className={labelClassName} htmlFor="password">Password</label>
|
|
1134
|
+
<span className="text-xs text-muted-foreground">Email + password</span>
|
|
1135
|
+
</div>
|
|
1136
|
+
<input
|
|
1137
|
+
autoComplete="current-password"
|
|
1138
|
+
className={inputClassName}
|
|
1139
|
+
id="password"
|
|
1140
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
1141
|
+
placeholder="Enter your password"
|
|
1142
|
+
type="password"
|
|
1143
|
+
value={password}
|
|
1144
|
+
/>
|
|
1145
|
+
{passwordError ? <p className="text-sm text-destructive">{passwordError}</p> : null}
|
|
1146
|
+
</div>
|
|
1147
|
+
|
|
1148
|
+
{globalError ? (
|
|
1149
|
+
<div className="flex items-start gap-3 rounded-md border border-destructive/25 bg-destructive/10 px-4 py-3 text-sm text-destructive" role="alert">
|
|
1150
|
+
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
|
1151
|
+
<p>{globalError}</p>
|
|
1152
|
+
</div>
|
|
1153
|
+
) : null}
|
|
1154
|
+
|
|
1155
|
+
<Button className="w-full" disabled={isSubmitting} type="submit">
|
|
1156
|
+
{isSubmitting ? <Loader2 className="size-4 animate-spin" /> : <ArrowRight className="size-4" />}
|
|
1157
|
+
Continue to dashboard
|
|
1158
|
+
</Button>
|
|
1159
|
+
</form>
|
|
1160
|
+
|
|
1161
|
+
<p className="mt-5 text-center text-sm text-muted-foreground">
|
|
1162
|
+
Need an account?{' '}
|
|
1163
|
+
<Link className={cn(buttonVariants({ variant: 'link', size: 'sm' }), 'h-auto px-0 align-baseline')} href="/sign-up">
|
|
1164
|
+
Create one
|
|
1165
|
+
</Link>
|
|
1166
|
+
</p>
|
|
1167
|
+
</div>
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
`);
|
|
1171
|
+
writeFile(path.join(root, 'components/auth/sign-up-panel.tsx'), `'use client';
|
|
1172
|
+
|
|
1173
|
+
import Link from 'next/link';
|
|
1174
|
+
import { useRouter } from 'next/navigation';
|
|
1175
|
+
import * as React from 'react';
|
|
1176
|
+
import { useSignUp } from '@clerk/nextjs';
|
|
1177
|
+
import { AlertCircle, ArrowRight, BadgeCheck, Loader2, MailCheck, RotateCcw } from 'lucide-react';
|
|
1178
|
+
import { Button } from '@/components/ui/button';
|
|
1179
|
+
import { buttonVariants } from '@/lib/ui/button';
|
|
1180
|
+
import { cn } from '@/lib/utils';
|
|
1181
|
+
|
|
1182
|
+
type ClerkHookError = {
|
|
1183
|
+
longMessage?: string;
|
|
1184
|
+
message?: string;
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
type ClerkHookErrorState = {
|
|
1188
|
+
fields?: Record<string, ClerkHookError | undefined>;
|
|
1189
|
+
global?: ClerkHookError[] | null;
|
|
1190
|
+
raw?: ClerkHookError[] | null;
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const inputClassName = 'flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none transition placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50';
|
|
1194
|
+
const labelClassName = 'text-sm font-medium';
|
|
1195
|
+
|
|
1196
|
+
function getMessage(error?: ClerkHookError | null) {
|
|
1197
|
+
return error?.longMessage ?? error?.message ?? null;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function getGlobalError(errors?: ClerkHookErrorState | null, fallback?: string | null) {
|
|
1201
|
+
return (
|
|
1202
|
+
fallback ??
|
|
1203
|
+
getMessage(errors?.global?.[0]) ??
|
|
1204
|
+
getMessage(errors?.raw?.[0]) ??
|
|
1205
|
+
null
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
async function finalizeSignUp(
|
|
1210
|
+
signUp: Awaited<ReturnType<typeof useSignUp>>['signUp'],
|
|
1211
|
+
router: ReturnType<typeof useRouter>,
|
|
1212
|
+
) {
|
|
1213
|
+
await signUp.finalize({
|
|
1214
|
+
navigate: async ({ decorateUrl }) => {
|
|
1215
|
+
const url = decorateUrl('/dashboard');
|
|
1216
|
+
|
|
1217
|
+
if (url.startsWith('http')) {
|
|
1218
|
+
window.location.href = url;
|
|
1219
|
+
} else {
|
|
1220
|
+
router.replace(url);
|
|
1221
|
+
}
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export function SignUpPanel() {
|
|
1227
|
+
const router = useRouter();
|
|
1228
|
+
const { signUp, errors, fetchStatus } = useSignUp();
|
|
1229
|
+
const [step, setStep] = React.useState<'details' | 'verify'>('details');
|
|
1230
|
+
const [email, setEmail] = React.useState('');
|
|
1231
|
+
const [password, setPassword] = React.useState('');
|
|
1232
|
+
const [verificationCode, setVerificationCode] = React.useState('');
|
|
1233
|
+
const [unexpectedError, setUnexpectedError] = React.useState<string | null>(null);
|
|
1234
|
+
const isSubmitting = fetchStatus === 'fetching';
|
|
1235
|
+
const typedErrors = errors as unknown as ClerkHookErrorState | null | undefined;
|
|
1236
|
+
const emailError = getMessage(typedErrors?.fields?.emailAddress);
|
|
1237
|
+
const passwordError = getMessage(typedErrors?.fields?.password);
|
|
1238
|
+
const codeError = getMessage(typedErrors?.fields?.code);
|
|
1239
|
+
const globalError = getGlobalError(typedErrors, unexpectedError);
|
|
1240
|
+
|
|
1241
|
+
async function onCreateAccount(event: React.FormEvent<HTMLFormElement>) {
|
|
1242
|
+
event.preventDefault();
|
|
1243
|
+
if (isSubmitting) return;
|
|
1244
|
+
|
|
1245
|
+
setUnexpectedError(null);
|
|
1246
|
+
|
|
1247
|
+
try {
|
|
1248
|
+
const { error } = await signUp.password({
|
|
1249
|
+
emailAddress: email.trim(),
|
|
1250
|
+
password,
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
if (error) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (signUp.status === 'complete') {
|
|
1258
|
+
await finalizeSignUp(signUp, router);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const { error: sendEmailError } = await signUp.verifications.sendEmailCode();
|
|
1263
|
+
if (sendEmailError) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
setStep('verify');
|
|
1268
|
+
setVerificationCode('');
|
|
1269
|
+
} catch (submissionError) {
|
|
1270
|
+
setUnexpectedError(submissionError instanceof Error ? submissionError.message : 'Something went wrong. Please try again.');
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async function onVerify(event: React.FormEvent<HTMLFormElement>) {
|
|
1275
|
+
event.preventDefault();
|
|
1276
|
+
if (isSubmitting) return;
|
|
1277
|
+
|
|
1278
|
+
setUnexpectedError(null);
|
|
1279
|
+
|
|
1280
|
+
try {
|
|
1281
|
+
const { error } = await signUp.verifications.verifyEmailCode({
|
|
1282
|
+
code: verificationCode.trim(),
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
if (error) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (signUp.status === 'complete') {
|
|
1290
|
+
await finalizeSignUp(signUp, router);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
setUnexpectedError('Verification is still incomplete. Request a fresh code and try again.');
|
|
1295
|
+
} catch (verificationError) {
|
|
1296
|
+
setUnexpectedError(verificationError instanceof Error ? verificationError.message : 'Something went wrong. Please try again.');
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
async function onResendCode() {
|
|
1301
|
+
if (isSubmitting) return;
|
|
1302
|
+
|
|
1303
|
+
setUnexpectedError(null);
|
|
1304
|
+
|
|
1305
|
+
try {
|
|
1306
|
+
const { error } = await signUp.verifications.sendEmailCode();
|
|
1307
|
+
if (error) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
} catch (resendError) {
|
|
1311
|
+
setUnexpectedError(resendError instanceof Error ? resendError.message : 'Unable to send a new code right now.');
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
return (
|
|
1316
|
+
<div className="w-full max-w-md rounded-lg border bg-card p-6 text-card-foreground">
|
|
1317
|
+
<div className="mb-6">
|
|
1318
|
+
<h2 className="mt-3 text-3xl font-semibold tracking-tight">
|
|
1319
|
+
{step === 'details' ? 'Create your account' : 'Verify your email'}
|
|
1320
|
+
</h2>
|
|
1321
|
+
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
|
1322
|
+
{step === 'details'
|
|
1323
|
+
? 'This route is already wired to Clerk hooks and redirects to the dashboard after signup.'
|
|
1324
|
+
: \`Enter the verification code Clerk sent to \${email.trim() || 'your inbox'} to finish account creation.\`}
|
|
1325
|
+
</p>
|
|
1326
|
+
</div>
|
|
1327
|
+
|
|
1328
|
+
{step === 'details' ? (
|
|
1329
|
+
<form className="space-y-4" onSubmit={onCreateAccount}>
|
|
1330
|
+
<div className="space-y-2">
|
|
1331
|
+
<label className={labelClassName} htmlFor="sign-up-email">Email</label>
|
|
1332
|
+
<input
|
|
1333
|
+
autoComplete="email"
|
|
1334
|
+
className={inputClassName}
|
|
1335
|
+
id="sign-up-email"
|
|
1336
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
1337
|
+
placeholder="founder@company.com"
|
|
1338
|
+
type="email"
|
|
1339
|
+
value={email}
|
|
1340
|
+
/>
|
|
1341
|
+
{emailError ? <p className="text-sm text-destructive">{emailError}</p> : null}
|
|
1342
|
+
</div>
|
|
1343
|
+
|
|
1344
|
+
<div className="space-y-2">
|
|
1345
|
+
<label className={labelClassName} htmlFor="sign-up-password">Password</label>
|
|
1346
|
+
<input
|
|
1347
|
+
autoComplete="new-password"
|
|
1348
|
+
className={inputClassName}
|
|
1349
|
+
id="sign-up-password"
|
|
1350
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
1351
|
+
placeholder="Choose a strong password"
|
|
1352
|
+
type="password"
|
|
1353
|
+
value={password}
|
|
1354
|
+
/>
|
|
1355
|
+
{passwordError ? <p className="text-sm text-destructive">{passwordError}</p> : null}
|
|
1356
|
+
</div>
|
|
1357
|
+
|
|
1358
|
+
{globalError ? (
|
|
1359
|
+
<div className="flex items-start gap-3 rounded-md border border-destructive/25 bg-destructive/10 px-4 py-3 text-sm text-destructive" role="alert">
|
|
1360
|
+
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
|
1361
|
+
<p>{globalError}</p>
|
|
1362
|
+
</div>
|
|
1363
|
+
) : null}
|
|
1364
|
+
|
|
1365
|
+
<div id="clerk-captcha" />
|
|
1366
|
+
|
|
1367
|
+
<Button className="w-full" disabled={isSubmitting} type="submit">
|
|
1368
|
+
{isSubmitting ? <Loader2 className="size-4 animate-spin" /> : <ArrowRight className="size-4" />}
|
|
1369
|
+
Create account
|
|
1370
|
+
</Button>
|
|
1371
|
+
</form>
|
|
1372
|
+
) : (
|
|
1373
|
+
<form className="space-y-4" onSubmit={onVerify}>
|
|
1374
|
+
<div className="rounded-md border bg-muted/50 p-4 text-sm text-muted-foreground">
|
|
1375
|
+
<div className="flex items-center gap-2 font-medium text-foreground">
|
|
1376
|
+
<MailCheck className="size-4" />
|
|
1377
|
+
Email verification
|
|
1378
|
+
</div>
|
|
1379
|
+
<p className="mt-2">
|
|
1380
|
+
Clerk handles the verification challenge while this route stays inside your app.
|
|
1381
|
+
</p>
|
|
1382
|
+
</div>
|
|
1383
|
+
|
|
1384
|
+
<div className="space-y-2">
|
|
1385
|
+
<label className={labelClassName} htmlFor="verification-code">Verification code</label>
|
|
1386
|
+
<input
|
|
1387
|
+
className={inputClassName}
|
|
1388
|
+
id="verification-code"
|
|
1389
|
+
inputMode="numeric"
|
|
1390
|
+
onChange={(event) => setVerificationCode(event.target.value)}
|
|
1391
|
+
placeholder="123456"
|
|
1392
|
+
type="text"
|
|
1393
|
+
value={verificationCode}
|
|
1394
|
+
/>
|
|
1395
|
+
{codeError ? <p className="text-sm text-destructive">{codeError}</p> : null}
|
|
1396
|
+
</div>
|
|
1397
|
+
|
|
1398
|
+
{globalError ? (
|
|
1399
|
+
<div className="flex items-start gap-3 rounded-md border border-destructive/25 bg-destructive/10 px-4 py-3 text-sm text-destructive" role="alert">
|
|
1400
|
+
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
|
1401
|
+
<p>{globalError}</p>
|
|
1402
|
+
</div>
|
|
1403
|
+
) : null}
|
|
1404
|
+
|
|
1405
|
+
<div className="grid gap-3 md:grid-cols-3">
|
|
1406
|
+
<Button className="md:col-span-2" disabled={isSubmitting} type="submit">
|
|
1407
|
+
{isSubmitting ? <Loader2 className="size-4 animate-spin" /> : <BadgeCheck className="size-4" />}
|
|
1408
|
+
Verify and continue
|
|
1409
|
+
</Button>
|
|
1410
|
+
|
|
1411
|
+
<Button
|
|
1412
|
+
disabled={isSubmitting}
|
|
1413
|
+
onClick={onResendCode}
|
|
1414
|
+
type="button"
|
|
1415
|
+
variant="secondary"
|
|
1416
|
+
>
|
|
1417
|
+
<RotateCcw className="size-4" />
|
|
1418
|
+
Resend code
|
|
1419
|
+
</Button>
|
|
1420
|
+
|
|
1421
|
+
<Button
|
|
1422
|
+
className="md:col-span-3"
|
|
1423
|
+
disabled={isSubmitting}
|
|
1424
|
+
onClick={() => {
|
|
1425
|
+
setStep('details');
|
|
1426
|
+
setVerificationCode('');
|
|
1427
|
+
setUnexpectedError(null);
|
|
1428
|
+
}}
|
|
1429
|
+
type="button"
|
|
1430
|
+
variant="outline"
|
|
1431
|
+
>
|
|
1432
|
+
Use another email
|
|
1433
|
+
</Button>
|
|
1434
|
+
</div>
|
|
1435
|
+
</form>
|
|
1436
|
+
)}
|
|
1437
|
+
|
|
1438
|
+
<p className="mt-5 text-center text-sm text-muted-foreground">
|
|
1439
|
+
Already have an account?{' '}
|
|
1440
|
+
<Link className={cn(buttonVariants({ variant: 'link', size: 'sm' }), 'h-auto px-0 align-baseline')} href="/sign-in">
|
|
1441
|
+
Sign in
|
|
1442
|
+
</Link>
|
|
1443
|
+
</p>
|
|
1444
|
+
</div>
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
`);
|
|
1448
|
+
writeFile(path.join(root, 'components/theme-toggle.tsx'), `'use client';
|
|
1449
|
+
|
|
1450
|
+
import * as React from 'react';
|
|
1451
|
+
import { Moon, Sun } from 'lucide-react';
|
|
1452
|
+
import { useTheme } from 'next-themes';
|
|
1453
|
+
import { Button } from '@/components/ui/button';
|
|
1454
|
+
|
|
1455
|
+
export function ThemeToggle() {
|
|
1456
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
1457
|
+
const [mounted, setMounted] = React.useState(false);
|
|
1458
|
+
|
|
1459
|
+
React.useEffect(() => {
|
|
1460
|
+
setMounted(true);
|
|
1461
|
+
}, []);
|
|
1462
|
+
|
|
1463
|
+
const isDark = mounted && resolvedTheme === 'dark';
|
|
1464
|
+
|
|
1465
|
+
return (
|
|
1466
|
+
<Button
|
|
1467
|
+
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
1468
|
+
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
|
1469
|
+
size="icon"
|
|
1470
|
+
type="button"
|
|
1471
|
+
variant="outline"
|
|
1472
|
+
>
|
|
1473
|
+
{isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
|
|
1474
|
+
</Button>
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
`);
|
|
1478
|
+
writeFile(path.join(root, 'app/layout.tsx'), `import { ClerkProvider } from '@clerk/nextjs';
|
|
985
1479
|
import type { Metadata } from 'next';
|
|
986
1480
|
import './globals.css';
|
|
987
1481
|
import { ThemeProvider } from '@/components/theme-provider';
|
|
1482
|
+
import { clerkEnvReady } from '@/lib/demo-env';
|
|
988
1483
|
|
|
989
1484
|
export const metadata: Metadata = {
|
|
990
|
-
title: '
|
|
1485
|
+
title: 'Anhedral',
|
|
991
1486
|
description: 'Anhedral starter with Clerk auth, Stripe billing, Neon + Drizzle, and R2 avatars.',
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
variables: {
|
|
997
|
-
colorPrimary: 'oklch(0.205 0 0)',
|
|
998
|
-
colorBackground: 'oklch(1 0 0)',
|
|
999
|
-
borderRadius: '1rem',
|
|
1000
|
-
},
|
|
1001
|
-
options: {
|
|
1002
|
-
logoImageUrl: '${ANHEDRAL_LOGO_PUBLIC_PATH}',
|
|
1003
|
-
socialButtonsVariant: 'iconButton' as const,
|
|
1004
|
-
socialButtonsPlacement: 'top' as const,
|
|
1487
|
+
icons: {
|
|
1488
|
+
icon: '/favicon.ico',
|
|
1489
|
+
shortcut: '/favicon.ico',
|
|
1490
|
+
apple: '/favicon.ico',
|
|
1005
1491
|
},
|
|
1006
1492
|
};
|
|
1007
1493
|
|
|
@@ -1010,106 +1496,247 @@ export default function RootLayout({
|
|
|
1010
1496
|
}: Readonly<{
|
|
1011
1497
|
children: React.ReactNode;
|
|
1012
1498
|
}>) {
|
|
1499
|
+
const content = (
|
|
1500
|
+
<html
|
|
1501
|
+
lang="en"
|
|
1502
|
+
suppressHydrationWarning
|
|
1503
|
+
className="antialiased font-sans"
|
|
1504
|
+
>
|
|
1505
|
+
<body>
|
|
1506
|
+
<ThemeProvider>
|
|
1507
|
+
{children}
|
|
1508
|
+
</ThemeProvider>
|
|
1509
|
+
</body>
|
|
1510
|
+
</html>
|
|
1511
|
+
);
|
|
1512
|
+
|
|
1513
|
+
if (!clerkEnvReady) {
|
|
1514
|
+
return content;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1013
1517
|
return (
|
|
1014
|
-
<ClerkProvider
|
|
1015
|
-
|
|
1016
|
-
lang="en"
|
|
1017
|
-
suppressHydrationWarning
|
|
1018
|
-
className="antialiased font-sans"
|
|
1019
|
-
>
|
|
1020
|
-
<body>
|
|
1021
|
-
<ThemeProvider>{children}</ThemeProvider>
|
|
1022
|
-
</body>
|
|
1023
|
-
</html>
|
|
1518
|
+
<ClerkProvider>
|
|
1519
|
+
{content}
|
|
1024
1520
|
</ClerkProvider>
|
|
1025
1521
|
);
|
|
1026
1522
|
}
|
|
1027
1523
|
`);
|
|
1028
|
-
writeFile(path.join(root, '
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const pillars = [
|
|
1524
|
+
writeFile(path.join(root, 'lib/demo-env.ts'), `const REQUIRED_ENV_GROUPS = [
|
|
1525
|
+
{
|
|
1526
|
+
title: 'App',
|
|
1527
|
+
fields: ['NEXT_PUBLIC_APP_URL'],
|
|
1528
|
+
},
|
|
1035
1529
|
{
|
|
1036
|
-
title: '
|
|
1037
|
-
|
|
1038
|
-
|
|
1530
|
+
title: 'Clerk',
|
|
1531
|
+
fields: [
|
|
1532
|
+
'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY',
|
|
1533
|
+
'CLERK_SECRET_KEY',
|
|
1534
|
+
'NEXT_PUBLIC_CLERK_SIGN_IN_URL',
|
|
1535
|
+
'NEXT_PUBLIC_CLERK_SIGN_UP_URL',
|
|
1536
|
+
'NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL',
|
|
1537
|
+
'NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL',
|
|
1538
|
+
],
|
|
1039
1539
|
},
|
|
1040
1540
|
{
|
|
1041
|
-
title: '
|
|
1042
|
-
|
|
1043
|
-
icon: ShieldCheck,
|
|
1541
|
+
title: 'Neon + Drizzle',
|
|
1542
|
+
fields: ['DATABASE_URL'],
|
|
1044
1543
|
},
|
|
1045
1544
|
{
|
|
1046
|
-
title: '
|
|
1047
|
-
|
|
1048
|
-
icon: CreditCard,
|
|
1545
|
+
title: 'Cloudflare R2',
|
|
1546
|
+
fields: ['R2_ACCOUNT_ID', 'R2_ACCESS_KEY_ID', 'R2_SECRET_ACCESS_KEY', 'R2_BUCKET'],
|
|
1049
1547
|
},
|
|
1050
1548
|
{
|
|
1051
|
-
title: '
|
|
1052
|
-
|
|
1053
|
-
icon: Database,
|
|
1549
|
+
title: 'Stripe',
|
|
1550
|
+
fields: ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', 'STRIPE_PRICE_STARTER'],
|
|
1054
1551
|
},
|
|
1055
|
-
];
|
|
1552
|
+
] as const;
|
|
1553
|
+
|
|
1554
|
+
function hasRealValue(value: string | undefined) {
|
|
1555
|
+
if (!value) {
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
return !value.includes('***') && !value.includes('demo_placeholder');
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
export const clerkEnvReady =
|
|
1563
|
+
hasRealValue(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) &&
|
|
1564
|
+
hasRealValue(process.env.CLERK_SECRET_KEY);
|
|
1565
|
+
|
|
1566
|
+
export const requiredEnvGroups = REQUIRED_ENV_GROUPS;
|
|
1567
|
+
`);
|
|
1568
|
+
writeFile(path.join(root, 'app/page.tsx'), `import { auth } from '@clerk/nextjs/server';
|
|
1569
|
+
import { AlertCircle, ArrowRight, Cloud, CreditCard, Database, ShieldCheck } from 'lucide-react';
|
|
1570
|
+
import Link from 'next/link';
|
|
1571
|
+
import { ThemeToggle } from '@/components/theme-toggle';
|
|
1572
|
+
import { clerkEnvReady, requiredEnvGroups } from '@/lib/demo-env';
|
|
1573
|
+
import { buttonVariants } from '@/lib/ui/button';
|
|
1574
|
+
import { cn } from '@/lib/utils';
|
|
1575
|
+
|
|
1576
|
+
// Edit the content in this file to turn the starter into your own app.
|
|
1577
|
+
const homePageContent = {
|
|
1578
|
+
hero: {
|
|
1579
|
+
title: 'A simple starter, ready to customize.',
|
|
1580
|
+
description:
|
|
1581
|
+
'This template keeps auth, billing, data, and storage wired up without adding extra visual noise. Replace the placeholder sections with your product-specific content and workflows.',
|
|
1582
|
+
primaryLabel: 'Create your first account',
|
|
1583
|
+
signedInPrimaryLabel: 'Go to dashboard',
|
|
1584
|
+
secondaryLabel: 'Open sign-in',
|
|
1585
|
+
},
|
|
1586
|
+
setup: {
|
|
1587
|
+
title: 'Add your env vars',
|
|
1588
|
+
description:
|
|
1589
|
+
'Copy .env.example to .env, replace every placeholder below, then restart the dev server.',
|
|
1590
|
+
steps: [
|
|
1591
|
+
'Copy .env.example to .env.',
|
|
1592
|
+
'Fill in every App, Clerk, database, storage, and Stripe value listed below.',
|
|
1593
|
+
'Restart pnpm dev after saving the file.',
|
|
1594
|
+
],
|
|
1595
|
+
},
|
|
1596
|
+
sections: [
|
|
1597
|
+
{
|
|
1598
|
+
title: 'Landing content',
|
|
1599
|
+
description:
|
|
1600
|
+
'Replace this placeholder with your headline, screenshots, pricing, and any product proof you want on the homepage.',
|
|
1601
|
+
href: '#hero',
|
|
1602
|
+
linkLabel: 'Jump to hero copy',
|
|
1603
|
+
icon: Cloud,
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
title: 'Authentication',
|
|
1607
|
+
description:
|
|
1608
|
+
'Sign-in and sign-up routes are already mounted, so you can focus on your app-specific onboarding and copy.',
|
|
1609
|
+
href: '/sign-in',
|
|
1610
|
+
linkLabel: 'Open sign-in',
|
|
1611
|
+
icon: ShieldCheck,
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
title: 'Billing',
|
|
1615
|
+
description:
|
|
1616
|
+
'Checkout, subscription state, and starter credit surfaces are already scaffolded into the signed-in experience.',
|
|
1617
|
+
href: '/dashboard',
|
|
1618
|
+
linkLabel: 'Open billing shell',
|
|
1619
|
+
icon: CreditCard,
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
title: 'Data and storage',
|
|
1623
|
+
description:
|
|
1624
|
+
'Neon, Drizzle, and R2 hooks are in place for account data, uploads, and the rest of your product-specific models.',
|
|
1625
|
+
href: '/dashboard',
|
|
1626
|
+
linkLabel: 'Open app shell',
|
|
1627
|
+
icon: Database,
|
|
1628
|
+
},
|
|
1629
|
+
],
|
|
1630
|
+
} as const;
|
|
1056
1631
|
|
|
1057
1632
|
export default async function HomePage() {
|
|
1058
|
-
const { userId } = await auth();
|
|
1633
|
+
const { userId } = clerkEnvReady ? await auth() : { userId: null };
|
|
1059
1634
|
|
|
1060
1635
|
return (
|
|
1061
|
-
<main className="min-h-screen bg-
|
|
1062
|
-
<
|
|
1063
|
-
<
|
|
1064
|
-
<div>
|
|
1065
|
-
<
|
|
1066
|
-
<p className="text-
|
|
1636
|
+
<main className="min-h-screen bg-background">
|
|
1637
|
+
<header className="border-b">
|
|
1638
|
+
<div className="container mx-auto flex w-full max-w-5xl flex-wrap items-center justify-between gap-3 px-4 py-3 sm:px-6">
|
|
1639
|
+
<div className="flex items-center gap-3">
|
|
1640
|
+
<img alt="Anhedral logo" className="size-7" src="/anhedral.svg" />
|
|
1641
|
+
<p className="text-base font-semibold tracking-tight">Anhedral</p>
|
|
1067
1642
|
</div>
|
|
1068
1643
|
|
|
1069
|
-
<div className="flex items-center gap-3">
|
|
1070
|
-
<
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1644
|
+
<div className="flex items-center gap-2 sm:gap-3">
|
|
1645
|
+
<ThemeToggle />
|
|
1646
|
+
{clerkEnvReady ? (
|
|
1647
|
+
<>
|
|
1648
|
+
<Link className={cn(buttonVariants({ variant: 'ghost' }))} href="/sign-in">
|
|
1649
|
+
Sign in
|
|
1650
|
+
</Link>
|
|
1651
|
+
<Link className={cn(buttonVariants())} href={userId ? '/dashboard' : '/sign-up'}>
|
|
1652
|
+
{userId ? 'Open dashboard' : 'Start with sign up'}
|
|
1653
|
+
</Link>
|
|
1654
|
+
</>
|
|
1655
|
+
) : (
|
|
1656
|
+
<div className="inline-flex h-8 items-center rounded-md border bg-muted/50 px-3 text-sm text-muted-foreground">
|
|
1657
|
+
Setup mode
|
|
1658
|
+
</div>
|
|
1659
|
+
)}
|
|
1076
1660
|
</div>
|
|
1077
|
-
</
|
|
1661
|
+
</div>
|
|
1662
|
+
</header>
|
|
1663
|
+
|
|
1664
|
+
<div className="container mx-auto max-w-5xl px-4 py-8 sm:px-6">
|
|
1665
|
+
<section className="grid gap-6 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
|
1666
|
+
{clerkEnvReady ? (
|
|
1667
|
+
<section id="hero" className="min-w-0 rounded-lg border bg-card p-6 text-card-foreground">
|
|
1668
|
+
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">
|
|
1669
|
+
{homePageContent.hero.title}
|
|
1670
|
+
</h1>
|
|
1671
|
+
<p className="mt-3 max-w-2xl text-sm text-muted-foreground sm:text-base">
|
|
1672
|
+
{homePageContent.hero.description}
|
|
1673
|
+
</p>
|
|
1078
1674
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1675
|
+
<div className="mt-6 flex flex-wrap gap-3">
|
|
1676
|
+
<Link className={cn(buttonVariants())} href={userId ? '/dashboard' : '/sign-up'}>
|
|
1677
|
+
{userId ? homePageContent.hero.signedInPrimaryLabel : homePageContent.hero.primaryLabel}
|
|
1678
|
+
<ArrowRight className="ml-2 size-4" />
|
|
1679
|
+
</Link>
|
|
1680
|
+
<Link className={cn(buttonVariants({ variant: 'outline' }))} href="/sign-in">
|
|
1681
|
+
{homePageContent.hero.secondaryLabel}
|
|
1682
|
+
</Link>
|
|
1683
|
+
</div>
|
|
1684
|
+
</section>
|
|
1685
|
+
) : (
|
|
1686
|
+
<section className="rounded-lg border bg-card p-6 text-card-foreground sm:p-8">
|
|
1687
|
+
<div className="inline-flex items-center gap-2 rounded-md border bg-muted px-3 py-1 text-sm text-muted-foreground">
|
|
1688
|
+
<AlertCircle className="size-3.5" />
|
|
1689
|
+
Setup mode
|
|
1690
|
+
</div>
|
|
1691
|
+
<h2 className="mt-4 text-xl font-semibold tracking-tight">{homePageContent.setup.title}</h2>
|
|
1692
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
1693
|
+
{homePageContent.setup.description}
|
|
1694
|
+
</p>
|
|
1091
1695
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1696
|
+
<div className="mt-4 space-y-4">
|
|
1697
|
+
<div className="rounded-md border bg-muted/50 p-5">
|
|
1698
|
+
<p className="text-sm font-medium text-muted-foreground">Environment checklist</p>
|
|
1699
|
+
<ol className="mt-3 space-y-2 text-sm text-muted-foreground">
|
|
1700
|
+
{homePageContent.setup.steps.map((step, index) => (
|
|
1701
|
+
<li key={step}>
|
|
1702
|
+
{index + 1}. {step}
|
|
1703
|
+
</li>
|
|
1704
|
+
))}
|
|
1705
|
+
</ol>
|
|
1706
|
+
</div>
|
|
1707
|
+
|
|
1708
|
+
<div className="space-y-4">
|
|
1709
|
+
{requiredEnvGroups.map((group) => (
|
|
1710
|
+
<article key={group.title} className="min-w-0 rounded-md border bg-card p-4">
|
|
1711
|
+
<h3 className="text-sm font-medium text-muted-foreground">{group.title}</h3>
|
|
1712
|
+
<ul className="mt-3 space-y-2 text-foreground/90">
|
|
1713
|
+
{group.fields.map((field) => (
|
|
1714
|
+
<li key={field} className="min-w-0">
|
|
1715
|
+
<code className="block break-all font-mono text-xs">{field}</code>
|
|
1716
|
+
</li>
|
|
1717
|
+
))}
|
|
1718
|
+
</ul>
|
|
1719
|
+
</article>
|
|
1720
|
+
))}
|
|
1721
|
+
</div>
|
|
1722
|
+
</div>
|
|
1723
|
+
</section>
|
|
1724
|
+
)}
|
|
1102
1725
|
|
|
1103
|
-
<div className="grid gap-4">
|
|
1104
|
-
{
|
|
1105
|
-
<article key={title} className="rounded-
|
|
1726
|
+
<div className="min-w-0 grid gap-4">
|
|
1727
|
+
{homePageContent.sections.map(({ title, description, href, linkLabel, icon: Icon }) => (
|
|
1728
|
+
<article key={title} className="min-w-0 rounded-lg border bg-card p-5 text-card-foreground">
|
|
1106
1729
|
<div className="flex items-center gap-3">
|
|
1107
|
-
<div className="flex size-
|
|
1730
|
+
<div className="flex size-9 items-center justify-center rounded-md bg-muted text-foreground">
|
|
1108
1731
|
<Icon className="size-5" />
|
|
1109
1732
|
</div>
|
|
1110
|
-
<h2 className="text-
|
|
1733
|
+
<h2 className="text-base font-semibold">{title}</h2>
|
|
1111
1734
|
</div>
|
|
1112
1735
|
<p className="mt-3 text-sm leading-6 text-muted-foreground">{description}</p>
|
|
1736
|
+
<Link className={cn(buttonVariants({ size: 'sm', variant: 'link' }), 'mt-4 h-auto px-0')} href={href}>
|
|
1737
|
+
{linkLabel}
|
|
1738
|
+
<ArrowRight className="ml-1 size-4" />
|
|
1739
|
+
</Link>
|
|
1113
1740
|
</article>
|
|
1114
1741
|
))}
|
|
1115
1742
|
</div>
|
|
@@ -1119,84 +1746,37 @@ export default async function HomePage() {
|
|
|
1119
1746
|
);
|
|
1120
1747
|
}
|
|
1121
1748
|
`);
|
|
1122
|
-
writeFile(path.join(root, 'app/sign-in/[[...sign-in]]/page.tsx'), `import {
|
|
1123
|
-
import
|
|
1749
|
+
writeFile(path.join(root, 'app/sign-in/[[...sign-in]]/page.tsx'), `import { redirect } from 'next/navigation';
|
|
1750
|
+
import { SignInPanel } from '@/components/auth/sign-in-panel';
|
|
1751
|
+
import { clerkEnvReady } from '@/lib/demo-env';
|
|
1124
1752
|
|
|
1125
1753
|
export default function SignInPage() {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
<div>
|
|
1130
|
-
<p className="text-xs uppercase tracking-[0.26em] text-muted-foreground">Clerk custom UI</p>
|
|
1131
|
-
<h1 className="mt-4 text-4xl font-semibold tracking-tight sm:text-5xl">Sign in without rebuilding the basics.</h1>
|
|
1132
|
-
<p className="mt-4 max-w-xl text-base text-muted-foreground">
|
|
1133
|
-
This route is intentionally branded and lives alongside your app shell. Swap the copy and continue shipping.
|
|
1134
|
-
</p>
|
|
1135
|
-
</div>
|
|
1136
|
-
|
|
1137
|
-
<div className="rounded-[1.75rem] border border-border/70 bg-muted/30 p-5 text-sm text-muted-foreground">
|
|
1138
|
-
<p className="font-medium text-foreground">Already wired:</p>
|
|
1139
|
-
<ul className="mt-3 space-y-2">
|
|
1140
|
-
<li>Custom route at /sign-in</li>
|
|
1141
|
-
<li>Clerk shadcn theme through ClerkProvider</li>
|
|
1142
|
-
<li>Dashboard redirect after authentication</li>
|
|
1143
|
-
</ul>
|
|
1144
|
-
<p className="mt-4">
|
|
1145
|
-
Need a different conversion flow? Pair this page with the installed Clerk skill and replace it with a hook-driven form later.
|
|
1146
|
-
</p>
|
|
1147
|
-
</div>
|
|
1148
|
-
</section>
|
|
1754
|
+
if (!clerkEnvReady) {
|
|
1755
|
+
redirect('/');
|
|
1756
|
+
}
|
|
1149
1757
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
Need an account?{' '}
|
|
1155
|
-
<Link className="font-medium text-foreground underline-offset-4 hover:underline" href="/sign-up">
|
|
1156
|
-
Create one
|
|
1157
|
-
</Link>
|
|
1158
|
-
</p>
|
|
1159
|
-
</div>
|
|
1758
|
+
return (
|
|
1759
|
+
<main className="min-h-screen bg-background">
|
|
1760
|
+
<section className="container mx-auto flex min-h-screen max-w-5xl items-center justify-center px-4 py-8 sm:px-6">
|
|
1761
|
+
<SignInPanel />
|
|
1160
1762
|
</section>
|
|
1161
1763
|
</main>
|
|
1162
1764
|
);
|
|
1163
1765
|
}
|
|
1164
1766
|
`);
|
|
1165
|
-
writeFile(path.join(root, 'app/sign-up/[[...sign-up]]/page.tsx'), `import {
|
|
1166
|
-
import
|
|
1767
|
+
writeFile(path.join(root, 'app/sign-up/[[...sign-up]]/page.tsx'), `import { redirect } from 'next/navigation';
|
|
1768
|
+
import { SignUpPanel } from '@/components/auth/sign-up-panel';
|
|
1769
|
+
import { clerkEnvReady } from '@/lib/demo-env';
|
|
1167
1770
|
|
|
1168
1771
|
export default function SignUpPage() {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
<div className="w-full max-w-md rounded-[2rem] border border-border/70 bg-background/95 p-4 shadow-xl">
|
|
1173
|
-
<SignUp forceRedirectUrl="/dashboard" path="/sign-up" routing="path" signInUrl="/sign-in" />
|
|
1174
|
-
<p className="px-4 pb-2 text-center text-sm text-muted-foreground">
|
|
1175
|
-
Already have an account?{' '}
|
|
1176
|
-
<Link className="font-medium text-foreground underline-offset-4 hover:underline" href="/sign-in">
|
|
1177
|
-
Sign in
|
|
1178
|
-
</Link>
|
|
1179
|
-
</p>
|
|
1180
|
-
</div>
|
|
1181
|
-
</section>
|
|
1182
|
-
|
|
1183
|
-
<section className="flex flex-col justify-between rounded-[2rem] border border-border/70 bg-background/90 p-8 shadow-sm lg:order-1">
|
|
1184
|
-
<div>
|
|
1185
|
-
<p className="text-xs uppercase tracking-[0.26em] text-muted-foreground">Starter onboarding</p>
|
|
1186
|
-
<h1 className="mt-4 text-4xl font-semibold tracking-tight sm:text-5xl">Get users into the product shell immediately.</h1>
|
|
1187
|
-
<p className="mt-4 max-w-xl text-base text-muted-foreground">
|
|
1188
|
-
The generated starter sends new accounts straight into the dashboard with billing, credits, and avatar plumbing already waiting.
|
|
1189
|
-
</p>
|
|
1190
|
-
</div>
|
|
1772
|
+
if (!clerkEnvReady) {
|
|
1773
|
+
redirect('/');
|
|
1774
|
+
}
|
|
1191
1775
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
<li>Add your real onboarding questionnaire or workspace creation step</li>
|
|
1197
|
-
<li>Connect webhook-driven billing state once Stripe products are finalized</li>
|
|
1198
|
-
</ul>
|
|
1199
|
-
</div>
|
|
1776
|
+
return (
|
|
1777
|
+
<main className="min-h-screen bg-background">
|
|
1778
|
+
<section className="container mx-auto flex min-h-screen max-w-5xl items-center justify-center px-4 py-8 sm:px-6">
|
|
1779
|
+
<SignUpPanel />
|
|
1200
1780
|
</section>
|
|
1201
1781
|
</main>
|
|
1202
1782
|
);
|
|
@@ -1205,14 +1785,12 @@ export default function SignUpPage() {
|
|
|
1205
1785
|
writeFile(path.join(root, 'app/globals.css'), `@import 'tailwindcss';
|
|
1206
1786
|
@import 'tw-animate-css';
|
|
1207
1787
|
@import 'shadcn/tailwind.css';
|
|
1208
|
-
@import '@clerk/ui/themes/shadcn.css';
|
|
1209
1788
|
|
|
1210
1789
|
@custom-variant dark (&:is(.dark *));
|
|
1211
1790
|
|
|
1212
1791
|
@theme inline {
|
|
1213
|
-
--font-
|
|
1214
|
-
--font-
|
|
1215
|
-
--font-mono: var(--font-app-mono);
|
|
1792
|
+
--font-sans: var(--font-geist-sans);
|
|
1793
|
+
--font-mono: var(--font-geist-mono);
|
|
1216
1794
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
1217
1795
|
--color-sidebar-border: var(--sidebar-border);
|
|
1218
1796
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
@@ -1254,8 +1832,6 @@ export default function SignUpPage() {
|
|
|
1254
1832
|
}
|
|
1255
1833
|
|
|
1256
1834
|
:root {
|
|
1257
|
-
--font-app-sans: 'Avenir Next', 'Inter', 'Segoe UI', sans-serif;
|
|
1258
|
-
--font-app-mono: 'JetBrains Mono', 'SFMono-Regular', monospace;
|
|
1259
1835
|
--background: oklch(1 0 0);
|
|
1260
1836
|
--foreground: oklch(0.145 0 0);
|
|
1261
1837
|
--card: oklch(1 0 0);
|
|
@@ -1279,7 +1855,7 @@ export default function SignUpPage() {
|
|
|
1279
1855
|
--chart-3: oklch(0.439 0 0);
|
|
1280
1856
|
--chart-4: oklch(0.371 0 0);
|
|
1281
1857
|
--chart-5: oklch(0.269 0 0);
|
|
1282
|
-
--radius: 0.
|
|
1858
|
+
--radius: 0.625rem;
|
|
1283
1859
|
--sidebar: oklch(0.985 0 0);
|
|
1284
1860
|
--sidebar-foreground: oklch(0.145 0 0);
|
|
1285
1861
|
--sidebar-primary: oklch(0.205 0 0);
|
|
@@ -1342,20 +1918,26 @@ export default function SignUpPage() {
|
|
|
1342
1918
|
import { fileURLToPath } from 'node:url';
|
|
1343
1919
|
|
|
1344
1920
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
1921
|
+
const monorepoRoot = path.resolve(__dirname, '../..');
|
|
1345
1922
|
|
|
1346
1923
|
/** @type {import('next').NextConfig} */
|
|
1347
1924
|
const nextConfig = {
|
|
1348
1925
|
reactCompiler: true,
|
|
1926
|
+
transpilePackages: ['@anhedral/db'],
|
|
1349
1927
|
turbopack: {
|
|
1350
|
-
root:
|
|
1928
|
+
root: monorepoRoot,
|
|
1351
1929
|
},
|
|
1352
1930
|
};
|
|
1353
1931
|
|
|
1354
1932
|
export default nextConfig;
|
|
1355
1933
|
`);
|
|
1356
1934
|
writeFile(path.join(root, 'proxy.ts'), `import { clerkMiddleware } from '@clerk/nextjs/server';
|
|
1935
|
+
import { NextResponse } from 'next/server';
|
|
1936
|
+
import { clerkEnvReady } from './lib/demo-env';
|
|
1937
|
+
|
|
1938
|
+
const proxy = clerkEnvReady ? clerkMiddleware() : () => NextResponse.next();
|
|
1357
1939
|
|
|
1358
|
-
export default
|
|
1940
|
+
export default proxy;
|
|
1359
1941
|
|
|
1360
1942
|
export const config = {
|
|
1361
1943
|
matcher: [
|
|
@@ -1364,5 +1946,5 @@ export const config = {
|
|
|
1364
1946
|
],
|
|
1365
1947
|
};
|
|
1366
1948
|
`);
|
|
1367
|
-
|
|
1949
|
+
writeAnhedralWebBranding(root);
|
|
1368
1950
|
}
|