This view is limited to 50 files because it contains too many changes.  See the raw diff here.
Files changed (50) hide show
  1. .env.template +22 -0
  2. .eslintrc.json +3 -0
  3. README.md +2 -1
  4. components.json +1 -1
  5. package.json +11 -3
  6. pnpm-lock.yaml +0 -0
  7. src/app/api/auth/check-bypass/route.ts +1 -1
  8. src/app/api/auth/login/route.ts +7 -6
  9. src/app/api/generate-code/route.ts +35 -31
  10. src/app/api/improve-prompt/route.ts +12 -12
  11. src/app/api/login/route.ts +2 -2
  12. src/app/api/share-link/route.ts +13 -10
  13. src/app/layout.tsx +21 -15
  14. src/app/providers.tsx +12 -16
  15. src/components/app-container.tsx +108 -95
  16. src/components/auth-error-popup.tsx +29 -18
  17. src/components/code-editor.tsx +33 -22
  18. src/components/color-panel.tsx +67 -48
  19. src/components/header.tsx +22 -14
  20. src/components/logo.tsx +1 -1
  21. src/components/model-selector.tsx +33 -25
  22. src/components/preview.tsx +97 -56
  23. src/components/prompt-input.tsx +66 -48
  24. src/components/share-dialog.tsx +26 -22
  25. src/components/theme-provider.tsx +4 -4
  26. src/components/ui/accordion.tsx +14 -14
  27. src/components/ui/alert-dialog.tsx +30 -30
  28. src/components/ui/alert.tsx +12 -12
  29. src/components/ui/aspect-ratio.tsx +4 -4
  30. src/components/ui/avatar.tsx +13 -13
  31. src/components/ui/badge.tsx +7 -7
  32. src/components/ui/breadcrumb.tsx +24 -24
  33. src/components/ui/button.tsx +23 -14
  34. src/components/ui/calendar.tsx +12 -12
  35. src/components/ui/card.tsx +24 -17
  36. src/components/ui/carousel.tsx +80 -80
  37. src/components/ui/chart.tsx +85 -85
  38. src/components/ui/checkbox.tsx +9 -9
  39. src/components/ui/collapsible.tsx +6 -6
  40. src/components/ui/command.tsx +32 -32
  41. src/components/ui/context-menu.tsx +42 -42
  42. src/components/ui/dialog.tsx +27 -27
  43. src/components/ui/drawer.tsx +24 -24
  44. src/components/ui/dropdown-menu.tsx +41 -41
  45. src/components/ui/error-message.tsx +23 -7
  46. src/components/ui/form.tsx +52 -51
  47. src/components/ui/fullscreen-toggle.tsx +20 -11
  48. src/components/ui/hover-card.tsx +10 -10
  49. src/components/ui/icons.tsx +49 -9
  50. src/components/ui/input-otp.tsx +19 -19
.env.template ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # App configurations
2
+ APP_PORT=
3
+ NODE_ENV=
4
+
5
+ # For debugging
6
+ DEFAULT_HF_TOKEN=
7
+ INFERENCE_ENDPOINT_URL=
8
+
9
+ # Hugging Face OAuth2.0 configurations.
10
+ REDIRECT_URI=
11
+ OAUTH_CLIENT_ID=
12
+ OAUTH_CLIENT_SECRET=
13
+
14
+ # Anysite gallery service API auth token.
15
+ ANYSITE_GALLERY_AUTH_TOKEN=
16
+
17
+ # optional. POSTHOG Auth configurations.
18
+ NEXT_PUBLIC_POSTHOG_HOST=
19
+ NEXT_PUBLIC_POSTHOG_KEY=
20
+
21
+ # Novita AI API Key
22
+ NOVITA_API_TOKEN=
.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": "next"
3
+ }
README.md CHANGED
@@ -18,4 +18,5 @@ models:
18
  ---
19
 
20
  ## Credits
21
- This space is inspired by https://huggingface.co/spaces/enzostvs/deepsite.
 
 
18
  ---
19
 
20
  ## Credits
21
+
22
+ This space is inspired by https://huggingface.co/spaces/enzostvs/deepsite.
components.json CHANGED
@@ -18,4 +18,4 @@
18
  "hooks": "@/hooks"
19
  },
20
  "iconLibrary": "lucide"
21
- }
 
18
  "hooks": "@/hooks"
19
  },
20
  "iconLibrary": "lucide"
21
+ }
package.json CHANGED
@@ -1,12 +1,16 @@
1
  {
2
- "name": "my-v0-project",
3
  "version": "0.1.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p ${APP_PORT:-5001}",
7
  "build": "next build",
8
  "start": "next start -p ${APP_PORT:-5001}",
9
- "lint": "next lint"
 
 
 
 
10
  },
11
  "dependencies": {
12
  "@hookform/resolvers": "^3.9.1",
@@ -48,6 +52,7 @@
48
  "date-fns": "4.1.0",
49
  "embla-carousel-react": "8.5.1",
50
  "input-otp": "1.4.1",
 
51
  "lucide-react": "^0.454.0",
52
  "nanoid": "^5.1.5",
53
  "next": "15.2.4",
@@ -66,11 +71,14 @@
66
  "zod": "^3.24.1"
67
  },
68
  "devDependencies": {
 
69
  "@types/node": "^22",
70
  "@types/react": "^19",
71
  "@types/react-dom": "^19",
 
 
72
  "postcss": "^8",
73
  "tailwindcss": "^3.4.17",
74
  "typescript": "^5"
75
  }
76
- }
 
1
  {
2
+ "name": "novita-anysite",
3
  "version": "0.1.0",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p ${APP_PORT:-5001}",
7
  "build": "next build",
8
  "start": "next start -p ${APP_PORT:-5001}",
9
+ "lint": "next lint",
10
+ "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,css,scss}\"",
11
+ "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,css,scss}\"",
12
+ "format:ts": "prettier --write \"**/*.{ts,tsx}\"",
13
+ "type-check": "tsc --noEmit"
14
  },
15
  "dependencies": {
16
  "@hookform/resolvers": "^3.9.1",
 
52
  "date-fns": "4.1.0",
53
  "embla-carousel-react": "8.5.1",
54
  "input-otp": "1.4.1",
55
+ "liquid-glass-react": "^1.1.1",
56
  "lucide-react": "^0.454.0",
57
  "nanoid": "^5.1.5",
58
  "next": "15.2.4",
 
71
  "zod": "^3.24.1"
72
  },
73
  "devDependencies": {
74
+ "prettier": "^3.5.3",
75
  "@types/node": "^22",
76
  "@types/react": "^19",
77
  "@types/react-dom": "^19",
78
+ "eslint": "^9.28.0",
79
+ "eslint-config-next": "15.3.3",
80
  "postcss": "^8",
81
  "tailwindcss": "^3.4.17",
82
  "typescript": "^5"
83
  }
84
+ }
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff
 
src/app/api/auth/check-bypass/route.ts CHANGED
@@ -6,4 +6,4 @@ export async function GET(request: NextRequest): Promise<NextResponse> {
6
  const fingerprint = generateFingerprintFromHeaders(request);
7
  const canBypass = evaluateLoginBypass(fingerprint, true);
8
  return new NextResponse(String(canBypass), { status: 200 });
9
- }
 
6
  const fingerprint = generateFingerprintFromHeaders(request);
7
  const canBypass = evaluateLoginBypass(fingerprint, true);
8
  return new NextResponse(String(canBypass), { status: 200 });
9
+ }
src/app/api/auth/login/route.ts CHANGED
@@ -7,7 +7,8 @@ export async function GET(request: NextRequest) {
7
  const searchParams = request.nextUrl.searchParams;
8
  const code = searchParams.get("code");
9
 
10
- const host = request.headers.get("x-forwarded-host") || request.headers.get("host");
 
11
  const protocol = request.headers.get("x-forwarded-proto") || "http";
12
  const baseUrl = `${protocol}://${host}`;
13
 
@@ -16,7 +17,7 @@ export async function GET(request: NextRequest) {
16
  }
17
 
18
  const Authorization = `Basic ${Buffer.from(
19
- `${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`
20
  ).toString("base64")}`;
21
 
22
  try {
@@ -32,7 +33,7 @@ export async function GET(request: NextRequest) {
32
  redirect_uri: REDIRECT_URI,
33
  }),
34
  });
35
-
36
  const response = await request_auth.json();
37
  if (!response.access_token) {
38
  return NextResponse.redirect(new URL("/", baseUrl));
@@ -40,8 +41,8 @@ export async function GET(request: NextRequest) {
40
 
41
  const res = NextResponse.redirect(new URL("/", baseUrl));
42
  res.cookies.set("hf_token", response.access_token, {
43
- httpOnly: false,
44
- secure: true,
45
  sameSite: "none",
46
  maxAge: 30 * 24 * 60 * 60 * 1000,
47
  path: "/",
@@ -52,4 +53,4 @@ export async function GET(request: NextRequest) {
52
  console.error("Error during login:", error);
53
  return NextResponse.redirect(new URL("/", baseUrl));
54
  }
55
- }
 
7
  const searchParams = request.nextUrl.searchParams;
8
  const code = searchParams.get("code");
9
 
10
+ const host =
11
+ request.headers.get("x-forwarded-host") || request.headers.get("host");
12
  const protocol = request.headers.get("x-forwarded-proto") || "http";
13
  const baseUrl = `${protocol}://${host}`;
14
 
 
17
  }
18
 
19
  const Authorization = `Basic ${Buffer.from(
20
+ `${process.env.OAUTH_CLIENT_ID}:${process.env.OAUTH_CLIENT_SECRET}`,
21
  ).toString("base64")}`;
22
 
23
  try {
 
33
  redirect_uri: REDIRECT_URI,
34
  }),
35
  });
36
+
37
  const response = await request_auth.json();
38
  if (!response.access_token) {
39
  return NextResponse.redirect(new URL("/", baseUrl));
 
41
 
42
  const res = NextResponse.redirect(new URL("/", baseUrl));
43
  res.cookies.set("hf_token", response.access_token, {
44
+ httpOnly: false,
45
+ secure: true,
46
  sameSite: "none",
47
  maxAge: 30 * 24 * 60 * 60 * 1000,
48
  path: "/",
 
53
  console.error("Error during login:", error);
54
  return NextResponse.redirect(new URL("/", baseUrl));
55
  }
56
+ }
src/app/api/generate-code/route.ts CHANGED
@@ -1,11 +1,11 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { MODEL_CONFIG_CODE_GENERATION } from "@/lib/constants";
3
- import {
4
- getInferenceToken,
5
- checkTokenLimit,
6
- createInferenceClient,
7
  createStreamResponse,
8
- getInferenceOptions
9
  } from "@/lib/inference-utils";
10
 
11
  export const dynamic = "force-dynamic";
@@ -13,7 +13,8 @@ const NO_THINK_TAG = " /no_think";
13
 
14
  export async function POST(request: NextRequest) {
15
  try {
16
- const { prompt, html, previousPrompt, colors, modelId } = await request.json();
 
17
 
18
  if (!prompt) {
19
  return NextResponse.json(
@@ -21,13 +22,14 @@ export async function POST(request: NextRequest) {
21
  ok: false,
22
  message: "Missing required fields",
23
  },
24
- { status: 400 }
25
  );
26
  }
27
 
28
  // Find the model config by modelId or use the first one as default
29
- const modelConfig = modelId
30
- ? MODEL_CONFIG_CODE_GENERATION.find(config => config.id === modelId) || MODEL_CONFIG_CODE_GENERATION[0]
 
31
  : MODEL_CONFIG_CODE_GENERATION[0];
32
 
33
  // Get inference token
@@ -39,7 +41,7 @@ export async function POST(request: NextRequest) {
39
  openLogin: tokenResult.error.openLogin,
40
  message: tokenResult.error.message,
41
  },
42
- { status: tokenResult.error.status }
43
  );
44
  }
45
 
@@ -54,8 +56,10 @@ export async function POST(request: NextRequest) {
54
  return NextResponse.json(tokenLimitError, { status: 400 });
55
  }
56
 
57
- const actualNoThinkTag = modelConfig.default_enable_thinking ? NO_THINK_TAG : "";
58
-
 
 
59
  // Use streaming response
60
  return createStreamResponse(async (controller) => {
61
  const client = createInferenceClient(tokenResult);
@@ -68,27 +72,27 @@ export async function POST(request: NextRequest) {
68
  },
69
  ...(previousPrompt
70
  ? [
71
- {
72
- role: "user",
73
- content: previousPrompt,
74
- },
75
- ]
76
  : []),
77
  ...(html
78
  ? [
79
- {
80
- role: "assistant",
81
- content: `The current code is: ${html}.`,
82
- },
83
- ]
84
  : []),
85
- ...((colors && colors.length > 0)
86
  ? [
87
- {
88
- role: "user",
89
- content: `Use the following color palette in your UI design: ${colors.join(', ')}`,
90
- },
91
- ]
92
  : []),
93
  {
94
  role: "user",
@@ -97,7 +101,7 @@ export async function POST(request: NextRequest) {
97
  ];
98
 
99
  const chatCompletion = client.chatCompletionStream(
100
- getInferenceOptions(modelConfig, messages, modelConfig.max_tokens)
101
  );
102
  const encoder = new TextEncoder();
103
  for await (const chunk of chatCompletion) {
@@ -117,7 +121,7 @@ export async function POST(request: NextRequest) {
117
  ok: false,
118
  message: error instanceof Error ? error.message : "Unknown error",
119
  },
120
- { status: 500 }
121
  );
122
  }
123
- }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { MODEL_CONFIG_CODE_GENERATION } from "@/lib/constants";
3
+ import {
4
+ getInferenceToken,
5
+ checkTokenLimit,
6
+ createInferenceClient,
7
  createStreamResponse,
8
+ getInferenceOptions,
9
  } from "@/lib/inference-utils";
10
 
11
  export const dynamic = "force-dynamic";
 
13
 
14
  export async function POST(request: NextRequest) {
15
  try {
16
+ const { prompt, html, previousPrompt, colors, modelId } =
17
+ await request.json();
18
 
19
  if (!prompt) {
20
  return NextResponse.json(
 
22
  ok: false,
23
  message: "Missing required fields",
24
  },
25
+ { status: 400 },
26
  );
27
  }
28
 
29
  // Find the model config by modelId or use the first one as default
30
+ const modelConfig = modelId
31
+ ? MODEL_CONFIG_CODE_GENERATION.find((config) => config.id === modelId) ||
32
+ MODEL_CONFIG_CODE_GENERATION[0]
33
  : MODEL_CONFIG_CODE_GENERATION[0];
34
 
35
  // Get inference token
 
41
  openLogin: tokenResult.error.openLogin,
42
  message: tokenResult.error.message,
43
  },
44
+ { status: tokenResult.error.status },
45
  );
46
  }
47
 
 
56
  return NextResponse.json(tokenLimitError, { status: 400 });
57
  }
58
 
59
+ const actualNoThinkTag = modelConfig.default_enable_thinking
60
+ ? NO_THINK_TAG
61
+ : "";
62
+
63
  // Use streaming response
64
  return createStreamResponse(async (controller) => {
65
  const client = createInferenceClient(tokenResult);
 
72
  },
73
  ...(previousPrompt
74
  ? [
75
+ {
76
+ role: "user",
77
+ content: previousPrompt,
78
+ },
79
+ ]
80
  : []),
81
  ...(html
82
  ? [
83
+ {
84
+ role: "assistant",
85
+ content: `The current code is: ${html}.`,
86
+ },
87
+ ]
88
  : []),
89
+ ...(colors && colors.length > 0
90
  ? [
91
+ {
92
+ role: "user",
93
+ content: `Use the following color palette in your UI design: ${colors.join(", ")}`,
94
+ },
95
+ ]
96
  : []),
97
  {
98
  role: "user",
 
101
  ];
102
 
103
  const chatCompletion = client.chatCompletionStream(
104
+ getInferenceOptions(modelConfig, messages, modelConfig.max_tokens),
105
  );
106
  const encoder = new TextEncoder();
107
  for await (const chunk of chatCompletion) {
 
121
  ok: false,
122
  message: error instanceof Error ? error.message : "Unknown error",
123
  },
124
+ { status: 500 },
125
  );
126
  }
127
+ }
src/app/api/improve-prompt/route.ts CHANGED
@@ -1,11 +1,11 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { MODEL_CONFIG_PROMPT_IMPROVEMENT } from "@/lib/constants";
3
- import {
4
- getInferenceToken,
5
- checkTokenLimit,
6
- createInferenceClient,
7
  createStreamResponse,
8
- getInferenceOptions
9
  } from "@/lib/inference-utils";
10
 
11
  export const dynamic = "force-dynamic";
@@ -20,7 +20,7 @@ export async function POST(request: NextRequest) {
20
  ok: false,
21
  message: "Missing required fields",
22
  },
23
- { status: 400 }
24
  );
25
  }
26
 
@@ -33,12 +33,12 @@ export async function POST(request: NextRequest) {
33
  openLogin: tokenResult.error.openLogin,
34
  message: tokenResult.error.message,
35
  },
36
- { status: tokenResult.error.status }
37
  );
38
  }
39
 
40
  let TOKENS_USED = prompt?.length || 0;
41
-
42
  let modelConfig = MODEL_CONFIG_PROMPT_IMPROVEMENT;
43
 
44
  // Check token limit
@@ -52,7 +52,7 @@ export async function POST(request: NextRequest) {
52
  // Use streaming response
53
  return createStreamResponse(async (controller) => {
54
  const client = createInferenceClient(tokenResult);
55
-
56
  const messages = [
57
  {
58
  role: "system",
@@ -65,7 +65,7 @@ export async function POST(request: NextRequest) {
65
  ];
66
 
67
  const chatCompletion = client.chatCompletionStream(
68
- getInferenceOptions(modelConfig, messages, modelConfig.max_tokens)
69
  );
70
 
71
  const encoder = new TextEncoder();
@@ -82,7 +82,7 @@ export async function POST(request: NextRequest) {
82
  ok: false,
83
  message: error instanceof Error ? error.message : "Unknown error",
84
  },
85
- { status: 500 }
86
  );
87
  }
88
- }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { MODEL_CONFIG_PROMPT_IMPROVEMENT } from "@/lib/constants";
3
+ import {
4
+ getInferenceToken,
5
+ checkTokenLimit,
6
+ createInferenceClient,
7
  createStreamResponse,
8
+ getInferenceOptions,
9
  } from "@/lib/inference-utils";
10
 
11
  export const dynamic = "force-dynamic";
 
20
  ok: false,
21
  message: "Missing required fields",
22
  },
23
+ { status: 400 },
24
  );
25
  }
26
 
 
33
  openLogin: tokenResult.error.openLogin,
34
  message: tokenResult.error.message,
35
  },
36
+ { status: tokenResult.error.status },
37
  );
38
  }
39
 
40
  let TOKENS_USED = prompt?.length || 0;
41
+
42
  let modelConfig = MODEL_CONFIG_PROMPT_IMPROVEMENT;
43
 
44
  // Check token limit
 
52
  // Use streaming response
53
  return createStreamResponse(async (controller) => {
54
  const client = createInferenceClient(tokenResult);
55
+
56
  const messages = [
57
  {
58
  role: "system",
 
65
  ];
66
 
67
  const chatCompletion = client.chatCompletionStream(
68
+ getInferenceOptions(modelConfig, messages, modelConfig.max_tokens),
69
  );
70
 
71
  const encoder = new TextEncoder();
 
82
  ok: false,
83
  message: error instanceof Error ? error.message : "Unknown error",
84
  },
85
+ { status: 500 },
86
  );
87
  }
88
+ }
src/app/api/login/route.ts CHANGED
@@ -4,6 +4,6 @@ export const dynamic = "force-dynamic";
4
 
5
  export async function GET() {
6
  const url = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=openid%20profile%20inference-api&prompt=consent&state=1234567890`;
7
-
8
  return NextResponse.redirect(url);
9
- }
 
4
 
5
  export async function GET() {
6
  const url = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=openid%20profile%20inference-api&prompt=consent&state=1234567890`;
7
+
8
  return NextResponse.redirect(url);
9
+ }
src/app/api/share-link/route.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
 
3
  const BASE_URL = "https://anysite-gallery.novita.ai";
4
 
@@ -7,10 +7,10 @@ export async function POST(request: NextRequest) {
7
  const { filename, code } = await request.json();
8
 
9
  const response = await fetch(`${BASE_URL}/api/upload-code`, {
10
- method: 'POST',
11
  headers: {
12
- 'Content-Type': 'application/json',
13
- 'x-token': process.env.ANYSITE_GALLERY_AUTH_TOKEN || "",
14
  },
15
  body: JSON.stringify({
16
  filename,
@@ -20,18 +20,21 @@ export async function POST(request: NextRequest) {
20
 
21
  if (!response.ok) {
22
  return NextResponse.json(
23
- { success: false, message: `Failed to upload: ${response.status} ${response.statusText}` },
24
- { status: response.status }
 
 
 
25
  );
26
  }
27
 
28
  const result = await response.json();
29
  return NextResponse.json(result);
30
  } catch (error) {
31
- console.error('Error in share-link API:', error);
32
  return NextResponse.json(
33
- { success: false, message: 'Internal server error' },
34
- { status: 500 }
35
  );
36
  }
37
- }
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
 
3
  const BASE_URL = "https://anysite-gallery.novita.ai";
4
 
 
7
  const { filename, code } = await request.json();
8
 
9
  const response = await fetch(`${BASE_URL}/api/upload-code`, {
10
+ method: "POST",
11
  headers: {
12
+ "Content-Type": "application/json",
13
+ "x-token": process.env.ANYSITE_GALLERY_AUTH_TOKEN || "",
14
  },
15
  body: JSON.stringify({
16
  filename,
 
20
 
21
  if (!response.ok) {
22
  return NextResponse.json(
23
+ {
24
+ success: false,
25
+ message: `Failed to upload: ${response.status} ${response.statusText}`,
26
+ },
27
+ { status: response.status },
28
  );
29
  }
30
 
31
  const result = await response.json();
32
  return NextResponse.json(result);
33
  } catch (error) {
34
+ console.error("Error in share-link API:", error);
35
  return NextResponse.json(
36
+ { success: false, message: "Internal server error" },
37
+ { status: 500 },
38
  );
39
  }
40
+ }
src/app/layout.tsx CHANGED
@@ -1,30 +1,36 @@
1
- import type React from "react"
2
- import type { Metadata } from "next"
3
- import { Inter } from "next/font/google"
4
- import "./globals.css"
5
- import { ThemeProvider } from "@/components/theme-provider"
6
- import { TooltipProvider } from "@/components/ui/tooltip"
7
- import { Toaster } from "sonner"
8
- import { ModelProvider } from "@/lib/contexts/model-context"
9
- import { PostHogProvider } from './providers'
10
 
11
- const inter = Inter({ subsets: ["latin"] })
12
 
13
  export const metadata: Metadata = {
14
  title: "Novita AnySite",
15
- description: "Create stunning websites with cutting-edge AI models powered by Novita AI.",
16
- }
 
17
 
18
  export default function RootLayout({
19
  children,
20
  }: Readonly<{
21
- children: React.ReactNode
22
  }>) {
23
  return (
24
  <html lang="en" suppressHydrationWarning>
25
  <body className={inter.className} suppressHydrationWarning>
26
  <PostHogProvider>
27
- <ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
 
 
 
 
 
28
  <ModelProvider>
29
  <TooltipProvider>{children}</TooltipProvider>
30
  <Toaster richColors />
@@ -33,5 +39,5 @@ export default function RootLayout({
33
  </PostHogProvider>
34
  </body>
35
  </html>
36
- )
37
  }
 
1
+ import type React from "react";
2
+ import type { Metadata } from "next";
3
+ import { Inter } from "next/font/google";
4
+ import "./globals.css";
5
+ import { ThemeProvider } from "@/components/theme-provider";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import { Toaster } from "sonner";
8
+ import { ModelProvider } from "@/lib/contexts/model-context";
9
+ import { PostHogProvider } from "./providers";
10
 
11
+ const inter = Inter({ subsets: ["latin"] });
12
 
13
  export const metadata: Metadata = {
14
  title: "Novita AnySite",
15
+ description:
16
+ "Create stunning websites with cutting-edge AI models powered by Novita AI.",
17
+ };
18
 
19
  export default function RootLayout({
20
  children,
21
  }: Readonly<{
22
+ children: React.ReactNode;
23
  }>) {
24
  return (
25
  <html lang="en" suppressHydrationWarning>
26
  <body className={inter.className} suppressHydrationWarning>
27
  <PostHogProvider>
28
+ <ThemeProvider
29
+ attribute="class"
30
+ defaultTheme="dark"
31
+ enableSystem
32
+ disableTransitionOnChange
33
+ >
34
  <ModelProvider>
35
  <TooltipProvider>{children}</TooltipProvider>
36
  <Toaster richColors />
 
39
  </PostHogProvider>
40
  </body>
41
  </html>
42
+ );
43
  }
src/app/providers.tsx CHANGED
@@ -1,21 +1,17 @@
1
  // app/providers.tsx
2
- 'use client'
3
 
4
- import posthog from 'posthog-js'
5
- import { PostHogProvider as PHProvider } from 'posthog-js/react'
6
- import { useEffect } from 'react'
7
 
8
  export function PostHogProvider({ children }: { children: React.ReactNode }) {
9
- useEffect(() => {
10
- posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY || '', {
11
- api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
12
- defaults: '2025-05-24',
13
- })
14
- }, [])
15
 
16
- return (
17
- <PHProvider client={posthog}>
18
- {children}
19
- </PHProvider>
20
- )
21
- }
 
1
  // app/providers.tsx
2
+ "use client";
3
 
4
+ import posthog from "posthog-js";
5
+ import { PostHogProvider as PHProvider } from "posthog-js/react";
6
+ import { useEffect } from "react";
7
 
8
  export function PostHogProvider({ children }: { children: React.ReactNode }) {
9
+ useEffect(() => {
10
+ posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY || "", {
11
+ api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
12
+ defaults: "2025-05-24",
13
+ });
14
+ }, []);
15
 
16
+ return <PHProvider client={posthog}>{children}</PHProvider>;
17
+ }
 
 
 
 
src/components/app-container.tsx CHANGED
@@ -1,17 +1,21 @@
1
- "use client"
2
-
3
- import { useState, useRef, useEffect } from "react"
4
- import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable"
5
- import { CodeEditor } from "./code-editor"
6
- import { Preview } from "./preview"
7
- import { PromptInput } from "./prompt-input"
8
- import { Header } from "./header"
9
- import { VersionDropdown } from "./version-dropdown"
10
- import { AuthErrorPopup } from "./auth-error-popup"
11
- import { toast } from "sonner"
12
- import { DEFAULT_HTML } from "@/lib/constants"
13
- import { Version, PreviewRef } from "@/lib/types"
14
- import { ErrorMessage } from "./ui/error-message"
 
 
 
 
15
 
16
  export function AppContainer() {
17
  const [code, setCode] = useState<string>("");
@@ -35,15 +39,17 @@ export function AppContainer() {
35
  // Initialize current version ID from localStorage on mount
36
  useEffect(() => {
37
  setInitialLoading(true);
38
-
39
- const storedVersions = localStorage.getItem('novita-versions');
40
  if (storedVersions) {
41
  const parsedVersions = JSON.parse(storedVersions) as Version[];
42
  if (parsedVersions.length > 0) {
43
  // Sort by creation time, newest first
44
- const sortedVersions = parsedVersions.sort((a, b) => b.createdAt - a.createdAt);
 
 
45
  const newestVersion = sortedVersions[0];
46
-
47
  // Set the current version ID and also load its code and prompt
48
  setCurrentVersionId(newestVersion.id);
49
  latestVersionIdRef.current = newestVersion.id;
@@ -61,9 +67,9 @@ export function AppContainer() {
61
  setCode(DEFAULT_HTML);
62
  setCurrentVersionId(null);
63
  latestVersionIdRef.current = null;
64
- localStorage.setItem('novita-versions', JSON.stringify([]));
65
  }
66
-
67
  // Finish loading after a short delay to ensure everything renders properly
68
  setTimeout(() => {
69
  setInitialLoading(false);
@@ -73,23 +79,23 @@ export function AppContainer() {
73
  const handlePromptSubmit = async (newPrompt: string, colors: string[]) => {
74
  // Store the current prompt as previous prompt
75
  const oldPrompt = currentPromptRef.current;
76
-
77
  // Update prompt state
78
  setPrompt(newPrompt);
79
  currentPromptRef.current = newPrompt; // Update the ref immediately
80
-
81
  // Clear code and preview when submit is pressed
82
  setCode("");
83
-
84
  // Create a new version immediately with empty code when submit is clicked
85
  const newVersionId = saveVersionInitial(newPrompt);
86
  setCurrentVersionId(newVersionId);
87
-
88
  if (previewRef.current) {
89
  // Pass both the new prompt and old prompt for context
90
  await previewRef.current.generateCode(newPrompt, colors, oldPrompt);
91
  }
92
- }
93
 
94
  // Handle code change and save version
95
  const handleCodeChange = (newCode: string, save = false) => {
@@ -101,73 +107,74 @@ export function AppContainer() {
101
  // Make sure we're updating the latest version
102
  updateVersionWithFinalCode(newCode);
103
  }
104
- }
105
-
106
  // Explicitly save version when loading completes
107
  const handleLoadingChange = (isLoading: boolean) => {
108
  setLoading(isLoading);
109
- }
110
 
111
  // Function to create a new version initially with empty code
112
  const saveVersionInitial = (promptToSave: string) => {
113
  // Get existing versions
114
- const storedVersions = localStorage.getItem('novita-versions');
115
  let versions: Version[] = storedVersions ? JSON.parse(storedVersions) : [];
116
-
117
  // Create new version with empty code
118
  const newVersion: Version = {
119
  id: Date.now().toString(),
120
  code: "", // Empty code field initially
121
  prompt: promptToSave,
122
- createdAt: Date.now()
123
  };
124
-
125
  // Add to beginning (newest first)
126
  versions = [newVersion, ...versions];
127
-
128
  // Save to localStorage
129
- localStorage.setItem('novita-versions', JSON.stringify(versions));
130
-
131
  // Update current version ID
132
  setCurrentVersionId(newVersion.id);
133
  // Also update the latest version ID ref
134
  latestVersionIdRef.current = newVersion.id;
135
-
136
  return newVersion.id;
137
- }
138
 
139
  // Function to update existing version with final code
140
  const updateVersionWithFinalCode = (finalCode: string) => {
141
  // Get existing versions
142
- const storedVersions = localStorage.getItem('novita-versions');
143
  if (!storedVersions) return;
144
-
145
  let versions: Version[] = JSON.parse(storedVersions);
146
-
147
  if (!versions.length) return;
148
-
149
  // Prioritize using the latestVersionIdRef, then fall back to currentVersionId, then to the most recent version
150
- const versionIdToUpdate = latestVersionIdRef.current || currentVersionId || versions[0].id;
151
-
 
152
  if (!versionIdToUpdate) return;
153
-
154
  // Find and update the current version
155
- const updatedVersions = versions.map(version => {
156
  if (version.id === versionIdToUpdate) {
157
  return { ...version, code: finalCode };
158
  }
159
  return version;
160
  });
161
-
162
  // Save to localStorage
163
- localStorage.setItem('novita-versions', JSON.stringify(updatedVersions));
164
-
165
  // Show a subtle toast notification when code generation completes
166
- toast.success('Generated code saved', {
167
- position: 'top-right',
168
  duration: 2000,
169
  });
170
- }
171
 
172
  // Handle selecting a version from the dropdown
173
  const handleVersionSelect = (version: Version) => {
@@ -176,77 +183,78 @@ export function AppContainer() {
176
  currentPromptRef.current = version.prompt;
177
  setCurrentVersionId(version.id);
178
  latestVersionIdRef.current = version.id;
179
- }
180
-
181
  // Handle clearing all versions
182
  const handleClearAll = () => {
183
  // Reset to default HTML
184
  setCode(DEFAULT_HTML);
185
- setPrompt('');
186
- currentPromptRef.current = '';
187
  setCurrentVersionId(null);
188
  latestVersionIdRef.current = null;
189
- }
190
 
191
  const handleManualCodeEdit = (newCode: string) => {
192
  setCode(newCode);
193
-
194
  if (currentVersionId) {
195
  updateVersionWithCode(newCode);
196
  }
197
  };
198
 
199
- const updateVersionWithCode = (editedCode: string) => { // Get existing versions
200
- const storedVersions = localStorage.getItem('novita-versions');
 
201
  if (!storedVersions || !currentVersionId) return;
202
-
203
  let versions: Version[] = JSON.parse(storedVersions);
204
-
205
- const updatedVersions = versions.map(version => {
206
  if (version.id === currentVersionId) {
207
  return { ...version, code: editedCode };
208
  }
209
  return version;
210
  });
211
-
212
- localStorage.setItem('novita-versions', JSON.stringify(updatedVersions));
213
  };
214
 
215
  return (
216
  <div className="flex flex-col h-screen bg-novita-dark text-white">
217
- <Header
218
  onVersionSelect={handleVersionSelect}
219
  currentVersion={currentVersionId || undefined}
220
  onClearAll={handleClearAll}
221
  />
222
  <div className="flex flex-1 overflow-hidden relative">
223
  <ResizablePanelGroup direction="horizontal" className="h-full">
224
- <ResizablePanel
225
- defaultSize={33}
226
- minSize={20}
227
  maxSize={60}
228
  className="flex flex-col relative"
229
  >
230
- <CodeEditor
231
- code={code}
232
- isLoading={loading}
233
  onCodeChange={handleManualCodeEdit}
234
  />
235
-
236
- <AuthErrorPopup
237
- show={showAuthError}
238
- onClose={() => setShowAuthError(false)}
239
  />
240
-
241
- <PromptInput
242
- onSubmit={handlePromptSubmit}
243
- isLoading={loading}
244
  initialPrompt={prompt}
245
  onImproveError={setImproveError}
246
  />
247
  </ResizablePanel>
248
-
249
- <ResizableHandle
250
  withHandle={false}
251
  className="w-1 bg-novita-gray/20 hover:bg-novita-gray/40 transition-colors duration-200 relative group cursor-col-resize"
252
  >
@@ -254,16 +262,16 @@ export function AppContainer() {
254
  <div className="w-0.5 h-8 bg-novita-gray/60 rounded-full" />
255
  </div>
256
  </ResizableHandle>
257
-
258
- <ResizablePanel
259
- defaultSize={67}
260
  minSize={40}
261
  className="flex flex-col"
262
  >
263
- <Preview
264
  ref={previewRef}
265
- initialHtml={code}
266
- onCodeChange={handleCodeChange}
267
  onLoadingChange={handleLoadingChange}
268
  onAuthErrorChange={setShowAuthError}
269
  onErrorChange={setGenerationError}
@@ -271,29 +279,34 @@ export function AppContainer() {
271
  />
272
  </ResizablePanel>
273
  </ResizablePanelGroup>
274
-
275
  {initialLoading && (
276
  <div className="absolute inset-0 bg-novita-dark/80 backdrop-blur-sm flex items-center justify-center z-[999]">
277
  <div className="p-4 text-center">
278
- <div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]" role="status">
279
- <span className="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]">Loading...</span>
 
 
 
 
 
280
  </div>
281
  <p className="mt-2">Preparing the workspace...</p>
282
  </div>
283
  </div>
284
  )}
285
  </div>
286
-
287
- <ErrorMessage
288
- message={improveError || generationError}
289
  onClose={() => {
290
  if (improveError) {
291
  setImproveError(null);
292
  } else if (generationError) {
293
  setGenerationError(null);
294
  }
295
- }}
296
  />
297
  </div>
298
- )
299
  }
 
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect } from "react";
4
+ import {
5
+ ResizablePanelGroup,
6
+ ResizablePanel,
7
+ ResizableHandle,
8
+ } from "@/components/ui/resizable";
9
+ import { CodeEditor } from "./code-editor";
10
+ import { Preview } from "./preview";
11
+ import { PromptInput } from "./prompt-input";
12
+ import { Header } from "./header";
13
+ import { VersionDropdown } from "./version-dropdown";
14
+ import { AuthErrorPopup } from "./auth-error-popup";
15
+ import { toast } from "sonner";
16
+ import { DEFAULT_HTML } from "@/lib/constants";
17
+ import { Version, PreviewRef } from "@/lib/types";
18
+ import { ErrorMessage } from "./ui/error-message";
19
 
20
  export function AppContainer() {
21
  const [code, setCode] = useState<string>("");
 
39
  // Initialize current version ID from localStorage on mount
40
  useEffect(() => {
41
  setInitialLoading(true);
42
+
43
+ const storedVersions = localStorage.getItem("novita-versions");
44
  if (storedVersions) {
45
  const parsedVersions = JSON.parse(storedVersions) as Version[];
46
  if (parsedVersions.length > 0) {
47
  // Sort by creation time, newest first
48
+ const sortedVersions = parsedVersions.sort(
49
+ (a, b) => b.createdAt - a.createdAt,
50
+ );
51
  const newestVersion = sortedVersions[0];
52
+
53
  // Set the current version ID and also load its code and prompt
54
  setCurrentVersionId(newestVersion.id);
55
  latestVersionIdRef.current = newestVersion.id;
 
67
  setCode(DEFAULT_HTML);
68
  setCurrentVersionId(null);
69
  latestVersionIdRef.current = null;
70
+ localStorage.setItem("novita-versions", JSON.stringify([]));
71
  }
72
+
73
  // Finish loading after a short delay to ensure everything renders properly
74
  setTimeout(() => {
75
  setInitialLoading(false);
 
79
  const handlePromptSubmit = async (newPrompt: string, colors: string[]) => {
80
  // Store the current prompt as previous prompt
81
  const oldPrompt = currentPromptRef.current;
82
+
83
  // Update prompt state
84
  setPrompt(newPrompt);
85
  currentPromptRef.current = newPrompt; // Update the ref immediately
86
+
87
  // Clear code and preview when submit is pressed
88
  setCode("");
89
+
90
  // Create a new version immediately with empty code when submit is clicked
91
  const newVersionId = saveVersionInitial(newPrompt);
92
  setCurrentVersionId(newVersionId);
93
+
94
  if (previewRef.current) {
95
  // Pass both the new prompt and old prompt for context
96
  await previewRef.current.generateCode(newPrompt, colors, oldPrompt);
97
  }
98
+ };
99
 
100
  // Handle code change and save version
101
  const handleCodeChange = (newCode: string, save = false) => {
 
107
  // Make sure we're updating the latest version
108
  updateVersionWithFinalCode(newCode);
109
  }
110
+ };
111
+
112
  // Explicitly save version when loading completes
113
  const handleLoadingChange = (isLoading: boolean) => {
114
  setLoading(isLoading);
115
+ };
116
 
117
  // Function to create a new version initially with empty code
118
  const saveVersionInitial = (promptToSave: string) => {
119
  // Get existing versions
120
+ const storedVersions = localStorage.getItem("novita-versions");
121
  let versions: Version[] = storedVersions ? JSON.parse(storedVersions) : [];
122
+
123
  // Create new version with empty code
124
  const newVersion: Version = {
125
  id: Date.now().toString(),
126
  code: "", // Empty code field initially
127
  prompt: promptToSave,
128
+ createdAt: Date.now(),
129
  };
130
+
131
  // Add to beginning (newest first)
132
  versions = [newVersion, ...versions];
133
+
134
  // Save to localStorage
135
+ localStorage.setItem("novita-versions", JSON.stringify(versions));
136
+
137
  // Update current version ID
138
  setCurrentVersionId(newVersion.id);
139
  // Also update the latest version ID ref
140
  latestVersionIdRef.current = newVersion.id;
141
+
142
  return newVersion.id;
143
+ };
144
 
145
  // Function to update existing version with final code
146
  const updateVersionWithFinalCode = (finalCode: string) => {
147
  // Get existing versions
148
+ const storedVersions = localStorage.getItem("novita-versions");
149
  if (!storedVersions) return;
150
+
151
  let versions: Version[] = JSON.parse(storedVersions);
152
+
153
  if (!versions.length) return;
154
+
155
  // Prioritize using the latestVersionIdRef, then fall back to currentVersionId, then to the most recent version
156
+ const versionIdToUpdate =
157
+ latestVersionIdRef.current || currentVersionId || versions[0].id;
158
+
159
  if (!versionIdToUpdate) return;
160
+
161
  // Find and update the current version
162
+ const updatedVersions = versions.map((version) => {
163
  if (version.id === versionIdToUpdate) {
164
  return { ...version, code: finalCode };
165
  }
166
  return version;
167
  });
168
+
169
  // Save to localStorage
170
+ localStorage.setItem("novita-versions", JSON.stringify(updatedVersions));
171
+
172
  // Show a subtle toast notification when code generation completes
173
+ toast.success("Generated code saved", {
174
+ position: "top-right",
175
  duration: 2000,
176
  });
177
+ };
178
 
179
  // Handle selecting a version from the dropdown
180
  const handleVersionSelect = (version: Version) => {
 
183
  currentPromptRef.current = version.prompt;
184
  setCurrentVersionId(version.id);
185
  latestVersionIdRef.current = version.id;
186
+ };
187
+
188
  // Handle clearing all versions
189
  const handleClearAll = () => {
190
  // Reset to default HTML
191
  setCode(DEFAULT_HTML);
192
+ setPrompt("");
193
+ currentPromptRef.current = "";
194
  setCurrentVersionId(null);
195
  latestVersionIdRef.current = null;
196
+ };
197
 
198
  const handleManualCodeEdit = (newCode: string) => {
199
  setCode(newCode);
200
+
201
  if (currentVersionId) {
202
  updateVersionWithCode(newCode);
203
  }
204
  };
205
 
206
+ const updateVersionWithCode = (editedCode: string) => {
207
+ // Get existing versions
208
+ const storedVersions = localStorage.getItem("novita-versions");
209
  if (!storedVersions || !currentVersionId) return;
210
+
211
  let versions: Version[] = JSON.parse(storedVersions);
212
+
213
+ const updatedVersions = versions.map((version) => {
214
  if (version.id === currentVersionId) {
215
  return { ...version, code: editedCode };
216
  }
217
  return version;
218
  });
219
+
220
+ localStorage.setItem("novita-versions", JSON.stringify(updatedVersions));
221
  };
222
 
223
  return (
224
  <div className="flex flex-col h-screen bg-novita-dark text-white">
225
+ <Header
226
  onVersionSelect={handleVersionSelect}
227
  currentVersion={currentVersionId || undefined}
228
  onClearAll={handleClearAll}
229
  />
230
  <div className="flex flex-1 overflow-hidden relative">
231
  <ResizablePanelGroup direction="horizontal" className="h-full">
232
+ <ResizablePanel
233
+ defaultSize={33}
234
+ minSize={20}
235
  maxSize={60}
236
  className="flex flex-col relative"
237
  >
238
+ <CodeEditor
239
+ code={code}
240
+ isLoading={loading}
241
  onCodeChange={handleManualCodeEdit}
242
  />
243
+
244
+ <AuthErrorPopup
245
+ show={showAuthError}
246
+ onClose={() => setShowAuthError(false)}
247
  />
248
+
249
+ <PromptInput
250
+ onSubmit={handlePromptSubmit}
251
+ isLoading={loading}
252
  initialPrompt={prompt}
253
  onImproveError={setImproveError}
254
  />
255
  </ResizablePanel>
256
+
257
+ <ResizableHandle
258
  withHandle={false}
259
  className="w-1 bg-novita-gray/20 hover:bg-novita-gray/40 transition-colors duration-200 relative group cursor-col-resize"
260
  >
 
262
  <div className="w-0.5 h-8 bg-novita-gray/60 rounded-full" />
263
  </div>
264
  </ResizableHandle>
265
+
266
+ <ResizablePanel
267
+ defaultSize={67}
268
  minSize={40}
269
  className="flex flex-col"
270
  >
271
+ <Preview
272
  ref={previewRef}
273
+ initialHtml={code}
274
+ onCodeChange={handleCodeChange}
275
  onLoadingChange={handleLoadingChange}
276
  onAuthErrorChange={setShowAuthError}
277
  onErrorChange={setGenerationError}
 
279
  />
280
  </ResizablePanel>
281
  </ResizablePanelGroup>
282
+
283
  {initialLoading && (
284
  <div className="absolute inset-0 bg-novita-dark/80 backdrop-blur-sm flex items-center justify-center z-[999]">
285
  <div className="p-4 text-center">
286
+ <div
287
+ className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"
288
+ role="status"
289
+ >
290
+ <span className="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]">
291
+ Loading...
292
+ </span>
293
  </div>
294
  <p className="mt-2">Preparing the workspace...</p>
295
  </div>
296
  </div>
297
  )}
298
  </div>
299
+
300
+ <ErrorMessage
301
+ message={improveError || generationError}
302
  onClose={() => {
303
  if (improveError) {
304
  setImproveError(null);
305
  } else if (generationError) {
306
  setGenerationError(null);
307
  }
308
+ }}
309
  />
310
  </div>
311
+ );
312
  }
src/components/auth-error-popup.tsx CHANGED
@@ -1,40 +1,51 @@
1
- import React from 'react'
2
 
3
  interface AuthErrorPopupProps {
4
- show: boolean
5
- authUrl?: string
6
- onClose?: () => void
7
  }
8
 
9
- export function AuthErrorPopup({
10
- show,
11
- authUrl = '/api/login',
12
- onClose
13
  }: AuthErrorPopupProps) {
14
- if (!show) return null
15
-
16
  return (
17
  <div className="absolute right-0 bottom-20 mr-4 w-fit px-2.5 bg-gray-800 border border-gray-600 rounded-lg p-4 shadow-xl z-50">
18
  {onClose && (
19
- <button
20
  onClick={onClose}
21
  className="absolute top-1 right-1 text-gray-400 hover:text-white p-1 rounded-full hover:bg-gray-700 focus:outline-none"
22
  aria-label="Close"
23
  >
24
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
25
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
 
 
 
 
 
 
 
 
 
 
 
26
  </svg>
27
  </button>
28
  )}
29
  <div className="flex flex-col items-center">
30
  <p className="mb-3 text-center">Sign in continue</p>
31
  <a href={authUrl} className="block w-[214px] h-[40px]">
32
- <img
33
- src="https://huggingface.co/datasets/huggingface/badges/resolve/main/sign-in-with-huggingface-lg-dark.svg"
34
- alt="Sign in with Hugging Face"
35
  />
36
  </a>
37
  </div>
38
  </div>
39
- )
40
- }
 
1
+ import React from "react";
2
 
3
  interface AuthErrorPopupProps {
4
+ show: boolean;
5
+ authUrl?: string;
6
+ onClose?: () => void;
7
  }
8
 
9
+ export function AuthErrorPopup({
10
+ show,
11
+ authUrl = "/api/login",
12
+ onClose,
13
  }: AuthErrorPopupProps) {
14
+ if (!show) return null;
15
+
16
  return (
17
  <div className="absolute right-0 bottom-20 mr-4 w-fit px-2.5 bg-gray-800 border border-gray-600 rounded-lg p-4 shadow-xl z-50">
18
  {onClose && (
19
+ <button
20
  onClick={onClose}
21
  className="absolute top-1 right-1 text-gray-400 hover:text-white p-1 rounded-full hover:bg-gray-700 focus:outline-none"
22
  aria-label="Close"
23
  >
24
+ <svg
25
+ className="w-4 h-4"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ viewBox="0 0 24 24"
29
+ xmlns="http://www.w3.org/2000/svg"
30
+ >
31
+ <path
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ strokeWidth={2}
35
+ d="M6 18L18 6M6 6l12 12"
36
+ />
37
  </svg>
38
  </button>
39
  )}
40
  <div className="flex flex-col items-center">
41
  <p className="mb-3 text-center">Sign in continue</p>
42
  <a href={authUrl} className="block w-[214px] h-[40px]">
43
+ <img
44
+ src="https://huggingface.co/datasets/huggingface/badges/resolve/main/sign-in-with-huggingface-lg-dark.svg"
45
+ alt="Sign in with Hugging Face"
46
  />
47
  </a>
48
  </div>
49
  </div>
50
+ );
51
+ }
src/components/code-editor.tsx CHANGED
@@ -1,4 +1,4 @@
1
- "use client"
2
 
3
  import { useEffect, useRef, useState, useCallback } from "react";
4
  import { debounce, highlightHTML } from "@/lib/utils";
@@ -9,7 +9,11 @@ interface CodeEditorProps {
9
  onCodeChange?: (code: string) => void;
10
  }
11
 
12
- export function CodeEditor({ code, isLoading = false, onCodeChange }: CodeEditorProps) {
 
 
 
 
13
  const containerRef = useRef<HTMLDivElement>(null);
14
  const textareaRef = useRef<HTMLTextAreaElement>(null);
15
  const highlightRef = useRef<HTMLDivElement>(null);
@@ -22,7 +26,7 @@ export function CodeEditor({ code, isLoading = false, onCodeChange }: CodeEditor
22
  onCodeChange(newCode);
23
  }
24
  }, 1000),
25
- [onCodeChange]
26
  );
27
 
28
  useEffect(() => {
@@ -48,7 +52,11 @@ export function CodeEditor({ code, isLoading = false, onCodeChange }: CodeEditor
48
  };
49
 
50
  useEffect(() => {
51
- if (isAtBottom && textareaRef.current && document.activeElement !== textareaRef.current) {
 
 
 
 
52
  textareaRef.current.scrollTop = textareaRef.current.scrollHeight;
53
  if (highlightRef.current) {
54
  highlightRef.current.scrollTop = highlightRef.current.scrollHeight;
@@ -58,16 +66,17 @@ export function CodeEditor({ code, isLoading = false, onCodeChange }: CodeEditor
58
 
59
  // Handle tab key for indentation
60
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
61
- if (e.key === 'Tab') {
62
  e.preventDefault();
63
  const textarea = e.currentTarget;
64
  const start = textarea.selectionStart;
65
  const end = textarea.selectionEnd;
66
-
67
- const newValue = localCode.substring(0, start) + ' ' + localCode.substring(end);
 
68
  setLocalCode(newValue);
69
  debouncedSave(newValue);
70
-
71
  setTimeout(() => {
72
  textarea.selectionStart = textarea.selectionEnd = start + 2;
73
  }, 0);
@@ -78,20 +87,21 @@ export function CodeEditor({ code, isLoading = false, onCodeChange }: CodeEditor
78
 
79
  return (
80
  <div className="flex-1 overflow-hidden p-4 pr-2">
81
- <div
82
- ref={containerRef}
83
  className="code-area rounded-md font-mono text-sm h-full border border-novita-gray/20 relative overflow-hidden"
84
  >
85
  <div className="relative h-full">
86
  {/* Syntax highlighted background - always visible */}
87
- <div
88
  ref={highlightRef}
89
  className="absolute inset-0 overflow-auto p-4 pointer-events-none whitespace-pre-wrap font-mono text-sm leading-relaxed"
90
- style={{
91
- fontSize: '0.875rem',
92
- lineHeight: '1.5',
93
- fontFamily: 'ui-monospace, SFMono-Regular, \"SF Mono\", Consolas, \"Liberation Mono\", Menlo, monospace',
94
- color: '#d4d4d4'
 
95
  }}
96
  dangerouslySetInnerHTML={{ __html: highlightedCode }}
97
  />
@@ -105,20 +115,21 @@ export function CodeEditor({ code, isLoading = false, onCodeChange }: CodeEditor
105
  onKeyDown={handleKeyDown}
106
  disabled={isLoading}
107
  className="absolute inset-0 w-full h-full bg-transparent text-transparent caret-white resize-none outline-none whitespace-pre-wrap font-mono text-sm leading-relaxed p-4 selection:bg-blue-500/30"
108
- style={{
109
- fontSize: '0.875rem',
110
- lineHeight: '1.5',
111
- fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace'
 
112
  }}
113
  placeholder="Your generated code will appear here..."
114
  spellCheck={false}
115
  />
116
  </div>
117
-
118
  {isLoading && (
119
  <span className="absolute bottom-4 right-4 inline-block h-4 w-2 bg-white/70 animate-pulse z-10"></span>
120
  )}
121
  </div>
122
  </div>
123
- )
124
  }
 
1
+ "use client";
2
 
3
  import { useEffect, useRef, useState, useCallback } from "react";
4
  import { debounce, highlightHTML } from "@/lib/utils";
 
9
  onCodeChange?: (code: string) => void;
10
  }
11
 
12
+ export function CodeEditor({
13
+ code,
14
+ isLoading = false,
15
+ onCodeChange,
16
+ }: CodeEditorProps) {
17
  const containerRef = useRef<HTMLDivElement>(null);
18
  const textareaRef = useRef<HTMLTextAreaElement>(null);
19
  const highlightRef = useRef<HTMLDivElement>(null);
 
26
  onCodeChange(newCode);
27
  }
28
  }, 1000),
29
+ [onCodeChange],
30
  );
31
 
32
  useEffect(() => {
 
52
  };
53
 
54
  useEffect(() => {
55
+ if (
56
+ isAtBottom &&
57
+ textareaRef.current &&
58
+ document.activeElement !== textareaRef.current
59
+ ) {
60
  textareaRef.current.scrollTop = textareaRef.current.scrollHeight;
61
  if (highlightRef.current) {
62
  highlightRef.current.scrollTop = highlightRef.current.scrollHeight;
 
66
 
67
  // Handle tab key for indentation
68
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
69
+ if (e.key === "Tab") {
70
  e.preventDefault();
71
  const textarea = e.currentTarget;
72
  const start = textarea.selectionStart;
73
  const end = textarea.selectionEnd;
74
+
75
+ const newValue =
76
+ localCode.substring(0, start) + " " + localCode.substring(end);
77
  setLocalCode(newValue);
78
  debouncedSave(newValue);
79
+
80
  setTimeout(() => {
81
  textarea.selectionStart = textarea.selectionEnd = start + 2;
82
  }, 0);
 
87
 
88
  return (
89
  <div className="flex-1 overflow-hidden p-4 pr-2">
90
+ <div
91
+ ref={containerRef}
92
  className="code-area rounded-md font-mono text-sm h-full border border-novita-gray/20 relative overflow-hidden"
93
  >
94
  <div className="relative h-full">
95
  {/* Syntax highlighted background - always visible */}
96
+ <div
97
  ref={highlightRef}
98
  className="absolute inset-0 overflow-auto p-4 pointer-events-none whitespace-pre-wrap font-mono text-sm leading-relaxed"
99
+ style={{
100
+ fontSize: "0.875rem",
101
+ lineHeight: "1.5",
102
+ fontFamily:
103
+ 'ui-monospace, SFMono-Regular, \"SF Mono\", Consolas, \"Liberation Mono\", Menlo, monospace',
104
+ color: "#d4d4d4",
105
  }}
106
  dangerouslySetInnerHTML={{ __html: highlightedCode }}
107
  />
 
115
  onKeyDown={handleKeyDown}
116
  disabled={isLoading}
117
  className="absolute inset-0 w-full h-full bg-transparent text-transparent caret-white resize-none outline-none whitespace-pre-wrap font-mono text-sm leading-relaxed p-4 selection:bg-blue-500/30"
118
+ style={{
119
+ fontSize: "0.875rem",
120
+ lineHeight: "1.5",
121
+ fontFamily:
122
+ 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
123
  }}
124
  placeholder="Your generated code will appear here..."
125
  spellCheck={false}
126
  />
127
  </div>
128
+
129
  {isLoading && (
130
  <span className="absolute bottom-4 right-4 inline-block h-4 w-2 bg-white/70 animate-pulse z-10"></span>
131
  )}
132
  </div>
133
  </div>
134
+ );
135
  }
src/components/color-panel.tsx CHANGED
@@ -1,26 +1,42 @@
1
- "use client"
2
 
3
- import { useState, useEffect, useRef } from "react"
4
- import { Plus, X } from "lucide-react"
5
- import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip"
6
- import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
7
- import { cn } from "@/lib/utils"
8
- import { Button } from "@/components/ui/button"
 
 
 
 
 
 
 
 
 
9
 
10
  interface ColorCircleProps {
11
- color: string
12
- onClick?: () => void
13
- onDelete?: () => void
14
- className?: string
15
- size?: "sm" | "md"
16
- showDeleteIcon?: boolean
17
  }
18
 
19
- function ColorCircle({ color, onClick, onDelete, className, size = "sm", showDeleteIcon = false }: ColorCircleProps) {
 
 
 
 
 
 
 
20
  const sizeClasses = {
21
  sm: "w-5 h-5",
22
  md: "w-6 h-6",
23
- }
24
 
25
  return (
26
  <div className="relative group h-5">
@@ -49,7 +65,7 @@ function ColorCircle({ color, onClick, onDelete, className, size = "sm", showDel
49
  </button>
50
  )}
51
  </div>
52
- )
53
  }
54
 
55
  const PRESET_COLORS = [
@@ -65,27 +81,27 @@ const PRESET_COLORS = [
65
  "#14b8a6", // Teal
66
  "#f97316", // Orange
67
  "#6366f1", // Indigo
68
- ]
69
 
70
  interface ColorPanelProps {
71
  onColorsChange?: (colors: string[]) => void;
72
  }
73
 
74
  export function ColorPanel({ onColorsChange }: ColorPanelProps) {
75
- const maxColors = 6
76
- const [colors, setColors] = useState<string[]>([])
77
- const [isPopoverOpen, setIsPopoverOpen] = useState(false)
78
- const [selectedColor, setSelectedColor] = useState<string | null>(null)
79
- const [isAddingDisabled, setIsAddingDisabled] = useState(false)
80
- const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null)
81
 
82
  useEffect(() => {
83
  return () => {
84
  if (debounceTimeoutRef.current) {
85
- clearTimeout(debounceTimeoutRef.current)
86
  }
87
- }
88
- }, [])
89
 
90
  useEffect(() => {
91
  // Call onColorsChange when colors change
@@ -93,37 +109,38 @@ export function ColorPanel({ onColorsChange }: ColorPanelProps) {
93
  }, [colors, onColorsChange]);
94
 
95
  const addColor = () => {
96
- if (colors.length >= maxColors || !selectedColor || isAddingDisabled) return
97
-
 
98
  if (!colors.includes(selectedColor)) {
99
- setIsAddingDisabled(true)
100
- setColors([...colors, selectedColor])
101
- setSelectedColor(null)
102
- setIsPopoverOpen(false)
103
-
104
  // Debounce to prevent rapid clicking
105
  debounceTimeoutRef.current = setTimeout(() => {
106
- setIsAddingDisabled(false)
107
- }, 500)
108
  }
109
- }
110
 
111
  const selectColor = (color: string) => {
112
- setSelectedColor(color)
113
- }
114
 
115
  const removeColor = (indexToRemove: number) => {
116
- setColors(colors.filter((_, index) => index !== indexToRemove))
117
- }
118
 
119
  return (
120
  <TooltipProvider>
121
  <div className="absolute top-2.5 left-3 flex items-center gap-1.5 z-10">
122
  {colors.map((color, index) => (
123
- <ColorCircle
124
- key={`${color}-${index}`}
125
- color={color}
126
- onClick={() => removeColor(index)}
127
  onDelete={() => removeColor(index)}
128
  showDeleteIcon={true}
129
  />
@@ -163,13 +180,15 @@ export function ColorPanel({ onColorsChange }: ColorPanelProps) {
163
  onClick={() => selectColor(color)}
164
  className={cn(
165
  selectedColor === color && "ring-2 ring-white/50",
166
- colors.includes(color) && "opacity-50"
167
  )}
168
  />
169
  ))}
170
  </div>
171
  <div className="pt-2 border-t border-novita-gray/30">
172
- <label className="block text-xs text-novita-gray mb-1.5">Custom color</label>
 
 
173
  <input
174
  type="color"
175
  className="w-full h-8 bg-transparent border border-novita-gray/30 rounded cursor-pointer"
@@ -178,7 +197,7 @@ export function ColorPanel({ onColorsChange }: ColorPanelProps) {
178
  />
179
  </div>
180
  <div className="mt-3">
181
- <Button
182
  onClick={addColor}
183
  disabled={!selectedColor || isAddingDisabled}
184
  className="w-full h-6 bg-novita-white hover:bg-novita-gray/90 text-white rounded border border-novita-gray/90"
@@ -192,5 +211,5 @@ export function ColorPanel({ onColorsChange }: ColorPanelProps) {
192
  )}
193
  </div>
194
  </TooltipProvider>
195
- )
196
  }
 
1
+ "use client";
2
 
3
+ import { useState, useEffect, useRef } from "react";
4
+ import { Plus, X } from "lucide-react";
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipTrigger,
9
+ TooltipProvider,
10
+ } from "@/components/ui/tooltip";
11
+ import {
12
+ Popover,
13
+ PopoverContent,
14
+ PopoverTrigger,
15
+ } from "@/components/ui/popover";
16
+ import { cn } from "@/lib/utils";
17
+ import { Button } from "@/components/ui/button";
18
 
19
  interface ColorCircleProps {
20
+ color: string;
21
+ onClick?: () => void;
22
+ onDelete?: () => void;
23
+ className?: string;
24
+ size?: "sm" | "md";
25
+ showDeleteIcon?: boolean;
26
  }
27
 
28
+ function ColorCircle({
29
+ color,
30
+ onClick,
31
+ onDelete,
32
+ className,
33
+ size = "sm",
34
+ showDeleteIcon = false,
35
+ }: ColorCircleProps) {
36
  const sizeClasses = {
37
  sm: "w-5 h-5",
38
  md: "w-6 h-6",
39
+ };
40
 
41
  return (
42
  <div className="relative group h-5">
 
65
  </button>
66
  )}
67
  </div>
68
+ );
69
  }
70
 
71
  const PRESET_COLORS = [
 
81
  "#14b8a6", // Teal
82
  "#f97316", // Orange
83
  "#6366f1", // Indigo
84
+ ];
85
 
86
  interface ColorPanelProps {
87
  onColorsChange?: (colors: string[]) => void;
88
  }
89
 
90
  export function ColorPanel({ onColorsChange }: ColorPanelProps) {
91
+ const maxColors = 6;
92
+ const [colors, setColors] = useState<string[]>([]);
93
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
94
+ const [selectedColor, setSelectedColor] = useState<string | null>(null);
95
+ const [isAddingDisabled, setIsAddingDisabled] = useState(false);
96
+ const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
97
 
98
  useEffect(() => {
99
  return () => {
100
  if (debounceTimeoutRef.current) {
101
+ clearTimeout(debounceTimeoutRef.current);
102
  }
103
+ };
104
+ }, []);
105
 
106
  useEffect(() => {
107
  // Call onColorsChange when colors change
 
109
  }, [colors, onColorsChange]);
110
 
111
  const addColor = () => {
112
+ if (colors.length >= maxColors || !selectedColor || isAddingDisabled)
113
+ return;
114
+
115
  if (!colors.includes(selectedColor)) {
116
+ setIsAddingDisabled(true);
117
+ setColors([...colors, selectedColor]);
118
+ setSelectedColor(null);
119
+ setIsPopoverOpen(false);
120
+
121
  // Debounce to prevent rapid clicking
122
  debounceTimeoutRef.current = setTimeout(() => {
123
+ setIsAddingDisabled(false);
124
+ }, 500);
125
  }
126
+ };
127
 
128
  const selectColor = (color: string) => {
129
+ setSelectedColor(color);
130
+ };
131
 
132
  const removeColor = (indexToRemove: number) => {
133
+ setColors(colors.filter((_, index) => index !== indexToRemove));
134
+ };
135
 
136
  return (
137
  <TooltipProvider>
138
  <div className="absolute top-2.5 left-3 flex items-center gap-1.5 z-10">
139
  {colors.map((color, index) => (
140
+ <ColorCircle
141
+ key={`${color}-${index}`}
142
+ color={color}
143
+ onClick={() => removeColor(index)}
144
  onDelete={() => removeColor(index)}
145
  showDeleteIcon={true}
146
  />
 
180
  onClick={() => selectColor(color)}
181
  className={cn(
182
  selectedColor === color && "ring-2 ring-white/50",
183
+ colors.includes(color) && "opacity-50",
184
  )}
185
  />
186
  ))}
187
  </div>
188
  <div className="pt-2 border-t border-novita-gray/30">
189
+ <label className="block text-xs text-novita-gray mb-1.5">
190
+ Custom color
191
+ </label>
192
  <input
193
  type="color"
194
  className="w-full h-8 bg-transparent border border-novita-gray/30 rounded cursor-pointer"
 
197
  />
198
  </div>
199
  <div className="mt-3">
200
+ <Button
201
  onClick={addColor}
202
  disabled={!selectedColor || isAddingDisabled}
203
  className="w-full h-6 bg-novita-white hover:bg-novita-gray/90 text-white rounded border border-novita-gray/90"
 
211
  )}
212
  </div>
213
  </TooltipProvider>
214
+ );
215
  }
src/components/header.tsx CHANGED
@@ -1,16 +1,16 @@
1
- "use client"
2
- import { Logo } from "./logo"
3
- import { ModelSelector } from "./model-selector"
4
- import { VersionDropdown } from "./version-dropdown"
5
 
6
  export function Header({
7
  onVersionSelect,
8
  currentVersion,
9
- onClearAll
10
- }: {
11
- onVersionSelect?: (version: any) => void,
12
- currentVersion?: string,
13
- onClearAll?: () => void
14
  }) {
15
  return (
16
  <header className="border-b border-novita-gray/20 p-3 flex justify-between items-center bg-novita-dark">
@@ -21,17 +21,25 @@ export function Header({
21
  <div className="flex items-center">
22
  <ModelSelector />
23
  {onVersionSelect && (
24
- <VersionDropdown
25
- onVersionSelect={onVersionSelect}
26
  currentVersion={currentVersion}
27
  onClearAll={onClearAll}
28
  />
29
  )}
30
  </div>
31
  </div>
32
- <span className="text-sm text-white/40"><a href="https://novita.ai/models/llm" target="_blank" className="underline hover:text-white/70 transition-colors duration-200">Powered by novita.ai</a></span>
 
 
 
 
 
 
 
 
33
  </div>
34
  </div>
35
  </header>
36
- )
37
- }
 
1
+ "use client";
2
+ import { Logo } from "./logo";
3
+ import { ModelSelector } from "./model-selector";
4
+ import { VersionDropdown } from "./version-dropdown";
5
 
6
  export function Header({
7
  onVersionSelect,
8
  currentVersion,
9
+ onClearAll,
10
+ }: {
11
+ onVersionSelect?: (version: any) => void;
12
+ currentVersion?: string;
13
+ onClearAll?: () => void;
14
  }) {
15
  return (
16
  <header className="border-b border-novita-gray/20 p-3 flex justify-between items-center bg-novita-dark">
 
21
  <div className="flex items-center">
22
  <ModelSelector />
23
  {onVersionSelect && (
24
+ <VersionDropdown
25
+ onVersionSelect={onVersionSelect}
26
  currentVersion={currentVersion}
27
  onClearAll={onClearAll}
28
  />
29
  )}
30
  </div>
31
  </div>
32
+ <span className="text-sm text-white/40">
33
+ <a
34
+ href="https://novita.ai/models/llm"
35
+ target="_blank"
36
+ className="underline hover:text-white/70 transition-colors duration-200"
37
+ >
38
+ Powered by novita.ai
39
+ </a>
40
+ </span>
41
  </div>
42
  </div>
43
  </header>
44
+ );
45
+ }
src/components/logo.tsx CHANGED
@@ -10,5 +10,5 @@ export function Logo() {
10
  >
11
  <path d="M50 0L100 50H50L0 0H50Z" fill="currentColor" />
12
  </svg>
13
- )
14
  }
 
10
  >
11
  <path d="M50 0L100 50H50L0 0H50Z" fill="currentColor" />
12
  </svg>
13
+ );
14
  }
src/components/model-selector.tsx CHANGED
@@ -1,35 +1,38 @@
1
- "use client"
2
 
3
- import { useState, useEffect, useRef } from 'react'
4
- import { MODEL_CONFIG_CODE_GENERATION } from '@/lib/constants'
5
- import { useModel } from '@/lib/contexts/model-context'
6
- import { ChevronDown } from 'lucide-react'
7
 
8
  export function ModelSelector() {
9
- const { selectedModelIndex, setSelectedModelIndex } = useModel()
10
- const [isOpen, setIsOpen] = useState(false)
11
- const selectorRef = useRef<HTMLDivElement>(null)
12
 
13
  const handleSelect = (index: number) => {
14
- setSelectedModelIndex(index)
15
- setIsOpen(false)
16
- }
17
 
18
  useEffect(() => {
19
  const handleClickOutside = (event: MouseEvent) => {
20
- if (selectorRef.current && !selectorRef.current.contains(event.target as Node)) {
21
- setIsOpen(false)
 
 
 
22
  }
23
- }
24
 
25
  if (isOpen) {
26
- document.addEventListener('mousedown', handleClickOutside)
27
  }
28
 
29
  return () => {
30
- document.removeEventListener('mousedown', handleClickOutside)
31
- }
32
- }, [isOpen])
33
 
34
  return (
35
  <div className="relative inline-block text-left" ref={selectorRef}>
@@ -42,13 +45,18 @@ export function ModelSelector() {
42
  border border-novita-gray/30 hover:border-novita-gray/60
43
  bg-novita-gray/5 hover:bg-novita-gray/20
44
  transition-all duration-200 ease-in-out
45
- ${isOpen ? 'border-novita-gray/60 bg-novita-gray/20' : ''}
46
  `}
47
  >
48
- <span className="text-sm">{MODEL_CONFIG_CODE_GENERATION[selectedModelIndex]?.id || 'Select model'}</span>
49
- <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'transform rotate-180' : ''}`} />
 
 
 
 
 
50
  </div>
51
-
52
  {isOpen && (
53
  <div className="absolute z-40 mt-1 w-full origin-top-right rounded-md bg-novita-dark border border-novita-gray/20 shadow-lg">
54
  <div className="py-1">
@@ -58,7 +66,7 @@ export function ModelSelector() {
58
  onClick={() => handleSelect(index)}
59
  className={`
60
  px-4 py-2 text-xs cursor-pointer hover:bg-novita-gray/20
61
- ${selectedModelIndex === index ? 'text-white bg-novita-gray/40' : 'text-white/70'}
62
  `}
63
  >
64
  {model.id}
@@ -68,5 +76,5 @@ export function ModelSelector() {
68
  </div>
69
  )}
70
  </div>
71
- )
72
- }
 
1
+ "use client";
2
 
3
+ import { useState, useEffect, useRef } from "react";
4
+ import { MODEL_CONFIG_CODE_GENERATION } from "@/lib/constants";
5
+ import { useModel } from "@/lib/contexts/model-context";
6
+ import { ChevronDown } from "lucide-react";
7
 
8
  export function ModelSelector() {
9
+ const { selectedModelIndex, setSelectedModelIndex } = useModel();
10
+ const [isOpen, setIsOpen] = useState(false);
11
+ const selectorRef = useRef<HTMLDivElement>(null);
12
 
13
  const handleSelect = (index: number) => {
14
+ setSelectedModelIndex(index);
15
+ setIsOpen(false);
16
+ };
17
 
18
  useEffect(() => {
19
  const handleClickOutside = (event: MouseEvent) => {
20
+ if (
21
+ selectorRef.current &&
22
+ !selectorRef.current.contains(event.target as Node)
23
+ ) {
24
+ setIsOpen(false);
25
  }
26
+ };
27
 
28
  if (isOpen) {
29
+ document.addEventListener("mousedown", handleClickOutside);
30
  }
31
 
32
  return () => {
33
+ document.removeEventListener("mousedown", handleClickOutside);
34
+ };
35
+ }, [isOpen]);
36
 
37
  return (
38
  <div className="relative inline-block text-left" ref={selectorRef}>
 
45
  border border-novita-gray/30 hover:border-novita-gray/60
46
  bg-novita-gray/5 hover:bg-novita-gray/20
47
  transition-all duration-200 ease-in-out
48
+ ${isOpen ? "border-novita-gray/60 bg-novita-gray/20" : ""}
49
  `}
50
  >
51
+ <span className="text-sm">
52
+ {MODEL_CONFIG_CODE_GENERATION[selectedModelIndex]?.id ||
53
+ "Select model"}
54
+ </span>
55
+ <ChevronDown
56
+ className={`w-4 h-4 transition-transform duration-200 ${isOpen ? "transform rotate-180" : ""}`}
57
+ />
58
  </div>
59
+
60
  {isOpen && (
61
  <div className="absolute z-40 mt-1 w-full origin-top-right rounded-md bg-novita-dark border border-novita-gray/20 shadow-lg">
62
  <div className="py-1">
 
66
  onClick={() => handleSelect(index)}
67
  className={`
68
  px-4 py-2 text-xs cursor-pointer hover:bg-novita-gray/20
69
+ ${selectedModelIndex === index ? "text-white bg-novita-gray/40" : "text-white/70"}
70
  `}
71
  >
72
  {model.id}
 
76
  </div>
77
  )}
78
  </div>
79
+ );
80
+ }
src/components/preview.tsx CHANGED
@@ -1,15 +1,26 @@
1
- "use client"
2
-
3
- import { useState, forwardRef, useImperativeHandle, useEffect, useRef } from "react"
4
- import { DEFAULT_HTML } from "@/lib/constants"
5
- import { PreviewRef } from "@/lib/types"
6
- import { MinimizeIcon, MaximizeIcon, DownloadIcon, RefreshIcon } from "./ui/icons"
7
- import { useModel } from "@/lib/contexts/model-context"
8
- import { Loader2, Share2 } from "lucide-react"
9
- import { cn } from "@/lib/utils"
10
- import { generateShareLink } from "@/lib/sharelink"
11
- import { ShareDialog } from "./share-dialog"
12
- import posthog from 'posthog-js'
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  interface PreviewProps {
15
  initialHtml?: string;
@@ -21,8 +32,15 @@ interface PreviewProps {
21
  }
22
 
23
  export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
24
- { initialHtml, onCodeChange, onAuthErrorChange, onLoadingChange, onErrorChange, currentVersion },
25
- ref
 
 
 
 
 
 
 
26
  ) {
27
  const [html, setHtml] = useState<string>(initialHtml || "");
28
  const [isFullscreen, setIsFullscreen] = useState(false);
@@ -53,9 +71,13 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
53
  }, [error, onErrorChange]);
54
 
55
  useImperativeHandle(ref, () => ({
56
- generateCode: async (prompt: string, colors: string[] = [], previousPrompt?: string) => {
 
 
 
 
57
  await generateCode(prompt, colors, previousPrompt);
58
- }
59
  }));
60
 
61
  const toggleFullscreen = () => {
@@ -64,31 +86,35 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
64
 
65
  const partialUpdate = (htmlStr: string) => {
66
  const parser = new DOMParser();
67
- const partialDoc = parser.parseFromString(htmlStr, 'text/html');
68
- const iframe = document.querySelector('iframe');
69
  if (!iframe || !iframe.contentDocument) return;
70
-
71
  const iframeContainer = iframe.contentDocument;
72
  if (iframeContainer?.body && iframeContainer) {
73
  iframeContainer.body.innerHTML = partialDoc.body?.innerHTML;
74
  }
75
  if (renderCount.current % 10 === 0 && !headUpdated.current) {
76
  setHtml(htmlStr);
77
- if (htmlStr.includes('</head>')) {
78
  setTimeout(() => {
79
  headUpdated.current = true;
80
  }, 1000);
81
  }
82
  }
83
  renderCount.current++;
84
- }
85
 
86
  const downloadHtml = () => {
87
  if (!html) return;
88
 
89
  // Get current version and generate filename
90
  // If we have a currentVersion, use it; otherwise omit version part
91
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
 
 
 
 
92
 
93
  // Load current version from localStorage if we have an ID
94
  let versionLabel = "";
@@ -99,9 +125,9 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
99
  // Format the filename with or without version
100
  const filename = `novita-anysite-generated${versionLabel}-${timestamp}.html`;
101
 
102
- const blob = new Blob([html], { type: 'text/html' });
103
  const url = window.URL.createObjectURL(blob);
104
- const a = document.createElement('a');
105
  a.href = url;
106
  a.download = filename;
107
  document.body.appendChild(a);
@@ -112,10 +138,14 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
112
 
113
  const refreshPreview = () => {
114
  if (!html) return;
115
- setRefreshKey(prev => prev + 1);
116
  };
117
 
118
- const generateCode = async (prompt: string, colors: string[] = [], previousPrompt?: string) => {
 
 
 
 
119
  setLoading(true);
120
  renderCount.current = 0;
121
  headUpdated.current = false;
@@ -132,30 +162,34 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
132
  setHtml("");
133
 
134
  // Initialize generated code variable at function scope so it's accessible in finally block
135
- let generatedCode = '';
136
 
137
  try {
138
  // Only include html in the request if it's not DEFAULT_HTML
139
  const isDefaultHtml = initialHtml === DEFAULT_HTML;
140
 
141
- posthog.capture("Generate code", {"model": selectedModelId});
142
 
143
- const response = await fetch('/api/generate-code', {
144
- method: 'POST',
145
  headers: {
146
- 'Content-Type': 'application/json',
147
  },
148
  body: JSON.stringify({
149
  prompt,
150
  html: isDefaultHtml ? undefined : html,
151
  previousPrompt: isDefaultHtml ? undefined : previousPrompt,
152
  colors,
153
- modelId: selectedModelId
154
  }),
155
  });
156
 
157
  if (!response.ok) {
158
- posthog.capture("Generate code", {"type": "failed", "model": selectedModelId, "status": response.status});
 
 
 
 
159
  // Check specifically for 401 error (authentication required)
160
  if (response.status === 401 || response.status === 403) {
161
  try {
@@ -165,7 +199,7 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
165
  if (onAuthErrorChange) {
166
  onAuthErrorChange(true);
167
  }
168
- throw new Error('Signing in to Hugging Face is required.');
169
  }
170
  } catch (e) {
171
  // Fall back to default auth error handling if JSON parsing fails
@@ -173,12 +207,12 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
173
  if (onAuthErrorChange) {
174
  onAuthErrorChange(true);
175
  }
176
- throw new Error('Signing in to Hugging Face is required.');
177
  }
178
  }
179
 
180
  const errorData = await response.json();
181
- throw new Error(errorData.message || 'Failed to generate code');
182
  }
183
 
184
  // Handle streaming response
@@ -190,11 +224,11 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
190
  while (true) {
191
  const { done, value } = await reader.read();
192
  if (done) {
193
- if (!generatedCode.includes('</html>')) {
194
- generatedCode += '</html>';
195
  }
196
  const finalCode = generatedCode.match(
197
- /<!DOCTYPE html>[\s\S]*<\/html>/
198
  )?.[0];
199
  if (finalCode) {
200
  // Update state with the final code
@@ -220,7 +254,7 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
220
  // Try to parse as JSON
221
  parsedChunk = JSON.parse(chunkText);
222
  } catch (parseError) {
223
- appended = true
224
  // If JSON parsing fails, treat it as plain text (backwards compatibility)
225
  generatedCode += chunkText;
226
  }
@@ -252,13 +286,18 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
252
  }
253
  }
254
  } catch (err) {
255
- const errorMessage = (err as Error).message || 'An error occurred while generating code';
256
- posthog.capture("Generate code", {"type": "failed", "model": selectedModelId, "error": errorMessage});
 
 
 
 
 
257
  setError(errorMessage);
258
  if (onErrorChange) {
259
  onErrorChange(errorMessage);
260
  }
261
- console.error('Error generating code:', err);
262
  } finally {
263
  setLoading(false);
264
  if (onLoadingChange) {
@@ -281,12 +320,12 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
281
  setShareUrl(uploadedUrl);
282
  setShareDialogOpen(true);
283
  } catch (err) {
284
- const errorMessage = (err as Error).message || 'Failed to share HTML';
285
  setError(errorMessage);
286
  if (onErrorChange) {
287
  onErrorChange(errorMessage);
288
  }
289
- console.error('Error sharing HTML:', err);
290
  } finally {
291
  setIsSharing(false);
292
  }
@@ -300,14 +339,16 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
300
  };
301
 
302
  return (
303
- <div className={`${isFullscreen ? 'fixed inset-0 z-10 bg-novita-dark' : 'h-full'} p-4 pl-2`}>
304
- { isPartialGenerating && (
 
 
305
  <div className="w-full bg-slate-50 border-b border-slate-200 py-2 px-4">
306
  <div className="container mx-auto flex items-center justify-center">
307
- <div className="flex items-center space-x-2 text-slate-700">
308
- <Loader2 className="h-4 w-4 animate-spin" />
309
- <span className="text-sm font-medium">building...</span>
310
- </div>
311
  </div>
312
  </div>
313
  )}
@@ -325,7 +366,7 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
325
  ) : (
326
  <Share2 className="h-3 w-3 mr-2" />
327
  )}
328
- {isSharing ? 'Sharing...' : 'Share Link'}
329
  </button>
330
 
331
  <button
@@ -347,8 +388,8 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
347
  <button
348
  onClick={toggleFullscreen}
349
  className="bg-novita-gray/90 text-white p-2 rounded-md shadow-md hover:bg-novita-gray/70 transition-colors flex items-center justify-center"
350
- aria-label={isFullscreen ? 'Exit Fullscreen' : 'Full Screen'}
351
- title={isFullscreen ? 'Exit Fullscreen' : 'Full Screen'}
352
  >
353
  {isFullscreen ? <MinimizeIcon /> : <MaximizeIcon />}
354
  </button>
@@ -363,11 +404,11 @@ export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
363
  />
364
  </div>
365
 
366
- <ShareDialog
367
  open={shareDialogOpen}
368
  onOpenChange={handleShareDialogClose}
369
  shareUrl={shareUrl}
370
  />
371
  </div>
372
- )
373
  });
 
1
+ "use client";
2
+
3
+ import {
4
+ useState,
5
+ forwardRef,
6
+ useImperativeHandle,
7
+ useEffect,
8
+ useRef,
9
+ } from "react";
10
+ import { DEFAULT_HTML } from "@/lib/constants";
11
+ import { PreviewRef } from "@/lib/types";
12
+ import {
13
+ MinimizeIcon,
14
+ MaximizeIcon,
15
+ DownloadIcon,
16
+ RefreshIcon,
17
+ } from "./ui/icons";
18
+ import { useModel } from "@/lib/contexts/model-context";
19
+ import { Loader2, Share2 } from "lucide-react";
20
+ import { cn } from "@/lib/utils";
21
+ import { generateShareLink } from "@/lib/sharelink";
22
+ import { ShareDialog } from "./share-dialog";
23
+ import posthog from "posthog-js";
24
 
25
  interface PreviewProps {
26
  initialHtml?: string;
 
32
  }
33
 
34
  export const Preview = forwardRef<PreviewRef, PreviewProps>(function Preview(
35
+ {
36
+ initialHtml,
37
+ onCodeChange,
38
+ onAuthErrorChange,
39
+ onLoadingChange,
40
+ onErrorChange,
41
+ currentVersion,
42
+ },
43
+ ref,
44
  ) {
45
  const [html, setHtml] = useState<string>(initialHtml || "");
46
  const [isFullscreen, setIsFullscreen] = useState(false);
 
71
  }, [error, onErrorChange]);
72
 
73
  useImperativeHandle(ref, () => ({
74
+ generateCode: async (
75
+ prompt: string,
76
+ colors: string[] = [],
77
+ previousPrompt?: string,
78
+ ) => {
79
  await generateCode(prompt, colors, previousPrompt);
80
+ },
81
  }));
82
 
83
  const toggleFullscreen = () => {
 
86
 
87
  const partialUpdate = (htmlStr: string) => {
88
  const parser = new DOMParser();
89
+ const partialDoc = parser.parseFromString(htmlStr, "text/html");
90
+ const iframe = document.querySelector("iframe");
91
  if (!iframe || !iframe.contentDocument) return;
92
+
93
  const iframeContainer = iframe.contentDocument;
94
  if (iframeContainer?.body && iframeContainer) {
95
  iframeContainer.body.innerHTML = partialDoc.body?.innerHTML;
96
  }
97
  if (renderCount.current % 10 === 0 && !headUpdated.current) {
98
  setHtml(htmlStr);
99
+ if (htmlStr.includes("</head>")) {
100
  setTimeout(() => {
101
  headUpdated.current = true;
102
  }, 1000);
103
  }
104
  }
105
  renderCount.current++;
106
+ };
107
 
108
  const downloadHtml = () => {
109
  if (!html) return;
110
 
111
  // Get current version and generate filename
112
  // If we have a currentVersion, use it; otherwise omit version part
113
+ const timestamp = new Date()
114
+ .toISOString()
115
+ .replace(/[:.]/g, "-")
116
+ .replace("T", "_")
117
+ .slice(0, 19);
118
 
119
  // Load current version from localStorage if we have an ID
120
  let versionLabel = "";
 
125
  // Format the filename with or without version
126
  const filename = `novita-anysite-generated${versionLabel}-${timestamp}.html`;
127
 
128
+ const blob = new Blob([html], { type: "text/html" });
129
  const url = window.URL.createObjectURL(blob);
130
+ const a = document.createElement("a");
131
  a.href = url;
132
  a.download = filename;
133
  document.body.appendChild(a);
 
138
 
139
  const refreshPreview = () => {
140
  if (!html) return;
141
+ setRefreshKey((prev) => prev + 1);
142
  };
143
 
144
+ const generateCode = async (
145
+ prompt: string,
146
+ colors: string[] = [],
147
+ previousPrompt?: string,
148
+ ) => {
149
  setLoading(true);
150
  renderCount.current = 0;
151
  headUpdated.current = false;
 
162
  setHtml("");
163
 
164
  // Initialize generated code variable at function scope so it's accessible in finally block
165
+ let generatedCode = "";
166
 
167
  try {
168
  // Only include html in the request if it's not DEFAULT_HTML
169
  const isDefaultHtml = initialHtml === DEFAULT_HTML;
170
 
171
+ posthog.capture("Generate code", { model: selectedModelId });
172
 
173
+ const response = await fetch("/api/generate-code", {
174
+ method: "POST",
175
  headers: {
176
+ "Content-Type": "application/json",
177
  },
178
  body: JSON.stringify({
179
  prompt,
180
  html: isDefaultHtml ? undefined : html,
181
  previousPrompt: isDefaultHtml ? undefined : previousPrompt,
182
  colors,
183
+ modelId: selectedModelId,
184
  }),
185
  });
186
 
187
  if (!response.ok) {
188
+ posthog.capture("Generate code", {
189
+ type: "failed",
190
+ model: selectedModelId,
191
+ status: response.status,
192
+ });
193
  // Check specifically for 401 error (authentication required)
194
  if (response.status === 401 || response.status === 403) {
195
  try {
 
199
  if (onAuthErrorChange) {
200
  onAuthErrorChange(true);
201
  }
202
+ throw new Error("Signing in to Hugging Face is required.");
203
  }
204
  } catch (e) {
205
  // Fall back to default auth error handling if JSON parsing fails
 
207
  if (onAuthErrorChange) {
208
  onAuthErrorChange(true);
209
  }
210
+ throw new Error("Signing in to Hugging Face is required.");
211
  }
212
  }
213
 
214
  const errorData = await response.json();
215
+ throw new Error(errorData.message || "Failed to generate code");
216
  }
217
 
218
  // Handle streaming response
 
224
  while (true) {
225
  const { done, value } = await reader.read();
226
  if (done) {
227
+ if (!generatedCode.includes("</html>")) {
228
+ generatedCode += "</html>";
229
  }
230
  const finalCode = generatedCode.match(
231
+ /<!DOCTYPE html>[\s\S]*<\/html>/,
232
  )?.[0];
233
  if (finalCode) {
234
  // Update state with the final code
 
254
  // Try to parse as JSON
255
  parsedChunk = JSON.parse(chunkText);
256
  } catch (parseError) {
257
+ appended = true;
258
  // If JSON parsing fails, treat it as plain text (backwards compatibility)
259
  generatedCode += chunkText;
260
  }
 
286
  }
287
  }
288
  } catch (err) {
289
+ const errorMessage =
290
+ (err as Error).message || "An error occurred while generating code";
291
+ posthog.capture("Generate code", {
292
+ type: "failed",
293
+ model: selectedModelId,
294
+ error: errorMessage,
295
+ });
296
  setError(errorMessage);
297
  if (onErrorChange) {
298
  onErrorChange(errorMessage);
299
  }
300
+ console.error("Error generating code:", err);
301
  } finally {
302
  setLoading(false);
303
  if (onLoadingChange) {
 
320
  setShareUrl(uploadedUrl);
321
  setShareDialogOpen(true);
322
  } catch (err) {
323
+ const errorMessage = (err as Error).message || "Failed to share HTML";
324
  setError(errorMessage);
325
  if (onErrorChange) {
326
  onErrorChange(errorMessage);
327
  }
328
+ console.error("Error sharing HTML:", err);
329
  } finally {
330
  setIsSharing(false);
331
  }
 
339
  };
340
 
341
  return (
342
+ <div
343
+ className={`${isFullscreen ? "fixed inset-0 z-10 bg-novita-dark" : "h-full"} p-4 pl-2`}
344
+ >
345
+ {isPartialGenerating && (
346
  <div className="w-full bg-slate-50 border-b border-slate-200 py-2 px-4">
347
  <div className="container mx-auto flex items-center justify-center">
348
+ <div className="flex items-center space-x-2 text-slate-700">
349
+ <Loader2 className="h-4 w-4 animate-spin" />
350
+ <span className="text-sm font-medium">building...</span>
351
+ </div>
352
  </div>
353
  </div>
354
  )}
 
366
  ) : (
367
  <Share2 className="h-3 w-3 mr-2" />
368
  )}
369
+ {isSharing ? "Sharing..." : "Share Link"}
370
  </button>
371
 
372
  <button
 
388
  <button
389
  onClick={toggleFullscreen}
390
  className="bg-novita-gray/90 text-white p-2 rounded-md shadow-md hover:bg-novita-gray/70 transition-colors flex items-center justify-center"
391
+ aria-label={isFullscreen ? "Exit Fullscreen" : "Full Screen"}
392
+ title={isFullscreen ? "Exit Fullscreen" : "Full Screen"}
393
  >
394
  {isFullscreen ? <MinimizeIcon /> : <MaximizeIcon />}
395
  </button>
 
404
  />
405
  </div>
406
 
407
+ <ShareDialog
408
  open={shareDialogOpen}
409
  onOpenChange={handleShareDialogClose}
410
  shareUrl={shareUrl}
411
  />
412
  </div>
413
+ );
414
  });
src/components/prompt-input.tsx CHANGED
@@ -1,17 +1,22 @@
1
- "use client"
2
 
3
- import type React from "react"
4
 
5
- import { useState, useEffect } from "react"
6
- import { Wand2, ArrowUp, Loader2, Maximize2, Minimize2 } from "lucide-react"
7
- import { Button } from "@/components/ui/button"
8
- import { Textarea } from "@/components/ui/textarea"
9
- import { ColorPanel } from "./color-panel"
10
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
11
- import { FullscreenToggle } from "./ui/fullscreen-toggle"
12
- import { AuthErrorPopup } from "./auth-error-popup"
13
- import { getInferenceToken } from "@/lib/auth"
14
- import posthog from 'posthog-js'
 
 
 
 
 
15
 
16
  interface PromptInputProps {
17
  onSubmit: (prompt: string, colors: string[]) => Promise<void>;
@@ -24,7 +29,7 @@ export function PromptInput({
24
  onSubmit,
25
  isLoading = false,
26
  initialPrompt = "",
27
- onImproveError
28
  }: PromptInputProps) {
29
  const [prompt, setPrompt] = useState(initialPrompt);
30
  const [isImprovingPrompt, setIsImprovingPrompt] = useState(false);
@@ -50,7 +55,9 @@ export function PromptInput({
50
  if (token) {
51
  return true;
52
  }
53
- const canBypass = await fetch("/api/auth/check-bypass").then(res => res.json());
 
 
54
  if (!canBypass) {
55
  throw new Error("Authentication required");
56
  }
@@ -59,11 +66,11 @@ export function PromptInput({
59
  setShowAuthError(true);
60
  return false;
61
  }
62
- }
63
 
64
  const handleSubmit = async (e: React.FormEvent) => {
65
  e.preventDefault();
66
- if (prompt.trim() === '' || isLoading) return;
67
 
68
  // Check for authentication
69
  const isAuthenticated = await checkAuth();
@@ -72,10 +79,10 @@ export function PromptInput({
72
  // Clear any previous errors
73
  setImproveError(null);
74
  await onSubmit(prompt, selectedColors);
75
- }
76
 
77
  const improvePrompt = async () => {
78
- if (prompt.trim() === '' || isImprovingPrompt || isLoading) return;
79
 
80
  // Check for authentication
81
  const isAuthenticated = await checkAuth();
@@ -95,22 +102,27 @@ export function PromptInput({
95
  "Content-Type": "application/json",
96
  },
97
  body: JSON.stringify({ prompt: prompt.trim() }),
98
- })
99
 
100
  if (!response.ok) {
101
- posthog.capture("Improve prompt", {"type": "failed", "status": response.status});
 
 
 
102
 
103
  // Handle auth error with openLogin flag
104
  if (response.status === 401) {
105
  const errorData = await response.json();
106
  if (errorData.openLogin) {
107
  setShowAuthError(true);
108
- throw new Error('Authentication required');
109
  }
110
  }
111
-
112
  const errorText = await response.text();
113
- throw new Error(errorText || `Failed to improve prompt (${response.status})`);
 
 
114
  }
115
 
116
  if (!response.body) {
@@ -135,37 +147,41 @@ export function PromptInput({
135
  } catch (parseError) {
136
  appended = true;
137
  // If JSON parsing fails, treat it as plain text (backwards compatibility)
138
- improvedPrompt += chunkText
139
- setPrompt(improvedPrompt)
140
  }
141
  if (parsedChunk && parsedChunk.type === "error") {
142
  throw new Error(parsedChunk.message || "An error occurred");
143
  } else if (!appended) {
144
- improvedPrompt += chunkText
145
- setPrompt(improvedPrompt)
146
  }
147
  }
148
  } catch (error) {
149
- posthog.capture("Improve prompt", {"type": "failed", "error": error});
150
- console.error("Error improving prompt:", error)
151
- setImproveError(error instanceof Error ? error.message : "Failed to improve prompt")
 
 
152
  } finally {
153
- setIsImprovingPrompt(false)
154
  }
155
- }
156
 
157
  const toggleFullScreen = () => {
158
- setIsFullScreen(!isFullScreen)
159
- }
160
 
161
  const handleColorsChange = (colors: string[]) => {
162
  setSelectedColors(colors);
163
- }
164
 
165
- const isPromptTooShort = prompt.length < 10
166
 
167
  return (
168
- <div className={`border-t border-novita-gray/20 p-4 relative transition-all duration-300 ease-in-out ${isFullScreen ? 'h-[50vh]' : ''}`}>
 
 
169
  <div className="absolute top-1 right-1 z-10">
170
  <FullscreenToggle
171
  isFullScreen={isFullScreen}
@@ -173,17 +189,17 @@ export function PromptInput({
173
  />
174
  </div>
175
  <form onSubmit={handleSubmit} className="flex flex-col gap-4 h-full">
176
- <div className={`relative ${isFullScreen ? 'h-full' : ''}`}>
177
  <ColorPanel onColorsChange={handleColorsChange} />
178
  <Textarea
179
  value={prompt}
180
  onChange={(e) => {
181
- setPrompt(e.target.value)
182
  // Clear error when user types
183
- if (improveError) setImproveError(null)
184
  }}
185
  placeholder="Describe what site you want to build. E.g., Build a snake game"
186
- className={`min-h-24 pr-20 pt-12 bg-novita-gray/20 border-novita-gray/30 text-white placeholder:text-novita-gray/70 resize-none ${isFullScreen ? 'h-full' : ''}`}
187
  disabled={isLoading || isImprovingPrompt}
188
  />
189
  <div className="absolute bottom-3 right-3 flex gap-2">
@@ -196,7 +212,9 @@ export function PromptInput({
196
  size="icon"
197
  variant="outline"
198
  className="h-8 w-8 bg-novita-gray/20 border-novita-gray/30 text-white hover:bg-novita-gray/30"
199
- disabled={isPromptTooShort || isLoading || isImprovingPrompt}
 
 
200
  onClick={improvePrompt}
201
  >
202
  {isImprovingPrompt ? (
@@ -210,7 +228,7 @@ export function PromptInput({
210
  </TooltipTrigger>
211
  {isPromptTooShort && (
212
  <TooltipContent className="bg-novita-gray text-white">
213
- <p>Your prompt is too simple, we can't improve it.</p>
214
  </TooltipContent>
215
  )}
216
  </Tooltip>
@@ -231,11 +249,11 @@ export function PromptInput({
231
  </div>
232
  </div>
233
  </form>
234
-
235
- <AuthErrorPopup
236
- show={showAuthError}
237
- onClose={() => setShowAuthError(false)}
238
  />
239
  </div>
240
- )
241
  }
 
1
+ "use client";
2
 
3
+ import type React from "react";
4
 
5
+ import { useState, useEffect } from "react";
6
+ import { Wand2, ArrowUp, Loader2, Maximize2, Minimize2 } from "lucide-react";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import { ColorPanel } from "./color-panel";
10
+ import {
11
+ Tooltip,
12
+ TooltipContent,
13
+ TooltipProvider,
14
+ TooltipTrigger,
15
+ } from "@/components/ui/tooltip";
16
+ import { FullscreenToggle } from "./ui/fullscreen-toggle";
17
+ import { AuthErrorPopup } from "./auth-error-popup";
18
+ import { getInferenceToken } from "@/lib/auth";
19
+ import posthog from "posthog-js";
20
 
21
  interface PromptInputProps {
22
  onSubmit: (prompt: string, colors: string[]) => Promise<void>;
 
29
  onSubmit,
30
  isLoading = false,
31
  initialPrompt = "",
32
+ onImproveError,
33
  }: PromptInputProps) {
34
  const [prompt, setPrompt] = useState(initialPrompt);
35
  const [isImprovingPrompt, setIsImprovingPrompt] = useState(false);
 
55
  if (token) {
56
  return true;
57
  }
58
+ const canBypass = await fetch("/api/auth/check-bypass").then((res) =>
59
+ res.json(),
60
+ );
61
  if (!canBypass) {
62
  throw new Error("Authentication required");
63
  }
 
66
  setShowAuthError(true);
67
  return false;
68
  }
69
+ };
70
 
71
  const handleSubmit = async (e: React.FormEvent) => {
72
  e.preventDefault();
73
+ if (prompt.trim() === "" || isLoading) return;
74
 
75
  // Check for authentication
76
  const isAuthenticated = await checkAuth();
 
79
  // Clear any previous errors
80
  setImproveError(null);
81
  await onSubmit(prompt, selectedColors);
82
+ };
83
 
84
  const improvePrompt = async () => {
85
+ if (prompt.trim() === "" || isImprovingPrompt || isLoading) return;
86
 
87
  // Check for authentication
88
  const isAuthenticated = await checkAuth();
 
102
  "Content-Type": "application/json",
103
  },
104
  body: JSON.stringify({ prompt: prompt.trim() }),
105
+ });
106
 
107
  if (!response.ok) {
108
+ posthog.capture("Improve prompt", {
109
+ type: "failed",
110
+ status: response.status,
111
+ });
112
 
113
  // Handle auth error with openLogin flag
114
  if (response.status === 401) {
115
  const errorData = await response.json();
116
  if (errorData.openLogin) {
117
  setShowAuthError(true);
118
+ throw new Error("Authentication required");
119
  }
120
  }
121
+
122
  const errorText = await response.text();
123
+ throw new Error(
124
+ errorText || `Failed to improve prompt (${response.status})`,
125
+ );
126
  }
127
 
128
  if (!response.body) {
 
147
  } catch (parseError) {
148
  appended = true;
149
  // If JSON parsing fails, treat it as plain text (backwards compatibility)
150
+ improvedPrompt += chunkText;
151
+ setPrompt(improvedPrompt);
152
  }
153
  if (parsedChunk && parsedChunk.type === "error") {
154
  throw new Error(parsedChunk.message || "An error occurred");
155
  } else if (!appended) {
156
+ improvedPrompt += chunkText;
157
+ setPrompt(improvedPrompt);
158
  }
159
  }
160
  } catch (error) {
161
+ posthog.capture("Improve prompt", { type: "failed", error: error });
162
+ console.error("Error improving prompt:", error);
163
+ setImproveError(
164
+ error instanceof Error ? error.message : "Failed to improve prompt",
165
+ );
166
  } finally {
167
+ setIsImprovingPrompt(false);
168
  }
169
+ };
170
 
171
  const toggleFullScreen = () => {
172
+ setIsFullScreen(!isFullScreen);
173
+ };
174
 
175
  const handleColorsChange = (colors: string[]) => {
176
  setSelectedColors(colors);
177
+ };
178
 
179
+ const isPromptTooShort = prompt.length < 10;
180
 
181
  return (
182
+ <div
183
+ className={`border-t border-novita-gray/20 p-4 relative transition-all duration-300 ease-in-out ${isFullScreen ? "h-[50vh]" : ""}`}
184
+ >
185
  <div className="absolute top-1 right-1 z-10">
186
  <FullscreenToggle
187
  isFullScreen={isFullScreen}
 
189
  />
190
  </div>
191
  <form onSubmit={handleSubmit} className="flex flex-col gap-4 h-full">
192
+ <div className={`relative ${isFullScreen ? "h-full" : ""}`}>
193
  <ColorPanel onColorsChange={handleColorsChange} />
194
  <Textarea
195
  value={prompt}
196
  onChange={(e) => {
197
+ setPrompt(e.target.value);
198
  // Clear error when user types
199
+ if (improveError) setImproveError(null);
200
  }}
201
  placeholder="Describe what site you want to build. E.g., Build a snake game"
202
+ className={`min-h-24 pr-20 pt-12 bg-novita-gray/20 border-novita-gray/30 text-white placeholder:text-novita-gray/70 resize-none ${isFullScreen ? "h-full" : ""}`}
203
  disabled={isLoading || isImprovingPrompt}
204
  />
205
  <div className="absolute bottom-3 right-3 flex gap-2">
 
212
  size="icon"
213
  variant="outline"
214
  className="h-8 w-8 bg-novita-gray/20 border-novita-gray/30 text-white hover:bg-novita-gray/30"
215
+ disabled={
216
+ isPromptTooShort || isLoading || isImprovingPrompt
217
+ }
218
  onClick={improvePrompt}
219
  >
220
  {isImprovingPrompt ? (
 
228
  </TooltipTrigger>
229
  {isPromptTooShort && (
230
  <TooltipContent className="bg-novita-gray text-white">
231
+ <p>Your prompt is too simple, we can&apos;t improve it.</p>
232
  </TooltipContent>
233
  )}
234
  </Tooltip>
 
249
  </div>
250
  </div>
251
  </form>
252
+
253
+ <AuthErrorPopup
254
+ show={showAuthError}
255
+ onClose={() => setShowAuthError(false)}
256
  />
257
  </div>
258
+ );
259
  }
src/components/share-dialog.tsx CHANGED
@@ -1,9 +1,15 @@
1
- "use client"
2
 
3
- import { useState } from "react"
4
- import { Copy, Check, AlertTriangle } from "lucide-react"
5
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog"
6
- import { Button } from "./ui/button"
 
 
 
 
 
 
7
 
8
  interface ShareDialogProps {
9
  open: boolean;
@@ -11,7 +17,11 @@ interface ShareDialogProps {
11
  shareUrl: string;
12
  }
13
 
14
- export function ShareDialog({ open, onOpenChange, shareUrl }: ShareDialogProps) {
 
 
 
 
15
  const [copied, setCopied] = useState(false);
16
 
17
  const handleCopyLink = async () => {
@@ -20,7 +30,7 @@ export function ShareDialog({ open, onOpenChange, shareUrl }: ShareDialogProps)
20
  setCopied(true);
21
  setTimeout(() => setCopied(false), 2000);
22
  } catch (err) {
23
- console.error('Failed to copy link:', err);
24
  }
25
  };
26
 
@@ -35,11 +45,12 @@ export function ShareDialog({ open, onOpenChange, shareUrl }: ShareDialogProps)
35
  <DialogHeader>
36
  <DialogTitle>Share Your Creation</DialogTitle>
37
  </DialogHeader>
38
-
39
  <div className="flex items-start space-x-2 p-3 rounded-md">
40
  <AlertTriangle className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
41
  <p className="text-sm text-muted-foreground">
42
- <strong>Note:</strong> This shared link is temporary and may not be permanently available.
 
43
  </p>
44
  </div>
45
 
@@ -55,9 +66,9 @@ export function ShareDialog({ open, onOpenChange, shareUrl }: ShareDialogProps)
55
  className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
56
  />
57
  </div>
58
- <Button
59
- type="button"
60
- size="sm"
61
  className="px-3"
62
  onClick={handleCopyLink}
63
  >
@@ -70,21 +81,14 @@ export function ShareDialog({ open, onOpenChange, shareUrl }: ShareDialogProps)
70
  </Button>
71
  </div>
72
  <div className="flex justify-end space-x-2">
73
- <Button
74
- type="button"
75
- variant="outline"
76
- onClick={handleClose}
77
- >
78
  Close
79
  </Button>
80
- <Button
81
- type="button"
82
- onClick={() => window.open(shareUrl, '_blank')}
83
- >
84
  Open Link
85
  </Button>
86
  </div>
87
  </DialogContent>
88
  </Dialog>
89
  );
90
- }
 
1
+ "use client";
2
 
3
+ import { useState } from "react";
4
+ import { Copy, Check, AlertTriangle } from "lucide-react";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogDescription,
11
+ } from "./ui/dialog";
12
+ import { Button } from "./ui/button";
13
 
14
  interface ShareDialogProps {
15
  open: boolean;
 
17
  shareUrl: string;
18
  }
19
 
20
+ export function ShareDialog({
21
+ open,
22
+ onOpenChange,
23
+ shareUrl,
24
+ }: ShareDialogProps) {
25
  const [copied, setCopied] = useState(false);
26
 
27
  const handleCopyLink = async () => {
 
30
  setCopied(true);
31
  setTimeout(() => setCopied(false), 2000);
32
  } catch (err) {
33
+ console.error("Failed to copy link:", err);
34
  }
35
  };
36
 
 
45
  <DialogHeader>
46
  <DialogTitle>Share Your Creation</DialogTitle>
47
  </DialogHeader>
48
+
49
  <div className="flex items-start space-x-2 p-3 rounded-md">
50
  <AlertTriangle className="h-4 w-4 text-muted-foreground mt-0.5 flex-shrink-0" />
51
  <p className="text-sm text-muted-foreground">
52
+ <strong>Note:</strong> This shared link is temporary and may not be
53
+ permanently available.
54
  </p>
55
  </div>
56
 
 
66
  className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
67
  />
68
  </div>
69
+ <Button
70
+ type="button"
71
+ size="sm"
72
  className="px-3"
73
  onClick={handleCopyLink}
74
  >
 
81
  </Button>
82
  </div>
83
  <div className="flex justify-end space-x-2">
84
+ <Button type="button" variant="outline" onClick={handleClose}>
 
 
 
 
85
  Close
86
  </Button>
87
+ <Button type="button" onClick={() => window.open(shareUrl, "_blank")}>
 
 
 
88
  Open Link
89
  </Button>
90
  </div>
91
  </DialogContent>
92
  </Dialog>
93
  );
94
+ }
src/components/theme-provider.tsx CHANGED
@@ -1,11 +1,11 @@
1
- 'use client'
2
 
3
- import * as React from 'react'
4
  import {
5
  ThemeProvider as NextThemesProvider,
6
  type ThemeProviderProps,
7
- } from 'next-themes'
8
 
9
  export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
10
- return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11
  }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
  import {
5
  ThemeProvider as NextThemesProvider,
6
  type ThemeProviderProps,
7
+ } from "next-themes";
8
 
9
  export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
10
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
11
  }
src/components/ui/accordion.tsx CHANGED
@@ -1,12 +1,12 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
- import { ChevronDown } from "lucide-react"
6
 
7
- import { cn } from "@/lib/utils"
8
 
9
- const Accordion = AccordionPrimitive.Root
10
 
11
  const AccordionItem = React.forwardRef<
12
  React.ElementRef<typeof AccordionPrimitive.Item>,
@@ -17,8 +17,8 @@ const AccordionItem = React.forwardRef<
17
  className={cn("border-b", className)}
18
  {...props}
19
  />
20
- ))
21
- AccordionItem.displayName = "AccordionItem"
22
 
23
  const AccordionTrigger = React.forwardRef<
24
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
@@ -29,7 +29,7 @@ const AccordionTrigger = React.forwardRef<
29
  ref={ref}
30
  className={cn(
31
  "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
32
- className
33
  )}
34
  {...props}
35
  >
@@ -37,8 +37,8 @@ const AccordionTrigger = React.forwardRef<
37
  <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
38
  </AccordionPrimitive.Trigger>
39
  </AccordionPrimitive.Header>
40
- ))
41
- AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42
 
43
  const AccordionContent = React.forwardRef<
44
  React.ElementRef<typeof AccordionPrimitive.Content>,
@@ -51,8 +51,8 @@ const AccordionContent = React.forwardRef<
51
  >
52
  <div className={cn("pb-4 pt-0", className)}>{children}</div>
53
  </AccordionPrimitive.Content>
54
- ))
55
 
56
- AccordionContent.displayName = AccordionPrimitive.Content.displayName
57
 
58
- export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
5
+ import { ChevronDown } from "lucide-react";
6
 
7
+ import { cn } from "@/lib/utils";
8
 
9
+ const Accordion = AccordionPrimitive.Root;
10
 
11
  const AccordionItem = React.forwardRef<
12
  React.ElementRef<typeof AccordionPrimitive.Item>,
 
17
  className={cn("border-b", className)}
18
  {...props}
19
  />
20
+ ));
21
+ AccordionItem.displayName = "AccordionItem";
22
 
23
  const AccordionTrigger = React.forwardRef<
24
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
 
29
  ref={ref}
30
  className={cn(
31
  "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
32
+ className,
33
  )}
34
  {...props}
35
  >
 
37
  <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
38
  </AccordionPrimitive.Trigger>
39
  </AccordionPrimitive.Header>
40
+ ));
41
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42
 
43
  const AccordionContent = React.forwardRef<
44
  React.ElementRef<typeof AccordionPrimitive.Content>,
 
51
  >
52
  <div className={cn("pb-4 pt-0", className)}>{children}</div>
53
  </AccordionPrimitive.Content>
54
+ ));
55
 
56
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57
 
58
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
src/components/ui/alert-dialog.tsx CHANGED
@@ -1,16 +1,16 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
 
6
- import { cn } from "@/lib/utils"
7
- import { buttonVariants } from "@/components/ui/button"
8
 
9
- const AlertDialog = AlertDialogPrimitive.Root
10
 
11
- const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12
 
13
- const AlertDialogPortal = AlertDialogPrimitive.Portal
14
 
15
  const AlertDialogOverlay = React.forwardRef<
16
  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
@@ -19,13 +19,13 @@ const AlertDialogOverlay = React.forwardRef<
19
  <AlertDialogPrimitive.Overlay
20
  className={cn(
21
  "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
- className
23
  )}
24
  {...props}
25
  ref={ref}
26
  />
27
- ))
28
- AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29
 
30
  const AlertDialogContent = React.forwardRef<
31
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
@@ -37,13 +37,13 @@ const AlertDialogContent = React.forwardRef<
37
  ref={ref}
38
  className={cn(
39
  "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
40
- className
41
  )}
42
  {...props}
43
  />
44
  </AlertDialogPortal>
45
- ))
46
- AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47
 
48
  const AlertDialogHeader = ({
49
  className,
@@ -52,12 +52,12 @@ const AlertDialogHeader = ({
52
  <div
53
  className={cn(
54
  "flex flex-col space-y-2 text-center sm:text-left",
55
- className
56
  )}
57
  {...props}
58
  />
59
- )
60
- AlertDialogHeader.displayName = "AlertDialogHeader"
61
 
62
  const AlertDialogFooter = ({
63
  className,
@@ -66,12 +66,12 @@ const AlertDialogFooter = ({
66
  <div
67
  className={cn(
68
  "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
69
- className
70
  )}
71
  {...props}
72
  />
73
- )
74
- AlertDialogFooter.displayName = "AlertDialogFooter"
75
 
76
  const AlertDialogTitle = React.forwardRef<
77
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
@@ -82,8 +82,8 @@ const AlertDialogTitle = React.forwardRef<
82
  className={cn("text-lg font-semibold", className)}
83
  {...props}
84
  />
85
- ))
86
- AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87
 
88
  const AlertDialogDescription = React.forwardRef<
89
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
@@ -94,9 +94,9 @@ const AlertDialogDescription = React.forwardRef<
94
  className={cn("text-sm text-muted-foreground", className)}
95
  {...props}
96
  />
97
- ))
98
  AlertDialogDescription.displayName =
99
- AlertDialogPrimitive.Description.displayName
100
 
101
  const AlertDialogAction = React.forwardRef<
102
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
@@ -107,8 +107,8 @@ const AlertDialogAction = React.forwardRef<
107
  className={cn(buttonVariants(), className)}
108
  {...props}
109
  />
110
- ))
111
- AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112
 
113
  const AlertDialogCancel = React.forwardRef<
114
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
@@ -119,12 +119,12 @@ const AlertDialogCancel = React.forwardRef<
119
  className={cn(
120
  buttonVariants({ variant: "outline" }),
121
  "mt-2 sm:mt-0",
122
- className
123
  )}
124
  {...props}
125
  />
126
- ))
127
- AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128
 
129
  export {
130
  AlertDialog,
@@ -138,4 +138,4 @@ export {
138
  AlertDialogDescription,
139
  AlertDialogAction,
140
  AlertDialogCancel,
141
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
5
 
6
+ import { cn } from "@/lib/utils";
7
+ import { buttonVariants } from "@/components/ui/button";
8
 
9
+ const AlertDialog = AlertDialogPrimitive.Root;
10
 
11
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
12
 
13
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
14
 
15
  const AlertDialogOverlay = React.forwardRef<
16
  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
 
19
  <AlertDialogPrimitive.Overlay
20
  className={cn(
21
  "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
+ className,
23
  )}
24
  {...props}
25
  ref={ref}
26
  />
27
+ ));
28
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
29
 
30
  const AlertDialogContent = React.forwardRef<
31
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
 
37
  ref={ref}
38
  className={cn(
39
  "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
40
+ className,
41
  )}
42
  {...props}
43
  />
44
  </AlertDialogPortal>
45
+ ));
46
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
47
 
48
  const AlertDialogHeader = ({
49
  className,
 
52
  <div
53
  className={cn(
54
  "flex flex-col space-y-2 text-center sm:text-left",
55
+ className,
56
  )}
57
  {...props}
58
  />
59
+ );
60
+ AlertDialogHeader.displayName = "AlertDialogHeader";
61
 
62
  const AlertDialogFooter = ({
63
  className,
 
66
  <div
67
  className={cn(
68
  "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
69
+ className,
70
  )}
71
  {...props}
72
  />
73
+ );
74
+ AlertDialogFooter.displayName = "AlertDialogFooter";
75
 
76
  const AlertDialogTitle = React.forwardRef<
77
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
 
82
  className={cn("text-lg font-semibold", className)}
83
  {...props}
84
  />
85
+ ));
86
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
87
 
88
  const AlertDialogDescription = React.forwardRef<
89
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
 
94
  className={cn("text-sm text-muted-foreground", className)}
95
  {...props}
96
  />
97
+ ));
98
  AlertDialogDescription.displayName =
99
+ AlertDialogPrimitive.Description.displayName;
100
 
101
  const AlertDialogAction = React.forwardRef<
102
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
 
107
  className={cn(buttonVariants(), className)}
108
  {...props}
109
  />
110
+ ));
111
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
112
 
113
  const AlertDialogCancel = React.forwardRef<
114
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
 
119
  className={cn(
120
  buttonVariants({ variant: "outline" }),
121
  "mt-2 sm:mt-0",
122
+ className,
123
  )}
124
  {...props}
125
  />
126
+ ));
127
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
128
 
129
  export {
130
  AlertDialog,
 
138
  AlertDialogDescription,
139
  AlertDialogAction,
140
  AlertDialogCancel,
141
+ };
src/components/ui/alert.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
 
4
- import { cn } from "@/lib/utils"
5
 
6
  const alertVariants = cva(
7
  "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
@@ -16,8 +16,8 @@ const alertVariants = cva(
16
  defaultVariants: {
17
  variant: "default",
18
  },
19
- }
20
- )
21
 
22
  const Alert = React.forwardRef<
23
  HTMLDivElement,
@@ -29,8 +29,8 @@ const Alert = React.forwardRef<
29
  className={cn(alertVariants({ variant }), className)}
30
  {...props}
31
  />
32
- ))
33
- Alert.displayName = "Alert"
34
 
35
  const AlertTitle = React.forwardRef<
36
  HTMLParagraphElement,
@@ -41,8 +41,8 @@ const AlertTitle = React.forwardRef<
41
  className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
  {...props}
43
  />
44
- ))
45
- AlertTitle.displayName = "AlertTitle"
46
 
47
  const AlertDescription = React.forwardRef<
48
  HTMLParagraphElement,
@@ -53,7 +53,7 @@ const AlertDescription = React.forwardRef<
53
  className={cn("text-sm [&_p]:leading-relaxed", className)}
54
  {...props}
55
  />
56
- ))
57
- AlertDescription.displayName = "AlertDescription"
58
 
59
- export { Alert, AlertTitle, AlertDescription }
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
 
4
+ import { cn } from "@/lib/utils";
5
 
6
  const alertVariants = cva(
7
  "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
 
16
  defaultVariants: {
17
  variant: "default",
18
  },
19
+ },
20
+ );
21
 
22
  const Alert = React.forwardRef<
23
  HTMLDivElement,
 
29
  className={cn(alertVariants({ variant }), className)}
30
  {...props}
31
  />
32
+ ));
33
+ Alert.displayName = "Alert";
34
 
35
  const AlertTitle = React.forwardRef<
36
  HTMLParagraphElement,
 
41
  className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
  {...props}
43
  />
44
+ ));
45
+ AlertTitle.displayName = "AlertTitle";
46
 
47
  const AlertDescription = React.forwardRef<
48
  HTMLParagraphElement,
 
53
  className={cn("text-sm [&_p]:leading-relaxed", className)}
54
  {...props}
55
  />
56
+ ));
57
+ AlertDescription.displayName = "AlertDescription";
58
 
59
+ export { Alert, AlertTitle, AlertDescription };
src/components/ui/aspect-ratio.tsx CHANGED
@@ -1,7 +1,7 @@
1
- "use client"
2
 
3
- import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4
 
5
- const AspectRatio = AspectRatioPrimitive.Root
6
 
7
- export { AspectRatio }
 
1
+ "use client";
2
 
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
4
 
5
+ const AspectRatio = AspectRatioPrimitive.Root;
6
 
7
+ export { AspectRatio };
src/components/ui/avatar.tsx CHANGED
@@ -1,9 +1,9 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
 
6
- import { cn } from "@/lib/utils"
7
 
8
  const Avatar = React.forwardRef<
9
  React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
13
  ref={ref}
14
  className={cn(
15
  "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16
- className
17
  )}
18
  {...props}
19
  />
20
- ))
21
- Avatar.displayName = AvatarPrimitive.Root.displayName
22
 
23
  const AvatarImage = React.forwardRef<
24
  React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
29
  className={cn("aspect-square h-full w-full", className)}
30
  {...props}
31
  />
32
- ))
33
- AvatarImage.displayName = AvatarPrimitive.Image.displayName
34
 
35
  const AvatarFallback = React.forwardRef<
36
  React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef<
40
  ref={ref}
41
  className={cn(
42
  "flex h-full w-full items-center justify-center rounded-full bg-muted",
43
- className
44
  )}
45
  {...props}
46
  />
47
- ))
48
- AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49
 
50
- export { Avatar, AvatarImage, AvatarFallback }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
 
6
+ import { cn } from "@/lib/utils";
7
 
8
  const Avatar = React.forwardRef<
9
  React.ElementRef<typeof AvatarPrimitive.Root>,
 
13
  ref={ref}
14
  className={cn(
15
  "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16
+ className,
17
  )}
18
  {...props}
19
  />
20
+ ));
21
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
22
 
23
  const AvatarImage = React.forwardRef<
24
  React.ElementRef<typeof AvatarPrimitive.Image>,
 
29
  className={cn("aspect-square h-full w-full", className)}
30
  {...props}
31
  />
32
+ ));
33
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34
 
35
  const AvatarFallback = React.forwardRef<
36
  React.ElementRef<typeof AvatarPrimitive.Fallback>,
 
40
  ref={ref}
41
  className={cn(
42
  "flex h-full w-full items-center justify-center rounded-full bg-muted",
43
+ className,
44
  )}
45
  {...props}
46
  />
47
+ ));
48
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49
 
50
+ export { Avatar, AvatarImage, AvatarFallback };
src/components/ui/badge.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
 
4
- import { cn } from "@/lib/utils"
5
 
6
  const badgeVariants = cva(
7
  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -20,8 +20,8 @@ const badgeVariants = cva(
20
  defaultVariants: {
21
  variant: "default",
22
  },
23
- }
24
- )
25
 
26
  export interface BadgeProps
27
  extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
30
  function Badge({ className, variant, ...props }: BadgeProps) {
31
  return (
32
  <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
- )
34
  }
35
 
36
- export { Badge, badgeVariants }
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
 
4
+ import { cn } from "@/lib/utils";
5
 
6
  const badgeVariants = cva(
7
  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
 
20
  defaultVariants: {
21
  variant: "default",
22
  },
23
+ },
24
+ );
25
 
26
  export interface BadgeProps
27
  extends React.HTMLAttributes<HTMLDivElement>,
 
30
  function Badge({ className, variant, ...props }: BadgeProps) {
31
  return (
32
  <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ );
34
  }
35
 
36
+ export { Badge, badgeVariants };
src/components/ui/breadcrumb.tsx CHANGED
@@ -1,16 +1,16 @@
1
- import * as React from "react"
2
- import { Slot } from "@radix-ui/react-slot"
3
- import { ChevronRight, MoreHorizontal } from "lucide-react"
4
 
5
- import { cn } from "@/lib/utils"
6
 
7
  const Breadcrumb = React.forwardRef<
8
  HTMLElement,
9
  React.ComponentPropsWithoutRef<"nav"> & {
10
- separator?: React.ReactNode
11
  }
12
- >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
13
- Breadcrumb.displayName = "Breadcrumb"
14
 
15
  const BreadcrumbList = React.forwardRef<
16
  HTMLOListElement,
@@ -20,12 +20,12 @@ const BreadcrumbList = React.forwardRef<
20
  ref={ref}
21
  className={cn(
22
  "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
23
- className
24
  )}
25
  {...props}
26
  />
27
- ))
28
- BreadcrumbList.displayName = "BreadcrumbList"
29
 
30
  const BreadcrumbItem = React.forwardRef<
31
  HTMLLIElement,
@@ -36,16 +36,16 @@ const BreadcrumbItem = React.forwardRef<
36
  className={cn("inline-flex items-center gap-1.5", className)}
37
  {...props}
38
  />
39
- ))
40
- BreadcrumbItem.displayName = "BreadcrumbItem"
41
 
42
  const BreadcrumbLink = React.forwardRef<
43
  HTMLAnchorElement,
44
  React.ComponentPropsWithoutRef<"a"> & {
45
- asChild?: boolean
46
  }
47
  >(({ asChild, className, ...props }, ref) => {
48
- const Comp = asChild ? Slot : "a"
49
 
50
  return (
51
  <Comp
@@ -53,9 +53,9 @@ const BreadcrumbLink = React.forwardRef<
53
  className={cn("transition-colors hover:text-foreground", className)}
54
  {...props}
55
  />
56
- )
57
- })
58
- BreadcrumbLink.displayName = "BreadcrumbLink"
59
 
60
  const BreadcrumbPage = React.forwardRef<
61
  HTMLSpanElement,
@@ -69,8 +69,8 @@ const BreadcrumbPage = React.forwardRef<
69
  className={cn("font-normal text-foreground", className)}
70
  {...props}
71
  />
72
- ))
73
- BreadcrumbPage.displayName = "BreadcrumbPage"
74
 
75
  const BreadcrumbSeparator = ({
76
  children,
@@ -85,8 +85,8 @@ const BreadcrumbSeparator = ({
85
  >
86
  {children ?? <ChevronRight />}
87
  </li>
88
- )
89
- BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90
 
91
  const BreadcrumbEllipsis = ({
92
  className,
@@ -101,8 +101,8 @@ const BreadcrumbEllipsis = ({
101
  <MoreHorizontal className="h-4 w-4" />
102
  <span className="sr-only">More</span>
103
  </span>
104
- )
105
- BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106
 
107
  export {
108
  Breadcrumb,
@@ -112,4 +112,4 @@ export {
112
  BreadcrumbPage,
113
  BreadcrumbSeparator,
114
  BreadcrumbEllipsis,
115
- }
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
4
 
5
+ import { cn } from "@/lib/utils";
6
 
7
  const Breadcrumb = React.forwardRef<
8
  HTMLElement,
9
  React.ComponentPropsWithoutRef<"nav"> & {
10
+ separator?: React.ReactNode;
11
  }
12
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
13
+ Breadcrumb.displayName = "Breadcrumb";
14
 
15
  const BreadcrumbList = React.forwardRef<
16
  HTMLOListElement,
 
20
  ref={ref}
21
  className={cn(
22
  "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
23
+ className,
24
  )}
25
  {...props}
26
  />
27
+ ));
28
+ BreadcrumbList.displayName = "BreadcrumbList";
29
 
30
  const BreadcrumbItem = React.forwardRef<
31
  HTMLLIElement,
 
36
  className={cn("inline-flex items-center gap-1.5", className)}
37
  {...props}
38
  />
39
+ ));
40
+ BreadcrumbItem.displayName = "BreadcrumbItem";
41
 
42
  const BreadcrumbLink = React.forwardRef<
43
  HTMLAnchorElement,
44
  React.ComponentPropsWithoutRef<"a"> & {
45
+ asChild?: boolean;
46
  }
47
  >(({ asChild, className, ...props }, ref) => {
48
+ const Comp = asChild ? Slot : "a";
49
 
50
  return (
51
  <Comp
 
53
  className={cn("transition-colors hover:text-foreground", className)}
54
  {...props}
55
  />
56
+ );
57
+ });
58
+ BreadcrumbLink.displayName = "BreadcrumbLink";
59
 
60
  const BreadcrumbPage = React.forwardRef<
61
  HTMLSpanElement,
 
69
  className={cn("font-normal text-foreground", className)}
70
  {...props}
71
  />
72
+ ));
73
+ BreadcrumbPage.displayName = "BreadcrumbPage";
74
 
75
  const BreadcrumbSeparator = ({
76
  children,
 
85
  >
86
  {children ?? <ChevronRight />}
87
  </li>
88
+ );
89
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
90
 
91
  const BreadcrumbEllipsis = ({
92
  className,
 
101
  <MoreHorizontal className="h-4 w-4" />
102
  <span className="sr-only">More</span>
103
  </span>
104
+ );
105
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
106
 
107
  export {
108
  Breadcrumb,
 
112
  BreadcrumbPage,
113
  BreadcrumbSeparator,
114
  BreadcrumbEllipsis,
115
+ };
src/components/ui/button.tsx CHANGED
@@ -1,8 +1,8 @@
1
- import * as React from "react"
2
- import { Slot } from "@radix-ui/react-slot"
3
- import { cva, type VariantProps } from "class-variance-authority"
4
 
5
- import { cn } from "@/lib/utils"
6
 
7
  const buttonVariants = cva(
8
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
@@ -10,9 +10,12 @@ const buttonVariants = cva(
10
  variants: {
11
  variant: {
12
  default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
- destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14
- outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
 
 
 
16
  ghost: "hover:bg-accent hover:text-accent-foreground",
17
  link: "text-primary underline-offset-4 hover:underline",
18
  },
@@ -28,20 +31,26 @@ const buttonVariants = cva(
28
  size: "default",
29
  },
30
  },
31
- )
32
 
33
  export interface ButtonProps
34
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
  VariantProps<typeof buttonVariants> {
36
- asChild?: boolean
37
  }
38
 
39
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
  ({ className, variant, size, asChild = false, ...props }, ref) => {
41
- const Comp = asChild ? Slot : "button"
42
- return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
 
 
 
 
 
 
43
  },
44
- )
45
- Button.displayName = "Button"
46
 
47
- export { Button, buttonVariants }
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
 
5
+ import { cn } from "@/lib/utils";
6
 
7
  const buttonVariants = cva(
8
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
 
10
  variants: {
11
  variant: {
12
  default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
  ghost: "hover:bg-accent hover:text-accent-foreground",
20
  link: "text-primary underline-offset-4 hover:underline",
21
  },
 
31
  size: "default",
32
  },
33
  },
34
+ );
35
 
36
  export interface ButtonProps
37
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
  VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
  }
41
 
42
  const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
  ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
  },
53
+ );
54
+ Button.displayName = "Button";
55
 
56
+ export { Button, buttonVariants };
src/components/ui/calendar.tsx CHANGED
@@ -1,13 +1,13 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import { ChevronLeft, ChevronRight } from "lucide-react"
5
- import { DayPicker } from "react-day-picker"
6
 
7
- import { cn } from "@/lib/utils"
8
- import { buttonVariants } from "@/components/ui/button"
9
 
10
- export type CalendarProps = React.ComponentProps<typeof DayPicker>
11
 
12
  function Calendar({
13
  className,
@@ -27,7 +27,7 @@ function Calendar({
27
  nav: "space-x-1 flex items-center",
28
  nav_button: cn(
29
  buttonVariants({ variant: "outline" }),
30
- "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
31
  ),
32
  nav_button_previous: "absolute left-1",
33
  nav_button_next: "absolute right-1",
@@ -39,7 +39,7 @@ function Calendar({
39
  cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
40
  day: cn(
41
  buttonVariants({ variant: "ghost" }),
42
- "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
43
  ),
44
  day_range_end: "day-range-end",
45
  day_selected:
@@ -59,8 +59,8 @@ function Calendar({
59
  }}
60
  {...props}
61
  />
62
- )
63
  }
64
- Calendar.displayName = "Calendar"
65
 
66
- export { Calendar }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import { DayPicker } from "react-day-picker";
6
 
7
+ import { cn } from "@/lib/utils";
8
+ import { buttonVariants } from "@/components/ui/button";
9
 
10
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>;
11
 
12
  function Calendar({
13
  className,
 
27
  nav: "space-x-1 flex items-center",
28
  nav_button: cn(
29
  buttonVariants({ variant: "outline" }),
30
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
31
  ),
32
  nav_button_previous: "absolute left-1",
33
  nav_button_next: "absolute right-1",
 
39
  cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
40
  day: cn(
41
  buttonVariants({ variant: "ghost" }),
42
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100",
43
  ),
44
  day_range_end: "day-range-end",
45
  day_selected:
 
59
  }}
60
  {...props}
61
  />
62
+ );
63
  }
64
+ Calendar.displayName = "Calendar";
65
 
66
+ export { Calendar };
src/components/ui/card.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import * as React from "react"
2
 
3
- import { cn } from "@/lib/utils"
4
 
5
  const Card = React.forwardRef<
6
  HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
10
  ref={ref}
11
  className={cn(
12
  "rounded-lg border bg-card text-card-foreground shadow-sm",
13
- className
14
  )}
15
  {...props}
16
  />
17
- ))
18
- Card.displayName = "Card"
19
 
20
  const CardHeader = React.forwardRef<
21
  HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
26
  className={cn("flex flex-col space-y-1.5 p-6", className)}
27
  {...props}
28
  />
29
- ))
30
- CardHeader.displayName = "CardHeader"
31
 
32
  const CardTitle = React.forwardRef<
33
  HTMLDivElement,
@@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
37
  ref={ref}
38
  className={cn(
39
  "text-2xl font-semibold leading-none tracking-tight",
40
- className
41
  )}
42
  {...props}
43
  />
44
- ))
45
- CardTitle.displayName = "CardTitle"
46
 
47
  const CardDescription = React.forwardRef<
48
  HTMLDivElement,
@@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
53
  className={cn("text-sm text-muted-foreground", className)}
54
  {...props}
55
  />
56
- ))
57
- CardDescription.displayName = "CardDescription"
58
 
59
  const CardContent = React.forwardRef<
60
  HTMLDivElement,
61
  React.HTMLAttributes<HTMLDivElement>
62
  >(({ className, ...props }, ref) => (
63
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
- ))
65
- CardContent.displayName = "CardContent"
66
 
67
  const CardFooter = React.forwardRef<
68
  HTMLDivElement,
@@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
73
  className={cn("flex items-center p-6 pt-0", className)}
74
  {...props}
75
  />
76
- ))
77
- CardFooter.displayName = "CardFooter"
78
 
79
- export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
 
3
+ import { cn } from "@/lib/utils";
4
 
5
  const Card = React.forwardRef<
6
  HTMLDivElement,
 
10
  ref={ref}
11
  className={cn(
12
  "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className,
14
  )}
15
  {...props}
16
  />
17
+ ));
18
+ Card.displayName = "Card";
19
 
20
  const CardHeader = React.forwardRef<
21
  HTMLDivElement,
 
26
  className={cn("flex flex-col space-y-1.5 p-6", className)}
27
  {...props}
28
  />
29
+ ));
30
+ CardHeader.displayName = "CardHeader";
31
 
32
  const CardTitle = React.forwardRef<
33
  HTMLDivElement,
 
37
  ref={ref}
38
  className={cn(
39
  "text-2xl font-semibold leading-none tracking-tight",
40
+ className,
41
  )}
42
  {...props}
43
  />
44
+ ));
45
+ CardTitle.displayName = "CardTitle";
46
 
47
  const CardDescription = React.forwardRef<
48
  HTMLDivElement,
 
53
  className={cn("text-sm text-muted-foreground", className)}
54
  {...props}
55
  />
56
+ ));
57
+ CardDescription.displayName = "CardDescription";
58
 
59
  const CardContent = React.forwardRef<
60
  HTMLDivElement,
61
  React.HTMLAttributes<HTMLDivElement>
62
  >(({ className, ...props }, ref) => (
63
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ));
65
+ CardContent.displayName = "CardContent";
66
 
67
  const CardFooter = React.forwardRef<
68
  HTMLDivElement,
 
73
  className={cn("flex items-center p-6 pt-0", className)}
74
  {...props}
75
  />
76
+ ));
77
+ CardFooter.displayName = "CardFooter";
78
 
79
+ export {
80
+ Card,
81
+ CardHeader,
82
+ CardFooter,
83
+ CardTitle,
84
+ CardDescription,
85
+ CardContent,
86
+ };
src/components/ui/carousel.tsx CHANGED
@@ -1,45 +1,45 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
  import useEmblaCarousel, {
5
  type UseEmblaCarouselType,
6
- } from "embla-carousel-react"
7
- import { ArrowLeft, ArrowRight } from "lucide-react"
8
 
9
- import { cn } from "@/lib/utils"
10
- import { Button } from "@/components/ui/button"
11
 
12
- type CarouselApi = UseEmblaCarouselType[1]
13
- type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
14
- type CarouselOptions = UseCarouselParameters[0]
15
- type CarouselPlugin = UseCarouselParameters[1]
16
 
17
  type CarouselProps = {
18
- opts?: CarouselOptions
19
- plugins?: CarouselPlugin
20
- orientation?: "horizontal" | "vertical"
21
- setApi?: (api: CarouselApi) => void
22
- }
23
 
24
  type CarouselContextProps = {
25
- carouselRef: ReturnType<typeof useEmblaCarousel>[0]
26
- api: ReturnType<typeof useEmblaCarousel>[1]
27
- scrollPrev: () => void
28
- scrollNext: () => void
29
- canScrollPrev: boolean
30
- canScrollNext: boolean
31
- } & CarouselProps
32
 
33
- const CarouselContext = React.createContext<CarouselContextProps | null>(null)
34
 
35
  function useCarousel() {
36
- const context = React.useContext(CarouselContext)
37
 
38
  if (!context) {
39
- throw new Error("useCarousel must be used within a <Carousel />")
40
  }
41
 
42
- return context
43
  }
44
 
45
  const Carousel = React.forwardRef<
@@ -56,69 +56,69 @@ const Carousel = React.forwardRef<
56
  children,
57
  ...props
58
  },
59
- ref
60
  ) => {
61
  const [carouselRef, api] = useEmblaCarousel(
62
  {
63
  ...opts,
64
  axis: orientation === "horizontal" ? "x" : "y",
65
  },
66
- plugins
67
- )
68
- const [canScrollPrev, setCanScrollPrev] = React.useState(false)
69
- const [canScrollNext, setCanScrollNext] = React.useState(false)
70
 
71
  const onSelect = React.useCallback((api: CarouselApi) => {
72
  if (!api) {
73
- return
74
  }
75
 
76
- setCanScrollPrev(api.canScrollPrev())
77
- setCanScrollNext(api.canScrollNext())
78
- }, [])
79
 
80
  const scrollPrev = React.useCallback(() => {
81
- api?.scrollPrev()
82
- }, [api])
83
 
84
  const scrollNext = React.useCallback(() => {
85
- api?.scrollNext()
86
- }, [api])
87
 
88
  const handleKeyDown = React.useCallback(
89
  (event: React.KeyboardEvent<HTMLDivElement>) => {
90
  if (event.key === "ArrowLeft") {
91
- event.preventDefault()
92
- scrollPrev()
93
  } else if (event.key === "ArrowRight") {
94
- event.preventDefault()
95
- scrollNext()
96
  }
97
  },
98
- [scrollPrev, scrollNext]
99
- )
100
 
101
  React.useEffect(() => {
102
  if (!api || !setApi) {
103
- return
104
  }
105
 
106
- setApi(api)
107
- }, [api, setApi])
108
 
109
  React.useEffect(() => {
110
  if (!api) {
111
- return
112
  }
113
 
114
- onSelect(api)
115
- api.on("reInit", onSelect)
116
- api.on("select", onSelect)
117
 
118
  return () => {
119
- api?.off("select", onSelect)
120
- }
121
- }, [api, onSelect])
122
 
123
  return (
124
  <CarouselContext.Provider
@@ -145,16 +145,16 @@ const Carousel = React.forwardRef<
145
  {children}
146
  </div>
147
  </CarouselContext.Provider>
148
- )
149
- }
150
- )
151
- Carousel.displayName = "Carousel"
152
 
153
  const CarouselContent = React.forwardRef<
154
  HTMLDivElement,
155
  React.HTMLAttributes<HTMLDivElement>
156
  >(({ className, ...props }, ref) => {
157
- const { carouselRef, orientation } = useCarousel()
158
 
159
  return (
160
  <div ref={carouselRef} className="overflow-hidden">
@@ -163,20 +163,20 @@ const CarouselContent = React.forwardRef<
163
  className={cn(
164
  "flex",
165
  orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
166
- className
167
  )}
168
  {...props}
169
  />
170
  </div>
171
- )
172
- })
173
- CarouselContent.displayName = "CarouselContent"
174
 
175
  const CarouselItem = React.forwardRef<
176
  HTMLDivElement,
177
  React.HTMLAttributes<HTMLDivElement>
178
  >(({ className, ...props }, ref) => {
179
- const { orientation } = useCarousel()
180
 
181
  return (
182
  <div
@@ -186,19 +186,19 @@ const CarouselItem = React.forwardRef<
186
  className={cn(
187
  "min-w-0 shrink-0 grow-0 basis-full",
188
  orientation === "horizontal" ? "pl-4" : "pt-4",
189
- className
190
  )}
191
  {...props}
192
  />
193
- )
194
- })
195
- CarouselItem.displayName = "CarouselItem"
196
 
197
  const CarouselPrevious = React.forwardRef<
198
  HTMLButtonElement,
199
  React.ComponentProps<typeof Button>
200
  >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
201
- const { orientation, scrollPrev, canScrollPrev } = useCarousel()
202
 
203
  return (
204
  <Button
@@ -210,7 +210,7 @@ const CarouselPrevious = React.forwardRef<
210
  orientation === "horizontal"
211
  ? "-left-12 top-1/2 -translate-y-1/2"
212
  : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
213
- className
214
  )}
215
  disabled={!canScrollPrev}
216
  onClick={scrollPrev}
@@ -219,15 +219,15 @@ const CarouselPrevious = React.forwardRef<
219
  <ArrowLeft className="h-4 w-4" />
220
  <span className="sr-only">Previous slide</span>
221
  </Button>
222
- )
223
- })
224
- CarouselPrevious.displayName = "CarouselPrevious"
225
 
226
  const CarouselNext = React.forwardRef<
227
  HTMLButtonElement,
228
  React.ComponentProps<typeof Button>
229
  >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
230
- const { orientation, scrollNext, canScrollNext } = useCarousel()
231
 
232
  return (
233
  <Button
@@ -239,7 +239,7 @@ const CarouselNext = React.forwardRef<
239
  orientation === "horizontal"
240
  ? "-right-12 top-1/2 -translate-y-1/2"
241
  : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
242
- className
243
  )}
244
  disabled={!canScrollNext}
245
  onClick={scrollNext}
@@ -248,9 +248,9 @@ const CarouselNext = React.forwardRef<
248
  <ArrowRight className="h-4 w-4" />
249
  <span className="sr-only">Next slide</span>
250
  </Button>
251
- )
252
- })
253
- CarouselNext.displayName = "CarouselNext"
254
 
255
  export {
256
  type CarouselApi,
@@ -259,4 +259,4 @@ export {
259
  CarouselItem,
260
  CarouselPrevious,
261
  CarouselNext,
262
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
  import useEmblaCarousel, {
5
  type UseEmblaCarouselType,
6
+ } from "embla-carousel-react";
7
+ import { ArrowLeft, ArrowRight } from "lucide-react";
8
 
9
+ import { cn } from "@/lib/utils";
10
+ import { Button } from "@/components/ui/button";
11
 
12
+ type CarouselApi = UseEmblaCarouselType[1];
13
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
14
+ type CarouselOptions = UseCarouselParameters[0];
15
+ type CarouselPlugin = UseCarouselParameters[1];
16
 
17
  type CarouselProps = {
18
+ opts?: CarouselOptions;
19
+ plugins?: CarouselPlugin;
20
+ orientation?: "horizontal" | "vertical";
21
+ setApi?: (api: CarouselApi) => void;
22
+ };
23
 
24
  type CarouselContextProps = {
25
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
26
+ api: ReturnType<typeof useEmblaCarousel>[1];
27
+ scrollPrev: () => void;
28
+ scrollNext: () => void;
29
+ canScrollPrev: boolean;
30
+ canScrollNext: boolean;
31
+ } & CarouselProps;
32
 
33
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
34
 
35
  function useCarousel() {
36
+ const context = React.useContext(CarouselContext);
37
 
38
  if (!context) {
39
+ throw new Error("useCarousel must be used within a <Carousel />");
40
  }
41
 
42
+ return context;
43
  }
44
 
45
  const Carousel = React.forwardRef<
 
56
  children,
57
  ...props
58
  },
59
+ ref,
60
  ) => {
61
  const [carouselRef, api] = useEmblaCarousel(
62
  {
63
  ...opts,
64
  axis: orientation === "horizontal" ? "x" : "y",
65
  },
66
+ plugins,
67
+ );
68
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
69
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
70
 
71
  const onSelect = React.useCallback((api: CarouselApi) => {
72
  if (!api) {
73
+ return;
74
  }
75
 
76
+ setCanScrollPrev(api.canScrollPrev());
77
+ setCanScrollNext(api.canScrollNext());
78
+ }, []);
79
 
80
  const scrollPrev = React.useCallback(() => {
81
+ api?.scrollPrev();
82
+ }, [api]);
83
 
84
  const scrollNext = React.useCallback(() => {
85
+ api?.scrollNext();
86
+ }, [api]);
87
 
88
  const handleKeyDown = React.useCallback(
89
  (event: React.KeyboardEvent<HTMLDivElement>) => {
90
  if (event.key === "ArrowLeft") {
91
+ event.preventDefault();
92
+ scrollPrev();
93
  } else if (event.key === "ArrowRight") {
94
+ event.preventDefault();
95
+ scrollNext();
96
  }
97
  },
98
+ [scrollPrev, scrollNext],
99
+ );
100
 
101
  React.useEffect(() => {
102
  if (!api || !setApi) {
103
+ return;
104
  }
105
 
106
+ setApi(api);
107
+ }, [api, setApi]);
108
 
109
  React.useEffect(() => {
110
  if (!api) {
111
+ return;
112
  }
113
 
114
+ onSelect(api);
115
+ api.on("reInit", onSelect);
116
+ api.on("select", onSelect);
117
 
118
  return () => {
119
+ api?.off("select", onSelect);
120
+ };
121
+ }, [api, onSelect]);
122
 
123
  return (
124
  <CarouselContext.Provider
 
145
  {children}
146
  </div>
147
  </CarouselContext.Provider>
148
+ );
149
+ },
150
+ );
151
+ Carousel.displayName = "Carousel";
152
 
153
  const CarouselContent = React.forwardRef<
154
  HTMLDivElement,
155
  React.HTMLAttributes<HTMLDivElement>
156
  >(({ className, ...props }, ref) => {
157
+ const { carouselRef, orientation } = useCarousel();
158
 
159
  return (
160
  <div ref={carouselRef} className="overflow-hidden">
 
163
  className={cn(
164
  "flex",
165
  orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
166
+ className,
167
  )}
168
  {...props}
169
  />
170
  </div>
171
+ );
172
+ });
173
+ CarouselContent.displayName = "CarouselContent";
174
 
175
  const CarouselItem = React.forwardRef<
176
  HTMLDivElement,
177
  React.HTMLAttributes<HTMLDivElement>
178
  >(({ className, ...props }, ref) => {
179
+ const { orientation } = useCarousel();
180
 
181
  return (
182
  <div
 
186
  className={cn(
187
  "min-w-0 shrink-0 grow-0 basis-full",
188
  orientation === "horizontal" ? "pl-4" : "pt-4",
189
+ className,
190
  )}
191
  {...props}
192
  />
193
+ );
194
+ });
195
+ CarouselItem.displayName = "CarouselItem";
196
 
197
  const CarouselPrevious = React.forwardRef<
198
  HTMLButtonElement,
199
  React.ComponentProps<typeof Button>
200
  >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
201
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
202
 
203
  return (
204
  <Button
 
210
  orientation === "horizontal"
211
  ? "-left-12 top-1/2 -translate-y-1/2"
212
  : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
213
+ className,
214
  )}
215
  disabled={!canScrollPrev}
216
  onClick={scrollPrev}
 
219
  <ArrowLeft className="h-4 w-4" />
220
  <span className="sr-only">Previous slide</span>
221
  </Button>
222
+ );
223
+ });
224
+ CarouselPrevious.displayName = "CarouselPrevious";
225
 
226
  const CarouselNext = React.forwardRef<
227
  HTMLButtonElement,
228
  React.ComponentProps<typeof Button>
229
  >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
230
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
231
 
232
  return (
233
  <Button
 
239
  orientation === "horizontal"
240
  ? "-right-12 top-1/2 -translate-y-1/2"
241
  : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
242
+ className,
243
  )}
244
  disabled={!canScrollNext}
245
  onClick={scrollNext}
 
248
  <ArrowRight className="h-4 w-4" />
249
  <span className="sr-only">Next slide</span>
250
  </Button>
251
+ );
252
+ });
253
+ CarouselNext.displayName = "CarouselNext";
254
 
255
  export {
256
  type CarouselApi,
 
259
  CarouselItem,
260
  CarouselPrevious,
261
  CarouselNext,
262
+ };
src/components/ui/chart.tsx CHANGED
@@ -1,50 +1,50 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as RechartsPrimitive from "recharts"
5
 
6
- import { cn } from "@/lib/utils"
7
 
8
  // Format: { THEME_NAME: CSS_SELECTOR }
9
- const THEMES = { light: "", dark: ".dark" } as const
10
 
11
  export type ChartConfig = {
12
  [k in string]: {
13
- label?: React.ReactNode
14
- icon?: React.ComponentType
15
  } & (
16
  | { color?: string; theme?: never }
17
  | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
- )
19
- }
20
 
21
  type ChartContextProps = {
22
- config: ChartConfig
23
- }
24
 
25
- const ChartContext = React.createContext<ChartContextProps | null>(null)
26
 
27
  function useChart() {
28
- const context = React.useContext(ChartContext)
29
 
30
  if (!context) {
31
- throw new Error("useChart must be used within a <ChartContainer />")
32
  }
33
 
34
- return context
35
  }
36
 
37
  const ChartContainer = React.forwardRef<
38
  HTMLDivElement,
39
  React.ComponentProps<"div"> & {
40
- config: ChartConfig
41
  children: React.ComponentProps<
42
  typeof RechartsPrimitive.ResponsiveContainer
43
- >["children"]
44
  }
45
  >(({ id, className, children, config, ...props }, ref) => {
46
- const uniqueId = React.useId()
47
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
48
 
49
  return (
50
  <ChartContext.Provider value={{ config }}>
@@ -53,7 +53,7 @@ const ChartContainer = React.forwardRef<
53
  ref={ref}
54
  className={cn(
55
  "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
56
- className
57
  )}
58
  {...props}
59
  >
@@ -63,17 +63,17 @@ const ChartContainer = React.forwardRef<
63
  </RechartsPrimitive.ResponsiveContainer>
64
  </div>
65
  </ChartContext.Provider>
66
- )
67
- })
68
- ChartContainer.displayName = "Chart"
69
 
70
  const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71
  const colorConfig = Object.entries(config).filter(
72
- ([_, config]) => config.theme || config.color
73
- )
74
 
75
  if (!colorConfig.length) {
76
- return null
77
  }
78
 
79
  return (
@@ -87,30 +87,30 @@ ${colorConfig
87
  .map(([key, itemConfig]) => {
88
  const color =
89
  itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
90
- itemConfig.color
91
- return color ? ` --color-${key}: ${color};` : null
92
  })
93
  .join("\n")}
94
  }
95
- `
96
  )
97
  .join("\n"),
98
  }}
99
  />
100
- )
101
- }
102
 
103
- const ChartTooltip = RechartsPrimitive.Tooltip
104
 
105
  const ChartTooltipContent = React.forwardRef<
106
  HTMLDivElement,
107
  React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
108
  React.ComponentProps<"div"> & {
109
- hideLabel?: boolean
110
- hideIndicator?: boolean
111
- indicator?: "line" | "dot" | "dashed"
112
- nameKey?: string
113
- labelKey?: string
114
  }
115
  >(
116
  (
@@ -129,36 +129,36 @@ const ChartTooltipContent = React.forwardRef<
129
  nameKey,
130
  labelKey,
131
  },
132
- ref
133
  ) => {
134
- const { config } = useChart()
135
 
136
  const tooltipLabel = React.useMemo(() => {
137
  if (hideLabel || !payload?.length) {
138
- return null
139
  }
140
 
141
- const [item] = payload
142
- const key = `${labelKey || item.dataKey || item.name || "value"}`
143
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
144
  const value =
145
  !labelKey && typeof label === "string"
146
  ? config[label as keyof typeof config]?.label || label
147
- : itemConfig?.label
148
 
149
  if (labelFormatter) {
150
  return (
151
  <div className={cn("font-medium", labelClassName)}>
152
  {labelFormatter(value, payload)}
153
  </div>
154
- )
155
  }
156
 
157
  if (!value) {
158
- return null
159
  }
160
 
161
- return <div className={cn("font-medium", labelClassName)}>{value}</div>
162
  }, [
163
  label,
164
  labelFormatter,
@@ -167,35 +167,35 @@ const ChartTooltipContent = React.forwardRef<
167
  labelClassName,
168
  config,
169
  labelKey,
170
- ])
171
 
172
  if (!active || !payload?.length) {
173
- return null
174
  }
175
 
176
- const nestLabel = payload.length === 1 && indicator !== "dot"
177
 
178
  return (
179
  <div
180
  ref={ref}
181
  className={cn(
182
  "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
183
- className
184
  )}
185
  >
186
  {!nestLabel ? tooltipLabel : null}
187
  <div className="grid gap-1.5">
188
  {payload.map((item, index) => {
189
- const key = `${nameKey || item.name || item.dataKey || "value"}`
190
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
191
- const indicatorColor = color || item.payload.fill || item.color
192
 
193
  return (
194
  <div
195
  key={item.dataKey}
196
  className={cn(
197
  "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
198
- indicator === "dot" && "items-center"
199
  )}
200
  >
201
  {formatter && item?.value !== undefined && item.name ? (
@@ -215,7 +215,7 @@ const ChartTooltipContent = React.forwardRef<
215
  "w-0 border-[1.5px] border-dashed bg-transparent":
216
  indicator === "dashed",
217
  "my-0.5": nestLabel && indicator === "dashed",
218
- }
219
  )}
220
  style={
221
  {
@@ -229,7 +229,7 @@ const ChartTooltipContent = React.forwardRef<
229
  <div
230
  className={cn(
231
  "flex flex-1 justify-between leading-none",
232
- nestLabel ? "items-end" : "items-center"
233
  )}
234
  >
235
  <div className="grid gap-1.5">
@@ -247,33 +247,33 @@ const ChartTooltipContent = React.forwardRef<
247
  </>
248
  )}
249
  </div>
250
- )
251
  })}
252
  </div>
253
  </div>
254
- )
255
- }
256
- )
257
- ChartTooltipContent.displayName = "ChartTooltip"
258
 
259
- const ChartLegend = RechartsPrimitive.Legend
260
 
261
  const ChartLegendContent = React.forwardRef<
262
  HTMLDivElement,
263
  React.ComponentProps<"div"> &
264
  Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
265
- hideIcon?: boolean
266
- nameKey?: string
267
  }
268
  >(
269
  (
270
  { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
271
- ref
272
  ) => {
273
- const { config } = useChart()
274
 
275
  if (!payload?.length) {
276
- return null
277
  }
278
 
279
  return (
@@ -282,18 +282,18 @@ const ChartLegendContent = React.forwardRef<
282
  className={cn(
283
  "flex items-center justify-center gap-4",
284
  verticalAlign === "top" ? "pb-3" : "pt-3",
285
- className
286
  )}
287
  >
288
  {payload.map((item) => {
289
- const key = `${nameKey || item.dataKey || "value"}`
290
- const itemConfig = getPayloadConfigFromPayload(config, item, key)
291
 
292
  return (
293
  <div
294
  key={item.value}
295
  className={cn(
296
- "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
297
  )}
298
  >
299
  {itemConfig?.icon && !hideIcon ? (
@@ -308,22 +308,22 @@ const ChartLegendContent = React.forwardRef<
308
  )}
309
  {itemConfig?.label}
310
  </div>
311
- )
312
  })}
313
  </div>
314
- )
315
- }
316
- )
317
- ChartLegendContent.displayName = "ChartLegend"
318
 
319
  // Helper to extract item config from a payload.
320
  function getPayloadConfigFromPayload(
321
  config: ChartConfig,
322
  payload: unknown,
323
- key: string
324
  ) {
325
  if (typeof payload !== "object" || payload === null) {
326
- return undefined
327
  }
328
 
329
  const payloadPayload =
@@ -331,15 +331,15 @@ function getPayloadConfigFromPayload(
331
  typeof payload.payload === "object" &&
332
  payload.payload !== null
333
  ? payload.payload
334
- : undefined
335
 
336
- let configLabelKey: string = key
337
 
338
  if (
339
  key in payload &&
340
  typeof payload[key as keyof typeof payload] === "string"
341
  ) {
342
- configLabelKey = payload[key as keyof typeof payload] as string
343
  } else if (
344
  payloadPayload &&
345
  key in payloadPayload &&
@@ -347,12 +347,12 @@ function getPayloadConfigFromPayload(
347
  ) {
348
  configLabelKey = payloadPayload[
349
  key as keyof typeof payloadPayload
350
- ] as string
351
  }
352
 
353
  return configLabelKey in config
354
  ? config[configLabelKey]
355
- : config[key as keyof typeof config]
356
  }
357
 
358
  export {
@@ -362,4 +362,4 @@ export {
362
  ChartLegend,
363
  ChartLegendContent,
364
  ChartStyle,
365
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as RechartsPrimitive from "recharts";
5
 
6
+ import { cn } from "@/lib/utils";
7
 
8
  // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const;
10
 
11
  export type ChartConfig = {
12
  [k in string]: {
13
+ label?: React.ReactNode;
14
+ icon?: React.ComponentType;
15
  } & (
16
  | { color?: string; theme?: never }
17
  | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
+ );
19
+ };
20
 
21
  type ChartContextProps = {
22
+ config: ChartConfig;
23
+ };
24
 
25
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
26
 
27
  function useChart() {
28
+ const context = React.useContext(ChartContext);
29
 
30
  if (!context) {
31
+ throw new Error("useChart must be used within a <ChartContainer />");
32
  }
33
 
34
+ return context;
35
  }
36
 
37
  const ChartContainer = React.forwardRef<
38
  HTMLDivElement,
39
  React.ComponentProps<"div"> & {
40
+ config: ChartConfig;
41
  children: React.ComponentProps<
42
  typeof RechartsPrimitive.ResponsiveContainer
43
+ >["children"];
44
  }
45
  >(({ id, className, children, config, ...props }, ref) => {
46
+ const uniqueId = React.useId();
47
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
48
 
49
  return (
50
  <ChartContext.Provider value={{ config }}>
 
53
  ref={ref}
54
  className={cn(
55
  "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
56
+ className,
57
  )}
58
  {...props}
59
  >
 
63
  </RechartsPrimitive.ResponsiveContainer>
64
  </div>
65
  </ChartContext.Provider>
66
+ );
67
+ });
68
+ ChartContainer.displayName = "Chart";
69
 
70
  const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71
  const colorConfig = Object.entries(config).filter(
72
+ ([_, config]) => config.theme || config.color,
73
+ );
74
 
75
  if (!colorConfig.length) {
76
+ return null;
77
  }
78
 
79
  return (
 
87
  .map(([key, itemConfig]) => {
88
  const color =
89
  itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
90
+ itemConfig.color;
91
+ return color ? ` --color-${key}: ${color};` : null;
92
  })
93
  .join("\n")}
94
  }
95
+ `,
96
  )
97
  .join("\n"),
98
  }}
99
  />
100
+ );
101
+ };
102
 
103
+ const ChartTooltip = RechartsPrimitive.Tooltip;
104
 
105
  const ChartTooltipContent = React.forwardRef<
106
  HTMLDivElement,
107
  React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
108
  React.ComponentProps<"div"> & {
109
+ hideLabel?: boolean;
110
+ hideIndicator?: boolean;
111
+ indicator?: "line" | "dot" | "dashed";
112
+ nameKey?: string;
113
+ labelKey?: string;
114
  }
115
  >(
116
  (
 
129
  nameKey,
130
  labelKey,
131
  },
132
+ ref,
133
  ) => {
134
+ const { config } = useChart();
135
 
136
  const tooltipLabel = React.useMemo(() => {
137
  if (hideLabel || !payload?.length) {
138
+ return null;
139
  }
140
 
141
+ const [item] = payload;
142
+ const key = `${labelKey || item.dataKey || item.name || "value"}`;
143
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
144
  const value =
145
  !labelKey && typeof label === "string"
146
  ? config[label as keyof typeof config]?.label || label
147
+ : itemConfig?.label;
148
 
149
  if (labelFormatter) {
150
  return (
151
  <div className={cn("font-medium", labelClassName)}>
152
  {labelFormatter(value, payload)}
153
  </div>
154
+ );
155
  }
156
 
157
  if (!value) {
158
+ return null;
159
  }
160
 
161
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
162
  }, [
163
  label,
164
  labelFormatter,
 
167
  labelClassName,
168
  config,
169
  labelKey,
170
+ ]);
171
 
172
  if (!active || !payload?.length) {
173
+ return null;
174
  }
175
 
176
+ const nestLabel = payload.length === 1 && indicator !== "dot";
177
 
178
  return (
179
  <div
180
  ref={ref}
181
  className={cn(
182
  "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
183
+ className,
184
  )}
185
  >
186
  {!nestLabel ? tooltipLabel : null}
187
  <div className="grid gap-1.5">
188
  {payload.map((item, index) => {
189
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
190
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
191
+ const indicatorColor = color || item.payload.fill || item.color;
192
 
193
  return (
194
  <div
195
  key={item.dataKey}
196
  className={cn(
197
  "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
198
+ indicator === "dot" && "items-center",
199
  )}
200
  >
201
  {formatter && item?.value !== undefined && item.name ? (
 
215
  "w-0 border-[1.5px] border-dashed bg-transparent":
216
  indicator === "dashed",
217
  "my-0.5": nestLabel && indicator === "dashed",
218
+ },
219
  )}
220
  style={
221
  {
 
229
  <div
230
  className={cn(
231
  "flex flex-1 justify-between leading-none",
232
+ nestLabel ? "items-end" : "items-center",
233
  )}
234
  >
235
  <div className="grid gap-1.5">
 
247
  </>
248
  )}
249
  </div>
250
+ );
251
  })}
252
  </div>
253
  </div>
254
+ );
255
+ },
256
+ );
257
+ ChartTooltipContent.displayName = "ChartTooltip";
258
 
259
+ const ChartLegend = RechartsPrimitive.Legend;
260
 
261
  const ChartLegendContent = React.forwardRef<
262
  HTMLDivElement,
263
  React.ComponentProps<"div"> &
264
  Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
265
+ hideIcon?: boolean;
266
+ nameKey?: string;
267
  }
268
  >(
269
  (
270
  { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
271
+ ref,
272
  ) => {
273
+ const { config } = useChart();
274
 
275
  if (!payload?.length) {
276
+ return null;
277
  }
278
 
279
  return (
 
282
  className={cn(
283
  "flex items-center justify-center gap-4",
284
  verticalAlign === "top" ? "pb-3" : "pt-3",
285
+ className,
286
  )}
287
  >
288
  {payload.map((item) => {
289
+ const key = `${nameKey || item.dataKey || "value"}`;
290
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
291
 
292
  return (
293
  <div
294
  key={item.value}
295
  className={cn(
296
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
297
  )}
298
  >
299
  {itemConfig?.icon && !hideIcon ? (
 
308
  )}
309
  {itemConfig?.label}
310
  </div>
311
+ );
312
  })}
313
  </div>
314
+ );
315
+ },
316
+ );
317
+ ChartLegendContent.displayName = "ChartLegend";
318
 
319
  // Helper to extract item config from a payload.
320
  function getPayloadConfigFromPayload(
321
  config: ChartConfig,
322
  payload: unknown,
323
+ key: string,
324
  ) {
325
  if (typeof payload !== "object" || payload === null) {
326
+ return undefined;
327
  }
328
 
329
  const payloadPayload =
 
331
  typeof payload.payload === "object" &&
332
  payload.payload !== null
333
  ? payload.payload
334
+ : undefined;
335
 
336
+ let configLabelKey: string = key;
337
 
338
  if (
339
  key in payload &&
340
  typeof payload[key as keyof typeof payload] === "string"
341
  ) {
342
+ configLabelKey = payload[key as keyof typeof payload] as string;
343
  } else if (
344
  payloadPayload &&
345
  key in payloadPayload &&
 
347
  ) {
348
  configLabelKey = payloadPayload[
349
  key as keyof typeof payloadPayload
350
+ ] as string;
351
  }
352
 
353
  return configLabelKey in config
354
  ? config[configLabelKey]
355
+ : config[key as keyof typeof config];
356
  }
357
 
358
  export {
 
362
  ChartLegend,
363
  ChartLegendContent,
364
  ChartStyle,
365
+ };
src/components/ui/checkbox.tsx CHANGED
@@ -1,10 +1,10 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5
- import { Check } from "lucide-react"
6
 
7
- import { cn } from "@/lib/utils"
8
 
9
  const Checkbox = React.forwardRef<
10
  React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -14,7 +14,7 @@ const Checkbox = React.forwardRef<
14
  ref={ref}
15
  className={cn(
16
  "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
17
- className
18
  )}
19
  {...props}
20
  >
@@ -24,7 +24,7 @@ const Checkbox = React.forwardRef<
24
  <Check className="h-4 w-4" />
25
  </CheckboxPrimitive.Indicator>
26
  </CheckboxPrimitive.Root>
27
- ))
28
- Checkbox.displayName = CheckboxPrimitive.Root.displayName
29
 
30
- export { Checkbox }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
+ import { Check } from "lucide-react";
6
 
7
+ import { cn } from "@/lib/utils";
8
 
9
  const Checkbox = React.forwardRef<
10
  React.ElementRef<typeof CheckboxPrimitive.Root>,
 
14
  ref={ref}
15
  className={cn(
16
  "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
17
+ className,
18
  )}
19
  {...props}
20
  >
 
24
  <Check className="h-4 w-4" />
25
  </CheckboxPrimitive.Indicator>
26
  </CheckboxPrimitive.Root>
27
+ ));
28
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29
 
30
+ export { Checkbox };
src/components/ui/collapsible.tsx CHANGED
@@ -1,11 +1,11 @@
1
- "use client"
2
 
3
- import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4
 
5
- const Collapsible = CollapsiblePrimitive.Root
6
 
7
- const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8
 
9
- const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10
 
11
- export { Collapsible, CollapsibleTrigger, CollapsibleContent }
 
1
+ "use client";
2
 
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
 
5
+ const Collapsible = CollapsiblePrimitive.Root;
6
 
7
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8
 
9
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10
 
11
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
src/components/ui/command.tsx CHANGED
@@ -1,12 +1,12 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import { type DialogProps } from "@radix-ui/react-dialog"
5
- import { Command as CommandPrimitive } from "cmdk"
6
- import { Search } from "lucide-react"
7
 
8
- import { cn } from "@/lib/utils"
9
- import { Dialog, DialogContent } from "@/components/ui/dialog"
10
 
11
  const Command = React.forwardRef<
12
  React.ElementRef<typeof CommandPrimitive>,
@@ -16,12 +16,12 @@ const Command = React.forwardRef<
16
  ref={ref}
17
  className={cn(
18
  "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19
- className
20
  )}
21
  {...props}
22
  />
23
- ))
24
- Command.displayName = CommandPrimitive.displayName
25
 
26
  const CommandDialog = ({ children, ...props }: DialogProps) => {
27
  return (
@@ -32,8 +32,8 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
32
  </Command>
33
  </DialogContent>
34
  </Dialog>
35
- )
36
- }
37
 
38
  const CommandInput = React.forwardRef<
39
  React.ElementRef<typeof CommandPrimitive.Input>,
@@ -45,14 +45,14 @@ const CommandInput = React.forwardRef<
45
  ref={ref}
46
  className={cn(
47
  "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
- className
49
  )}
50
  {...props}
51
  />
52
  </div>
53
- ))
54
 
55
- CommandInput.displayName = CommandPrimitive.Input.displayName
56
 
57
  const CommandList = React.forwardRef<
58
  React.ElementRef<typeof CommandPrimitive.List>,
@@ -63,9 +63,9 @@ const CommandList = React.forwardRef<
63
  className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
  {...props}
65
  />
66
- ))
67
 
68
- CommandList.displayName = CommandPrimitive.List.displayName
69
 
70
  const CommandEmpty = React.forwardRef<
71
  React.ElementRef<typeof CommandPrimitive.Empty>,
@@ -76,9 +76,9 @@ const CommandEmpty = React.forwardRef<
76
  className="py-6 text-center text-sm"
77
  {...props}
78
  />
79
- ))
80
 
81
- CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82
 
83
  const CommandGroup = React.forwardRef<
84
  React.ElementRef<typeof CommandPrimitive.Group>,
@@ -88,13 +88,13 @@ const CommandGroup = React.forwardRef<
88
  ref={ref}
89
  className={cn(
90
  "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
91
- className
92
  )}
93
  {...props}
94
  />
95
- ))
96
 
97
- CommandGroup.displayName = CommandPrimitive.Group.displayName
98
 
99
  const CommandSeparator = React.forwardRef<
100
  React.ElementRef<typeof CommandPrimitive.Separator>,
@@ -105,8 +105,8 @@ const CommandSeparator = React.forwardRef<
105
  className={cn("-mx-1 h-px bg-border", className)}
106
  {...props}
107
  />
108
- ))
109
- CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110
 
111
  const CommandItem = React.forwardRef<
112
  React.ElementRef<typeof CommandPrimitive.Item>,
@@ -116,13 +116,13 @@ const CommandItem = React.forwardRef<
116
  ref={ref}
117
  className={cn(
118
  "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
119
- className
120
  )}
121
  {...props}
122
  />
123
- ))
124
 
125
- CommandItem.displayName = CommandPrimitive.Item.displayName
126
 
127
  const CommandShortcut = ({
128
  className,
@@ -132,13 +132,13 @@ const CommandShortcut = ({
132
  <span
133
  className={cn(
134
  "ml-auto text-xs tracking-widest text-muted-foreground",
135
- className
136
  )}
137
  {...props}
138
  />
139
- )
140
- }
141
- CommandShortcut.displayName = "CommandShortcut"
142
 
143
  export {
144
  Command,
@@ -150,4 +150,4 @@ export {
150
  CommandItem,
151
  CommandShortcut,
152
  CommandSeparator,
153
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import { type DialogProps } from "@radix-ui/react-dialog";
5
+ import { Command as CommandPrimitive } from "cmdk";
6
+ import { Search } from "lucide-react";
7
 
8
+ import { cn } from "@/lib/utils";
9
+ import { Dialog, DialogContent } from "@/components/ui/dialog";
10
 
11
  const Command = React.forwardRef<
12
  React.ElementRef<typeof CommandPrimitive>,
 
16
  ref={ref}
17
  className={cn(
18
  "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19
+ className,
20
  )}
21
  {...props}
22
  />
23
+ ));
24
+ Command.displayName = CommandPrimitive.displayName;
25
 
26
  const CommandDialog = ({ children, ...props }: DialogProps) => {
27
  return (
 
32
  </Command>
33
  </DialogContent>
34
  </Dialog>
35
+ );
36
+ };
37
 
38
  const CommandInput = React.forwardRef<
39
  React.ElementRef<typeof CommandPrimitive.Input>,
 
45
  ref={ref}
46
  className={cn(
47
  "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
+ className,
49
  )}
50
  {...props}
51
  />
52
  </div>
53
+ ));
54
 
55
+ CommandInput.displayName = CommandPrimitive.Input.displayName;
56
 
57
  const CommandList = React.forwardRef<
58
  React.ElementRef<typeof CommandPrimitive.List>,
 
63
  className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
  {...props}
65
  />
66
+ ));
67
 
68
+ CommandList.displayName = CommandPrimitive.List.displayName;
69
 
70
  const CommandEmpty = React.forwardRef<
71
  React.ElementRef<typeof CommandPrimitive.Empty>,
 
76
  className="py-6 text-center text-sm"
77
  {...props}
78
  />
79
+ ));
80
 
81
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
82
 
83
  const CommandGroup = React.forwardRef<
84
  React.ElementRef<typeof CommandPrimitive.Group>,
 
88
  ref={ref}
89
  className={cn(
90
  "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
91
+ className,
92
  )}
93
  {...props}
94
  />
95
+ ));
96
 
97
+ CommandGroup.displayName = CommandPrimitive.Group.displayName;
98
 
99
  const CommandSeparator = React.forwardRef<
100
  React.ElementRef<typeof CommandPrimitive.Separator>,
 
105
  className={cn("-mx-1 h-px bg-border", className)}
106
  {...props}
107
  />
108
+ ));
109
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
110
 
111
  const CommandItem = React.forwardRef<
112
  React.ElementRef<typeof CommandPrimitive.Item>,
 
116
  ref={ref}
117
  className={cn(
118
  "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
119
+ className,
120
  )}
121
  {...props}
122
  />
123
+ ));
124
 
125
+ CommandItem.displayName = CommandPrimitive.Item.displayName;
126
 
127
  const CommandShortcut = ({
128
  className,
 
132
  <span
133
  className={cn(
134
  "ml-auto text-xs tracking-widest text-muted-foreground",
135
+ className,
136
  )}
137
  {...props}
138
  />
139
+ );
140
+ };
141
+ CommandShortcut.displayName = "CommandShortcut";
142
 
143
  export {
144
  Command,
 
150
  CommandItem,
151
  CommandShortcut,
152
  CommandSeparator,
153
+ };
src/components/ui/context-menu.tsx CHANGED
@@ -1,27 +1,27 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
5
- import { Check, ChevronRight, Circle } from "lucide-react"
6
 
7
- import { cn } from "@/lib/utils"
8
 
9
- const ContextMenu = ContextMenuPrimitive.Root
10
 
11
- const ContextMenuTrigger = ContextMenuPrimitive.Trigger
12
 
13
- const ContextMenuGroup = ContextMenuPrimitive.Group
14
 
15
- const ContextMenuPortal = ContextMenuPrimitive.Portal
16
 
17
- const ContextMenuSub = ContextMenuPrimitive.Sub
18
 
19
- const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
20
 
21
  const ContextMenuSubTrigger = React.forwardRef<
22
  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
23
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
24
- inset?: boolean
25
  }
26
  >(({ className, inset, children, ...props }, ref) => (
27
  <ContextMenuPrimitive.SubTrigger
@@ -29,15 +29,15 @@ const ContextMenuSubTrigger = React.forwardRef<
29
  className={cn(
30
  "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
31
  inset && "pl-8",
32
- className
33
  )}
34
  {...props}
35
  >
36
  {children}
37
  <ChevronRight className="ml-auto h-4 w-4" />
38
  </ContextMenuPrimitive.SubTrigger>
39
- ))
40
- ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
41
 
42
  const ContextMenuSubContent = React.forwardRef<
43
  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
@@ -47,12 +47,12 @@ const ContextMenuSubContent = React.forwardRef<
47
  ref={ref}
48
  className={cn(
49
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
50
- className
51
  )}
52
  {...props}
53
  />
54
- ))
55
- ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
56
 
57
  const ContextMenuContent = React.forwardRef<
58
  React.ElementRef<typeof ContextMenuPrimitive.Content>,
@@ -63,18 +63,18 @@ const ContextMenuContent = React.forwardRef<
63
  ref={ref}
64
  className={cn(
65
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
66
- className
67
  )}
68
  {...props}
69
  />
70
  </ContextMenuPrimitive.Portal>
71
- ))
72
- ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
73
 
74
  const ContextMenuItem = React.forwardRef<
75
  React.ElementRef<typeof ContextMenuPrimitive.Item>,
76
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
77
- inset?: boolean
78
  }
79
  >(({ className, inset, ...props }, ref) => (
80
  <ContextMenuPrimitive.Item
@@ -82,12 +82,12 @@ const ContextMenuItem = React.forwardRef<
82
  className={cn(
83
  "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
84
  inset && "pl-8",
85
- className
86
  )}
87
  {...props}
88
  />
89
- ))
90
- ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
91
 
92
  const ContextMenuCheckboxItem = React.forwardRef<
93
  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
@@ -97,7 +97,7 @@ const ContextMenuCheckboxItem = React.forwardRef<
97
  ref={ref}
98
  className={cn(
99
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
100
- className
101
  )}
102
  checked={checked}
103
  {...props}
@@ -109,9 +109,9 @@ const ContextMenuCheckboxItem = React.forwardRef<
109
  </span>
110
  {children}
111
  </ContextMenuPrimitive.CheckboxItem>
112
- ))
113
  ContextMenuCheckboxItem.displayName =
114
- ContextMenuPrimitive.CheckboxItem.displayName
115
 
116
  const ContextMenuRadioItem = React.forwardRef<
117
  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
@@ -121,7 +121,7 @@ const ContextMenuRadioItem = React.forwardRef<
121
  ref={ref}
122
  className={cn(
123
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
124
- className
125
  )}
126
  {...props}
127
  >
@@ -132,13 +132,13 @@ const ContextMenuRadioItem = React.forwardRef<
132
  </span>
133
  {children}
134
  </ContextMenuPrimitive.RadioItem>
135
- ))
136
- ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
137
 
138
  const ContextMenuLabel = React.forwardRef<
139
  React.ElementRef<typeof ContextMenuPrimitive.Label>,
140
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
141
- inset?: boolean
142
  }
143
  >(({ className, inset, ...props }, ref) => (
144
  <ContextMenuPrimitive.Label
@@ -146,12 +146,12 @@ const ContextMenuLabel = React.forwardRef<
146
  className={cn(
147
  "px-2 py-1.5 text-sm font-semibold text-foreground",
148
  inset && "pl-8",
149
- className
150
  )}
151
  {...props}
152
  />
153
- ))
154
- ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
155
 
156
  const ContextMenuSeparator = React.forwardRef<
157
  React.ElementRef<typeof ContextMenuPrimitive.Separator>,
@@ -162,8 +162,8 @@ const ContextMenuSeparator = React.forwardRef<
162
  className={cn("-mx-1 my-1 h-px bg-border", className)}
163
  {...props}
164
  />
165
- ))
166
- ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
167
 
168
  const ContextMenuShortcut = ({
169
  className,
@@ -173,13 +173,13 @@ const ContextMenuShortcut = ({
173
  <span
174
  className={cn(
175
  "ml-auto text-xs tracking-widest text-muted-foreground",
176
- className
177
  )}
178
  {...props}
179
  />
180
- )
181
- }
182
- ContextMenuShortcut.displayName = "ContextMenuShortcut"
183
 
184
  export {
185
  ContextMenu,
@@ -197,4 +197,4 @@ export {
197
  ContextMenuSubContent,
198
  ContextMenuSubTrigger,
199
  ContextMenuRadioGroup,
200
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
 
7
+ import { cn } from "@/lib/utils";
8
 
9
+ const ContextMenu = ContextMenuPrimitive.Root;
10
 
11
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
12
 
13
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
14
 
15
+ const ContextMenuPortal = ContextMenuPrimitive.Portal;
16
 
17
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
18
 
19
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
20
 
21
  const ContextMenuSubTrigger = React.forwardRef<
22
  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
23
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
24
+ inset?: boolean;
25
  }
26
  >(({ className, inset, children, ...props }, ref) => (
27
  <ContextMenuPrimitive.SubTrigger
 
29
  className={cn(
30
  "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
31
  inset && "pl-8",
32
+ className,
33
  )}
34
  {...props}
35
  >
36
  {children}
37
  <ChevronRight className="ml-auto h-4 w-4" />
38
  </ContextMenuPrimitive.SubTrigger>
39
+ ));
40
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
41
 
42
  const ContextMenuSubContent = React.forwardRef<
43
  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
 
47
  ref={ref}
48
  className={cn(
49
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
50
+ className,
51
  )}
52
  {...props}
53
  />
54
+ ));
55
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
56
 
57
  const ContextMenuContent = React.forwardRef<
58
  React.ElementRef<typeof ContextMenuPrimitive.Content>,
 
63
  ref={ref}
64
  className={cn(
65
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
66
+ className,
67
  )}
68
  {...props}
69
  />
70
  </ContextMenuPrimitive.Portal>
71
+ ));
72
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
73
 
74
  const ContextMenuItem = React.forwardRef<
75
  React.ElementRef<typeof ContextMenuPrimitive.Item>,
76
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
77
+ inset?: boolean;
78
  }
79
  >(({ className, inset, ...props }, ref) => (
80
  <ContextMenuPrimitive.Item
 
82
  className={cn(
83
  "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
84
  inset && "pl-8",
85
+ className,
86
  )}
87
  {...props}
88
  />
89
+ ));
90
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
91
 
92
  const ContextMenuCheckboxItem = React.forwardRef<
93
  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
 
97
  ref={ref}
98
  className={cn(
99
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
100
+ className,
101
  )}
102
  checked={checked}
103
  {...props}
 
109
  </span>
110
  {children}
111
  </ContextMenuPrimitive.CheckboxItem>
112
+ ));
113
  ContextMenuCheckboxItem.displayName =
114
+ ContextMenuPrimitive.CheckboxItem.displayName;
115
 
116
  const ContextMenuRadioItem = React.forwardRef<
117
  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
 
121
  ref={ref}
122
  className={cn(
123
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
124
+ className,
125
  )}
126
  {...props}
127
  >
 
132
  </span>
133
  {children}
134
  </ContextMenuPrimitive.RadioItem>
135
+ ));
136
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
137
 
138
  const ContextMenuLabel = React.forwardRef<
139
  React.ElementRef<typeof ContextMenuPrimitive.Label>,
140
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
141
+ inset?: boolean;
142
  }
143
  >(({ className, inset, ...props }, ref) => (
144
  <ContextMenuPrimitive.Label
 
146
  className={cn(
147
  "px-2 py-1.5 text-sm font-semibold text-foreground",
148
  inset && "pl-8",
149
+ className,
150
  )}
151
  {...props}
152
  />
153
+ ));
154
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
155
 
156
  const ContextMenuSeparator = React.forwardRef<
157
  React.ElementRef<typeof ContextMenuPrimitive.Separator>,
 
162
  className={cn("-mx-1 my-1 h-px bg-border", className)}
163
  {...props}
164
  />
165
+ ));
166
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
167
 
168
  const ContextMenuShortcut = ({
169
  className,
 
173
  <span
174
  className={cn(
175
  "ml-auto text-xs tracking-widest text-muted-foreground",
176
+ className,
177
  )}
178
  {...props}
179
  />
180
+ );
181
+ };
182
+ ContextMenuShortcut.displayName = "ContextMenuShortcut";
183
 
184
  export {
185
  ContextMenu,
 
197
  ContextMenuSubContent,
198
  ContextMenuSubTrigger,
199
  ContextMenuRadioGroup,
200
+ };
src/components/ui/dialog.tsx CHANGED
@@ -1,18 +1,18 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as DialogPrimitive from "@radix-ui/react-dialog"
5
- import { X } from "lucide-react"
6
 
7
- import { cn } from "@/lib/utils"
8
 
9
- const Dialog = DialogPrimitive.Root
10
 
11
- const DialogTrigger = DialogPrimitive.Trigger
12
 
13
- const DialogPortal = DialogPrimitive.Portal
14
 
15
- const DialogClose = DialogPrimitive.Close
16
 
17
  const DialogOverlay = React.forwardRef<
18
  React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -22,12 +22,12 @@ const DialogOverlay = React.forwardRef<
22
  ref={ref}
23
  className={cn(
24
  "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
- className
26
  )}
27
  {...props}
28
  />
29
- ))
30
- DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31
 
32
  const DialogContent = React.forwardRef<
33
  React.ElementRef<typeof DialogPrimitive.Content>,
@@ -39,7 +39,7 @@ const DialogContent = React.forwardRef<
39
  ref={ref}
40
  className={cn(
41
  "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
42
- className
43
  )}
44
  {...props}
45
  >
@@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
50
  </DialogPrimitive.Close>
51
  </DialogPrimitive.Content>
52
  </DialogPortal>
53
- ))
54
- DialogContent.displayName = DialogPrimitive.Content.displayName
55
 
56
  const DialogHeader = ({
57
  className,
@@ -60,12 +60,12 @@ const DialogHeader = ({
60
  <div
61
  className={cn(
62
  "flex flex-col space-y-1.5 text-center sm:text-left",
63
- className
64
  )}
65
  {...props}
66
  />
67
- )
68
- DialogHeader.displayName = "DialogHeader"
69
 
70
  const DialogFooter = ({
71
  className,
@@ -74,12 +74,12 @@ const DialogFooter = ({
74
  <div
75
  className={cn(
76
  "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77
- className
78
  )}
79
  {...props}
80
  />
81
- )
82
- DialogFooter.displayName = "DialogFooter"
83
 
84
  const DialogTitle = React.forwardRef<
85
  React.ElementRef<typeof DialogPrimitive.Title>,
@@ -89,12 +89,12 @@ const DialogTitle = React.forwardRef<
89
  ref={ref}
90
  className={cn(
91
  "text-lg font-semibold leading-none tracking-tight",
92
- className
93
  )}
94
  {...props}
95
  />
96
- ))
97
- DialogTitle.displayName = DialogPrimitive.Title.displayName
98
 
99
  const DialogDescription = React.forwardRef<
100
  React.ElementRef<typeof DialogPrimitive.Description>,
@@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
105
  className={cn("text-sm text-muted-foreground", className)}
106
  {...props}
107
  />
108
- ))
109
- DialogDescription.displayName = DialogPrimitive.Description.displayName
110
 
111
  export {
112
  Dialog,
@@ -119,4 +119,4 @@ export {
119
  DialogFooter,
120
  DialogTitle,
121
  DialogDescription,
122
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { X } from "lucide-react";
6
 
7
+ import { cn } from "@/lib/utils";
8
 
9
+ const Dialog = DialogPrimitive.Root;
10
 
11
+ const DialogTrigger = DialogPrimitive.Trigger;
12
 
13
+ const DialogPortal = DialogPrimitive.Portal;
14
 
15
+ const DialogClose = DialogPrimitive.Close;
16
 
17
  const DialogOverlay = React.forwardRef<
18
  React.ElementRef<typeof DialogPrimitive.Overlay>,
 
22
  ref={ref}
23
  className={cn(
24
  "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className,
26
  )}
27
  {...props}
28
  />
29
+ ));
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31
 
32
  const DialogContent = React.forwardRef<
33
  React.ElementRef<typeof DialogPrimitive.Content>,
 
39
  ref={ref}
40
  className={cn(
41
  "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
42
+ className,
43
  )}
44
  {...props}
45
  >
 
50
  </DialogPrimitive.Close>
51
  </DialogPrimitive.Content>
52
  </DialogPortal>
53
+ ));
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
55
 
56
  const DialogHeader = ({
57
  className,
 
60
  <div
61
  className={cn(
62
  "flex flex-col space-y-1.5 text-center sm:text-left",
63
+ className,
64
  )}
65
  {...props}
66
  />
67
+ );
68
+ DialogHeader.displayName = "DialogHeader";
69
 
70
  const DialogFooter = ({
71
  className,
 
74
  <div
75
  className={cn(
76
  "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77
+ className,
78
  )}
79
  {...props}
80
  />
81
+ );
82
+ DialogFooter.displayName = "DialogFooter";
83
 
84
  const DialogTitle = React.forwardRef<
85
  React.ElementRef<typeof DialogPrimitive.Title>,
 
89
  ref={ref}
90
  className={cn(
91
  "text-lg font-semibold leading-none tracking-tight",
92
+ className,
93
  )}
94
  {...props}
95
  />
96
+ ));
97
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
98
 
99
  const DialogDescription = React.forwardRef<
100
  React.ElementRef<typeof DialogPrimitive.Description>,
 
105
  className={cn("text-sm text-muted-foreground", className)}
106
  {...props}
107
  />
108
+ ));
109
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
110
 
111
  export {
112
  Dialog,
 
119
  DialogFooter,
120
  DialogTitle,
121
  DialogDescription,
122
+ };
src/components/ui/drawer.tsx CHANGED
@@ -1,9 +1,9 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import { Drawer as DrawerPrimitive } from "vaul"
5
 
6
- import { cn } from "@/lib/utils"
7
 
8
  const Drawer = ({
9
  shouldScaleBackground = true,
@@ -13,14 +13,14 @@ const Drawer = ({
13
  shouldScaleBackground={shouldScaleBackground}
14
  {...props}
15
  />
16
- )
17
- Drawer.displayName = "Drawer"
18
 
19
- const DrawerTrigger = DrawerPrimitive.Trigger
20
 
21
- const DrawerPortal = DrawerPrimitive.Portal
22
 
23
- const DrawerClose = DrawerPrimitive.Close
24
 
25
  const DrawerOverlay = React.forwardRef<
26
  React.ElementRef<typeof DrawerPrimitive.Overlay>,
@@ -31,8 +31,8 @@ const DrawerOverlay = React.forwardRef<
31
  className={cn("fixed inset-0 z-50 bg-black/80", className)}
32
  {...props}
33
  />
34
- ))
35
- DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36
 
37
  const DrawerContent = React.forwardRef<
38
  React.ElementRef<typeof DrawerPrimitive.Content>,
@@ -44,7 +44,7 @@ const DrawerContent = React.forwardRef<
44
  ref={ref}
45
  className={cn(
46
  "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
47
- className
48
  )}
49
  {...props}
50
  >
@@ -52,8 +52,8 @@ const DrawerContent = React.forwardRef<
52
  {children}
53
  </DrawerPrimitive.Content>
54
  </DrawerPortal>
55
- ))
56
- DrawerContent.displayName = "DrawerContent"
57
 
58
  const DrawerHeader = ({
59
  className,
@@ -63,8 +63,8 @@ const DrawerHeader = ({
63
  className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
64
  {...props}
65
  />
66
- )
67
- DrawerHeader.displayName = "DrawerHeader"
68
 
69
  const DrawerFooter = ({
70
  className,
@@ -74,8 +74,8 @@ const DrawerFooter = ({
74
  className={cn("mt-auto flex flex-col gap-2 p-4", className)}
75
  {...props}
76
  />
77
- )
78
- DrawerFooter.displayName = "DrawerFooter"
79
 
80
  const DrawerTitle = React.forwardRef<
81
  React.ElementRef<typeof DrawerPrimitive.Title>,
@@ -85,12 +85,12 @@ const DrawerTitle = React.forwardRef<
85
  ref={ref}
86
  className={cn(
87
  "text-lg font-semibold leading-none tracking-tight",
88
- className
89
  )}
90
  {...props}
91
  />
92
- ))
93
- DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94
 
95
  const DrawerDescription = React.forwardRef<
96
  React.ElementRef<typeof DrawerPrimitive.Description>,
@@ -101,8 +101,8 @@ const DrawerDescription = React.forwardRef<
101
  className={cn("text-sm text-muted-foreground", className)}
102
  {...props}
103
  />
104
- ))
105
- DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106
 
107
  export {
108
  Drawer,
@@ -115,4 +115,4 @@ export {
115
  DrawerFooter,
116
  DrawerTitle,
117
  DrawerDescription,
118
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import { Drawer as DrawerPrimitive } from "vaul";
5
 
6
+ import { cn } from "@/lib/utils";
7
 
8
  const Drawer = ({
9
  shouldScaleBackground = true,
 
13
  shouldScaleBackground={shouldScaleBackground}
14
  {...props}
15
  />
16
+ );
17
+ Drawer.displayName = "Drawer";
18
 
19
+ const DrawerTrigger = DrawerPrimitive.Trigger;
20
 
21
+ const DrawerPortal = DrawerPrimitive.Portal;
22
 
23
+ const DrawerClose = DrawerPrimitive.Close;
24
 
25
  const DrawerOverlay = React.forwardRef<
26
  React.ElementRef<typeof DrawerPrimitive.Overlay>,
 
31
  className={cn("fixed inset-0 z-50 bg-black/80", className)}
32
  {...props}
33
  />
34
+ ));
35
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36
 
37
  const DrawerContent = React.forwardRef<
38
  React.ElementRef<typeof DrawerPrimitive.Content>,
 
44
  ref={ref}
45
  className={cn(
46
  "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
47
+ className,
48
  )}
49
  {...props}
50
  >
 
52
  {children}
53
  </DrawerPrimitive.Content>
54
  </DrawerPortal>
55
+ ));
56
+ DrawerContent.displayName = "DrawerContent";
57
 
58
  const DrawerHeader = ({
59
  className,
 
63
  className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
64
  {...props}
65
  />
66
+ );
67
+ DrawerHeader.displayName = "DrawerHeader";
68
 
69
  const DrawerFooter = ({
70
  className,
 
74
  className={cn("mt-auto flex flex-col gap-2 p-4", className)}
75
  {...props}
76
  />
77
+ );
78
+ DrawerFooter.displayName = "DrawerFooter";
79
 
80
  const DrawerTitle = React.forwardRef<
81
  React.ElementRef<typeof DrawerPrimitive.Title>,
 
85
  ref={ref}
86
  className={cn(
87
  "text-lg font-semibold leading-none tracking-tight",
88
+ className,
89
  )}
90
  {...props}
91
  />
92
+ ));
93
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94
 
95
  const DrawerDescription = React.forwardRef<
96
  React.ElementRef<typeof DrawerPrimitive.Description>,
 
101
  className={cn("text-sm text-muted-foreground", className)}
102
  {...props}
103
  />
104
+ ));
105
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106
 
107
  export {
108
  Drawer,
 
115
  DrawerFooter,
116
  DrawerTitle,
117
  DrawerDescription,
118
+ };
src/components/ui/dropdown-menu.tsx CHANGED
@@ -1,27 +1,27 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
- import { Check, ChevronRight, Circle } from "lucide-react"
6
 
7
- import { cn } from "@/lib/utils"
8
 
9
- const DropdownMenu = DropdownMenuPrimitive.Root
10
 
11
- const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12
 
13
- const DropdownMenuGroup = DropdownMenuPrimitive.Group
14
 
15
- const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16
 
17
- const DropdownMenuSub = DropdownMenuPrimitive.Sub
18
 
19
- const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20
 
21
  const DropdownMenuSubTrigger = React.forwardRef<
22
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
- inset?: boolean
25
  }
26
  >(({ className, inset, children, ...props }, ref) => (
27
  <DropdownMenuPrimitive.SubTrigger
@@ -29,16 +29,16 @@ const DropdownMenuSubTrigger = React.forwardRef<
29
  className={cn(
30
  "flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31
  inset && "pl-8",
32
- className
33
  )}
34
  {...props}
35
  >
36
  {children}
37
  <ChevronRight className="ml-auto" />
38
  </DropdownMenuPrimitive.SubTrigger>
39
- ))
40
  DropdownMenuSubTrigger.displayName =
41
- DropdownMenuPrimitive.SubTrigger.displayName
42
 
43
  const DropdownMenuSubContent = React.forwardRef<
44
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -48,13 +48,13 @@ const DropdownMenuSubContent = React.forwardRef<
48
  ref={ref}
49
  className={cn(
50
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
- className
52
  )}
53
  {...props}
54
  />
55
- ))
56
  DropdownMenuSubContent.displayName =
57
- DropdownMenuPrimitive.SubContent.displayName
58
 
59
  const DropdownMenuContent = React.forwardRef<
60
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -66,18 +66,18 @@ const DropdownMenuContent = React.forwardRef<
66
  sideOffset={sideOffset}
67
  className={cn(
68
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
69
- className
70
  )}
71
  {...props}
72
  />
73
  </DropdownMenuPrimitive.Portal>
74
- ))
75
- DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76
 
77
  const DropdownMenuItem = React.forwardRef<
78
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
79
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
80
- inset?: boolean
81
  }
82
  >(({ className, inset, ...props }, ref) => (
83
  <DropdownMenuPrimitive.Item
@@ -85,12 +85,12 @@ const DropdownMenuItem = React.forwardRef<
85
  className={cn(
86
  "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
87
  inset && "pl-8",
88
- className
89
  )}
90
  {...props}
91
  />
92
- ))
93
- DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94
 
95
  const DropdownMenuCheckboxItem = React.forwardRef<
96
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
@@ -100,7 +100,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
100
  ref={ref}
101
  className={cn(
102
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
103
- className
104
  )}
105
  checked={checked}
106
  {...props}
@@ -112,9 +112,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
112
  </span>
113
  {children}
114
  </DropdownMenuPrimitive.CheckboxItem>
115
- ))
116
  DropdownMenuCheckboxItem.displayName =
117
- DropdownMenuPrimitive.CheckboxItem.displayName
118
 
119
  const DropdownMenuRadioItem = React.forwardRef<
120
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -124,7 +124,7 @@ const DropdownMenuRadioItem = React.forwardRef<
124
  ref={ref}
125
  className={cn(
126
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
127
- className
128
  )}
129
  {...props}
130
  >
@@ -135,13 +135,13 @@ const DropdownMenuRadioItem = React.forwardRef<
135
  </span>
136
  {children}
137
  </DropdownMenuPrimitive.RadioItem>
138
- ))
139
- DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140
 
141
  const DropdownMenuLabel = React.forwardRef<
142
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
143
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
144
- inset?: boolean
145
  }
146
  >(({ className, inset, ...props }, ref) => (
147
  <DropdownMenuPrimitive.Label
@@ -149,12 +149,12 @@ const DropdownMenuLabel = React.forwardRef<
149
  className={cn(
150
  "px-2 py-1.5 text-sm font-semibold",
151
  inset && "pl-8",
152
- className
153
  )}
154
  {...props}
155
  />
156
- ))
157
- DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158
 
159
  const DropdownMenuSeparator = React.forwardRef<
160
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
@@ -165,8 +165,8 @@ const DropdownMenuSeparator = React.forwardRef<
165
  className={cn("-mx-1 my-1 h-px bg-muted", className)}
166
  {...props}
167
  />
168
- ))
169
- DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170
 
171
  const DropdownMenuShortcut = ({
172
  className,
@@ -177,9 +177,9 @@ const DropdownMenuShortcut = ({
177
  className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
178
  {...props}
179
  />
180
- )
181
- }
182
- DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183
 
184
  export {
185
  DropdownMenu,
@@ -197,4 +197,4 @@ export {
197
  DropdownMenuSubContent,
198
  DropdownMenuSubTrigger,
199
  DropdownMenuRadioGroup,
200
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { Check, ChevronRight, Circle } from "lucide-react";
6
 
7
+ import { cn } from "@/lib/utils";
8
 
9
+ const DropdownMenu = DropdownMenuPrimitive.Root;
10
 
11
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12
 
13
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14
 
15
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16
 
17
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18
 
19
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20
 
21
  const DropdownMenuSubTrigger = React.forwardRef<
22
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
+ inset?: boolean;
25
  }
26
  >(({ className, inset, children, ...props }, ref) => (
27
  <DropdownMenuPrimitive.SubTrigger
 
29
  className={cn(
30
  "flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31
  inset && "pl-8",
32
+ className,
33
  )}
34
  {...props}
35
  >
36
  {children}
37
  <ChevronRight className="ml-auto" />
38
  </DropdownMenuPrimitive.SubTrigger>
39
+ ));
40
  DropdownMenuSubTrigger.displayName =
41
+ DropdownMenuPrimitive.SubTrigger.displayName;
42
 
43
  const DropdownMenuSubContent = React.forwardRef<
44
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
 
48
  ref={ref}
49
  className={cn(
50
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
+ className,
52
  )}
53
  {...props}
54
  />
55
+ ));
56
  DropdownMenuSubContent.displayName =
57
+ DropdownMenuPrimitive.SubContent.displayName;
58
 
59
  const DropdownMenuContent = React.forwardRef<
60
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
 
66
  sideOffset={sideOffset}
67
  className={cn(
68
  "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
69
+ className,
70
  )}
71
  {...props}
72
  />
73
  </DropdownMenuPrimitive.Portal>
74
+ ));
75
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
76
 
77
  const DropdownMenuItem = React.forwardRef<
78
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
79
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
80
+ inset?: boolean;
81
  }
82
  >(({ className, inset, ...props }, ref) => (
83
  <DropdownMenuPrimitive.Item
 
85
  className={cn(
86
  "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
87
  inset && "pl-8",
88
+ className,
89
  )}
90
  {...props}
91
  />
92
+ ));
93
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
94
 
95
  const DropdownMenuCheckboxItem = React.forwardRef<
96
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
 
100
  ref={ref}
101
  className={cn(
102
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
103
+ className,
104
  )}
105
  checked={checked}
106
  {...props}
 
112
  </span>
113
  {children}
114
  </DropdownMenuPrimitive.CheckboxItem>
115
+ ));
116
  DropdownMenuCheckboxItem.displayName =
117
+ DropdownMenuPrimitive.CheckboxItem.displayName;
118
 
119
  const DropdownMenuRadioItem = React.forwardRef<
120
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
 
124
  ref={ref}
125
  className={cn(
126
  "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
127
+ className,
128
  )}
129
  {...props}
130
  >
 
135
  </span>
136
  {children}
137
  </DropdownMenuPrimitive.RadioItem>
138
+ ));
139
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
140
 
141
  const DropdownMenuLabel = React.forwardRef<
142
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
143
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
144
+ inset?: boolean;
145
  }
146
  >(({ className, inset, ...props }, ref) => (
147
  <DropdownMenuPrimitive.Label
 
149
  className={cn(
150
  "px-2 py-1.5 text-sm font-semibold",
151
  inset && "pl-8",
152
+ className,
153
  )}
154
  {...props}
155
  />
156
+ ));
157
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
158
 
159
  const DropdownMenuSeparator = React.forwardRef<
160
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
 
165
  className={cn("-mx-1 my-1 h-px bg-muted", className)}
166
  {...props}
167
  />
168
+ ));
169
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
170
 
171
  const DropdownMenuShortcut = ({
172
  className,
 
177
  className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
178
  {...props}
179
  />
180
+ );
181
+ };
182
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
183
 
184
  export {
185
  DropdownMenu,
 
197
  DropdownMenuSubContent,
198
  DropdownMenuSubTrigger,
199
  DropdownMenuRadioGroup,
200
+ };
src/components/ui/error-message.tsx CHANGED
@@ -6,18 +6,34 @@ interface ErrorMessageProps {
6
  className?: string;
7
  }
8
 
9
- export function ErrorMessage({ message, onClose, className = "" }: ErrorMessageProps) {
 
 
 
 
10
  if (!message) return null;
11
-
12
  return (
13
- <div className={`p-2 bg-red-600/20 text-red-200 text-sm text-center relative ${className}`}>
 
 
14
  {onClose && (
15
- <button
16
- onClick={onClose}
17
  className="absolute right-2 top-1/2 -translate-y-1/2 text-red-200 hover:text-white"
18
  aria-label="Close error message"
19
  >
20
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
 
 
 
 
 
 
 
 
 
 
21
  <line x1="18" y1="6" x2="6" y2="18"></line>
22
  <line x1="6" y1="6" x2="18" y2="18"></line>
23
  </svg>
@@ -26,4 +42,4 @@ export function ErrorMessage({ message, onClose, className = "" }: ErrorMessageP
26
  {message}
27
  </div>
28
  );
29
- }
 
6
  className?: string;
7
  }
8
 
9
+ export function ErrorMessage({
10
+ message,
11
+ onClose,
12
+ className = "",
13
+ }: ErrorMessageProps) {
14
  if (!message) return null;
15
+
16
  return (
17
+ <div
18
+ className={`p-2 bg-red-600/20 text-red-200 text-sm text-center relative ${className}`}
19
+ >
20
  {onClose && (
21
+ <button
22
+ onClick={onClose}
23
  className="absolute right-2 top-1/2 -translate-y-1/2 text-red-200 hover:text-white"
24
  aria-label="Close error message"
25
  >
26
+ <svg
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ width="16"
29
+ height="16"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ strokeWidth="2"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ >
37
  <line x1="18" y1="6" x2="6" y2="18"></line>
38
  <line x1="6" y1="6" x2="18" y2="18"></line>
39
  </svg>
 
42
  {message}
43
  </div>
44
  );
45
+ }
src/components/ui/form.tsx CHANGED
@@ -1,8 +1,8 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as LabelPrimitive from "@radix-ui/react-label"
5
- import { Slot } from "@radix-ui/react-slot"
6
  import {
7
  Controller,
8
  ControllerProps,
@@ -10,27 +10,27 @@ import {
10
  FieldValues,
11
  FormProvider,
12
  useFormContext,
13
- } from "react-hook-form"
14
 
15
- import { cn } from "@/lib/utils"
16
- import { Label } from "@/components/ui/label"
17
 
18
- const Form = FormProvider
19
 
20
  type FormFieldContextValue<
21
  TFieldValues extends FieldValues = FieldValues,
22
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
23
  > = {
24
- name: TName
25
- }
26
 
27
  const FormFieldContext = React.createContext<FormFieldContextValue>(
28
- {} as FormFieldContextValue
29
- )
30
 
31
  const FormField = <
32
  TFieldValues extends FieldValues = FieldValues,
33
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
34
  >({
35
  ...props
36
  }: ControllerProps<TFieldValues, TName>) => {
@@ -38,21 +38,21 @@ const FormField = <
38
  <FormFieldContext.Provider value={{ name: props.name }}>
39
  <Controller {...props} />
40
  </FormFieldContext.Provider>
41
- )
42
- }
43
 
44
  const useFormField = () => {
45
- const fieldContext = React.useContext(FormFieldContext)
46
- const itemContext = React.useContext(FormItemContext)
47
- const { getFieldState, formState } = useFormContext()
48
 
49
- const fieldState = getFieldState(fieldContext.name, formState)
50
 
51
  if (!fieldContext) {
52
- throw new Error("useFormField should be used within <FormField>")
53
  }
54
 
55
- const { id } = itemContext
56
 
57
  return {
58
  id,
@@ -61,36 +61,36 @@ const useFormField = () => {
61
  formDescriptionId: `${id}-form-item-description`,
62
  formMessageId: `${id}-form-item-message`,
63
  ...fieldState,
64
- }
65
- }
66
 
67
  type FormItemContextValue = {
68
- id: string
69
- }
70
 
71
  const FormItemContext = React.createContext<FormItemContextValue>(
72
- {} as FormItemContextValue
73
- )
74
 
75
  const FormItem = React.forwardRef<
76
  HTMLDivElement,
77
  React.HTMLAttributes<HTMLDivElement>
78
  >(({ className, ...props }, ref) => {
79
- const id = React.useId()
80
 
81
  return (
82
  <FormItemContext.Provider value={{ id }}>
83
  <div ref={ref} className={cn("space-y-2", className)} {...props} />
84
  </FormItemContext.Provider>
85
- )
86
- })
87
- FormItem.displayName = "FormItem"
88
 
89
  const FormLabel = React.forwardRef<
90
  React.ElementRef<typeof LabelPrimitive.Root>,
91
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
92
  >(({ className, ...props }, ref) => {
93
- const { error, formItemId } = useFormField()
94
 
95
  return (
96
  <Label
@@ -99,15 +99,16 @@ const FormLabel = React.forwardRef<
99
  htmlFor={formItemId}
100
  {...props}
101
  />
102
- )
103
- })
104
- FormLabel.displayName = "FormLabel"
105
 
106
  const FormControl = React.forwardRef<
107
  React.ElementRef<typeof Slot>,
108
  React.ComponentPropsWithoutRef<typeof Slot>
109
  >(({ ...props }, ref) => {
110
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
 
111
 
112
  return (
113
  <Slot
@@ -121,15 +122,15 @@ const FormControl = React.forwardRef<
121
  aria-invalid={!!error}
122
  {...props}
123
  />
124
- )
125
- })
126
- FormControl.displayName = "FormControl"
127
 
128
  const FormDescription = React.forwardRef<
129
  HTMLParagraphElement,
130
  React.HTMLAttributes<HTMLParagraphElement>
131
  >(({ className, ...props }, ref) => {
132
- const { formDescriptionId } = useFormField()
133
 
134
  return (
135
  <p
@@ -138,19 +139,19 @@ const FormDescription = React.forwardRef<
138
  className={cn("text-sm text-muted-foreground", className)}
139
  {...props}
140
  />
141
- )
142
- })
143
- FormDescription.displayName = "FormDescription"
144
 
145
  const FormMessage = React.forwardRef<
146
  HTMLParagraphElement,
147
  React.HTMLAttributes<HTMLParagraphElement>
148
  >(({ className, children, ...props }, ref) => {
149
- const { error, formMessageId } = useFormField()
150
- const body = error ? String(error?.message) : children
151
 
152
  if (!body) {
153
- return null
154
  }
155
 
156
  return (
@@ -162,9 +163,9 @@ const FormMessage = React.forwardRef<
162
  >
163
  {body}
164
  </p>
165
- )
166
- })
167
- FormMessage.displayName = "FormMessage"
168
 
169
  export {
170
  useFormField,
@@ -175,4 +176,4 @@ export {
175
  FormDescription,
176
  FormMessage,
177
  FormField,
178
- }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+ import { Slot } from "@radix-ui/react-slot";
6
  import {
7
  Controller,
8
  ControllerProps,
 
10
  FieldValues,
11
  FormProvider,
12
  useFormContext,
13
+ } from "react-hook-form";
14
 
15
+ import { cn } from "@/lib/utils";
16
+ import { Label } from "@/components/ui/label";
17
 
18
+ const Form = FormProvider;
19
 
20
  type FormFieldContextValue<
21
  TFieldValues extends FieldValues = FieldValues,
22
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
23
  > = {
24
+ name: TName;
25
+ };
26
 
27
  const FormFieldContext = React.createContext<FormFieldContextValue>(
28
+ {} as FormFieldContextValue,
29
+ );
30
 
31
  const FormField = <
32
  TFieldValues extends FieldValues = FieldValues,
33
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
34
  >({
35
  ...props
36
  }: ControllerProps<TFieldValues, TName>) => {
 
38
  <FormFieldContext.Provider value={{ name: props.name }}>
39
  <Controller {...props} />
40
  </FormFieldContext.Provider>
41
+ );
42
+ };
43
 
44
  const useFormField = () => {
45
+ const fieldContext = React.useContext(FormFieldContext);
46
+ const itemContext = React.useContext(FormItemContext);
47
+ const { getFieldState, formState } = useFormContext();
48
 
49
+ const fieldState = getFieldState(fieldContext.name, formState);
50
 
51
  if (!fieldContext) {
52
+ throw new Error("useFormField should be used within <FormField>");
53
  }
54
 
55
+ const { id } = itemContext;
56
 
57
  return {
58
  id,
 
61
  formDescriptionId: `${id}-form-item-description`,
62
  formMessageId: `${id}-form-item-message`,
63
  ...fieldState,
64
+ };
65
+ };
66
 
67
  type FormItemContextValue = {
68
+ id: string;
69
+ };
70
 
71
  const FormItemContext = React.createContext<FormItemContextValue>(
72
+ {} as FormItemContextValue,
73
+ );
74
 
75
  const FormItem = React.forwardRef<
76
  HTMLDivElement,
77
  React.HTMLAttributes<HTMLDivElement>
78
  >(({ className, ...props }, ref) => {
79
+ const id = React.useId();
80
 
81
  return (
82
  <FormItemContext.Provider value={{ id }}>
83
  <div ref={ref} className={cn("space-y-2", className)} {...props} />
84
  </FormItemContext.Provider>
85
+ );
86
+ });
87
+ FormItem.displayName = "FormItem";
88
 
89
  const FormLabel = React.forwardRef<
90
  React.ElementRef<typeof LabelPrimitive.Root>,
91
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
92
  >(({ className, ...props }, ref) => {
93
+ const { error, formItemId } = useFormField();
94
 
95
  return (
96
  <Label
 
99
  htmlFor={formItemId}
100
  {...props}
101
  />
102
+ );
103
+ });
104
+ FormLabel.displayName = "FormLabel";
105
 
106
  const FormControl = React.forwardRef<
107
  React.ElementRef<typeof Slot>,
108
  React.ComponentPropsWithoutRef<typeof Slot>
109
  >(({ ...props }, ref) => {
110
+ const { error, formItemId, formDescriptionId, formMessageId } =
111
+ useFormField();
112
 
113
  return (
114
  <Slot
 
122
  aria-invalid={!!error}
123
  {...props}
124
  />
125
+ );
126
+ });
127
+ FormControl.displayName = "FormControl";
128
 
129
  const FormDescription = React.forwardRef<
130
  HTMLParagraphElement,
131
  React.HTMLAttributes<HTMLParagraphElement>
132
  >(({ className, ...props }, ref) => {
133
+ const { formDescriptionId } = useFormField();
134
 
135
  return (
136
  <p
 
139
  className={cn("text-sm text-muted-foreground", className)}
140
  {...props}
141
  />
142
+ );
143
+ });
144
+ FormDescription.displayName = "FormDescription";
145
 
146
  const FormMessage = React.forwardRef<
147
  HTMLParagraphElement,
148
  React.HTMLAttributes<HTMLParagraphElement>
149
  >(({ className, children, ...props }, ref) => {
150
+ const { error, formMessageId } = useFormField();
151
+ const body = error ? String(error?.message) : children;
152
 
153
  if (!body) {
154
+ return null;
155
  }
156
 
157
  return (
 
163
  >
164
  {body}
165
  </p>
166
+ );
167
+ });
168
+ FormMessage.displayName = "FormMessage";
169
 
170
  export {
171
  useFormField,
 
176
  FormDescription,
177
  FormMessage,
178
  FormField,
179
+ };
src/components/ui/fullscreen-toggle.tsx CHANGED
@@ -1,16 +1,25 @@
1
- "use client"
2
 
3
- import { Maximize2, Minimize2 } from "lucide-react"
4
- import { Button } from "@/components/ui/button"
5
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
 
 
 
 
 
6
 
7
  interface FullscreenToggleProps {
8
- isFullScreen: boolean
9
- onClick: () => void
10
- className?: string
11
  }
12
 
13
- export function FullscreenToggle({ isFullScreen, onClick, className = "" }: FullscreenToggleProps) {
 
 
 
 
14
  return (
15
  <TooltipProvider>
16
  <Tooltip>
@@ -31,9 +40,9 @@ export function FullscreenToggle({ isFullScreen, onClick, className = "" }: Full
31
  </Button>
32
  </TooltipTrigger>
33
  <TooltipContent className="bg-novita-gray text-white">
34
- <p>{isFullScreen ? 'Exit full screen' : 'Enter full screen'}</p>
35
  </TooltipContent>
36
  </Tooltip>
37
  </TooltipProvider>
38
- )
39
- }
 
1
+ "use client";
2
 
3
+ import { Maximize2, Minimize2 } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipProvider,
9
+ TooltipTrigger,
10
+ } from "@/components/ui/tooltip";
11
 
12
  interface FullscreenToggleProps {
13
+ isFullScreen: boolean;
14
+ onClick: () => void;
15
+ className?: string;
16
  }
17
 
18
+ export function FullscreenToggle({
19
+ isFullScreen,
20
+ onClick,
21
+ className = "",
22
+ }: FullscreenToggleProps) {
23
  return (
24
  <TooltipProvider>
25
  <Tooltip>
 
40
  </Button>
41
  </TooltipTrigger>
42
  <TooltipContent className="bg-novita-gray text-white">
43
+ <p>{isFullScreen ? "Exit full screen" : "Enter full screen"}</p>
44
  </TooltipContent>
45
  </Tooltip>
46
  </TooltipProvider>
47
+ );
48
+ }
src/components/ui/hover-card.tsx CHANGED
@@ -1,13 +1,13 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5
 
6
- import { cn } from "@/lib/utils"
7
 
8
- const HoverCard = HoverCardPrimitive.Root
9
 
10
- const HoverCardTrigger = HoverCardPrimitive.Trigger
11
 
12
  const HoverCardContent = React.forwardRef<
13
  React.ElementRef<typeof HoverCardPrimitive.Content>,
@@ -19,11 +19,11 @@ const HoverCardContent = React.forwardRef<
19
  sideOffset={sideOffset}
20
  className={cn(
21
  "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
22
- className
23
  )}
24
  {...props}
25
  />
26
- ))
27
- HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28
 
29
- export { HoverCard, HoverCardTrigger, HoverCardContent }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5
 
6
+ import { cn } from "@/lib/utils";
7
 
8
+ const HoverCard = HoverCardPrimitive.Root;
9
 
10
+ const HoverCardTrigger = HoverCardPrimitive.Trigger;
11
 
12
  const HoverCardContent = React.forwardRef<
13
  React.ElementRef<typeof HoverCardPrimitive.Content>,
 
19
  sideOffset={sideOffset}
20
  className={cn(
21
  "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
22
+ className,
23
  )}
24
  {...props}
25
  />
26
+ ));
27
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28
 
29
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
src/components/ui/icons.tsx CHANGED
@@ -1,42 +1,82 @@
1
  export function MinimizeIcon() {
2
  return (
3
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
 
 
 
 
 
 
 
 
 
 
4
  <path d="M8 3v3a2 2 0 0 1-2 2H3"></path>
5
  <path d="M21 8h-3a2 2 0 0 1-2-2V3"></path>
6
  <path d="M3 16h3a2 2 0 0 1 2 2v3"></path>
7
  <path d="M16 21v-3a2 2 0 0 1 2-2h3"></path>
8
  </svg>
9
- )
10
  }
11
 
12
  export function MaximizeIcon() {
13
  return (
14
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
 
 
 
 
 
 
 
 
 
 
15
  <polyline points="15 3 21 3 21 9"></polyline>
16
  <polyline points="9 21 3 21 3 15"></polyline>
17
  <line x1="21" y1="3" x2="14" y2="10"></line>
18
  <line x1="3" y1="21" x2="10" y2="14"></line>
19
  </svg>
20
- )
21
  }
22
 
23
  export function DownloadIcon() {
24
  return (
25
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
 
 
 
 
 
 
 
 
 
 
26
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
27
  <polyline points="7 10 12 15 17 10"></polyline>
28
  <line x1="12" y1="15" x2="12" y2="3"></line>
29
  </svg>
30
- )
31
  }
32
 
33
  export function RefreshIcon() {
34
  return (
35
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
 
 
 
 
 
 
 
 
 
 
36
  <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
37
  <path d="M21 3v5h-5"></path>
38
  <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
39
  <path d="M3 21v-5h5"></path>
40
  </svg>
41
- )
42
- }
 
1
  export function MinimizeIcon() {
2
  return (
3
+ <svg
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="16"
6
+ height="16"
7
+ viewBox="0 0 24 24"
8
+ fill="none"
9
+ stroke="currentColor"
10
+ strokeWidth="2"
11
+ strokeLinecap="round"
12
+ strokeLinejoin="round"
13
+ >
14
  <path d="M8 3v3a2 2 0 0 1-2 2H3"></path>
15
  <path d="M21 8h-3a2 2 0 0 1-2-2V3"></path>
16
  <path d="M3 16h3a2 2 0 0 1 2 2v3"></path>
17
  <path d="M16 21v-3a2 2 0 0 1 2-2h3"></path>
18
  </svg>
19
+ );
20
  }
21
 
22
  export function MaximizeIcon() {
23
  return (
24
+ <svg
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ width="16"
27
+ height="16"
28
+ viewBox="0 0 24 24"
29
+ fill="none"
30
+ stroke="currentColor"
31
+ strokeWidth="2"
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ >
35
  <polyline points="15 3 21 3 21 9"></polyline>
36
  <polyline points="9 21 3 21 3 15"></polyline>
37
  <line x1="21" y1="3" x2="14" y2="10"></line>
38
  <line x1="3" y1="21" x2="10" y2="14"></line>
39
  </svg>
40
+ );
41
  }
42
 
43
  export function DownloadIcon() {
44
  return (
45
+ <svg
46
+ xmlns="http://www.w3.org/2000/svg"
47
+ width="16"
48
+ height="16"
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ stroke="currentColor"
52
+ strokeWidth="2"
53
+ strokeLinecap="round"
54
+ strokeLinejoin="round"
55
+ >
56
  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
57
  <polyline points="7 10 12 15 17 10"></polyline>
58
  <line x1="12" y1="15" x2="12" y2="3"></line>
59
  </svg>
60
+ );
61
  }
62
 
63
  export function RefreshIcon() {
64
  return (
65
+ <svg
66
+ xmlns="http://www.w3.org/2000/svg"
67
+ width="16"
68
+ height="16"
69
+ viewBox="0 0 24 24"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ strokeWidth="2"
73
+ strokeLinecap="round"
74
+ strokeLinejoin="round"
75
+ >
76
  <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path>
77
  <path d="M21 3v5h-5"></path>
78
  <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path>
79
  <path d="M3 21v-5h5"></path>
80
  </svg>
81
+ );
82
+ }
src/components/ui/input-otp.tsx CHANGED
@@ -1,10 +1,10 @@
1
- "use client"
2
 
3
- import * as React from "react"
4
- import { OTPInput, OTPInputContext } from "input-otp"
5
- import { Dot } from "lucide-react"
6
 
7
- import { cn } from "@/lib/utils"
8
 
9
  const InputOTP = React.forwardRef<
10
  React.ElementRef<typeof OTPInput>,
@@ -14,28 +14,28 @@ const InputOTP = React.forwardRef<
14
  ref={ref}
15
  containerClassName={cn(
16
  "flex items-center gap-2 has-[:disabled]:opacity-50",
17
- containerClassName
18
  )}
19
  className={cn("disabled:cursor-not-allowed", className)}
20
  {...props}
21
  />
22
- ))
23
- InputOTP.displayName = "InputOTP"
24
 
25
  const InputOTPGroup = React.forwardRef<
26
  React.ElementRef<"div">,
27
  React.ComponentPropsWithoutRef<"div">
28
  >(({ className, ...props }, ref) => (
29
  <div ref={ref} className={cn("flex items-center", className)} {...props} />
30
- ))
31
- InputOTPGroup.displayName = "InputOTPGroup"
32
 
33
  const InputOTPSlot = React.forwardRef<
34
  React.ElementRef<"div">,
35
  React.ComponentPropsWithoutRef<"div"> & { index: number }
36
  >(({ index, className, ...props }, ref) => {
37
- const inputOTPContext = React.useContext(OTPInputContext)
38
- const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
39
 
40
  return (
41
  <div
@@ -43,7 +43,7 @@ const InputOTPSlot = React.forwardRef<
43
  className={cn(
44
  "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
45
  isActive && "z-10 ring-2 ring-ring ring-offset-background",
46
- className
47
  )}
48
  {...props}
49
  >
@@ -54,9 +54,9 @@ const InputOTPSlot = React.forwardRef<
54
  </div>
55
  )}
56
  </div>
57
- )
58
- })
59
- InputOTPSlot.displayName = "InputOTPSlot"
60
 
61
  const InputOTPSeparator = React.forwardRef<
62
  React.ElementRef<"div">,
@@ -65,7 +65,7 @@ const InputOTPSeparator = React.forwardRef<
65
  <div ref={ref} role="separator" {...props}>
66
  <Dot />
67
  </div>
68
- ))
69
- InputOTPSeparator.displayName = "InputOTPSeparator"
70
 
71
- export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
 
1
+ "use client";
2
 
3
+ import * as React from "react";
4
+ import { OTPInput, OTPInputContext } from "input-otp";
5
+ import { Dot } from "lucide-react";
6
 
7
+ import { cn } from "@/lib/utils";
8
 
9
  const InputOTP = React.forwardRef<
10
  React.ElementRef<typeof OTPInput>,
 
14
  ref={ref}
15
  containerClassName={cn(
16
  "flex items-center gap-2 has-[:disabled]:opacity-50",
17
+ containerClassName,
18
  )}
19
  className={cn("disabled:cursor-not-allowed", className)}
20
  {...props}
21
  />
22
+ ));
23
+ InputOTP.displayName = "InputOTP";
24
 
25
  const InputOTPGroup = React.forwardRef<
26
  React.ElementRef<"div">,
27
  React.ComponentPropsWithoutRef<"div">
28
  >(({ className, ...props }, ref) => (
29
  <div ref={ref} className={cn("flex items-center", className)} {...props} />
30
+ ));
31
+ InputOTPGroup.displayName = "InputOTPGroup";
32
 
33
  const InputOTPSlot = React.forwardRef<
34
  React.ElementRef<"div">,
35
  React.ComponentPropsWithoutRef<"div"> & { index: number }
36
  >(({ index, className, ...props }, ref) => {
37
+ const inputOTPContext = React.useContext(OTPInputContext);
38
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
39
 
40
  return (
41
  <div
 
43
  className={cn(
44
  "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
45
  isActive && "z-10 ring-2 ring-ring ring-offset-background",
46
+ className,
47
  )}
48
  {...props}
49
  >
 
54
  </div>
55
  )}
56
  </div>
57
+ );
58
+ });
59
+ InputOTPSlot.displayName = "InputOTPSlot";
60
 
61
  const InputOTPSeparator = React.forwardRef<
62
  React.ElementRef<"div">,
 
65
  <div ref={ref} role="separator" {...props}>
66
  <Dot />
67
  </div>
68
+ ));
69
+ InputOTPSeparator.displayName = "InputOTPSeparator";
70
 
71
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };