@promakeai/cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/index.js +209 -415
  2. package/dist/registry/about-page.json +3 -3
  3. package/dist/registry/about-section.json +4 -4
  4. package/dist/registry/animations.json +2 -2
  5. package/dist/registry/announcement-bar.json +4 -4
  6. package/dist/registry/api.json +1 -1
  7. package/dist/registry/auth-core.json +3 -3
  8. package/dist/registry/bento-grid-section.json +4 -4
  9. package/dist/registry/blog-core.json +5 -5
  10. package/dist/registry/blog-list-page.json +3 -3
  11. package/dist/registry/blog-section.json +4 -4
  12. package/dist/registry/cards-carousel-section.json +4 -4
  13. package/dist/registry/cart-drawer.json +3 -3
  14. package/dist/registry/cart-page.json +3 -3
  15. package/dist/registry/case-study-page.json +3 -3
  16. package/dist/registry/category-section.json +3 -3
  17. package/dist/registry/checkout-page.json +3 -3
  18. package/dist/registry/coming-soon-page-minimal.json +4 -4
  19. package/dist/registry/coming-soon-page.json +4 -4
  20. package/dist/registry/contact-info-grid.json +4 -4
  21. package/dist/registry/contact-page-centered.json +4 -4
  22. package/dist/registry/contact-page-split.json +4 -4
  23. package/dist/registry/contact-page.json +3 -3
  24. package/dist/registry/content-section.json +4 -4
  25. package/dist/registry/cookie-consent.json +4 -4
  26. package/dist/registry/cookies-page.json +3 -3
  27. package/dist/registry/cta-section.json +3 -3
  28. package/dist/registry/ecommerce-core.json +10 -10
  29. package/dist/registry/empty-page.json +3 -3
  30. package/dist/registry/faq-categorized.json +4 -4
  31. package/dist/registry/faq-simple.json +4 -4
  32. package/dist/registry/favorites-blog-block.json +1 -1
  33. package/dist/registry/favorites-blog-page.json +4 -4
  34. package/dist/registry/favorites-ecommerce-block.json +1 -1
  35. package/dist/registry/favorites-ecommerce-page.json +4 -4
  36. package/dist/registry/feature-section.json +3 -3
  37. package/dist/registry/featured-products.json +3 -3
  38. package/dist/registry/footer-detailed.json +4 -4
  39. package/dist/registry/footer-minimal.json +3 -3
  40. package/dist/registry/footer.json +3 -3
  41. package/dist/registry/forgot-password-page-split.json +4 -4
  42. package/dist/registry/forgot-password-page.json +4 -4
  43. package/dist/registry/google-adsense.json +4 -4
  44. package/dist/registry/google-map.json +2 -2
  45. package/dist/registry/header-centered-pill.json +4 -4
  46. package/dist/registry/header-ecommerce.json +3 -3
  47. package/dist/registry/header-mega.json +4 -4
  48. package/dist/registry/header-minimal.json +4 -4
  49. package/dist/registry/header-simple.json +3 -3
  50. package/dist/registry/hero-carousel.json +3 -3
  51. package/dist/registry/hero-cta.json +4 -4
  52. package/dist/registry/hero-gradient.json +4 -4
  53. package/dist/registry/hero-grid.json +4 -4
  54. package/dist/registry/hero-profile.json +3 -3
  55. package/dist/registry/hero.json +3 -3
  56. package/dist/registry/landing-page-app.json +3 -3
  57. package/dist/registry/landing-page-saas.json +3 -3
  58. package/dist/registry/login-page-split.json +4 -4
  59. package/dist/registry/login-page.json +4 -4
  60. package/dist/registry/logo-cloud.json +4 -4
  61. package/dist/registry/masonry-grid.json +3 -3
  62. package/dist/registry/my-orders-page.json +4 -4
  63. package/dist/registry/newsletter-section.json +4 -4
  64. package/dist/registry/order-card-compact.json +1 -1
  65. package/dist/registry/order-confirmation-page.json +4 -4
  66. package/dist/registry/order-detail-block.json +1 -1
  67. package/dist/registry/orders-list-block.json +1 -1
  68. package/dist/registry/payment-success-block.json +1 -1
  69. package/dist/registry/portfolio-page.json +4 -4
  70. package/dist/registry/post-card.json +3 -3
  71. package/dist/registry/post-detail-block.json +1 -1
  72. package/dist/registry/post-detail-page.json +4 -4
  73. package/dist/registry/pricing-card.json +3 -3
  74. package/dist/registry/pricing-page.json +4 -4
  75. package/dist/registry/pricing-section.json +4 -4
  76. package/dist/registry/privacy-page.json +3 -3
  77. package/dist/registry/product-card-detailed.json +4 -4
  78. package/dist/registry/product-card-hover.json +4 -4
  79. package/dist/registry/product-card.json +3 -3
  80. package/dist/registry/product-detail-block.json +1 -1
  81. package/dist/registry/product-detail-page.json +4 -4
  82. package/dist/registry/product-detail-section.json +4 -4
  83. package/dist/registry/product-quick-view.json +4 -4
  84. package/dist/registry/products-page.json +3 -3
  85. package/dist/registry/reading-progress.json +4 -4
  86. package/dist/registry/register-page-split.json +4 -4
  87. package/dist/registry/register-page.json +4 -4
  88. package/dist/registry/related-posts-block.json +1 -1
  89. package/dist/registry/related-products-block.json +1 -1
  90. package/dist/registry/reset-password-page-split.json +4 -4
  91. package/dist/registry/service-card.json +1 -1
  92. package/dist/registry/share-buttons.json +4 -4
  93. package/dist/registry/skill-card.json +1 -1
  94. package/dist/registry/team-page.json +4 -4
  95. package/dist/registry/terms-page.json +3 -3
  96. package/dist/registry/testimonials-carousel.json +4 -4
  97. package/dist/registry/testimonials-grid.json +4 -4
  98. package/dist/registry/timeline-section.json +4 -4
  99. package/dist/registry/video-hero.json +4 -4
  100. package/dist/registry/youtube-embed.json +4 -4
  101. package/package.json +2 -2
  102. package/template/.env +6 -6
  103. package/template/public/_redirects +1 -1
  104. package/template/public/robots.txt +14 -14
  105. package/template/src/components/GoogleAnalytics.tsx +34 -34
  106. package/template/src/components/LanguageSwitcher.tsx +53 -53
  107. package/template/src/components/ScriptInjector.tsx +62 -62
  108. package/template/src/lib/env.ts +19 -19
  109. package/template/src/router.tsx +14 -14
  110. package/template/src/vite-env.d.ts +1 -1
  111. package/dist/registry/auth.json +0 -70
  112. package/dist/registry/docs/reset-password-page.md +0 -36
  113. package/dist/registry/reset-password-page.json +0 -39
@@ -1,34 +1,34 @@
1
- import { useEffect, useRef } from "react";
2
- import constants from "@/constants/constants.json";
3
-
4
- export function GoogleAnalytics() {
5
- const injected = useRef(false);
6
- const gaId = constants.scripts.gaId;
7
-
8
- useEffect(() => {
9
- if (!gaId || injected.current) return;
10
- if (document.querySelector(`[data-injected="gtag"]`)) return;
11
-
12
- injected.current = true;
13
-
14
- // Inject gtag.js script
15
- const script = document.createElement("script");
16
- script.async = true;
17
- script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`;
18
- script.setAttribute("data-injected", "gtag");
19
- document.head.appendChild(script);
20
-
21
- // Inject inline config script
22
- const inlineScript = document.createElement("script");
23
- inlineScript.setAttribute("data-injected", "gtag-config");
24
- inlineScript.innerHTML = `
25
- window.dataLayer = window.dataLayer || [];
26
- function gtag(){dataLayer.push(arguments);}
27
- gtag('js', new Date());
28
- gtag('config', '${gaId}');
29
- `;
30
- document.head.appendChild(inlineScript);
31
- }, []);
32
-
33
- return null;
34
- }
1
+ import { useEffect, useRef } from "react";
2
+ import constants from "@/constants/constants.json";
3
+
4
+ export function GoogleAnalytics() {
5
+ const injected = useRef(false);
6
+ const gaId = constants.scripts.gaId;
7
+
8
+ useEffect(() => {
9
+ if (!gaId || injected.current) return;
10
+ if (document.querySelector(`[data-injected="gtag"]`)) return;
11
+
12
+ injected.current = true;
13
+
14
+ // Inject gtag.js script
15
+ const script = document.createElement("script");
16
+ script.async = true;
17
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${gaId}`;
18
+ script.setAttribute("data-injected", "gtag");
19
+ document.head.appendChild(script);
20
+
21
+ // Inject inline config script
22
+ const inlineScript = document.createElement("script");
23
+ inlineScript.setAttribute("data-injected", "gtag-config");
24
+ inlineScript.innerHTML = `
25
+ window.dataLayer = window.dataLayer || [];
26
+ function gtag(){dataLayer.push(arguments);}
27
+ gtag('js', new Date());
28
+ gtag('config', '${gaId}');
29
+ `;
30
+ document.head.appendChild(inlineScript);
31
+ }, []);
32
+
33
+ return null;
34
+ }
@@ -1,53 +1,53 @@
1
- import { useTranslation } from "react-i18next";
2
- import { Button } from "@/components/ui/button";
3
- import {
4
- DropdownMenu,
5
- DropdownMenuContent,
6
- DropdownMenuItem,
7
- DropdownMenuTrigger,
8
- } from "@/components/ui/dropdown-menu";
9
- import { changeLanguage } from "@/lang";
10
- import { cn } from "@/lib/utils";
11
- import constants from "@/constants/constants.json";
12
-
13
- interface LanguageSwitcherProps {
14
- className?: string;
15
- style?: React.CSSProperties;
16
- }
17
-
18
- const languages: Record<string, string> =
19
- constants?.site?.availableLanguages || {};
20
-
21
- export function LanguageSwitcher({ className, style }: LanguageSwitcherProps) {
22
- const { i18n } = useTranslation();
23
- const currentLang = i18n.language;
24
-
25
- return (
26
- <DropdownMenu>
27
- <DropdownMenuTrigger asChild>
28
- <Button
29
- variant="ghost"
30
- size="sm"
31
- className={cn("h-9 px-2 text-sm font-medium", className)}
32
- style={style}
33
- >
34
- {languages?.[currentLang] || currentLang.toUpperCase()}
35
- </Button>
36
- </DropdownMenuTrigger>
37
- <DropdownMenuContent align="end">
38
- {Object.entries(languages).map(([lang, label]) => (
39
- <DropdownMenuItem
40
- key={lang}
41
- onClick={() => changeLanguage(lang)}
42
- className={cn(
43
- currentLang === lang ? "bg-accent" : "",
44
- "hover:text-primary focus:text-primary",
45
- )}
46
- >
47
- {label || lang.toUpperCase()}
48
- </DropdownMenuItem>
49
- ))}
50
- </DropdownMenuContent>
51
- </DropdownMenu>
52
- );
53
- }
1
+ import { useTranslation } from "react-i18next";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ } from "@/components/ui/dropdown-menu";
9
+ import { changeLanguage } from "@/lang";
10
+ import { cn } from "@/lib/utils";
11
+ import constants from "@/constants/constants.json";
12
+
13
+ interface LanguageSwitcherProps {
14
+ className?: string;
15
+ style?: React.CSSProperties;
16
+ }
17
+
18
+ const languages: Record<string, string> =
19
+ constants?.site?.availableLanguages || {};
20
+
21
+ export function LanguageSwitcher({ className, style }: LanguageSwitcherProps) {
22
+ const { i18n } = useTranslation();
23
+ const currentLang = i18n.language;
24
+
25
+ return (
26
+ <DropdownMenu>
27
+ <DropdownMenuTrigger asChild>
28
+ <Button
29
+ variant="ghost"
30
+ size="sm"
31
+ className={cn("h-9 px-2 text-sm font-medium", className)}
32
+ style={style}
33
+ >
34
+ {languages?.[currentLang] || currentLang.toUpperCase()}
35
+ </Button>
36
+ </DropdownMenuTrigger>
37
+ <DropdownMenuContent align="end">
38
+ {Object.entries(languages).map(([lang, label]) => (
39
+ <DropdownMenuItem
40
+ key={lang}
41
+ onClick={() => changeLanguage(lang)}
42
+ className={cn(
43
+ currentLang === lang ? "bg-accent" : "",
44
+ "hover:text-primary focus:text-primary",
45
+ )}
46
+ >
47
+ {label || lang.toUpperCase()}
48
+ </DropdownMenuItem>
49
+ ))}
50
+ </DropdownMenuContent>
51
+ </DropdownMenu>
52
+ );
53
+ }
@@ -1,62 +1,62 @@
1
- import { useEffect, useRef } from "react";
2
- import constants from "@/constants/constants.json";
3
-
4
- function injectScript(html: string, target: "head" | "body", position: "start" | "end", marker: string) {
5
- if (!html) return;
6
-
7
- // Check if already injected
8
- if (document.querySelector(`[data-injected="${marker}"]`)) return;
9
-
10
- const container = document.createElement("div");
11
- container.innerHTML = html;
12
-
13
- const targetElement = target === "head" ? document.head : document.body;
14
-
15
- // Create wrapper with marker
16
- const wrapper = document.createDocumentFragment();
17
-
18
- Array.from(container.childNodes).forEach((node) => {
19
- if (node.nodeName === "SCRIPT") {
20
- // Recreate script for execution
21
- const script = node as HTMLScriptElement;
22
- const newScript = document.createElement("script");
23
- newScript.setAttribute("data-injected", marker);
24
- Array.from(script.attributes).forEach((attr) => {
25
- newScript.setAttribute(attr.name, attr.value);
26
- });
27
- if (script.innerHTML) {
28
- newScript.innerHTML = script.innerHTML;
29
- }
30
- wrapper.appendChild(newScript);
31
- } else {
32
- const clone = node.cloneNode(true) as HTMLElement;
33
- if (clone.setAttribute) {
34
- clone.setAttribute("data-injected", marker);
35
- }
36
- wrapper.appendChild(clone);
37
- }
38
- });
39
-
40
- if (position === "start") {
41
- targetElement.insertBefore(wrapper, targetElement.firstChild);
42
- } else {
43
- targetElement.appendChild(wrapper);
44
- }
45
- }
46
-
47
- export function ScriptInjector() {
48
- const injected = useRef(false);
49
- const { headStart, headEnd, bodyStart, bodyEnd } = constants.scripts;
50
-
51
- useEffect(() => {
52
- if (injected.current) return;
53
- injected.current = true;
54
-
55
- injectScript(headStart, "head", "start", "head-start");
56
- injectScript(headEnd, "head", "end", "head-end");
57
- injectScript(bodyStart, "body", "start", "body-start");
58
- injectScript(bodyEnd, "body", "end", "body-end");
59
- }, []);
60
-
61
- return null;
62
- }
1
+ import { useEffect, useRef } from "react";
2
+ import constants from "@/constants/constants.json";
3
+
4
+ function injectScript(html: string, target: "head" | "body", position: "start" | "end", marker: string) {
5
+ if (!html) return;
6
+
7
+ // Check if already injected
8
+ if (document.querySelector(`[data-injected="${marker}"]`)) return;
9
+
10
+ const container = document.createElement("div");
11
+ container.innerHTML = html;
12
+
13
+ const targetElement = target === "head" ? document.head : document.body;
14
+
15
+ // Create wrapper with marker
16
+ const wrapper = document.createDocumentFragment();
17
+
18
+ Array.from(container.childNodes).forEach((node) => {
19
+ if (node.nodeName === "SCRIPT") {
20
+ // Recreate script for execution
21
+ const script = node as HTMLScriptElement;
22
+ const newScript = document.createElement("script");
23
+ newScript.setAttribute("data-injected", marker);
24
+ Array.from(script.attributes).forEach((attr) => {
25
+ newScript.setAttribute(attr.name, attr.value);
26
+ });
27
+ if (script.innerHTML) {
28
+ newScript.innerHTML = script.innerHTML;
29
+ }
30
+ wrapper.appendChild(newScript);
31
+ } else {
32
+ const clone = node.cloneNode(true) as HTMLElement;
33
+ if (clone.setAttribute) {
34
+ clone.setAttribute("data-injected", marker);
35
+ }
36
+ wrapper.appendChild(clone);
37
+ }
38
+ });
39
+
40
+ if (position === "start") {
41
+ targetElement.insertBefore(wrapper, targetElement.firstChild);
42
+ } else {
43
+ targetElement.appendChild(wrapper);
44
+ }
45
+ }
46
+
47
+ export function ScriptInjector() {
48
+ const injected = useRef(false);
49
+ const { headStart, headEnd, bodyStart, bodyEnd } = constants.scripts;
50
+
51
+ useEffect(() => {
52
+ if (injected.current) return;
53
+ injected.current = true;
54
+
55
+ injectScript(headStart, "head", "start", "head-start");
56
+ injectScript(headEnd, "head", "end", "head-end");
57
+ injectScript(bodyStart, "body", "start", "body-start");
58
+ injectScript(bodyEnd, "body", "end", "body-end");
59
+ }, []);
60
+
61
+ return null;
62
+ }
@@ -1,20 +1,20 @@
1
- // Environment değişkenlerini window'a export et
2
- declare global {
3
- interface Window {
4
- ENV: {
5
- [key: string]: any;
6
- };
7
- }
8
- }
9
-
10
- window.ENV = {
11
- ...Object.keys(import.meta.env)
12
- .filter((key) => key.startsWith('VITE_'))
13
- .reduce(
14
- (acc, key) => ({
15
- ...acc,
16
- [key]: import.meta.env[key],
17
- }),
18
- {},
19
- ),
1
+ // Environment değişkenlerini window'a export et
2
+ declare global {
3
+ interface Window {
4
+ ENV: {
5
+ [key: string]: any;
6
+ };
7
+ }
8
+ }
9
+
10
+ window.ENV = {
11
+ ...Object.keys(import.meta.env)
12
+ .filter((key) => key.startsWith('VITE_'))
13
+ .reduce(
14
+ (acc, key) => ({
15
+ ...acc,
16
+ [key]: import.meta.env[key],
17
+ }),
18
+ {},
19
+ ),
20
20
  };
@@ -1,14 +1,14 @@
1
- import { BrowserRouter, Routes, Route } from "react-router";
2
- import Index from "./pages/Index";
3
- import NotFound from "./pages/NotFound";
4
-
5
- export const Router = () => {
6
- return (
7
- <BrowserRouter>
8
- <Routes>
9
- <Route path="/" element={<Index />} />
10
- <Route path="*" element={<NotFound />} />
11
- </Routes>
12
- </BrowserRouter>
13
- );
14
- };
1
+ import { BrowserRouter, Routes, Route } from "react-router";
2
+ import Index from "./pages/Index";
3
+ import NotFound from "./pages/NotFound";
4
+
5
+ export const Router = () => {
6
+ return (
7
+ <BrowserRouter>
8
+ <Routes>
9
+ <Route path="/" element={<Index />} />
10
+ <Route path="*" element={<NotFound />} />
11
+ </Routes>
12
+ </BrowserRouter>
13
+ );
14
+ };
@@ -1 +1 @@
1
- /// <reference types="vite/client" />
1
+ /// <reference types="vite/client" />
@@ -1,70 +0,0 @@
1
- {
2
- "name": "auth",
3
- "type": "registry:module",
4
- "title": "Authentication Module",
5
- "description": "Complete authentication system with Zustand store, JWT token management with auto-refresh, login/register/forgot-password pages, and header menu component. Includes secure token storage, automatic 401 handling, and seamless API integration.",
6
- "dependencies": [
7
- "zustand"
8
- ],
9
- "registryDependencies": [
10
- "api"
11
- ],
12
- "files": [
13
- {
14
- "path": "auth/index.ts",
15
- "type": "registry:index",
16
- "target": "$modules$/auth/index.ts",
17
- "content": "// Store\r\nexport { useAuthStore } from \"./auth-store\";\r\nexport type { User, AuthTokens } from \"./auth-store\";\r\n\r\n// Hook\r\nexport { useAuth } from \"./use-auth\";\r\n\r\n// Components\r\nexport { AuthHeaderMenu } from \"./auth-header-menu\";\r\nexport { default as LoginPage } from \"./login-page\";\r\nexport { default as RegisterPage } from \"./register-page\";\r\nexport { default as ForgotPasswordPage } from \"./forgot-password-page\";\r\n"
18
- },
19
- {
20
- "path": "auth/auth-store.ts",
21
- "type": "registry:store",
22
- "target": "$modules$/auth/auth-store.ts",
23
- "content": "import { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\n\nexport interface User {\n username: string;\n email?: string;\n}\n\nexport interface AuthTokens {\n accessToken: string;\n refreshToken?: string;\n idToken?: string;\n encryptionKey?: string;\n expiresAt?: number; // Unix timestamp in milliseconds\n}\n\ninterface AuthState {\n user: User | null;\n tokens: AuthTokens | null;\n isAuthenticated: boolean;\n setAuth: (user: User, tokens: AuthTokens) => void;\n updateTokens: (tokens: AuthTokens) => void;\n clearAuth: () => void;\n isTokenExpired: () => boolean;\n getTimeUntilExpiry: () => number | null;\n}\n\nexport const useAuthStore = create<AuthState>()(\n persist(\n (set, get) => ({\n user: null,\n tokens: null,\n isAuthenticated: false,\n\n setAuth: (user, tokens) => set({ user, tokens, isAuthenticated: true }),\n\n updateTokens: (tokens) => set({ tokens }),\n\n clearAuth: () =>\n set({ user: null, tokens: null, isAuthenticated: false }),\n\n isTokenExpired: () => {\n const { tokens } = get();\n if (!tokens?.expiresAt) return false;\n // Consider token expired 30 seconds before actual expiry for safety margin\n return Date.now() >= tokens.expiresAt - 30000;\n },\n\n getTimeUntilExpiry: () => {\n const { tokens } = get();\n if (!tokens?.expiresAt) return null;\n return tokens.expiresAt - Date.now();\n },\n }),\n { name: \"auth-storage\" },\n ),\n);\n"
24
- },
25
- {
26
- "path": "auth/use-auth.ts",
27
- "type": "registry:hook",
28
- "target": "$modules$/auth/use-auth.ts",
29
- "content": "import { useCallback, useEffect, useRef } from \"react\";\nimport {\n useAuthStore,\n type User,\n type AuthTokens,\n} from \"@/modules/auth/auth-store\";\nimport { customerClient } from \"@/modules/api/customer-client\";\n\n// Refresh token 1 minute before expiry\nconst REFRESH_BUFFER_MS = 60 * 1000;\n\nexport function useAuth() {\n const {\n user,\n tokens,\n isAuthenticated,\n setAuth,\n updateTokens,\n clearAuth,\n isTokenExpired,\n getTimeUntilExpiry,\n } = useAuthStore();\n\n const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const isRefreshingRef = useRef(false);\n\n // Refresh token using the refresh token\n const refreshAccessToken = useCallback(async (): Promise<boolean> => {\n const currentTokens = useAuthStore.getState().tokens;\n\n // Don't refresh if no refresh token exists\n if (!currentTokens?.refreshToken || isRefreshingRef.current) {\n console.log(\"⚠️ No refresh token available, skipping refresh\");\n return false;\n }\n\n isRefreshingRef.current = true;\n\n try {\n // Make a refresh request using the axios instance directly\n const response = await customerClient.axios.post<{\n accessToken: string;\n refreshToken?: string;\n expiresIn?: number;\n }>(\"/auth/refresh\", {\n refreshToken: currentTokens.refreshToken,\n });\n\n const { accessToken, refreshToken, expiresIn } = response.data;\n\n // Validate response has required data\n if (!accessToken) {\n console.error(\"❌ Refresh response missing accessToken\");\n return false;\n }\n\n const newTokens: AuthTokens = {\n accessToken,\n refreshToken: refreshToken || currentTokens.refreshToken,\n idToken: currentTokens.idToken, // Preserve existing idToken\n encryptionKey: currentTokens.encryptionKey, // Preserve existing encryptionKey\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,\n };\n\n customerClient.setToken(accessToken);\n updateTokens(newTokens);\n\n console.log(\"✅ Token refreshed successfully\");\n return true;\n } catch (error) {\n console.error(\"❌ Token refresh failed:\", error);\n // DON'T clear auth on refresh failure - just return false\n // User can still use their existing token until it expires\n return false;\n } finally {\n isRefreshingRef.current = false;\n }\n }, [updateTokens]);\n\n // Schedule automatic token refresh\n const scheduleTokenRefresh = useCallback(() => {\n // Clear any existing timeout\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n refreshTimeoutRef.current = null;\n }\n\n const timeUntilExpiry = getTimeUntilExpiry();\n\n // Only schedule if we have an expiry time and a refresh token\n if (timeUntilExpiry === null || !tokens?.refreshToken) {\n return;\n }\n\n // Calculate when to refresh (REFRESH_BUFFER_MS before expiry)\n const refreshIn = Math.max(timeUntilExpiry - REFRESH_BUFFER_MS, 0);\n\n // Don't schedule if expiry is too far in the future (> 24 hours)\n if (refreshIn > 24 * 60 * 60 * 1000) {\n return;\n }\n\n refreshTimeoutRef.current = setTimeout(async () => {\n const success = await refreshAccessToken();\n if (success) {\n // Reschedule for the new token\n scheduleTokenRefresh();\n }\n }, refreshIn);\n }, [getTimeUntilExpiry, tokens?.refreshToken, refreshAccessToken]);\n\n // Sync token with API client and set up refresh on mount and token changes\n useEffect(() => {\n if (tokens?.accessToken) {\n console.log(\"🔑 Setting token in API client\");\n customerClient.setToken(tokens.accessToken);\n\n // Only try to refresh if we have a refresh token AND token is expired\n if (isTokenExpired() && tokens.refreshToken) {\n console.log(\"⏰ Token expired, attempting refresh...\");\n refreshAccessToken().then((success) => {\n if (success) {\n scheduleTokenRefresh();\n } else {\n console.log(\"⚠️ Refresh failed, but keeping existing token\");\n }\n });\n } else if (tokens.refreshToken) {\n // Only schedule refresh if we have a refresh token\n scheduleTokenRefresh();\n }\n } else if (tokens && Object.keys(tokens).length === 0) {\n // tokens is empty object {} - this shouldn't happen, log it\n console.warn(\"⚠️ Tokens object is empty, this may indicate a bug\");\n } else {\n customerClient.setToken(null);\n }\n\n // Cleanup timeout on unmount\n return () => {\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n }\n };\n }, [\n tokens?.accessToken,\n tokens?.refreshToken,\n isTokenExpired,\n refreshAccessToken,\n scheduleTokenRefresh,\n ]);\n\n // Set up axios interceptor for 401 responses (token expired during request)\n useEffect(() => {\n const interceptorId = customerClient.axios.interceptors.response.use(\n (response) => response,\n async (error) => {\n const originalRequest = error.config;\n\n // Skip refresh for auth endpoints to prevent infinite loops\n const isAuthEndpoint = originalRequest?.url?.includes(\"/auth/\");\n\n // If we get a 401 and haven't retried yet, try to refresh\n if (\n error.response?.status === 401 &&\n !originalRequest._retry &&\n tokens?.refreshToken &&\n !isAuthEndpoint\n ) {\n originalRequest._retry = true;\n\n const success = await refreshAccessToken();\n if (success) {\n // Retry the original request with new token\n const newTokens = useAuthStore.getState().tokens;\n if (newTokens?.accessToken) {\n originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;\n return customerClient.axios(originalRequest);\n }\n }\n }\n\n return Promise.reject(error);\n },\n );\n\n return () => {\n customerClient.axios.interceptors.response.eject(interceptorId);\n };\n }, [tokens?.refreshToken, refreshAccessToken]);\n\n const login = useCallback(async (username: string, password: string) => {\n const response = await customerClient.auth.login({ username, password });\n\n console.log(\"🔐 Login response:\", response);\n console.log(\"🔐 accessToken:\", response.accessToken);\n console.log(\"🔐 refreshToken:\", response.refreshToken);\n console.log(\"🔐 encryptionKey:\", response.encryptionKey);\n\n const newTokens: AuthTokens = {\n accessToken: response.accessToken,\n refreshToken: response.refreshToken,\n idToken: response.idToken,\n encryptionKey: response.encryptionKey,\n expiresAt: response.expiresIn\n ? Date.now() + response.expiresIn * 1000\n : undefined,\n };\n\n console.log(\"🔐 newTokens object:\", newTokens);\n\n const newUser: User = {\n username,\n email: (response as any).email || (response as any).user?.email,\n };\n\n customerClient.setToken(newTokens.accessToken);\n setAuth(newUser, newTokens);\n\n console.log(\n \"🔐 Auth set complete, checking store:\",\n useAuthStore.getState().tokens,\n );\n }, []);\n\n const register = useCallback(\n async (username: string, email: string, password: string) => {\n await customerClient.auth.register({ username, email, password });\n },\n [],\n );\n\n const confirmEmail = useCallback(async (username: string, code: string) => {\n await customerClient.auth.confirm({ username, code });\n }, []);\n\n const forgotPassword = useCallback(async (username: string) => {\n await customerClient.auth.forgotPassword({ username });\n }, []);\n\n const resetPassword = useCallback(\n async (username: string, code: string, newPassword: string) => {\n await customerClient.auth.resetPassword({ username, code, newPassword });\n },\n [],\n );\n\n const logout = useCallback(() => {\n // Clear any scheduled refresh\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n refreshTimeoutRef.current = null;\n }\n\n customerClient.setToken(null);\n clearAuth();\n }, [clearAuth]);\n\n return {\n user,\n token: tokens?.accessToken ?? null,\n tokens,\n isAuthenticated,\n api: customerClient,\n login,\n register,\n confirmEmail,\n forgotPassword,\n resetPassword,\n logout,\n refreshAccessToken,\n };\n}\n"
30
- },
31
- {
32
- "path": "auth/auth-header-menu.tsx",
33
- "type": "registry:component",
34
- "target": "$modules$/auth/auth-header-menu.tsx",
35
- "content": "import type { ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { User, LogOut } from \"lucide-react\";\nimport { useAuth } from \"@/modules/auth/use-auth\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useTranslation } from \"react-i18next\";\nimport { toast } from \"sonner\";\n\ninterface AuthHeaderMenuProps {\n children?: ReactNode;\n variant: \"desktop\" | \"mobile\";\n onMenuClose?: () => void;\n}\n\nexport function AuthHeaderMenu({\n children,\n variant,\n onMenuClose,\n}: AuthHeaderMenuProps) {\n const { isAuthenticated, user, logout } = useAuth();\n const { t } = useTranslation(\"header\");\n\n const handleLogout = () => {\n logout();\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\n description: t(\n \"logoutToastDesc\",\n \"You have been logged out successfully.\",\n ),\n });\n onMenuClose?.();\n };\n\n if (variant === \"desktop\") {\n if (isAuthenticated) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\n <User className=\"h-5 w-5\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <DropdownMenuLabel className=\"font-normal\">\n <div className=\"flex flex-col space-y-1\">\n <p className=\"text-sm font-medium\">{user?.username}</p>\n {user?.email && (\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\n )}\n </div>\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n {children}\n {children && <DropdownMenuSeparator />}\n <DropdownMenuItem\n onClick={handleLogout}\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\n >\n <LogOut className=\"mr-2 h-4 w-4\" />\n {t(\"logout\", \"Logout\")}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n return (\n <Link to=\"/login\">\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\n <User className=\"h-5 w-5\" />\n </Button>\n </Link>\n );\n }\n\n // Mobile variant\n if (isAuthenticated) {\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\n <User className=\"h-5 w-5 text-primary\" />\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\n {user?.email && (\n <p className=\"text-xs text-muted-foreground truncate\">\n {user.email}\n </p>\n )}\n </div>\n </div>\n {children}\n <button\n onClick={handleLogout}\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\n >\n <LogOut className=\"h-5 w-5\" />\n <span>{t(\"logout\", \"Logout\")}</span>\n </button>\n </div>\n );\n }\n\n return (\n <Link\n to=\"/login\"\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\n onClick={onMenuClose}\n >\n <User className=\"h-5 w-5\" />\n <span>{t(\"login\", \"Login\")}</span>\n </Link>\n );\n}\n"
36
- },
37
- {
38
- "path": "auth/login-page.tsx",
39
- "type": "registry:page",
40
- "target": "$modules$/auth/login-page.tsx",
41
- "content": "import { useState, useEffect } from \"react\";\nimport { Link, useNavigate } from \"react-router\";\nimport { toast } from \"sonner\";\nimport { Layout } from \"@/components/Layout\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { useAuth } from \"@/modules/auth/use-auth\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { LogIn, Eye, EyeOff } from \"lucide-react\";\n\nexport default function LoginPage() {\n const { t } = useTranslation(\"login\");\n usePageTitle({ title: t(\"title\") });\n\n const navigate = useNavigate();\n const { login, isAuthenticated } = useAuth();\n\n const [formData, setFormData] = useState({\n username: \"\",\n password: \"\",\n });\n const [showPassword, setShowPassword] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Redirect when authenticated (works for both initial load and after login)\n useEffect(() => {\n if (isAuthenticated) {\n navigate(\"/\", { replace: true });\n }\n }, [isAuthenticated, navigate]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setError(null);\n\n try {\n await login(formData.username, formData.password);\n toast.success(t(\"toastSuccessTitle\", \"Welcome back!\"), {\n description: t(\"toastSuccessDesc\", \"You have successfully logged in.\"),\n });\n navigate(\"/\", { replace: true });\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"errorGeneric\"));\n setError(errorMessage);\n toast.error(t(\"toastErrorTitle\", \"Login failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Hero Section */}\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\n {t(\"description\")}\n </p>\n </div>\n\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <LogIn className=\"w-5 h-5 text-primary\" />\n {t(\"cardTitle\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <div>\n <Label htmlFor=\"username\">{t(\"username\")} *</Label>\n <Input\n id=\"username\"\n name=\"username\"\n type=\"text\"\n value={formData.username}\n onChange={handleChange}\n placeholder={t(\"usernamePlaceholder\")}\n required\n className=\"mt-1\"\n autoComplete=\"username\"\n />\n </div>\n\n <div>\n <div className=\"flex items-center justify-between\">\n <Label htmlFor=\"password\">{t(\"password\")} *</Label>\n <Link\n to=\"/forgot-password\"\n className=\"text-sm text-primary hover:underline\"\n >\n {t(\"forgotPassword\", \"Forgot password?\")}\n </Link>\n </div>\n <div className=\"relative\">\n <Input\n id=\"password\"\n name=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n value={formData.password}\n onChange={handleChange}\n placeholder={t(\"passwordPlaceholder\")}\n required\n className=\"mt-1 pr-10\"\n autoComplete=\"current-password\"\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n {showPassword ? (\n <EyeOff className=\"w-4 h-4\" />\n ) : (\n <Eye className=\"w-4 h-4\" />\n )}\n </button>\n </div>\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\n <p className=\"text-red-800 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"submitting\")}\n </>\n ) : (\n t(\"submit\")\n )}\n </Button>\n\n <div className=\"text-center text-sm text-muted-foreground\">\n {t(\"noAccount\")}{\" \"}\n <Link\n to=\"/register\"\n className=\"text-primary hover:underline font-medium\"\n >\n {t(\"registerLink\")}\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
42
- },
43
- {
44
- "path": "auth/register-page.tsx",
45
- "type": "registry:page",
46
- "target": "$modules$/auth/register-page.tsx",
47
- "content": "import { useState, useEffect } from \"react\";\nimport { Link, useNavigate } from \"react-router\";\nimport { toast } from \"sonner\";\nimport { Layout } from \"@/components/Layout\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { useAuth } from \"@/modules/auth/use-auth\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { UserPlus, Eye, EyeOff, CheckCircle } from \"lucide-react\";\n\nexport default function RegisterPage() {\n const { t } = useTranslation(\"register\");\n usePageTitle({ title: t(\"title\") });\n\n const navigate = useNavigate();\n const { register, isAuthenticated } = useAuth();\n\n const [formData, setFormData] = useState({\n username: \"\",\n email: \"\",\n password: \"\",\n confirmPassword: \"\",\n });\n const [showPassword, setShowPassword] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Redirect if already authenticated\n useEffect(() => {\n if (isAuthenticated) {\n navigate(\"/\", { replace: true });\n }\n }, [isAuthenticated, navigate]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setError(null);\n\n // Validate passwords match\n if (formData.password !== formData.confirmPassword) {\n setError(t(\"passwordMismatch\"));\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\n description: t(\"passwordMismatch\"),\n });\n setIsSubmitting(false);\n return;\n }\n\n try {\n await register(formData.username, formData.email, formData.password);\n setSuccess(true);\n toast.success(t(\"toastSuccessTitle\", \"Account created!\"), {\n description: t(\n \"toastSuccessDesc\",\n \"Please check your email to verify your account.\",\n ),\n });\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"errorGeneric\"));\n setError(errorMessage);\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n if (success) {\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"text-center space-y-4\">\n <CheckCircle className=\"w-16 h-16 text-green-500 mx-auto\" />\n <h2 className=\"text-2xl font-bold text-foreground\">\n {t(\"successTitle\")}\n </h2>\n <p className=\"text-muted-foreground\">\n {t(\"successMessage\")}\n </p>\n <Button asChild className=\"mt-4\">\n <Link to=\"/login\">{t(\"goToLogin\")}</Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Hero Section */}\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\n {t(\"description\")}\n </p>\n </div>\n\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <UserPlus className=\"w-5 h-5 text-primary\" />\n {t(\"cardTitle\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <div>\n <Label htmlFor=\"username\">{t(\"username\")} *</Label>\n <Input\n id=\"username\"\n name=\"username\"\n type=\"text\"\n value={formData.username}\n onChange={handleChange}\n placeholder={t(\"usernamePlaceholder\")}\n required\n className=\"mt-1\"\n autoComplete=\"username\"\n />\n </div>\n\n <div>\n <Label htmlFor=\"email\">{t(\"email\")} *</Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1\"\n autoComplete=\"email\"\n />\n </div>\n\n <div>\n <Label htmlFor=\"password\">{t(\"password\")} *</Label>\n <div className=\"relative\">\n <Input\n id=\"password\"\n name=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n value={formData.password}\n onChange={handleChange}\n placeholder={t(\"passwordPlaceholder\")}\n required\n className=\"mt-1 pr-10\"\n autoComplete=\"new-password\"\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n {showPassword ? (\n <EyeOff className=\"w-4 h-4\" />\n ) : (\n <Eye className=\"w-4 h-4\" />\n )}\n </button>\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"confirmPassword\">\n {t(\"confirmPassword\")} *\n </Label>\n <Input\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n type={showPassword ? \"text\" : \"password\"}\n value={formData.confirmPassword}\n onChange={handleChange}\n placeholder={t(\"confirmPasswordPlaceholder\")}\n required\n className=\"mt-1\"\n autoComplete=\"new-password\"\n />\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\n <p className=\"text-red-800 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"submitting\")}\n </>\n ) : (\n t(\"submit\")\n )}\n </Button>\n\n <div className=\"text-center text-sm text-muted-foreground\">\n {t(\"hasAccount\")}{\" \"}\n <Link\n to=\"/login\"\n className=\"text-primary hover:underline font-medium\"\n >\n {t(\"loginLink\")}\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
48
- },
49
- {
50
- "path": "auth/forgot-password-page.tsx",
51
- "type": "registry:page",
52
- "target": "$modules$/auth/forgot-password-page.tsx",
53
- "content": "import { useState } from \"react\";\nimport { Link } from \"react-router\";\nimport { toast } from \"sonner\";\nimport { Layout } from \"@/components/Layout\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { useAuth } from \"@/modules/auth/use-auth\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n CardDescription,\n} from \"@/components/ui/card\";\nimport { KeyRound, ArrowLeft, Eye, EyeOff, CheckCircle2 } from \"lucide-react\";\n\ntype Step = \"request\" | \"reset\" | \"success\";\n\nexport default function ForgotPasswordPage() {\n const { t } = useTranslation(\"forgotPassword\");\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\n\n const { forgotPassword, resetPassword } = useAuth();\n\n const [step, setStep] = useState<Step>(\"request\");\n const [username, setUsername] = useState(\"\");\n const [code, setCode] = useState(\"\");\n const [newPassword, setNewPassword] = useState(\"\");\n const [confirmPassword, setConfirmPassword] = useState(\"\");\n const [showPassword, setShowPassword] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const handleRequestCode = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setError(null);\n\n try {\n await forgotPassword(username);\n toast.success(t(\"codeSentTitle\", \"Code Sent!\"), {\n description: t(\n \"codeSentDesc\",\n \"A password reset code has been sent to your email.\",\n ),\n });\n setStep(\"reset\");\n } catch (err) {\n const errorMessage = getErrorMessage(\n err,\n t(\"errorGeneric\", \"Failed to send reset code. Please try again.\"),\n );\n setError(errorMessage);\n toast.error(t(\"errorTitle\", \"Error\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleResetPassword = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n // Validate passwords match\n if (newPassword !== confirmPassword) {\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\n return;\n }\n\n setIsSubmitting(true);\n\n try {\n await resetPassword(username, code, newPassword);\n toast.success(t(\"resetSuccessTitle\", \"Password Reset!\"), {\n description: t(\n \"resetSuccessDesc\",\n \"Your password has been successfully reset.\",\n ),\n });\n setStep(\"success\");\n } catch (err) {\n const errorMessage = getErrorMessage(\n err,\n t(\"errorResetGeneric\", \"Failed to reset password. Please try again.\"),\n );\n setError(errorMessage);\n toast.error(t(\"errorTitle\", \"Error\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // Success step\n if (step === \"success\") {\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardContent className=\"pt-8 pb-8 text-center\">\n <CheckCircle2 className=\"w-16 h-16 text-green-500 mx-auto mb-4\" />\n <h1 className=\"text-2xl font-bold mb-2\">\n {t(\"successTitle\", \"Password Reset Successfully!\")}\n </h1>\n <p className=\"text-muted-foreground mb-6\">\n {t(\n \"successDescription\",\n \"Your password has been changed. You can now login with your new password.\",\n )}\n </p>\n <Button asChild className=\"w-full\">\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Hero Section */}\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\", \"Forgot Password\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\n {step === \"request\"\n ? t(\n \"descriptionRequest\",\n \"Enter your username and we'll send you a code to reset your password.\",\n )\n : t(\n \"descriptionReset\",\n \"Enter the code sent to your email and your new password.\",\n )}\n </p>\n </div>\n\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <KeyRound className=\"w-5 h-5 text-primary\" />\n {step === \"request\"\n ? t(\"cardTitleRequest\", \"Request Reset Code\")\n : t(\"cardTitleReset\", \"Reset Password\")}\n </CardTitle>\n <CardDescription>\n {step === \"request\"\n ? t(\"cardDescRequest\", \"Step 1 of 2: Request a reset code\")\n : t(\n \"cardDescReset\",\n \"Step 2 of 2: Enter code and new password\",\n )}\n </CardDescription>\n </CardHeader>\n <CardContent>\n {step === \"request\" ? (\n // Step 1: Request Code\n <form onSubmit={handleRequestCode} className=\"space-y-6\">\n <div>\n <Label htmlFor=\"username\">\n {t(\"username\", \"Username\")} *\n </Label>\n <Input\n id=\"username\"\n type=\"text\"\n value={username}\n onChange={(e) => setUsername(e.target.value)}\n placeholder={t(\n \"usernamePlaceholder\",\n \"Enter your username\",\n )}\n required\n className=\"mt-1\"\n autoComplete=\"username\"\n />\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\n <p className=\"text-red-800 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\", \"Sending...\")}\n </>\n ) : (\n t(\"sendCode\", \"Send Reset Code\")\n )}\n </Button>\n\n <div className=\"text-center\">\n <Link\n to=\"/login\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1\"\n >\n <ArrowLeft className=\"w-4 h-4\" />\n {t(\"backToLogin\", \"Back to Login\")}\n </Link>\n </div>\n </form>\n ) : (\n // Step 2: Reset Password\n <form onSubmit={handleResetPassword} className=\"space-y-6\">\n <div className=\"p-3 bg-muted rounded-lg text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"codeFor\", \"Reset code for:\")}{\" \"}\n </span>\n <span className=\"font-medium\">{username}</span>\n </div>\n\n <div>\n <Label htmlFor=\"code\">{t(\"code\", \"Reset Code\")} *</Label>\n <Input\n id=\"code\"\n type=\"text\"\n value={code}\n onChange={(e) => setCode(e.target.value)}\n placeholder={t(\"codePlaceholder\", \"Enter 6-digit code\")}\n required\n className=\"mt-1\"\n maxLength={6}\n />\n </div>\n\n <div>\n <Label htmlFor=\"newPassword\">\n {t(\"newPassword\", \"New Password\")} *\n </Label>\n <div className=\"relative\">\n <Input\n id=\"newPassword\"\n type={showPassword ? \"text\" : \"password\"}\n value={newPassword}\n onChange={(e) => setNewPassword(e.target.value)}\n placeholder={t(\n \"newPasswordPlaceholder\",\n \"Enter new password\",\n )}\n required\n className=\"mt-1 pr-10\"\n autoComplete=\"new-password\"\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n {showPassword ? (\n <EyeOff className=\"w-4 h-4\" />\n ) : (\n <Eye className=\"w-4 h-4\" />\n )}\n </button>\n </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n {t(\n \"passwordRequirements\",\n \"At least 8 characters, 1 letter and 1 number\",\n )}\n </p>\n </div>\n\n <div>\n <Label htmlFor=\"confirmPassword\">\n {t(\"confirmPassword\", \"Confirm Password\")} *\n </Label>\n <Input\n id=\"confirmPassword\"\n type={showPassword ? \"text\" : \"password\"}\n value={confirmPassword}\n onChange={(e) => setConfirmPassword(e.target.value)}\n placeholder={t(\n \"confirmPasswordPlaceholder\",\n \"Confirm new password\",\n )}\n required\n className=\"mt-1\"\n autoComplete=\"new-password\"\n />\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\n <p className=\"text-red-800 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"resetting\", \"Resetting...\")}\n </>\n ) : (\n t(\"resetPassword\", \"Reset Password\")\n )}\n </Button>\n\n <div className=\"flex justify-between\">\n <button\n type=\"button\"\n onClick={() => {\n setStep(\"request\");\n setCode(\"\");\n setNewPassword(\"\");\n setConfirmPassword(\"\");\n setError(null);\n }}\n className=\"text-sm text-muted-foreground hover:text-primary\"\n >\n {t(\"changeUsername\", \"Change username\")}\n </button>\n <button\n type=\"button\"\n onClick={() =>\n handleRequestCode({\n preventDefault: () => {},\n } as React.FormEvent)\n }\n className=\"text-sm text-primary hover:underline\"\n disabled={isSubmitting}\n >\n {t(\"resendCode\", \"Resend code\")}\n </button>\n </div>\n </form>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
54
- }
55
- ],
56
- "exports": {
57
- "types": [
58
- "AuthTokens",
59
- "User"
60
- ],
61
- "variables": [
62
- "AuthHeaderMenu",
63
- "ForgotPasswordPage",
64
- "LoginPage",
65
- "RegisterPage",
66
- "useAuth",
67
- "useAuthStore"
68
- ]
69
- }
70
- }
@@ -1,36 +0,0 @@
1
- # Reset Password Page
2
-
3
- Split-screen password reset page with form on the left and full-height image on the right. Features new password input with confirmation, validates reset code from URL. Clean minimal design with API integration and responsive layout.
4
-
5
- ## Files
6
-
7
- | Target | Type |
8
- |--------|------|
9
- | `$modules$/reset-password-page/index.ts` | index |
10
- | `$modules$/reset-password-page/reset-password-page.tsx` | page |
11
- | `$modules$/reset-password-page/lang/en.json` | lang |
12
- | `$modules$/reset-password-page/lang/tr.json` | lang |
13
-
14
- ## Usage
15
-
16
- ```
17
- import ResetPasswordPage from '@/modules/reset-password-page';
18
-
19
- <ResetPasswordPage
20
- image="/images/reset-bg.jpg"
21
- />
22
-
23
- • Installed at: src/modules/reset-password-page/
24
- • Customize text: src/modules/reset-password-page/lang/*.json
25
- • Uses customerClient.auth.resetPassword() for API
26
- • Expects ?code= URL parameter from email link
27
- • Add to your router as a page component
28
- ```
29
-
30
- ## Dependencies
31
-
32
- This component requires:
33
- - `button`
34
- - `input`
35
- - `auth`
36
- - `api`
@@ -1,39 +0,0 @@
1
- {
2
- "name": "reset-password-page",
3
- "type": "registry:page",
4
- "title": "Reset Password Page",
5
- "description": "Split-screen password reset page with form on the left and full-height image on the right. Features new password input with confirmation, validates reset code from URL. Clean minimal design with API integration and responsive layout.",
6
- "registryDependencies": [
7
- "button",
8
- "input",
9
- "auth",
10
- "api"
11
- ],
12
- "usage": "import ResetPasswordPage from '@/modules/reset-password-page';\n\n<ResetPasswordPage\n image=\"/images/reset-bg.jpg\"\n/>\n\n• Installed at: src/modules/reset-password-page/\n• Customize text: src/modules/reset-password-page/lang/*.json\n• Uses customerClient.auth.resetPassword() for API\n• Expects ?code= URL parameter from email link\n• Add to your router as a page component",
13
- "route": {
14
- "path": "/reset-password",
15
- "componentName": "ResetPasswordPage"
16
- },
17
- "files": [
18
- {
19
- "path": "reset-password-page/index.ts",
20
- "type": "registry:index",
21
- "target": "$modules$/reset-password-page/index.ts"
22
- },
23
- {
24
- "path": "reset-password-page/reset-password-page.tsx",
25
- "type": "registry:page",
26
- "target": "$modules$/reset-password-page/reset-password-page.tsx"
27
- },
28
- {
29
- "path": "reset-password-page/lang/en.json",
30
- "type": "registry:lang",
31
- "target": "$modules$/reset-password-page/lang/en.json"
32
- },
33
- {
34
- "path": "reset-password-page/lang/tr.json",
35
- "type": "registry:lang",
36
- "target": "$modules$/reset-password-page/lang/tr.json"
37
- }
38
- ]
39
- }