scale-stack 0.0.1-alpha.2 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +67 -5
  3. package/dist/index.js +4081 -106
  4. package/package.json +7 -2
  5. package/templates/ai-chat/chat-panel.tsx.ejs +70 -0
  6. package/templates/ai-chat/layout.tsx.ejs +35 -0
  7. package/templates/ai-chat/page.tsx.ejs +37 -0
  8. package/templates/ai-chat/route.ts.ejs +68 -0
  9. package/templates/ai-chat/use-chat.ts.ejs +26 -0
  10. package/templates/analytics/analytics-provider.tsx.ejs +32 -0
  11. package/templates/analytics/analytics.ts.ejs +40 -0
  12. package/templates/auth/auth-client.ts.ejs +7 -0
  13. package/templates/auth/auth.ts.ejs +45 -0
  14. package/templates/auth/route.ts.ejs +4 -0
  15. package/templates/auth/sign-in-page.tsx.ejs +122 -0
  16. package/templates/auth/sign-up-page.tsx.ejs +137 -0
  17. package/templates/auth/unauthorized.tsx.ejs +28 -0
  18. package/templates/core/layout.tsx.ejs +46 -0
  19. package/templates/core/loading.tsx.ejs +7 -0
  20. package/templates/core/next.config.ts.ejs +33 -0
  21. package/templates/error-handling/catch-all-not-found-page.tsx.ejs +5 -0
  22. package/templates/error-handling/error.tsx.ejs +33 -0
  23. package/templates/error-handling/global-error.tsx.ejs +32 -0
  24. package/templates/error-handling/not-found.tsx.ejs +28 -0
  25. package/templates/eslint-prettier/.prettierignore.ejs +29 -0
  26. package/templates/eslint-prettier/eslint.config.mjs.ejs +31 -0
  27. package/templates/form-handling/dashboard-page.tsx.ejs +39 -0
  28. package/templates/form-handling/example-form-action.ts.ejs +50 -0
  29. package/templates/form-handling/example-form-schema.ts.ejs +87 -0
  30. package/templates/form-handling/example-form.tsx.ejs +428 -0
  31. package/templates/i18n/ar.json.ejs +77 -0
  32. package/templates/i18n/en.json.ejs +77 -0
  33. package/templates/i18n/locale-layout.tsx.ejs +81 -0
  34. package/templates/i18n/navigation.ts.ejs +5 -0
  35. package/templates/i18n/next-intl.d.ts.ejs +9 -0
  36. package/templates/i18n/request.ts.ejs +15 -0
  37. package/templates/i18n/routing.ts.ejs +7 -0
  38. package/templates/orm/prisma.config.ts.ejs +12 -0
  39. package/templates/orm/prisma.ts.ejs +17 -0
  40. package/templates/orm/schema.prisma.ejs +8 -0
  41. package/templates/pre-commit/prek.toml.ejs +35 -0
  42. package/templates/proxy/proxy.ts.ejs +81 -0
  43. package/templates/server-actions/safe-action.ts.ejs +51 -0
  44. package/templates/ui/client-side-wrappers.tsx.ejs +19 -0
  45. package/templates/ui/page.tsx.ejs +117 -0
  46. package/templates/utility-libs/date-fns.SKILL.md.ejs +34 -0
  47. package/templates/utility-libs/motion.SKILL.md.ejs +234 -0
  48. package/templates/utility-libs/ts-pattern.SKILL.md.ejs +44 -0
  49. package/templates/utility-libs/usehooks.SKILL.md.ejs +38 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scale-stack",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.2",
4
4
  "description": "SKALSTÅKK — flat-packed code efficiency delivered straight to your terminal",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -9,7 +9,9 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
- "README.md"
12
+ "README.md",
13
+ "CHANGELOG.md",
14
+ "templates"
13
15
  ],
14
16
  "publishConfig": {
15
17
  "access": "public"
@@ -19,6 +21,7 @@
19
21
  "run": "node dist/index.js",
20
22
  "cli": "tsx src/cli/index.ts",
21
23
  "test": "vitest run",
24
+ "test:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts",
22
25
  "test:watch": "vitest",
23
26
  "lint": "eslint . && prettier --check .",
24
27
  "fix": "eslint . --fix && prettier --write .",
@@ -55,7 +58,9 @@
55
58
  "dependencies": {
56
59
  "@clack/prompts": "^1.2.0",
57
60
  "commander": "^14.0.3",
61
+ "diff": "^9.0.0",
58
62
  "ejs": "^5.0.2",
63
+ "es-toolkit": "^1.45.1",
59
64
  "gradient-string": "^3.0.0",
60
65
  "picocolors": "^1.1.1",
61
66
  "semver": "^7.7.4"
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { type FormEvent, Fragment, useState } from "react";
4
+ import {
5
+ Conversation,
6
+ ConversationContent,
7
+ ConversationScrollButton,
8
+ } from "@/components/ai-elements/conversation";
9
+ import {
10
+ Message,
11
+ MessageContent,
12
+ MessageResponse,
13
+ } from "@/components/ai-elements/message";
14
+ import { useChat } from "../_hooks/useChat";
15
+
16
+ export function ChatPanel() {
17
+ const [text, setText] = useState("");
18
+ const { messages, sendMessage, status } = useChat();
19
+
20
+ const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
21
+ event.preventDefault();
22
+
23
+ const nextText = text.trim();
24
+ if (!nextText) {
25
+ return;
26
+ }
27
+
28
+ void sendMessage({ text: nextText });
29
+ setText("");
30
+ };
31
+
32
+ return (
33
+ <div className="flex min-h-[540px] flex-1 flex-col rounded-lg border bg-background p-4 shadow-sm">
34
+ <Conversation className="flex-1">
35
+ <ConversationContent>
36
+ {messages.map((message) => (
37
+ <Message from={message.role} key={message.id}>
38
+ <MessageContent>
39
+ {message.parts.map((part, index) => (
40
+ <Fragment key={`${message.id}-${index}`}>
41
+ {part.type === "text" ? (
42
+ <MessageResponse>{part.text}</MessageResponse>
43
+ ) : null}
44
+ </Fragment>
45
+ ))}
46
+ </MessageContent>
47
+ </Message>
48
+ ))}
49
+ </ConversationContent>
50
+ <ConversationScrollButton />
51
+ </Conversation>
52
+
53
+ <form className="mt-4 flex gap-2" onSubmit={handleSubmit}>
54
+ <textarea
55
+ className="min-h-20 flex-1 resize-none rounded-md border bg-background px-3 py-2 text-sm outline-none ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
56
+ onChange={(event) => setText(event.currentTarget.value)}
57
+ placeholder="Ask the mock assistant anything..."
58
+ value={text}
59
+ />
60
+ <button
61
+ className="self-end rounded-md bg-primary px-4 py-2 text-primary-foreground text-sm font-medium disabled:cursor-not-allowed disabled:opacity-50"
62
+ disabled={!text.trim() || status === "streaming"}
63
+ type="submit"
64
+ >
65
+ {status === "streaming" ? "Sending..." : "Send"}
66
+ </button>
67
+ </form>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,35 @@
1
+ import type { Metadata } from "next";
2
+ <% if (i18n) { %>import { getTranslations } from "next-intl/server";
3
+ import type { Locale } from "next-intl";
4
+ <% } %>
5
+
6
+ <% if (i18n) { %>type ChatLayoutProps = Readonly<{
7
+ children: React.ReactNode;
8
+ params: Promise<{ locale: string }>;
9
+ }>;
10
+
11
+ export async function generateMetadata({
12
+ params,
13
+ }: Pick<ChatLayoutProps, "params">): Promise<Metadata> {
14
+ const locale = (await params).locale as Locale;
15
+ const t = await getTranslations({ locale, namespace: "Chat" });
16
+
17
+ return {
18
+ title: t("metadataTitle"),
19
+ description: t("metadataDescription"),
20
+ };
21
+ }
22
+ <% } else { %>
23
+ export const metadata: Metadata = {
24
+ title: "Chat",
25
+ description: "Deterministic AI SDK-compatible mock chat",
26
+ };
27
+ <% } %>
28
+
29
+ export default function ChatLayout({
30
+ children,
31
+ }: <% if (i18n) { %>ChatLayoutProps<% } else { %>Readonly<{
32
+ children: React.ReactNode;
33
+ }><% } %>) {
34
+ return children;
35
+ }
@@ -0,0 +1,37 @@
1
+ <% if (i18n) { %>import { getTranslations } from "next-intl/server";
2
+ import type { Locale } from "next-intl";
3
+
4
+ <% } %>
5
+ import { ChatPanel } from "./_components/ChatPanel";
6
+
7
+ <% if (i18n) { %>type ChatPageProps = Readonly<{
8
+ params: Promise<{ locale: Locale }>;
9
+ }>;
10
+
11
+ <% } %>export default <% if (i18n) { %>async <% } %>function ChatPage(<% if (i18n) { %>{ params }: ChatPageProps<% } %>) {
12
+ <% if (i18n) { %> const { locale } = await params;
13
+ const t = await getTranslations({ locale, namespace: "Chat" });
14
+
15
+ <% } %>
16
+ return (
17
+ <main className="flex flex-1 flex-col px-6 py-16">
18
+ <section className="mx-auto flex min-h-[calc(100vh-8rem)] w-full max-w-5xl flex-col gap-6">
19
+ <div className="space-y-4">
20
+ <p className="text-sm font-medium tracking-[0.2em] text-muted-foreground uppercase">
21
+ <% if (i18n) { %>{t("eyebrow")}<% } else { %>AI Chat<% } %>
22
+ </p>
23
+ <div className="space-y-3">
24
+ <h1 className="max-w-3xl text-3xl font-semibold tracking-tight text-balance">
25
+ <% if (i18n) { %>{t("title")}<% } else { %>AI Elements and AI SDK are wired to a deterministic mock.<% } %>
26
+ </h1>
27
+ <p className="max-w-3xl text-muted-foreground text-balance">
28
+ <% if (i18n) { %>{t("description")}<% } else { %>The UI is ready for an external chat backend, but this scaffold
29
+ stays provider-free so the route works before credentials exist.<% } %>
30
+ </p>
31
+ </div>
32
+ </div>
33
+ <ChatPanel />
34
+ </section>
35
+ </main>
36
+ );
37
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ createUIMessageStream,
3
+ createUIMessageStreamResponse,
4
+ type UIMessage,
5
+ } from "ai";
6
+
7
+ export const maxDuration = 30;
8
+
9
+ const SIMULATED_TOKEN_DELAY_MS = 35;
10
+
11
+ interface ChatRequestBody {
12
+ messages?: UIMessage[];
13
+ }
14
+
15
+ function sleep(ms: number): Promise<void> {
16
+ return new Promise((resolve) => setTimeout(resolve, ms));
17
+ }
18
+
19
+ function getLastUserText(messages: UIMessage[]): string {
20
+ const lastUserMessage = messages.findLast(
21
+ (message) => message.role === "user",
22
+ );
23
+
24
+ const text = lastUserMessage?.parts
25
+ .filter((part) => part.type === "text")
26
+ .map((part) => part.text)
27
+ .join(" ")
28
+ .trim();
29
+
30
+ return text || "your message";
31
+ }
32
+
33
+ function splitIntoSimulatedTokens(text: string): string[] {
34
+ return text.match(/\S+\s*/g) ?? [text];
35
+ }
36
+
37
+ export async function POST(req: Request) {
38
+ const body = (await req.json().catch(() => ({}))) as ChatRequestBody;
39
+ const messages = Array.isArray(body.messages) ? body.messages : [];
40
+ const lastUserText = getLastUserText(messages);
41
+ const responseText = `Mock response: I received "${lastUserText}". Replace this route with your external chat backend when ready.`;
42
+
43
+ // TODO: Replace this deterministic mock with the external chat backend proxy.
44
+ return createUIMessageStreamResponse({
45
+ stream: createUIMessageStream({
46
+ async execute({ writer }) {
47
+ writer.write({
48
+ type: "text-start",
49
+ id: "mock-response",
50
+ });
51
+
52
+ for (const token of splitIntoSimulatedTokens(responseText)) {
53
+ writer.write({
54
+ type: "text-delta",
55
+ id: "mock-response",
56
+ delta: token,
57
+ });
58
+ await sleep(SIMULATED_TOKEN_DELAY_MS);
59
+ }
60
+
61
+ writer.write({
62
+ type: "text-end",
63
+ id: "mock-response",
64
+ });
65
+ },
66
+ }),
67
+ });
68
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { useChat as useAiChat } from "@ai-sdk/react";
4
+ import { DefaultChatTransport, type UIMessage } from "ai";
5
+
6
+ const initialMessages: UIMessage[] = [
7
+ {
8
+ id: "mock-assistant-welcome",
9
+ role: "assistant",
10
+ parts: [
11
+ {
12
+ type: "text",
13
+ text: "Hi! This is a deterministic mock assistant. Send a message to verify the AI SDK transport before connecting a real backend.",
14
+ },
15
+ ],
16
+ },
17
+ ];
18
+
19
+ export function useChat() {
20
+ return useAiChat({
21
+ messages: initialMessages,
22
+ transport: new DefaultChatTransport({
23
+ api: "/api/chat",
24
+ }),
25
+ });
26
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import Script from "next/script";
5
+ import { env } from "@/env";
6
+
7
+ export interface AnalyticsProviderProps {
8
+ children: ReactNode;
9
+ hasConsent?: boolean;
10
+ }
11
+
12
+ export function AnalyticsProvider({
13
+ children,
14
+ hasConsent = false,
15
+ }: AnalyticsProviderProps) {
16
+ if (!hasConsent) {
17
+ return <>{children}</>;
18
+ }
19
+
20
+ return (
21
+ <>
22
+ {children}
23
+ <Script id="plausible-analytics-bootstrap" strategy="afterInteractive">
24
+ {`window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init({captureOnLocalhost:${env.NEXT_PUBLIC_PLAUSIBLE_CAPTURE_LOCALHOST === "true"}})`}
25
+ </Script>
26
+ <Script
27
+ src={env.NEXT_PUBLIC_PLAUSIBLE_SCRIPT_SRC}
28
+ strategy="afterInteractive"
29
+ />
30
+ </>
31
+ );
32
+ }
@@ -0,0 +1,40 @@
1
+ export interface AnalyticsEvents {
2
+ page_viewed: undefined;
3
+ cta_clicked: {
4
+ location: string;
5
+ label?: string;
6
+ };
7
+ form_submitted: {
8
+ formId: string;
9
+ success: boolean;
10
+ };
11
+ }
12
+
13
+ export type AnalyticsEventName = keyof AnalyticsEvents;
14
+
15
+ type PlausibleProperties = Record<string, string | number | boolean | null>;
16
+
17
+ type PlausibleEvent = (
18
+ eventName: string,
19
+ options?: { props?: PlausibleProperties },
20
+ ) => void;
21
+
22
+ declare global {
23
+ interface Window {
24
+ plausible?: PlausibleEvent;
25
+ }
26
+ }
27
+
28
+ export function trackEvent<EventName extends AnalyticsEventName>(
29
+ eventName: EventName,
30
+ ...args: AnalyticsEvents[EventName] extends undefined
31
+ ? []
32
+ : [properties: AnalyticsEvents[EventName]]
33
+ ): void {
34
+ if (typeof window === "undefined" || typeof window.plausible !== "function") {
35
+ return;
36
+ }
37
+
38
+ const properties = args[0] as PlausibleProperties | undefined;
39
+ window.plausible(eventName, properties ? { props: properties } : undefined);
40
+ }
@@ -0,0 +1,7 @@
1
+ "use client";
2
+
3
+ import { createAuthClient } from "better-auth/react";
4
+
5
+ export const authClient = createAuthClient();
6
+
7
+ export const { signIn, signUp, signOut, useSession } = authClient;
@@ -0,0 +1,45 @@
1
+ import { betterAuth } from "better-auth";
2
+ <% if (isStateful) { %>import { prismaAdapter } from "better-auth/adapters/prisma";
3
+ import { prisma } from "@/lib/prisma";
4
+ <% } %>import { nextCookies } from "better-auth/next-js";
5
+ import { env } from "@/env";
6
+
7
+ export const auth = betterAuth({
8
+ baseURL: env.BETTER_AUTH_URL,
9
+ secret: env.BETTER_AUTH_SECRET,
10
+ advanced: {
11
+ disableCSRFCheck: false,
12
+ },
13
+ socialProviders: {
14
+ microsoft: {
15
+ clientId: env.MICROSOFT_CLIENT_ID,
16
+ clientSecret: env.MICROSOFT_CLIENT_SECRET,
17
+ tenantId: env.MICROSOFT_TENANT_ID,
18
+ authority: "https://login.microsoftonline.com",
19
+ prompt: "select_account",
20
+ // Create an app registration in Microsoft Entra admin center.
21
+ // Copy its Application (client) ID and client secret into env vars.
22
+ // Add this redirect URI for local dev:
23
+ // http://localhost:3000/api/auth/callback/microsoft
24
+ },
25
+ },
26
+ <% if (isStateful) { %> database: prismaAdapter(prisma, {
27
+ provider: "postgresql",
28
+ }),
29
+ emailAndPassword: {
30
+ enabled: true,
31
+ },
32
+ <% } else { %> session: {
33
+ cookieCache: {
34
+ enabled: true,
35
+ maxAge: 7 * 24 * 60 * 60,
36
+ strategy: "jwe",
37
+ refreshCache: true,
38
+ },
39
+ },
40
+ account: {
41
+ storeStateStrategy: "cookie",
42
+ storeAccountCookie: true,
43
+ },
44
+ <% } %> plugins: [nextCookies()],
45
+ });
@@ -0,0 +1,4 @@
1
+ import { auth } from "@/lib/auth";
2
+ import { toNextJsHandler } from "better-auth/next-js";
3
+
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ <% if (isStateful) { %>import type { FormEvent } from "react";
4
+ <% if (i18n) { %>import { Link } from "@/i18n/navigation";
5
+ <% } else { %>import Link from "next/link";
6
+ <% } %><% } %><% if (i18n) { %>import { useTranslations } from "next-intl";
7
+ <% } %>import { useState } from "react";
8
+ import { Button } from "@/components/ui/button";
9
+ import {
10
+ Card,
11
+ CardContent,
12
+ CardDescription,
13
+ <% if (isStateful) { %> CardFooter,
14
+ <% } %> CardHeader,
15
+ CardTitle,
16
+ } from "@/components/ui/card";
17
+ <% if (isStateful) { %>import { Input } from "@/components/ui/input";
18
+ import { Label } from "@/components/ui/label";
19
+ <% } %>import { authClient } from "@/lib/auth-client";
20
+
21
+ export default function SignInPage() {
22
+ <% if (i18n) { %> const t = useTranslations("Auth");
23
+ <% } %>
24
+ <% if (isStateful) { %> const [email, setEmail] = useState("");
25
+ const [password, setPassword] = useState("");
26
+ <% } %> const [error, setError] = useState<string | null>(null);
27
+ const [pending, setPending] = useState(false);
28
+
29
+ async function signInWithMicrosoft() {
30
+ setPending(true);
31
+ setError(null);
32
+ const result = await authClient.signIn.social({
33
+ provider: "microsoft",
34
+ callbackURL: "/",
35
+ });
36
+ setPending(false);
37
+
38
+ if (result.error) {
39
+ setError(result.error.message ?? <% if (i18n) { %>t("microsoftSignInFailed")<% } else { %>"Microsoft sign-in failed."<% } %>);
40
+ }
41
+ }
42
+ <% if (isStateful) { %>
43
+ async function signInWithEmail(event: FormEvent<HTMLFormElement>) {
44
+ event.preventDefault();
45
+ setPending(true);
46
+ setError(null);
47
+ const result = await authClient.signIn.email({
48
+ email,
49
+ password,
50
+ callbackURL: "/",
51
+ });
52
+ setPending(false);
53
+
54
+ if (result.error) {
55
+ setError(result.error.message ?? <% if (i18n) { %>t("emailSignInFailed")<% } else { %>"Email sign-in failed."<% } %>);
56
+ }
57
+ }
58
+ <% } %>
59
+ return (
60
+ <main className="flex flex-1 items-center justify-center px-6 py-24">
61
+ <Card className="w-full max-w-md">
62
+ <CardHeader>
63
+ <CardTitle><% if (i18n) { %>{t("signInTitle")}<% } else { %>Sign in<% } %></CardTitle>
64
+ <CardDescription>
65
+ <% if (i18n) { %>{t("<% if (isStateful) { %>signInDescriptionStateful<% } else { %>signInDescriptionStateless<% } %>")}<% } else { %>Use your Microsoft account<% if (isStateful) { %> or email and password<% } %> to continue.<% } %>
66
+ </CardDescription>
67
+ </CardHeader>
68
+ <CardContent className="space-y-6">
69
+ <Button
70
+ className="w-full"
71
+ type="button"
72
+ variant="outline"
73
+ onClick={signInWithMicrosoft}
74
+ disabled={pending}
75
+ >
76
+ <% if (i18n) { %>{t("continueWithMicrosoft")}<% } else { %>Continue with Microsoft<% } %>
77
+ </Button>
78
+ <% if (isStateful) { %>
79
+ <form className="space-y-4" onSubmit={signInWithEmail}>
80
+ <div className="space-y-2">
81
+ <Label htmlFor="email"><% if (i18n) { %>{t("email")}<% } else { %>Email<% } %></Label>
82
+ <Input
83
+ id="email"
84
+ type="email"
85
+ autoComplete="email"
86
+ value={email}
87
+ onChange={(event) => setEmail(event.target.value)}
88
+ required
89
+ />
90
+ </div>
91
+ <div className="space-y-2">
92
+ <Label htmlFor="password"><% if (i18n) { %>{t("password")}<% } else { %>Password<% } %></Label>
93
+ <Input
94
+ id="password"
95
+ type="password"
96
+ autoComplete="current-password"
97
+ value={password}
98
+ onChange={(event) => setPassword(event.target.value)}
99
+ required
100
+ />
101
+ </div>
102
+ <Button className="w-full" type="submit" disabled={pending}>
103
+ <% if (i18n) { %>{t("signInWithEmail")}<% } else { %>Sign in with email<% } %>
104
+ </Button>
105
+ </form>
106
+ <% } %>
107
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
108
+ </CardContent>
109
+ <% if (isStateful) { %>
110
+ <CardFooter>
111
+ <p className="text-sm text-muted-foreground">
112
+ <% if (i18n) { %>{t("noAccount")}<% } else { %>No account?<% } %>{" "}
113
+ <Link className="font-medium text-foreground underline" href="/sign-up">
114
+ <% if (i18n) { %>{t("createOne")}<% } else { %>Create one<% } %>
115
+ </Link>
116
+ </p>
117
+ </CardFooter>
118
+ <% } %>
119
+ </Card>
120
+ </main>
121
+ );
122
+ }
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ <% if (isStateful) { %>import type { FormEvent } from "react";
4
+ <% if (i18n) { %>import { Link } from "@/i18n/navigation";
5
+ <% } else { %>import Link from "next/link";
6
+ <% } %><% } %><% if (i18n) { %>import { useTranslations } from "next-intl";
7
+ <% } %>import { useState } from "react";
8
+ import { Button } from "@/components/ui/button";
9
+ import {
10
+ Card,
11
+ CardContent,
12
+ CardDescription,
13
+ <% if (isStateful) { %> CardFooter,
14
+ <% } %>
15
+ CardHeader,
16
+ CardTitle,
17
+ } from "@/components/ui/card";
18
+ <% if (isStateful) { %>import { Input } from "@/components/ui/input";
19
+ import { Label } from "@/components/ui/label";
20
+ <% } %>import { authClient } from "@/lib/auth-client";
21
+
22
+ export default function SignUpPage() {
23
+ <% if (i18n) { %> const t = useTranslations("Auth");
24
+ <% } %>
25
+ <% if (isStateful) { %> const [name, setName] = useState("");
26
+ const [email, setEmail] = useState("");
27
+ const [password, setPassword] = useState("");
28
+ <% } %> const [error, setError] = useState<string | null>(null);
29
+ const [pending, setPending] = useState(false);
30
+
31
+ async function signInWithMicrosoft() {
32
+ setPending(true);
33
+ setError(null);
34
+ const result = await authClient.signIn.social({
35
+ provider: "microsoft",
36
+ callbackURL: "/",
37
+ });
38
+ setPending(false);
39
+
40
+ if (result.error) {
41
+ setError(result.error.message ?? <% if (i18n) { %>t("microsoftSignInFailed")<% } else { %>"Microsoft sign-in failed."<% } %>);
42
+ }
43
+ }
44
+ <% if (isStateful) { %>
45
+ async function signUpWithEmail(event: FormEvent<HTMLFormElement>) {
46
+ event.preventDefault();
47
+ setPending(true);
48
+ setError(null);
49
+ const result = await authClient.signUp.email({
50
+ name,
51
+ email,
52
+ password,
53
+ callbackURL: "/",
54
+ });
55
+ setPending(false);
56
+
57
+ if (result.error) {
58
+ setError(result.error.message ?? <% if (i18n) { %>t("emailSignUpFailed")<% } else { %>"Email sign-up failed."<% } %>);
59
+ }
60
+ }
61
+ <% } %>
62
+ return (
63
+ <main className="flex flex-1 items-center justify-center px-6 py-24">
64
+ <Card className="w-full max-w-md">
65
+ <CardHeader>
66
+ <CardTitle><% if (i18n) { %>{t("<% if (isStateful) { %>createAccountTitle<% } else { %>createAccountMicrosoftTitle<% } %>")}<% } else { %><% if (isStateful) { %>Create account<% } else { %>Create account with Microsoft<% } %><% } %></CardTitle>
67
+ <CardDescription>
68
+ <% if (i18n) { %>{t("<% if (isStateful) { %>createAccountDescriptionStateful<% } else { %>createAccountDescriptionStateless<% } %>")}<% } else { %><% if (isStateful) { %>Use Microsoft or email/password to create your account.<% } else { %>Account creation is handled by Microsoft Entra for this stateless setup.<% } %><% } %>
69
+ </CardDescription>
70
+ </CardHeader>
71
+ <CardContent className="space-y-6">
72
+ <Button
73
+ className="w-full"
74
+ type="button"
75
+ variant="outline"
76
+ onClick={signInWithMicrosoft}
77
+ disabled={pending}
78
+ >
79
+ <% if (i18n) { %>{t("continueWithMicrosoft")}<% } else { %>Continue with Microsoft<% } %>
80
+ </Button>
81
+ <% if (isStateful) { %>
82
+ <form className="space-y-4" onSubmit={signUpWithEmail}>
83
+ <div className="space-y-2">
84
+ <Label htmlFor="name"><% if (i18n) { %>{t("name")}<% } else { %>Name<% } %></Label>
85
+ <Input
86
+ id="name"
87
+ type="text"
88
+ autoComplete="name"
89
+ value={name}
90
+ onChange={(event) => setName(event.target.value)}
91
+ required
92
+ />
93
+ </div>
94
+ <div className="space-y-2">
95
+ <Label htmlFor="email"><% if (i18n) { %>{t("email")}<% } else { %>Email<% } %></Label>
96
+ <Input
97
+ id="email"
98
+ type="email"
99
+ autoComplete="email"
100
+ value={email}
101
+ onChange={(event) => setEmail(event.target.value)}
102
+ required
103
+ />
104
+ </div>
105
+ <div className="space-y-2">
106
+ <Label htmlFor="password"><% if (i18n) { %>{t("password")}<% } else { %>Password<% } %></Label>
107
+ <Input
108
+ id="password"
109
+ type="password"
110
+ autoComplete="new-password"
111
+ value={password}
112
+ onChange={(event) => setPassword(event.target.value)}
113
+ minLength={8}
114
+ required
115
+ />
116
+ </div>
117
+ <Button className="w-full" type="submit" disabled={pending}>
118
+ <% if (i18n) { %>{t("createAccountTitle")}<% } else { %>Create account<% } %>
119
+ </Button>
120
+ </form>
121
+ <% } %>
122
+ {error ? <p className="text-sm text-destructive">{error}</p> : null}
123
+ </CardContent>
124
+ <% if (isStateful) { %>
125
+ <CardFooter>
126
+ <p className="text-sm text-muted-foreground">
127
+ <% if (i18n) { %>{t("alreadyHaveAccount")}<% } else { %>Already have an account?<% } %>{" "}
128
+ <Link className="font-medium text-foreground underline" href="/sign-in">
129
+ <% if (i18n) { %>{t("signInTitle")}<% } else { %>Sign in<% } %>
130
+ </Link>
131
+ </p>
132
+ </CardFooter>
133
+ <% } %>
134
+ </Card>
135
+ </main>
136
+ );
137
+ }