@promakeai/cli 0.2.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -494,7 +494,7 @@ constants.json (full)`)),console.log(C.dim(JSON.stringify(B.constantsConfig,null
494
494
  `)),D6("promake create <name>","Create new project"),D6("promake add <modules...>","Add modules"),D6("promake add <page> --module-name <name>","Add page with custom name"),D6("promake remove <modules...>","Remove modules"),D6("promake sync","Sync from promake.json"),D6("promake theme --list","List theme options"),D6("promake theme --preset <name>","Apply color preset"),D6("promake list","List available modules"),D6("promake doctor","Check project health"),D6("promake doctor --runtime","Include runtime checks"),D6("promake discover --json","Project info for AI"),D6("promake seo","Sync SEO meta tags"),D6("promake usage","Show this guide"),D6("promake <command> --help","Command-specific help"),console.log()}var br=new $5("info").description("Show detailed information about a module").argument("<module>","Module name").option("--json","Output as JSON").action(async(D,X)=>{let Z=E2(`Fetching ${D}...`).start();try{let J=await E4(D);if(Z.stop(),!J)console.error(C.red(`Module not found: ${D}`)),process.exit(1);if(X.json){console.log(JSON.stringify(J,null,2));return}if(console.log(""),console.log(C.bold.cyan(J.name)),console.log(C.dim("─".repeat(J.name.length+4))),J.title)console.log(`${C.gray("Title:")} ${J.title}`);if(console.log(`${C.gray("Type:")} ${J.type.replace("registry:","")}`),J.description)console.log(`${C.gray("Description:")} ${J.description}`);if(J.registryDependencies?.length)console.log(""),console.log(C.gray("Dependencies:")),J.registryDependencies.forEach((Y)=>{let $=!Y.includes("-")||["button","card","input","badge","dialog","sheet","dropdown-menu","accordion","tabs","select","checkbox","slider","separator","avatar","tooltip","popover","navigation-menu","scroll-area"].includes(Y)?C.dim("(shadcn)"):"";console.log(` ${C.yellow("•")} ${Y} ${$}`)});if(J.dependencies?.length)console.log(""),console.log(C.gray("NPM Packages:")),J.dependencies.forEach((Y)=>{console.log(` ${C.blue("•")} ${Y}`)});if(J.files?.length)console.log(""),console.log(C.gray("Files:")),J.files.forEach((Y)=>{console.log(` ${C.dim("•")} ${Y.path}`)});if(J.exports){if(console.log(""),console.log(C.gray("Exports:")),J.exports.types?.length)console.log(` ${C.magenta("Types:")} ${J.exports.types.join(", ")}`);if(J.exports.variables?.length)console.log(` ${C.green("Components:")} ${J.exports.variables.join(", ")}`)}if(J.route)console.log(""),console.log(C.gray("Route:")),console.log(` ${C.cyan("Path:")} ${J.route.path}`),console.log(` ${C.cyan("Component:")} ${J.route.componentName}`);if(J.usage)console.log(""),console.log(C.gray("Usage:")),console.log(C.dim("─".repeat(40))),console.log(J.usage);console.log("")}catch(J){if(Z.fail("Failed to fetch module info"),J instanceof Error)console.error(C.red(J.message));process.exit(1)}});import r91 from"path";var vK=k1(r8(),1);import{execSync as Px0}from"child_process";import xq from"path";async function rE(D,X=!1){let Z=Date.now(),J=[],Y=xq.join(D,"tsconfig.json");if(!await vK.default.pathExists(Y))return{name:"TypeScript",status:"skip",severity:"info",duration:Date.now()-Z,items:[],summary:"No tsconfig.json found"};let Q=xq.join(D,"package.json"),$=!1;if(await vK.default.pathExists(Q))$=!!(await vK.default.readJson(Q)).scripts?.check;let K=X$(D),G=K==="npm"?"npm run":`${K} run`;try{let F=$?`${G} check 2>&1`:"npx tsc --noEmit 2>&1";if(!X)console.log(C.dim(` $ ${$?`${G} check`:"tsc --noEmit"}`));let W=Px0(F,{cwd:D,encoding:"utf-8",stdio:"pipe"});if(!X&&W.trim())console.log(C.dim(W));return{name:"TypeScript",status:"pass",severity:"error",duration:Date.now()-Z,items:[],summary:"No type errors"}}catch(F){let W=F.stdout||F.stderr||F.message||"";if(W.includes("This is not the tsc command")||W.includes("typescript"))return{name:"TypeScript",status:"skip",severity:"info",duration:Date.now()-Z,items:[],summary:"TypeScript not installed"};if(!X&&W.trim())console.log(C.dim(W));return J.push(...wx0(W,D)),{name:"TypeScript",status:"fail",severity:"error",duration:Date.now()-Z,items:J,summary:`${J.length} type error(s)`}}}function wx0(D,X){let Z=[],J=/^(.+)\((\d+),(\d+)\):\s*(error|warning)\s*(TS\d+):\s*(.+)$/gm,Y=/^(.+):(\d+):(\d+)\s*-\s*(error|warning)\s*(TS\d+):\s*(.+)$/gm;for(let $ of[J,Y]){let K;while((K=$.exec(D))!==null){let G=K[1].trim();Z.push({file:xq.isAbsolute(G)?xq.relative(X,G):G,line:parseInt(K[2]),column:parseInt(K[3]),code:K[5],message:K[6]})}}let Q=new Set;return Z.filter(($)=>{let K=`${$.file}:${$.line}:${$.column}:${$.message}`;if(Q.has(K))return!1;return Q.add(K),!0})}var nD=k1(r8(),1);import{execSync as Sx0}from"child_process";import bK from"path";var _x0=["eslint.config.js","eslint.config.mjs","eslint.config.cjs",".eslintrc.js",".eslintrc.cjs",".eslintrc.json",".eslintrc.yml",".eslintrc.yaml",".eslintrc"];async function kx0(D){for(let Z of _x0)if(await nD.default.pathExists(bK.join(D,Z)))return!0;let X=bK.join(D,"package.json");if(await nD.default.pathExists(X))try{if((await nD.default.readJson(X)).eslintConfig)return!0}catch{}return!1}async function sE(D,X=!1,Z=!1){let J=Date.now();if(!await kx0(D))return{name:"ESLint",status:"skip",severity:"warning",duration:Date.now()-J,items:[],summary:"No ESLint config found"};let Y=bK.join(D,"package.json"),Q=!1;if(await nD.default.pathExists(Y))Q=!!(await nD.default.readJson(Y)).scripts?.lint;let $=X$(D),K=$==="npm"?"npm run":`${$} run`,G=X?" -- --fix":"";try{let F=Q?`${K} lint${G} 2>&1`:`npx eslint .${X?" --fix":""} 2>&1`;if(!Z)console.log(C.dim(` $ ${Q?`${K} lint${G}`:`eslint .${X?" --fix":""}`}`));let W=Sx0(F,{cwd:D,encoding:"utf-8",stdio:"pipe",maxBuffer:10485760});if(!Z&&W.trim())console.log(C.dim(W));return{name:"ESLint",status:"pass",severity:"warning",duration:Date.now()-J,items:[],summary:"No linting issues"}}catch(F){let W=F.stdout||F.stderr||F.message||"";if(W.includes("eslint: not found")||W.includes("'eslint' is not recognized"))return{name:"ESLint",status:"skip",severity:"info",duration:Date.now()-J,items:[],summary:"ESLint not installed"};if(!Z&&W.trim())console.log(C.dim(W));let V=hx0(W,D);if(V.length===0&&W.includes("error"))return{name:"ESLint",status:"fail",severity:"warning",duration:Date.now()-J,items:[{message:"ESLint encountered an error"}],summary:"ESLint error"};return{name:"ESLint",status:V.length>0?"fail":"pass",severity:"warning",duration:Date.now()-J,items:V,summary:V.length>0?`${V.length} linting issue(s)`:"No linting issues"}}}function hx0(D,X){let Z=[],J=/^([^\s].*\.(ts|tsx|js|jsx))$/gm,Y=/^\s+(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)?$/gm,Q=null,$=D.split(`
495
495
  `);for(let K of $){let G=K.match(/^([^\s].*\.(ts|tsx|js|jsx))$/);if(G){Q=G[1];continue}let F=K.match(/^\s+(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)?$/);if(F&&Q)Z.push({file:bK.isAbsolute(Q)?bK.relative(X,Q):Q,line:parseInt(F[1]),column:parseInt(F[2]),code:F[5]||void 0,message:F[4].trim()})}return Z}var yq=k1(r8(),1);import{execSync as xx0}from"child_process";import fK from"path";async function aE(D){let X=Date.now(),Z=["vite.config.ts","vite.config.js","vite.config.mjs"],J=!1;for(let Y of Z)if(await yq.default.pathExists(fK.join(D,Y))){J=!0;break}if(!J)return{name:"Build",status:"skip",severity:"error",duration:Date.now()-X,items:[],summary:"No vite.config found"};try{xx0("npx vite build 2>&1",{cwd:D,encoding:"utf-8",stdio:"pipe",env:{...process.env,NODE_ENV:"production"}});let Y=fK.join(D,"dist");if(await yq.default.pathExists(Y))await yq.default.remove(Y);return{name:"Build",status:"pass",severity:"error",duration:Date.now()-X,items:[],summary:"Build succeeded"}}catch(Y){let Q=Y.stdout||Y.stderr||Y.message||"",$=yx0(Q,D);return{name:"Build",status:"fail",severity:"error",duration:Date.now()-X,items:$.length>0?$:[{message:"Build failed"}],summary:"Build failed"}}}function yx0(D,X){let Z=[],J=D.split(`
496
496
  `),Y=[/^(.+):(\d+):(\d+):\s*(.+)$/,/\[vite\]:\s*Error:\s*(.+)/,/Error:\s*(.+?)\s+at\s+(.+):(\d+):(\d+)/,/Could not resolve ["']([^"']+)["']\s+from\s+["']([^"']+)["']/,/RollupError:\s*(.+)/];for(let $=0;$<J.length;$++){let K=J[$].trim();if(!K)continue;if(K.includes("building for production")||K.includes("transforming")||K.includes("rendering chunks")||K.includes("computing gzip")||K.startsWith("✓")||K.startsWith("vite v"))continue;for(let G of Y){let F=K.match(G);if(F){if(G.source.includes("Could not resolve"))Z.push({file:fK.relative(X,F[2]),message:`Cannot find module '${F[1]}'`,suggestion:"Check if the module is installed or the path is correct"});else if(F.length===5)Z.push({file:fK.isAbsolute(F[1]||F[2])?fK.relative(X,F[1]||F[2]):F[1]||F[2],line:parseInt(F[2]||F[3]),column:parseInt(F[3]||F[4]),message:F[4]||F[1]});else Z.push({message:F[1]||K});break}}if(Z.length===0&&(K.toLowerCase().includes("error")||K.toLowerCase().includes("failed")))Z.push({message:K})}let Q=new Set;return Z.filter(($)=>{let K=`${$.file}:${$.line}:${$.message}`;if(Q.has(K))return!1;return Q.add(K),!0})}var lS=k1(r8(),1);import DF from"path";var y91=[process.env["PROGRAMFILES(X86)"]&&DF.join(process.env["PROGRAMFILES(X86)"],"Google","Chrome","Application","chrome.exe"),process.env.PROGRAMFILES&&DF.join(process.env.PROGRAMFILES,"Google","Chrome","Application","chrome.exe"),process.env.LOCALAPPDATA&&DF.join(process.env.LOCALAPPDATA,"Google","Chrome","Application","chrome.exe"),process.env["PROGRAMFILES(X86)"]&&DF.join(process.env["PROGRAMFILES(X86)"],"Microsoft","Edge","Application","msedge.exe"),process.env.PROGRAMFILES&&DF.join(process.env.PROGRAMFILES,"Microsoft","Edge","Application","msedge.exe")].filter(Boolean),v91=["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge","/Applications/Chromium.app/Contents/MacOS/Chromium"],b91=["/usr/bin/google-chrome","/usr/bin/google-chrome-stable","/usr/bin/chromium-browser","/usr/bin/chromium","/snap/bin/chromium"];async function y$0(){let D=await f91();if(!D)return null;try{return{type:"chrome",browser:await(await Promise.resolve().then(() => (x$0(),h$0))).default.launch({executablePath:D,headless:!0,args:["--no-sandbox","--disable-setuid-sandbox","--disable-gpu"]})}}catch{return null}}async function f91(){if(process.env.CHROME_PATH){if(await lS.default.pathExists(process.env.CHROME_PATH))return process.env.CHROME_PATH}let D;switch(process.platform){case"win32":D=y91;break;case"darwin":D=v91;break;default:D=b91}for(let X of D)if(await lS.default.pathExists(X))return X;return null}async function v$0(D){try{await D.browser.close()}catch{}}async function pS(D,X,Z="/"){let J=Date.now();if(!await g91(D))return{name:"Runtime",status:"skip",severity:"error",duration:Date.now()-J,items:[{message:`No server running on port ${D}`,suggestion:"Start dev server first: npm run dev (or use --port to specify different port)"}],summary:`Skipped (no server on port ${D})`};let Q=null;try{Q=await y$0()}catch{}if(!Q)return{name:"Runtime",status:"skip",severity:"info",duration:Date.now()-J,items:[{message:"No browser available (Chrome/Chromium/Edge not found)",suggestion:"Set CHROME_PATH environment variable or install Chrome/Edge"}],summary:"Skipped (no browser)"};let $=[],{browser:K,type:G}=Q;try{let W=await K.newPage();W.on("console",(H)=>{let q=H.type();if(q==="error"||q==="warning"){let U=H.location();$.push({type:q==="warning"?"warning":"console",message:H.text(),fileName:U?.url,lineNumber:U?.lineNumber,columnNumber:U?.columnNumber})}}),W.on("pageerror",(H)=>{$.push({type:"error",message:H.message,stack:H.stack})});let V=Z.startsWith("/")?Z:`/${Z}`;await W.goto(`http://localhost:${D}${V}`,{waitUntil:"networkidle0",timeout:X}),await u91(3000);let z=await W.evaluate(()=>{return window.__earlyErrors||[]});$.push(...z)}catch(W){let V=W.message||"Unknown error";if(V.includes("timeout")||V.includes("Timeout"))$.push({type:"error",message:"Page load timeout - app may have crashed or is unresponsive"});else if(V.includes("net::ERR"))$.push({type:"error",message:`Network error: ${V}`});else $.push({type:"error",message:`Runtime check error: ${V}`})}finally{await v$0(Q)}let F=m91($);return{name:`Runtime (${G})`,status:F.length>0?"fail":"pass",severity:"error",duration:Date.now()-J,items:F,summary:F.length>0?`${F.length} runtime error(s)`:"No runtime errors"}}async function g91(D){try{let X=await fetch(`http://localhost:${D}`,{method:"HEAD",signal:AbortSignal.timeout(3000)});return X.ok||X.status<500}catch{return!1}}function u91(D){return new Promise((X)=>setTimeout(X,D))}function m91(D){return D.map((X)=>{let Z=[];for(let[J,Y]of Object.entries(X))if(Y!==void 0&&Y!==null)Z.push(`${J}: ${Y}`);return{message:Z.join(`
497
- `)}})}function iS(D,X){console.log(""),console.log(C.bold("Health Check")+C.dim(` (${o91(D.duration)})`)),console.log(C.dim("─".repeat(40)));for(let Z of D.checks)c91(Z,X);console.log(C.dim("─".repeat(40))),n91(D)}function c91(D,X){let Z=p91(D.status),J=i91(D.status,D.severity),Y=C.dim(`${D.duration}ms`);console.log(`${Z} ${J(D.name)} ${Y} ${C.dim("·")} ${C.dim(D.summary||"")}`);let Q=D.name.startsWith("Runtime"),$=X||Q?D.items:D.items.slice(0,3),K=D.items.length-$.length;for(let G of $)d91(G,D.severity);if(K>0)console.log(C.dim(` ... +${K} more (--verbose)`))}function d91(D,X){let Z=X==="error"?C.red:C.yellow,J=l91(D),Y=J?C.cyan(J)+" ":"";console.log(` ${Z("›")} ${Y}${D.message}`)}function l91(D){if(!D.file)return"";let X=D.file;if(D.line!==void 0){if(X+=`:${D.line}`,D.column!==void 0)X+=`:${D.column}`}return X}function p91(D){switch(D){case"pass":return C.green("✓");case"fail":return C.red("✗");case"skip":return C.dim("○");default:return C.dim("?")}}function i91(D,X){if(D==="fail")return X==="error"?C.red:C.yellow;if(D==="pass")return C.green;return C.dim}function n91(D){let{summary:X}=D,Z=[];if(X.passed>0)Z.push(C.green(`${X.passed} passed`));if(X.failed>0)Z.push(C.red(`${X.failed} failed`));if(X.skipped>0)Z.push(C.dim(`${X.skipped} skipped`));if(console.log(Z.join(C.dim(" · "))),X.errors>0)console.log(C.red(`${X.errors} error(s) need attention`));else if(X.warnings>0)console.log(C.yellow(`${X.warnings} warning(s) to review`));else console.log(C.green("All good!"))}function o91(D){if(D<1000)return`${D}ms`;return`${(D/1000).toFixed(1)}s`}var b$0=new $5("doctor").description("Analyze project for issues (TypeScript, ESLint, build, runtime)").option("--cwd <path>","Working directory").option("--port <number>","Vite dev server port","5174").option("--fix","Auto-fix ESLint issues where possible").option("--runtime","Run runtime error detection (requires running dev server)").option("--runtime-timeout <ms>","Runtime check timeout in ms","10000").option("--route <path>","Route path for runtime check","/").option("--json","Output as JSON (for CI/CD)").option("-v, --verbose","Show all details including all errors").option("-q, --quiet","Hide command output logs (only show summary)").option("--no-typecheck","Skip TypeScript type checking").option("--no-lint","Skip ESLint checking").option("--no-build","Skip Vite build checking").action(async(D)=>{let X=D.cwd||process.cwd(),Z=parseInt(D.port||"5173"),J=parseInt(D.runtimeTimeout||"10000"),Y=D.route||"/",Q=Date.now(),$=D.quiet||D.json||!1;if(!await T2(X)&&!D.json)console.log(C.yellow("Warning: No promake.json found. Running checks anyway..."));let G=D.json?null:E2("Running health checks...").start(),F=[];if(D.typecheck!==!1)F.push(()=>rE(X,$));if(D.lint!==!1)F.push(()=>sE(X,D.fix,$));if(D.build!==!1)F.push(()=>aE(X));if(D.runtime)F.push(()=>pS(Z,J,Y));let W=[];try{if(W=await s91(F,1),G)G.stop()}catch(z){if(G)G.fail("Health check failed");if(!D.json)console.error(C.red(z.message));process.exit(1)}let V={project:r91.basename(X),timestamp:new Date().toISOString(),duration:Date.now()-Q,checks:W,summary:{passed:W.filter((z)=>z.status==="pass").length,failed:W.filter((z)=>z.status==="fail").length,skipped:W.filter((z)=>z.status==="skip").length,errors:W.filter((z)=>z.status==="fail"&&z.severity==="error").reduce((z,H)=>z+H.items.length,0),warnings:W.filter((z)=>z.status==="fail"&&z.severity==="warning").reduce((z,H)=>z+H.items.length,0)}};if(D.json)console.log(JSON.stringify(V,null,2));else iS(V,D.verbose||!1);if(V.summary.errors>0)process.exit(1)});async function s91(D,X){let Z=[],J=0;async function Y(){let $=J++;if($>=D.length)return;Z[$]=await D[$](),await Y()}let Q=Array(Math.min(X,D.length)).fill(null).map(()=>Y());return await Promise.all(Q),Z}var f$0={name:"@promakeai/cli",version:"0.2.13",type:"module",bin:{promake:"dist/index.js"},files:["dist/index.js","dist/registry","template"],scripts:{dev:"bun run src/index.ts","dev:app":"cd dev && bun run dev","dev:populate":"bun run scripts/populate-dev.ts","dev:fresh":"bun run dev:populate && bun run dev:app","playground:create":"rm -rf playground && bun run dev -- create playground --template empty --pm bun","playground:reset":"bun run playground:create","playground:add":"cd playground && bun run ../src/index.ts add","playground:ecommerce":"rm -rf playground && bun run dev -- create playground --template ecommerce --pm bun",build:"bun run build:cli && bun run build:registry","build:cli":"bun build src/index.ts --outdir dist --target node --minify","build:registry":"bun run scripts/build-registry.ts",typecheck:"tsc --noEmit",prepublishOnly:"bun run build",test:"bun test","test:watch":"bun test --watch","test:coverage":"bun test --coverage",release:"bun run build && npm publish --access public"},dependencies:{"adm-zip":"^0.5.16",archiver:"^7.0.1",chalk:"^5.3.0",commander:"^12.1.0",culori:"^4.0.2",dotenv:"^17.2.3","fs-extra":"^11.2.0",glob:"^11.0.0",ora:"^8.1.1",prompts:"^2.4.2","puppeteer-core":"^24.36.0"},devDependencies:{"@types/archiver":"^7.0.0","@types/bun":"^1.1.14","@types/culori":"^4.0.1","@types/fs-extra":"^11.0.4","@types/node":"^22.10.2","@types/prompts":"^2.4.9",typescript:"^5.7.2"}};var P8=new $5;P8.name("promake").description(`Modular React template CLI - Build React apps with pre-built components
497
+ `)}})}function iS(D,X){console.log(""),console.log(C.bold("Health Check")+C.dim(` (${o91(D.duration)})`)),console.log(C.dim("─".repeat(40)));for(let Z of D.checks)c91(Z,X);console.log(C.dim("─".repeat(40))),n91(D)}function c91(D,X){let Z=p91(D.status),J=i91(D.status,D.severity),Y=C.dim(`${D.duration}ms`);console.log(`${Z} ${J(D.name)} ${Y} ${C.dim("·")} ${C.dim(D.summary||"")}`);let Q=D.name.startsWith("Runtime"),$=X||Q?D.items:D.items.slice(0,3),K=D.items.length-$.length;for(let G of $)d91(G,D.severity);if(K>0)console.log(C.dim(` ... +${K} more (--verbose)`))}function d91(D,X){let Z=X==="error"?C.red:C.yellow,J=l91(D),Y=J?C.cyan(J)+" ":"";console.log(` ${Z("›")} ${Y}${D.message}`)}function l91(D){if(!D.file)return"";let X=D.file;if(D.line!==void 0){if(X+=`:${D.line}`,D.column!==void 0)X+=`:${D.column}`}return X}function p91(D){switch(D){case"pass":return C.green("✓");case"fail":return C.red("✗");case"skip":return C.dim("○");default:return C.dim("?")}}function i91(D,X){if(D==="fail")return X==="error"?C.red:C.yellow;if(D==="pass")return C.green;return C.dim}function n91(D){let{summary:X}=D,Z=[];if(X.passed>0)Z.push(C.green(`${X.passed} passed`));if(X.failed>0)Z.push(C.red(`${X.failed} failed`));if(X.skipped>0)Z.push(C.dim(`${X.skipped} skipped`));if(console.log(Z.join(C.dim(" · "))),X.errors>0)console.log(C.red(`${X.errors} error(s) need attention`));else if(X.warnings>0)console.log(C.yellow(`${X.warnings} warning(s) to review`));else console.log(C.green("All good!"))}function o91(D){if(D<1000)return`${D}ms`;return`${(D/1000).toFixed(1)}s`}var b$0=new $5("doctor").description("Analyze project for issues (TypeScript, ESLint, build, runtime)").option("--cwd <path>","Working directory").option("--port <number>","Vite dev server port","5174").option("--fix","Auto-fix ESLint issues where possible").option("--runtime","Run runtime error detection (requires running dev server)").option("--runtime-timeout <ms>","Runtime check timeout in ms","10000").option("--route <path>","Route path for runtime check","/").option("--json","Output as JSON (for CI/CD)").option("-v, --verbose","Show all details including all errors").option("-q, --quiet","Hide command output logs (only show summary)").option("--no-typecheck","Skip TypeScript type checking").option("--no-lint","Skip ESLint checking").option("--no-build","Skip Vite build checking").action(async(D)=>{let X=D.cwd||process.cwd(),Z=parseInt(D.port||"5173"),J=parseInt(D.runtimeTimeout||"10000"),Y=D.route||"/",Q=Date.now(),$=D.quiet||D.json||!1;if(!await T2(X)&&!D.json)console.log(C.yellow("Warning: No promake.json found. Running checks anyway..."));let G=D.json?null:E2("Running health checks...").start(),F=[];if(D.typecheck!==!1)F.push(()=>rE(X,$));if(D.lint!==!1)F.push(()=>sE(X,D.fix,$));if(D.build!==!1)F.push(()=>aE(X));if(D.runtime)F.push(()=>pS(Z,J,Y));let W=[];try{if(W=await s91(F,1),G)G.stop()}catch(z){if(G)G.fail("Health check failed");if(!D.json)console.error(C.red(z.message));process.exit(1)}let V={project:r91.basename(X),timestamp:new Date().toISOString(),duration:Date.now()-Q,checks:W,summary:{passed:W.filter((z)=>z.status==="pass").length,failed:W.filter((z)=>z.status==="fail").length,skipped:W.filter((z)=>z.status==="skip").length,errors:W.filter((z)=>z.status==="fail"&&z.severity==="error").reduce((z,H)=>z+H.items.length,0),warnings:W.filter((z)=>z.status==="fail"&&z.severity==="warning").reduce((z,H)=>z+H.items.length,0)}};if(D.json)console.log(JSON.stringify(V,null,2));else iS(V,D.verbose||!1);if(V.summary.errors>0)process.exit(1)});async function s91(D,X){let Z=[],J=0;async function Y(){let $=J++;if($>=D.length)return;Z[$]=await D[$](),await Y()}let Q=Array(Math.min(X,D.length)).fill(null).map(()=>Y());return await Promise.all(Q),Z}var f$0={name:"@promakeai/cli",version:"0.3.0",type:"module",bin:{promake:"dist/index.js"},files:["dist/index.js","dist/registry","template"],scripts:{dev:"bun run src/index.ts","dev:app":"cd dev && bun run dev","dev:populate":"bun run scripts/populate-dev.ts","dev:fresh":"bun run dev:populate && bun run dev:app","playground:create":"rm -rf playground && bun run dev -- create playground --template empty --pm bun","playground:reset":"bun run playground:create","playground:add":"cd playground && bun run ../src/index.ts add","playground:ecommerce":"rm -rf playground && bun run dev -- create playground --template ecommerce --pm bun",build:"bun run build:cli && bun run build:registry","build:cli":"bun build src/index.ts --outdir dist --target node --minify","build:registry":"bun run scripts/build-registry.ts",typecheck:"tsc --noEmit",prepublishOnly:"bun run build",test:"bun test","test:watch":"bun test --watch","test:coverage":"bun test --coverage",release:"bun run build && npm publish --access public"},dependencies:{"adm-zip":"^0.5.16",archiver:"^7.0.1",chalk:"^5.3.0",commander:"^12.1.0",culori:"^4.0.2",dotenv:"^17.2.3","fs-extra":"^11.2.0",glob:"^11.0.0",ora:"^8.1.1",prompts:"^2.4.2","puppeteer-core":"^24.36.0"},devDependencies:{"@types/archiver":"^7.0.0","@types/bun":"^1.1.14","@types/culori":"^4.0.1","@types/fs-extra":"^11.0.4","@types/node":"^22.10.2","@types/prompts":"^2.4.9",typescript:"^5.7.2"}};var P8=new $5;P8.name("promake").description(`Modular React template CLI - Build React apps with pre-built components
498
498
 
499
499
  Quick Start:
500
500
  $ promake create my-app
@@ -27,7 +27,7 @@
27
27
  "path": "auth-core/use-auth.ts",
28
28
  "type": "registry:hook",
29
29
  "target": "$modules$/auth-core/use-auth.ts",
30
- "content": "import { useCallback, useEffect, useRef } from \"react\";\r\nimport {\r\n useAuthStore,\r\n type User,\r\n type AuthTokens,\r\n} from \"@/modules/auth-core/auth-store\";\r\nimport { customerClient } from \"@/modules/api/customer-client\";\r\n\r\n// Refresh token 1 minute before expiry\r\nconst REFRESH_BUFFER_MS = 60 * 1000;\r\n\r\nexport function useAuth() {\r\n const {\r\n user,\r\n tokens,\r\n isAuthenticated,\r\n setAuth,\r\n updateTokens,\r\n clearAuth,\r\n isTokenExpired,\r\n getTimeUntilExpiry,\r\n } = useAuthStore();\r\n\r\n const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n const isRefreshingRef = useRef(false);\r\n\r\n // Refresh token using the refresh token\r\n const refreshAccessToken = useCallback(async (): Promise<boolean> => {\r\n const currentTokens = useAuthStore.getState().tokens;\r\n\r\n // Don't refresh if no refresh token exists\r\n if (!currentTokens?.refreshToken || isRefreshingRef.current) {\r\n console.log(\"⚠️ No refresh token available, skipping refresh\");\r\n return false;\r\n }\r\n\r\n isRefreshingRef.current = true;\r\n\r\n try {\r\n // Make a refresh request using the axios instance directly\r\n const response = await customerClient.axios.post<{\r\n accessToken: string;\r\n refreshToken?: string;\r\n expiresIn?: number;\r\n }>(\"/auth/refresh\", {\r\n refreshToken: currentTokens.refreshToken,\r\n });\r\n\r\n const { accessToken, refreshToken, expiresIn } = response.data;\r\n\r\n // Validate response has required data\r\n if (!accessToken) {\r\n console.error(\"❌ Refresh response missing accessToken\");\r\n return false;\r\n }\r\n\r\n const newTokens: AuthTokens = {\r\n accessToken,\r\n refreshToken: refreshToken || currentTokens.refreshToken,\r\n idToken: currentTokens.idToken, // Preserve existing idToken\r\n encryptionKey: currentTokens.encryptionKey, // Preserve existing encryptionKey\r\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,\r\n };\r\n\r\n customerClient.setToken(accessToken);\r\n updateTokens(newTokens);\r\n\r\n console.log(\"✅ Token refreshed successfully\");\r\n return true;\r\n } catch (error) {\r\n console.error(\"❌ Token refresh failed:\", error);\r\n // DON'T clear auth on refresh failure - just return false\r\n // User can still use their existing token until it expires\r\n return false;\r\n } finally {\r\n isRefreshingRef.current = false;\r\n }\r\n }, [updateTokens]);\r\n\r\n // Schedule automatic token refresh\r\n const scheduleTokenRefresh = useCallback(() => {\r\n // Clear any existing timeout\r\n if (refreshTimeoutRef.current) {\r\n clearTimeout(refreshTimeoutRef.current);\r\n refreshTimeoutRef.current = null;\r\n }\r\n\r\n const timeUntilExpiry = getTimeUntilExpiry();\r\n\r\n // Only schedule if we have an expiry time and a refresh token\r\n if (timeUntilExpiry === null || !tokens?.refreshToken) {\r\n return;\r\n }\r\n\r\n // Calculate when to refresh (REFRESH_BUFFER_MS before expiry)\r\n const refreshIn = Math.max(timeUntilExpiry - REFRESH_BUFFER_MS, 0);\r\n\r\n // Don't schedule if expiry is too far in the future (> 24 hours)\r\n if (refreshIn > 24 * 60 * 60 * 1000) {\r\n return;\r\n }\r\n\r\n refreshTimeoutRef.current = setTimeout(async () => {\r\n const success = await refreshAccessToken();\r\n if (success) {\r\n // Reschedule for the new token\r\n scheduleTokenRefresh();\r\n }\r\n }, refreshIn);\r\n }, [getTimeUntilExpiry, tokens?.refreshToken, refreshAccessToken]);\r\n\r\n // Sync token with API client and set up refresh on mount and token changes\r\n useEffect(() => {\r\n if (tokens?.accessToken) {\r\n console.log(\"🔑 Setting token in API client\");\r\n customerClient.setToken(tokens.accessToken);\r\n\r\n // Only try to refresh if we have a refresh token AND token is expired\r\n if (isTokenExpired() && tokens.refreshToken) {\r\n console.log(\"⏰ Token expired, attempting refresh...\");\r\n refreshAccessToken().then((success) => {\r\n if (success) {\r\n scheduleTokenRefresh();\r\n } else {\r\n console.log(\"⚠️ Refresh failed, but keeping existing token\");\r\n }\r\n });\r\n } else if (tokens.refreshToken) {\r\n // Only schedule refresh if we have a refresh token\r\n scheduleTokenRefresh();\r\n }\r\n } else if (tokens && Object.keys(tokens).length === 0) {\r\n // tokens is empty object {} - this shouldn't happen, log it\r\n console.warn(\"⚠️ Tokens object is empty, this may indicate a bug\");\r\n } else {\r\n customerClient.setToken(null);\r\n }\r\n\r\n // Cleanup timeout on unmount\r\n return () => {\r\n if (refreshTimeoutRef.current) {\r\n clearTimeout(refreshTimeoutRef.current);\r\n }\r\n };\r\n }, [\r\n tokens?.accessToken,\r\n tokens?.refreshToken,\r\n isTokenExpired,\r\n refreshAccessToken,\r\n scheduleTokenRefresh,\r\n ]);\r\n\r\n // Set up axios interceptor for 401 responses (token expired during request)\r\n useEffect(() => {\r\n const interceptorId = customerClient.axios.interceptors.response.use(\r\n (response) => response,\r\n async (error) => {\r\n const originalRequest = error.config;\r\n\r\n // Skip refresh for auth endpoints to prevent infinite loops\r\n const isAuthEndpoint = originalRequest?.url?.includes(\"/auth/\");\r\n\r\n // If we get a 401 and haven't retried yet, try to refresh\r\n if (\r\n error.response?.status === 401 &&\r\n !originalRequest._retry &&\r\n tokens?.refreshToken &&\r\n !isAuthEndpoint\r\n ) {\r\n originalRequest._retry = true;\r\n\r\n const success = await refreshAccessToken();\r\n if (success) {\r\n // Retry the original request with new token\r\n const newTokens = useAuthStore.getState().tokens;\r\n if (newTokens?.accessToken) {\r\n originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;\r\n return customerClient.axios(originalRequest);\r\n }\r\n }\r\n }\r\n\r\n return Promise.reject(error);\r\n },\r\n );\r\n\r\n return () => {\r\n customerClient.axios.interceptors.response.eject(interceptorId);\r\n };\r\n }, [tokens?.refreshToken, refreshAccessToken]);\r\n\r\n const login = useCallback(async (username: string, password: string) => {\r\n const response = await customerClient.auth.login({ username, password });\r\n\r\n console.log(\"🔐 Login response:\", response);\r\n console.log(\"🔐 accessToken:\", response.accessToken);\r\n console.log(\"🔐 refreshToken:\", response.refreshToken);\r\n console.log(\"🔐 encryptionKey:\", response.encryptionKey);\r\n\r\n const newTokens: AuthTokens = {\r\n accessToken: response.accessToken,\r\n refreshToken: response.refreshToken,\r\n idToken: response.idToken,\r\n encryptionKey: response.encryptionKey,\r\n expiresAt: response.expiresIn\r\n ? Date.now() + response.expiresIn * 1000\r\n : undefined,\r\n };\r\n\r\n console.log(\"🔐 newTokens object:\", newTokens);\r\n\r\n const newUser: User = {\r\n username,\r\n email: (response as any).email || (response as any).user?.email,\r\n };\r\n\r\n customerClient.setToken(newTokens.accessToken);\r\n setAuth(newUser, newTokens);\r\n\r\n console.log(\r\n \"🔐 Auth set complete, checking store:\",\r\n useAuthStore.getState().tokens,\r\n );\r\n }, []);\r\n\r\n const register = useCallback(\r\n async (username: string, email: string, password: string) => {\r\n await customerClient.auth.register({ username, email, password });\r\n },\r\n [],\r\n );\r\n\r\n const confirmEmail = useCallback(async (username: string, code: string) => {\r\n await customerClient.auth.confirm({ username, code });\r\n }, []);\r\n\r\n const forgotPassword = useCallback(async (username: string) => {\r\n await customerClient.auth.forgotPassword({ username });\r\n }, []);\r\n\r\n const resetPassword = useCallback(\r\n async (username: string, code: string, newPassword: string) => {\r\n await customerClient.auth.resetPassword({ username, code, newPassword });\r\n },\r\n [],\r\n );\r\n\r\n const logout = useCallback(() => {\r\n // Clear any scheduled refresh\r\n if (refreshTimeoutRef.current) {\r\n clearTimeout(refreshTimeoutRef.current);\r\n refreshTimeoutRef.current = null;\r\n }\r\n\r\n customerClient.setToken(null);\r\n clearAuth();\r\n }, [clearAuth]);\r\n\r\n return {\r\n user,\r\n token: tokens?.accessToken ?? null,\r\n tokens,\r\n isAuthenticated,\r\n api: customerClient,\r\n login,\r\n register,\r\n confirmEmail,\r\n forgotPassword,\r\n resetPassword,\r\n logout,\r\n refreshAccessToken,\r\n };\r\n}\r\n"
30
+ "content": "import { useCallback, useEffect, useRef } from \"react\";\r\nimport {\r\n useAuthStore,\r\n type User,\r\n type AuthTokens,\r\n} from \"./auth-store\";\r\nimport { customerClient } from \"@/modules/api\";\r\n\r\n// Refresh token 1 minute before expiry\r\nconst REFRESH_BUFFER_MS = 60 * 1000;\r\n\r\nexport function useAuth() {\r\n const {\r\n user,\r\n tokens,\r\n isAuthenticated,\r\n setAuth,\r\n updateTokens,\r\n clearAuth,\r\n isTokenExpired,\r\n getTimeUntilExpiry,\r\n } = useAuthStore();\r\n\r\n const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n const isRefreshingRef = useRef(false);\r\n\r\n // Refresh token using the refresh token\r\n const refreshAccessToken = useCallback(async (): Promise<boolean> => {\r\n const currentTokens = useAuthStore.getState().tokens;\r\n\r\n // Don't refresh if no refresh token exists\r\n if (!currentTokens?.refreshToken || isRefreshingRef.current) {\r\n console.log(\"⚠️ No refresh token available, skipping refresh\");\r\n return false;\r\n }\r\n\r\n isRefreshingRef.current = true;\r\n\r\n try {\r\n // Make a refresh request using the axios instance directly\r\n const response = await customerClient.axios.post<{\r\n accessToken: string;\r\n refreshToken?: string;\r\n expiresIn?: number;\r\n }>(\"/auth/refresh\", {\r\n refreshToken: currentTokens.refreshToken,\r\n });\r\n\r\n const { accessToken, refreshToken, expiresIn } = response.data;\r\n\r\n // Validate response has required data\r\n if (!accessToken) {\r\n console.error(\"❌ Refresh response missing accessToken\");\r\n return false;\r\n }\r\n\r\n const newTokens: AuthTokens = {\r\n accessToken,\r\n refreshToken: refreshToken || currentTokens.refreshToken,\r\n idToken: currentTokens.idToken, // Preserve existing idToken\r\n encryptionKey: currentTokens.encryptionKey, // Preserve existing encryptionKey\r\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,\r\n };\r\n\r\n customerClient.setToken(accessToken);\r\n updateTokens(newTokens);\r\n\r\n console.log(\"✅ Token refreshed successfully\");\r\n return true;\r\n } catch (error) {\r\n console.error(\"❌ Token refresh failed:\", error);\r\n // DON'T clear auth on refresh failure - just return false\r\n // User can still use their existing token until it expires\r\n return false;\r\n } finally {\r\n isRefreshingRef.current = false;\r\n }\r\n }, [updateTokens]);\r\n\r\n // Schedule automatic token refresh\r\n const scheduleTokenRefresh = useCallback(() => {\r\n // Clear any existing timeout\r\n if (refreshTimeoutRef.current) {\r\n clearTimeout(refreshTimeoutRef.current);\r\n refreshTimeoutRef.current = null;\r\n }\r\n\r\n const timeUntilExpiry = getTimeUntilExpiry();\r\n\r\n // Only schedule if we have an expiry time and a refresh token\r\n if (timeUntilExpiry === null || !tokens?.refreshToken) {\r\n return;\r\n }\r\n\r\n // Calculate when to refresh (REFRESH_BUFFER_MS before expiry)\r\n const refreshIn = Math.max(timeUntilExpiry - REFRESH_BUFFER_MS, 0);\r\n\r\n // Don't schedule if expiry is too far in the future (> 24 hours)\r\n if (refreshIn > 24 * 60 * 60 * 1000) {\r\n return;\r\n }\r\n\r\n refreshTimeoutRef.current = setTimeout(async () => {\r\n const success = await refreshAccessToken();\r\n if (success) {\r\n // Reschedule for the new token\r\n scheduleTokenRefresh();\r\n }\r\n }, refreshIn);\r\n }, [getTimeUntilExpiry, tokens?.refreshToken, refreshAccessToken]);\r\n\r\n // Sync token with API client and set up refresh on mount and token changes\r\n useEffect(() => {\r\n if (tokens?.accessToken) {\r\n console.log(\"🔑 Setting token in API client\");\r\n customerClient.setToken(tokens.accessToken);\r\n\r\n // Only try to refresh if we have a refresh token AND token is expired\r\n if (isTokenExpired() && tokens.refreshToken) {\r\n console.log(\"⏰ Token expired, attempting refresh...\");\r\n refreshAccessToken().then((success) => {\r\n if (success) {\r\n scheduleTokenRefresh();\r\n } else {\r\n console.log(\"⚠️ Refresh failed, but keeping existing token\");\r\n }\r\n });\r\n } else if (tokens.refreshToken) {\r\n // Only schedule refresh if we have a refresh token\r\n scheduleTokenRefresh();\r\n }\r\n } else if (tokens && Object.keys(tokens).length === 0) {\r\n // tokens is empty object {} - this shouldn't happen, log it\r\n console.warn(\"⚠️ Tokens object is empty, this may indicate a bug\");\r\n } else {\r\n customerClient.setToken(null);\r\n }\r\n\r\n // Cleanup timeout on unmount\r\n return () => {\r\n if (refreshTimeoutRef.current) {\r\n clearTimeout(refreshTimeoutRef.current);\r\n }\r\n };\r\n }, [\r\n tokens?.accessToken,\r\n tokens?.refreshToken,\r\n isTokenExpired,\r\n refreshAccessToken,\r\n scheduleTokenRefresh,\r\n ]);\r\n\r\n // Set up axios interceptor for 401 responses (token expired during request)\r\n useEffect(() => {\r\n const interceptorId = customerClient.axios.interceptors.response.use(\r\n (response) => response,\r\n async (error) => {\r\n const originalRequest = error.config;\r\n\r\n // Skip refresh for auth endpoints to prevent infinite loops\r\n const isAuthEndpoint = originalRequest?.url?.includes(\"/auth/\");\r\n\r\n // If we get a 401 and haven't retried yet, try to refresh\r\n if (\r\n error.response?.status === 401 &&\r\n !originalRequest._retry &&\r\n tokens?.refreshToken &&\r\n !isAuthEndpoint\r\n ) {\r\n originalRequest._retry = true;\r\n\r\n const success = await refreshAccessToken();\r\n if (success) {\r\n // Retry the original request with new token\r\n const newTokens = useAuthStore.getState().tokens;\r\n if (newTokens?.accessToken) {\r\n originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;\r\n return customerClient.axios(originalRequest);\r\n }\r\n }\r\n }\r\n\r\n return Promise.reject(error);\r\n },\r\n );\r\n\r\n return () => {\r\n customerClient.axios.interceptors.response.eject(interceptorId);\r\n };\r\n }, [tokens?.refreshToken, refreshAccessToken]);\r\n\r\n const login = useCallback(async (username: string, password: string) => {\r\n const response = await customerClient.auth.login({ username, password });\r\n\r\n console.log(\"🔐 Login response:\", response);\r\n console.log(\"🔐 accessToken:\", response.accessToken);\r\n console.log(\"🔐 refreshToken:\", response.refreshToken);\r\n console.log(\"🔐 encryptionKey:\", response.encryptionKey);\r\n\r\n const newTokens: AuthTokens = {\r\n accessToken: response.accessToken,\r\n refreshToken: response.refreshToken,\r\n idToken: response.idToken,\r\n encryptionKey: response.encryptionKey,\r\n expiresAt: response.expiresIn\r\n ? Date.now() + response.expiresIn * 1000\r\n : undefined,\r\n };\r\n\r\n console.log(\"🔐 newTokens object:\", newTokens);\r\n\r\n const newUser: User = {\r\n username,\r\n email: (response as any).email || (response as any).user?.email,\r\n };\r\n\r\n customerClient.setToken(newTokens.accessToken);\r\n setAuth(newUser, newTokens);\r\n\r\n console.log(\r\n \"🔐 Auth set complete, checking store:\",\r\n useAuthStore.getState().tokens,\r\n );\r\n }, []);\r\n\r\n const register = useCallback(\r\n async (username: string, email: string, password: string) => {\r\n await customerClient.auth.register({ username, email, password });\r\n },\r\n [],\r\n );\r\n\r\n const confirmEmail = useCallback(async (username: string, code: string) => {\r\n await customerClient.auth.confirm({ username, code });\r\n }, []);\r\n\r\n const forgotPassword = useCallback(async (username: string) => {\r\n await customerClient.auth.forgotPassword({ username });\r\n }, []);\r\n\r\n const resetPassword = useCallback(\r\n async (username: string, code: string, newPassword: string) => {\r\n await customerClient.auth.resetPassword({ username, code, newPassword });\r\n },\r\n [],\r\n );\r\n\r\n const logout = useCallback(() => {\r\n // Clear any scheduled refresh\r\n if (refreshTimeoutRef.current) {\r\n clearTimeout(refreshTimeoutRef.current);\r\n refreshTimeoutRef.current = null;\r\n }\r\n\r\n customerClient.setToken(null);\r\n clearAuth();\r\n }, [clearAuth]);\r\n\r\n return {\r\n user,\r\n token: tokens?.accessToken ?? null,\r\n tokens,\r\n isAuthenticated,\r\n api: customerClient,\r\n login,\r\n register,\r\n confirmEmail,\r\n forgotPassword,\r\n resetPassword,\r\n logout,\r\n refreshAccessToken,\r\n };\r\n}\r\n"
31
31
  }
32
32
  ],
33
33
  "exports": {
@@ -24,7 +24,7 @@
24
24
  "path": "checkout-page/checkout-page.tsx",
25
25
  "type": "registry:page",
26
26
  "target": "$modules$/checkout-page/checkout-page.tsx",
27
- "content": "import { useState, useEffect } from \"react\";\nimport { Link } from \"react-router\";\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { toast } from \"sonner\";\nimport {\n useCart,\n formatPrice,\n type PaymentMethod,\n type OnlinePaymentProvider,\n getFilteredPaymentMethodConfigs,\n getOnlinePaymentProviders,\n ONLINE_PROVIDER_CONFIGS,\n} from \"@/modules/ecommerce-core\";\nimport { customerClient } from \"@/modules/api\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\ninterface Country {\n value: string;\n label: string;\n}\n\ninterface CheckoutFormData {\n firstName: string;\n lastName: string;\n email: string;\n phone: string;\n address: string;\n city: string;\n postalCode: string;\n country: string;\n notes: string;\n}\n\ninterface BankTransferInfo {\n bank_name: string;\n bank_account_name: string;\n iban: string;\n}\n\nconst DEFAULT_COUNTRIES: Country[] = [\n { value: \"US\", label: \"United States\" },\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"CA\", label: \"Canada\" },\n { value: \"AU\", label: \"Australia\" },\n { value: \"DE\", label: \"Germany\" },\n { value: \"FR\", label: \"France\" },\n { value: \"IT\", label: \"Italy\" },\n { value: \"ES\", label: \"Spain\" },\n { value: \"NL\", label: \"Netherlands\" },\n { value: \"TR\", label: \"Turkey\" },\n { value: \"JP\", label: \"Japan\" },\n];\n\nexport function CheckoutPage() {\n const { t } = useTranslation(\"checkout-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\n const { state, clearCart } = useCart();\n const { items, total } = state;\n\n const currency = (constants as any).site?.currency || \"USD\";\n const taxRate = (constants as any).payments?.taxRate || 0;\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\n\n // Calculate shipping and tax\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\n const tax = total * taxRate;\n\n const countries = DEFAULT_COUNTRIES;\n\n // Get available payment methods and providers from config\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\n const availableProviders = getOnlinePaymentProviders();\n\n const getProductPrice = (product: {\n price: number;\n sale_price?: number;\n on_sale?: boolean;\n }) => {\n return product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n };\n\n const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\n availablePaymentMethods[0]?.id || \"card\"\n );\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\n availableProviders[0] || \"stripe\"\n );\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [formData, setFormData] = useState<CheckoutFormData>({\n firstName: \"\",\n lastName: \"\",\n email: \"\",\n phone: \"\",\n address: \"\",\n city: \"\",\n postalCode: \"\",\n country: \"\",\n notes: \"\",\n });\n const [agreedToTerms, setAgreedToTerms] = useState(false);\n\n // Bank transfer info state\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\n\n const finalTotal = total + shipping + tax;\n\n // Fetch bank info when transfer is selected\n useEffect(() => {\n if (paymentMethod === \"transfer\") {\n const fetchBankInfo = async () => {\n setIsBankInfoLoading(true);\n setBankInfoError(null);\n try {\n const info = await (customerClient as any).payment.getBankTransferInfo();\n setBankInfo(info);\n } catch (err: any) {\n setBankInfoError(\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\n );\n } finally {\n setIsBankInfoLoading(false);\n }\n };\n fetchBankInfo();\n }\n }, [paymentMethod, t]);\n\n const handleInputChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n const { name, value } = e.target;\n setFormData((prev) => ({ ...prev, [name]: value }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!agreedToTerms) {\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n // Determine payment type based on selection\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\n\n if (paymentMethod === \"card\") {\n paymentType = selectedProvider;\n } else if (paymentMethod === \"transfer\") {\n paymentType = \"bank_transfer\";\n } else {\n paymentType = \"cash_on_delivery\";\n }\n\n // Save checkout data to localStorage for success page\n const checkoutData = {\n items: items,\n total: finalTotal,\n customerInfo: formData,\n paymentMethod,\n paymentProvider: paymentType,\n };\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\n\n // Build product data for checkout\n const productData = items.map((item) => {\n const price = getProductPrice(item.product);\n const qty = item.quantity || 1;\n\n return {\n quantity: qty,\n name: item.product.name || \"Product\",\n description: item.product.description || item.product.name || \"Product\",\n amount: Math.round(price * 100), // Convert to cents\n img: item.product.images?.[0] || \"/images/placeholder.png\",\n optionals: {\n productId: item.product.id,\n },\n };\n });\n\n // Tax amount in cents\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\n\n // Create checkout session\n const response = await (customerClient as any).payment.createCheckout({\n currency: currency.toLowerCase(),\n taxAmount: taxAmountInCents,\n paymentType: paymentType,\n productData,\n contactData: {\n firstname: formData.firstName,\n lastname: formData.lastName,\n email: formData.email,\n phone: formData.phone,\n },\n shippingData: {\n address: formData.address,\n country: formData.country,\n city: formData.city,\n zip: formData.postalCode,\n },\n });\n\n // Clear cart and redirect to payment URL\n clearCart();\n window.location.href = response.url;\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\n setError(errorMessage);\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // Get icon component based on payment method\n const getPaymentIcon = (iconName: string) => {\n switch (iconName) {\n case \"CreditCard\":\n return CreditCard;\n case \"Banknote\":\n return Banknote;\n case \"Truck\":\n return Truck;\n default:\n return CreditCard;\n }\n };\n\n // Get icon color based on payment method\n const getIconColor = (methodId: string) => {\n switch (methodId) {\n case \"card\":\n return \"text-blue-600\";\n case \"transfer\":\n return \"text-primary\";\n case \"cash\":\n return \"text-green-600 dark:text-green-400\";\n default:\n return \"text-primary\";\n }\n };\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center\">\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"cartEmpty\", \"Your cart is empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"cartEmptyDescription\",\n \"Please add items to your cart before proceeding to checkout.\"\n )}\n </p>\n <Button asChild>\n <Link to=\"/products\">\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/cart\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\n <p className=\"text-muted-foreground\">\n {t(\"completeOrder\", \"Complete your order\")}\n </p>\n </div>\n </FadeIn>\n\n <form onSubmit={handleSubmit}>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Contact Information */}\n <FadeIn delay={0.1}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"contactInformation\", \"Contact Information\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"firstName\">\n {t(\"firstName\", \"First Name\")} *\n </Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n value={formData.firstName}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"lastName\">\n {t(\"lastName\", \"Last Name\")} *\n </Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n value={formData.lastName}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">\n {t(\"email\", \"Email Address\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"phone\">\n {t(\"phone\", \"Phone Number\")} *\n </Label>\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleInputChange}\n required\n />\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Shipping Address */}\n <FadeIn delay={0.2}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"shippingAddress\", \"Shipping Address\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\n <Textarea\n id=\"address\"\n name=\"address\"\n value={formData.address}\n onChange={handleInputChange}\n placeholder={t(\n \"addressPlaceholder\",\n \"Street address, apartment, suite, etc.\"\n )}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\n <Select\n value={formData.country}\n onValueChange={(value) =>\n setFormData((prev) => ({ ...prev, country: value }))\n }\n required\n >\n <SelectTrigger id=\"country\">\n <SelectValue\n placeholder={t(\"selectCountry\", \"Select a country\")}\n />\n </SelectTrigger>\n <SelectContent>\n {countries.map((country) => (\n <SelectItem key={country.value} value={country.value}>\n {country.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\n <Input\n id=\"city\"\n name=\"city\"\n value={formData.city}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"postalCode\">\n {t(\"postalCode\", \"Postal Code\")} *\n </Label>\n <Input\n id=\"postalCode\"\n name=\"postalCode\"\n value={formData.postalCode}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Payment Method */}\n <FadeIn delay={0.3}>\n <Card>\n <CardHeader>\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <RadioGroup\n value={paymentMethod}\n onValueChange={(value) =>\n setPaymentMethod(value as PaymentMethod)\n }\n className=\"space-y-4\"\n >\n {availablePaymentMethods.map((method) => {\n const IconComponent = getPaymentIcon(method.icon);\n const iconColor = getIconColor(method.id);\n\n return (\n <div\n key={method.id}\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\n >\n <RadioGroupItem value={method.id} id={method.id} />\n <Label\n htmlFor={method.id}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"flex items-center gap-3\">\n <IconComponent\n className={`h-5 w-5 ${iconColor}`}\n />\n <div>\n <div className=\"font-medium\">\n {t(method.id, method.label)}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n {t(`${method.id}Description`, method.description)}\n </div>\n </div>\n </div>\n </Label>\n </div>\n );\n })}\n </RadioGroup>\n\n {/* Bank Transfer Details */}\n {paymentMethod === \"transfer\" && (\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\n <h4 className=\"font-medium mb-2\">\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\n </h4>\n {isBankInfoLoading ? (\n <div className=\"text-sm space-y-2\">\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n </div>\n ) : bankInfoError ? (\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\n ) : bankInfo ? (\n <div className=\"text-sm space-y-1\">\n <p>\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\n </p>\n <p>\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\n {bankInfo.bank_account_name}\n </p>\n <p>\n <strong>IBAN:</strong> {bankInfo.iban}\n </p>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\n </p>\n )}\n </div>\n )}\n\n {/* Card Payment - Provider Selection */}\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\n <div className=\"mt-4 space-y-4\">\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\n </h4>\n <RadioGroup\n value={selectedProvider}\n onValueChange={(value) =>\n setSelectedProvider(value as OnlinePaymentProvider)\n }\n className=\"space-y-2\"\n >\n {availableProviders.map((provider) => (\n <div\n key={provider}\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\n >\n <RadioGroupItem\n value={provider}\n id={`provider-${provider}`}\n />\n <Label\n htmlFor={`provider-${provider}`}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"font-medium\">\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\n </div>\n </Label>\n </div>\n ))}\n </RadioGroup>\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\n {t(\n \"creditCardRedirectNote\",\n \"You will be redirected to the secure payment page to complete your purchase.\"\n )}\n </p>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Order Notes */}\n <FadeIn delay={0.4}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Textarea\n name=\"notes\"\n value={formData.notes}\n onChange={handleInputChange}\n placeholder={t(\n \"orderNotesPlaceholder\",\n \"Special instructions for your order...\"\n )}\n rows={3}\n />\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n\n {/* Order Summary */}\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\n <Card className=\"sticky top-24\">\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-3\">\n {items.map((item) => (\n <div key={item.id} className=\"flex gap-3\">\n <img\n src={\n item.product.images?.[0] ||\n \"/images/placeholder.png\"\n }\n alt={item.product.name}\n className=\"w-12 h-12 object-cover rounded\"\n />\n <div className=\"flex-1 space-y-1\">\n <h4 className=\"text-sm font-medium leading-normal\">\n {item.product.name}\n </h4>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"qty\", \"Qty\")}: {item.quantity}\n </span>\n <span>\n {formatPrice(\n getProductPrice(item.product) * item.quantity,\n currency\n )}\n </span>\n </div>\n </div>\n </div>\n ))}\n </div>\n\n <Separator />\n\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0\n ? t(\"free\", \"Free\")\n : formatPrice(shipping, currency)}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"terms\"\n checked={agreedToTerms}\n onCheckedChange={(checked) =>\n setAgreedToTerms(checked as boolean)\n }\n />\n <span className=\"text-sm\">\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\n <Link\n to=\"/terms\"\n className=\"text-primary hover:underline\"\n >\n {t(\"termsOfService\", \"Terms of Service\")}\n </Link>{\" \"}\n {t(\"and\", \"and\")}{\" \"}\n <Link\n to=\"/privacy\"\n className=\"text-primary hover:underline\"\n >\n {t(\"privacyPolicy\", \"Privacy Policy\")}\n </Link>\n </span>\n </div>\n\n <Button\n type=\"submit\"\n className=\"w-full\"\n size=\"lg\"\n disabled={!agreedToTerms || isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"processing\", \"Processing...\")}\n </>\n ) : (\n <>\n <Check className=\"w-4 h-4 mr-2\" />\n {paymentMethod === \"card\"\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\n : t(\"placeOrder\", \"Place Order\")}\n </>\n )}\n </Button>\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n </form>\n </div>\n </Layout>\n );\n}\n\nexport default CheckoutPage;\n"
27
+ "content": "import { useState, useEffect } from \"react\";\nimport { Link } from \"react-router\";\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { toast } from \"sonner\";\nimport {\n useCart,\n formatPrice,\n type PaymentMethod,\n type OnlinePaymentProvider,\n getFilteredPaymentMethodConfigs,\n getOnlinePaymentProviders,\n ONLINE_PROVIDER_CONFIGS,\n} from \"@/modules/ecommerce-core\";\nimport { customerClient, getErrorMessage } from \"@/modules/api\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\ninterface Country {\n value: string;\n label: string;\n}\n\ninterface CheckoutFormData {\n firstName: string;\n lastName: string;\n email: string;\n phone: string;\n address: string;\n city: string;\n postalCode: string;\n country: string;\n notes: string;\n}\n\ninterface BankTransferInfo {\n bank_name: string;\n bank_account_name: string;\n iban: string;\n}\n\nconst DEFAULT_COUNTRIES: Country[] = [\n { value: \"US\", label: \"United States\" },\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"CA\", label: \"Canada\" },\n { value: \"AU\", label: \"Australia\" },\n { value: \"DE\", label: \"Germany\" },\n { value: \"FR\", label: \"France\" },\n { value: \"IT\", label: \"Italy\" },\n { value: \"ES\", label: \"Spain\" },\n { value: \"NL\", label: \"Netherlands\" },\n { value: \"TR\", label: \"Turkey\" },\n { value: \"JP\", label: \"Japan\" },\n];\n\nexport function CheckoutPage() {\n const { t } = useTranslation(\"checkout-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\n const { state, clearCart } = useCart();\n const { items, total } = state;\n\n const currency = (constants as any).site?.currency || \"USD\";\n const taxRate = (constants as any).payments?.taxRate || 0;\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\n\n // Calculate shipping and tax\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\n const tax = total * taxRate;\n\n const countries = DEFAULT_COUNTRIES;\n\n // Get available payment methods and providers from config\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\n const availableProviders = getOnlinePaymentProviders();\n\n const getProductPrice = (product: {\n price: number;\n sale_price?: number;\n on_sale?: boolean;\n }) => {\n return product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n };\n\n const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\n availablePaymentMethods[0]?.id || \"card\"\n );\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\n availableProviders[0] || \"stripe\"\n );\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [formData, setFormData] = useState<CheckoutFormData>({\n firstName: \"\",\n lastName: \"\",\n email: \"\",\n phone: \"\",\n address: \"\",\n city: \"\",\n postalCode: \"\",\n country: \"\",\n notes: \"\",\n });\n const [agreedToTerms, setAgreedToTerms] = useState(false);\n\n // Bank transfer info state\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\n\n const finalTotal = total + shipping + tax;\n\n // Fetch bank info when transfer is selected\n useEffect(() => {\n if (paymentMethod === \"transfer\") {\n const fetchBankInfo = async () => {\n setIsBankInfoLoading(true);\n setBankInfoError(null);\n try {\n const info = await (customerClient as any).payment.getBankTransferInfo();\n setBankInfo(info);\n } catch (err: any) {\n setBankInfoError(\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\n );\n } finally {\n setIsBankInfoLoading(false);\n }\n };\n fetchBankInfo();\n }\n }, [paymentMethod, t]);\n\n const handleInputChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n const { name, value } = e.target;\n setFormData((prev) => ({ ...prev, [name]: value }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!agreedToTerms) {\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n // Determine payment type based on selection\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\n\n if (paymentMethod === \"card\") {\n paymentType = selectedProvider;\n } else if (paymentMethod === \"transfer\") {\n paymentType = \"bank_transfer\";\n } else {\n paymentType = \"cash_on_delivery\";\n }\n\n // Save checkout data to localStorage for success page\n const checkoutData = {\n items: items,\n total: finalTotal,\n customerInfo: formData,\n paymentMethod,\n paymentProvider: paymentType,\n };\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\n\n // Build product data for checkout\n const productData = items.map((item) => {\n const price = getProductPrice(item.product);\n const qty = item.quantity || 1;\n\n return {\n quantity: qty,\n name: item.product.name || \"Product\",\n description: item.product.description || item.product.name || \"Product\",\n amount: Math.round(price * 100), // Convert to cents\n img: item.product.images?.[0] || \"/images/placeholder.png\",\n optionals: {\n productId: item.product.id,\n },\n };\n });\n\n // Tax amount in cents\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\n\n // Create checkout session\n const response = await (customerClient as any).payment.createCheckout({\n currency: currency.toLowerCase(),\n taxAmount: taxAmountInCents,\n paymentType: paymentType,\n productData,\n contactData: {\n firstname: formData.firstName,\n lastname: formData.lastName,\n email: formData.email,\n phone: formData.phone,\n },\n shippingData: {\n address: formData.address,\n country: formData.country,\n city: formData.city,\n zip: formData.postalCode,\n },\n });\n\n // Clear cart and redirect to payment URL\n clearCart();\n window.location.href = response.url;\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\n setError(errorMessage);\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // Get icon component based on payment method\n const getPaymentIcon = (iconName: string) => {\n switch (iconName) {\n case \"CreditCard\":\n return CreditCard;\n case \"Banknote\":\n return Banknote;\n case \"Truck\":\n return Truck;\n default:\n return CreditCard;\n }\n };\n\n // Get icon color based on payment method\n const getIconColor = (methodId: string) => {\n switch (methodId) {\n case \"card\":\n return \"text-blue-600\";\n case \"transfer\":\n return \"text-primary\";\n case \"cash\":\n return \"text-green-600 dark:text-green-400\";\n default:\n return \"text-primary\";\n }\n };\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center\">\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"cartEmpty\", \"Your cart is empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"cartEmptyDescription\",\n \"Please add items to your cart before proceeding to checkout.\"\n )}\n </p>\n <Button asChild>\n <Link to=\"/products\">\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/cart\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\n <p className=\"text-muted-foreground\">\n {t(\"completeOrder\", \"Complete your order\")}\n </p>\n </div>\n </FadeIn>\n\n <form onSubmit={handleSubmit}>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Contact Information */}\n <FadeIn delay={0.1}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"contactInformation\", \"Contact Information\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"firstName\">\n {t(\"firstName\", \"First Name\")} *\n </Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n value={formData.firstName}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"lastName\">\n {t(\"lastName\", \"Last Name\")} *\n </Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n value={formData.lastName}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">\n {t(\"email\", \"Email Address\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"phone\">\n {t(\"phone\", \"Phone Number\")} *\n </Label>\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleInputChange}\n required\n />\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Shipping Address */}\n <FadeIn delay={0.2}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"shippingAddress\", \"Shipping Address\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\n <Textarea\n id=\"address\"\n name=\"address\"\n value={formData.address}\n onChange={handleInputChange}\n placeholder={t(\n \"addressPlaceholder\",\n \"Street address, apartment, suite, etc.\"\n )}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\n <Select\n value={formData.country}\n onValueChange={(value) =>\n setFormData((prev) => ({ ...prev, country: value }))\n }\n required\n >\n <SelectTrigger id=\"country\">\n <SelectValue\n placeholder={t(\"selectCountry\", \"Select a country\")}\n />\n </SelectTrigger>\n <SelectContent>\n {countries.map((country) => (\n <SelectItem key={country.value} value={country.value}>\n {country.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\n <Input\n id=\"city\"\n name=\"city\"\n value={formData.city}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"postalCode\">\n {t(\"postalCode\", \"Postal Code\")} *\n </Label>\n <Input\n id=\"postalCode\"\n name=\"postalCode\"\n value={formData.postalCode}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Payment Method */}\n <FadeIn delay={0.3}>\n <Card>\n <CardHeader>\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <RadioGroup\n value={paymentMethod}\n onValueChange={(value) =>\n setPaymentMethod(value as PaymentMethod)\n }\n className=\"space-y-4\"\n >\n {availablePaymentMethods.map((method) => {\n const IconComponent = getPaymentIcon(method.icon);\n const iconColor = getIconColor(method.id);\n\n return (\n <div\n key={method.id}\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\n >\n <RadioGroupItem value={method.id} id={method.id} />\n <Label\n htmlFor={method.id}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"flex items-center gap-3\">\n <IconComponent\n className={`h-5 w-5 ${iconColor}`}\n />\n <div>\n <div className=\"font-medium\">\n {t(method.id, method.label)}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n {t(`${method.id}Description`, method.description)}\n </div>\n </div>\n </div>\n </Label>\n </div>\n );\n })}\n </RadioGroup>\n\n {/* Bank Transfer Details */}\n {paymentMethod === \"transfer\" && (\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\n <h4 className=\"font-medium mb-2\">\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\n </h4>\n {isBankInfoLoading ? (\n <div className=\"text-sm space-y-2\">\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n </div>\n ) : bankInfoError ? (\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\n ) : bankInfo ? (\n <div className=\"text-sm space-y-1\">\n <p>\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\n </p>\n <p>\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\n {bankInfo.bank_account_name}\n </p>\n <p>\n <strong>IBAN:</strong> {bankInfo.iban}\n </p>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\n </p>\n )}\n </div>\n )}\n\n {/* Card Payment - Provider Selection */}\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\n <div className=\"mt-4 space-y-4\">\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\n </h4>\n <RadioGroup\n value={selectedProvider}\n onValueChange={(value) =>\n setSelectedProvider(value as OnlinePaymentProvider)\n }\n className=\"space-y-2\"\n >\n {availableProviders.map((provider) => (\n <div\n key={provider}\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\n >\n <RadioGroupItem\n value={provider}\n id={`provider-${provider}`}\n />\n <Label\n htmlFor={`provider-${provider}`}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"font-medium\">\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\n </div>\n </Label>\n </div>\n ))}\n </RadioGroup>\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\n {t(\n \"creditCardRedirectNote\",\n \"You will be redirected to the secure payment page to complete your purchase.\"\n )}\n </p>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Order Notes */}\n <FadeIn delay={0.4}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Textarea\n name=\"notes\"\n value={formData.notes}\n onChange={handleInputChange}\n placeholder={t(\n \"orderNotesPlaceholder\",\n \"Special instructions for your order...\"\n )}\n rows={3}\n />\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n\n {/* Order Summary */}\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\n <Card className=\"sticky top-24\">\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-3\">\n {items.map((item) => (\n <div key={item.id} className=\"flex gap-3\">\n <img\n src={\n item.product.images?.[0] ||\n \"/images/placeholder.png\"\n }\n alt={item.product.name}\n className=\"w-12 h-12 object-cover rounded\"\n />\n <div className=\"flex-1 space-y-1\">\n <h4 className=\"text-sm font-medium leading-normal\">\n {item.product.name}\n </h4>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"qty\", \"Qty\")}: {item.quantity}\n </span>\n <span>\n {formatPrice(\n getProductPrice(item.product) * item.quantity,\n currency\n )}\n </span>\n </div>\n </div>\n </div>\n ))}\n </div>\n\n <Separator />\n\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0\n ? t(\"free\", \"Free\")\n : formatPrice(shipping, currency)}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"terms\"\n checked={agreedToTerms}\n onCheckedChange={(checked) =>\n setAgreedToTerms(checked as boolean)\n }\n />\n <span className=\"text-sm\">\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\n <Link\n to=\"/terms\"\n className=\"text-primary hover:underline\"\n >\n {t(\"termsOfService\", \"Terms of Service\")}\n </Link>{\" \"}\n {t(\"and\", \"and\")}{\" \"}\n <Link\n to=\"/privacy\"\n className=\"text-primary hover:underline\"\n >\n {t(\"privacyPolicy\", \"Privacy Policy\")}\n </Link>\n </span>\n </div>\n\n <Button\n type=\"submit\"\n className=\"w-full\"\n size=\"lg\"\n disabled={!agreedToTerms || isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"processing\", \"Processing...\")}\n </>\n ) : (\n <>\n <Check className=\"w-4 h-4 mr-2\" />\n {paymentMethod === \"card\"\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\n : t(\"placeOrder\", \"Place Order\")}\n </>\n )}\n </Button>\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n </form>\n </div>\n </Layout>\n );\n}\n\nexport default CheckoutPage;\n"
28
28
  },
29
29
  {
30
30
  "path": "checkout-page/lang/en.json",
@@ -1,6 +1,6 @@
1
1
  # Forgot Password Page Split
2
2
 
3
- Split-screen password recovery page with form on the left and full-height image on the right. Features email form with success state after submission. Uses customerClient for API calls.
3
+ Split-screen password recovery page with form on the left and full-height image on the right. Features username form with success state after submission. Uses useAuth hook from auth-core for password recovery.
4
4
 
5
5
  ## Files
6
6
 
@@ -30,16 +30,15 @@ import ForgotPasswordPageSplit from '@/modules/forgot-password-page-split';
30
30
 
31
31
  • Installed at: src/modules/forgot-password-page-split/
32
32
  • Customize text: src/modules/forgot-password-page-split/lang/*.json
33
- API Integration:
34
- - customerClient.auth.forgotPassword() for sending reset email
35
- - Shows success state after email sent
33
+ Uses useAuth() hook from auth-core:
34
+ const { forgotPassword } = useAuth();
35
+ await forgotPassword(username);
36
+ • Shows success state after email sent
36
37
  • Add to your router as a page component
37
38
  ```
38
39
 
39
40
  ## Dependencies
40
41
 
41
42
  This component requires:
42
- - `button`
43
- - `input`
44
43
  - `auth-core`
45
44
  - `api`
@@ -39,8 +39,5 @@ import ForgotPasswordPage from '@/modules/forgot-password-page';
39
39
  ## Dependencies
40
40
 
41
41
  This component requires:
42
- - `button`
43
- - `input`
44
- - `card`
45
42
  - `auth-core`
46
43
  - `api`
@@ -1,6 +1,6 @@
1
1
  # Login Page Split
2
2
 
3
- Split-screen login page with form on the left and full-height image on the right. Features email/password fields, remember me checkbox, sign up and forgot password links. Uses customerClient for API and useAuthStore for state management.
3
+ Split-screen login page with form on the left and full-height image on the right. Features username/password fields, sign up and forgot password links. Uses useAuth hook from auth-core for authentication.
4
4
 
5
5
  ## Files
6
6
 
@@ -30,11 +30,10 @@ import LoginPageSplit from '@/modules/login-page-split';
30
30
 
31
31
  • Installed at: src/modules/login-page-split/
32
32
  • Customize text: src/modules/login-page-split/lang/*.json
33
- API Integration:
34
- - customerClient.auth.login() for authentication
35
- - useAuthStore for state management
36
- - Automatically sets token after login
37
- • Supports remember me (7 days vs default)
33
+ Uses useAuth() hook from auth-core:
34
+ const { login } = useAuth();
35
+ await login(username, password);
36
+ Automatically sets token after login
38
37
  • Redirects to previous page after login
39
38
  • Add to your router as a page component
40
39
  ```
@@ -42,8 +41,5 @@ import LoginPageSplit from '@/modules/login-page-split';
42
41
  ## Dependencies
43
42
 
44
43
  This component requires:
45
- - `input`
46
- - `button`
47
- - `checkbox`
48
44
  - `auth-core`
49
45
  - `api`
@@ -1,6 +1,6 @@
1
1
  # Login Page
2
2
 
3
- Login page with email/password form, forgot password link, and create account link. Centered card layout with responsive design. Integrated with auth-core for authentication.
3
+ Login page with username/password form, forgot password link, and create account link. Centered card layout with responsive design. Uses useAuth hook from auth-core for authentication.
4
4
 
5
5
  ## Files
6
6
 
@@ -28,15 +28,15 @@ import LoginPage from '@/modules/login-page';
28
28
 
29
29
  • Installed at: src/modules/login-page/
30
30
  • Customize text: src/modules/login-page/lang/*.json
31
- Integrated with auth-core for API authentication
31
+ Uses useAuth() hook from auth-core:
32
+ const { login } = useAuth();
33
+ await login(username, password);
32
34
  • On success, redirects to previous page or home
35
+ • Add to your router as a page component
33
36
  ```
34
37
 
35
38
  ## Dependencies
36
39
 
37
40
  This component requires:
38
- - `button`
39
- - `input`
40
- - `label`
41
41
  - `auth-core`
42
42
  - `api`
@@ -1,6 +1,6 @@
1
1
  # Register Page Split
2
2
 
3
- Split-screen registration page with form on the left and full-height image on the right. Features username, email, password fields with confirmation. Uses customerClient for API calls.
3
+ Split-screen registration page with form on the left and full-height image on the right. Features username, email, password fields with confirmation. Uses useAuth hook from auth-core for registration.
4
4
 
5
5
  ## Files
6
6
 
@@ -30,16 +30,15 @@ import RegisterPageSplit from '@/modules/register-page-split';
30
30
 
31
31
  • Installed at: src/modules/register-page-split/
32
32
  • Customize text: src/modules/register-page-split/lang/*.json
33
- API Integration:
34
- - customerClient.auth.register() for registration
35
- - Shows success state after registration
33
+ Uses useAuth() hook from auth-core:
34
+ const { register } = useAuth();
35
+ await register(username, email, password);
36
+ • Shows success state after registration
36
37
  • Add to your router as a page component
37
38
  ```
38
39
 
39
40
  ## Dependencies
40
41
 
41
42
  This component requires:
42
- - `button`
43
- - `input`
44
43
  - `auth-core`
45
44
  - `api`
@@ -39,8 +39,5 @@ import RegisterPage from '@/modules/register-page';
39
39
  ## Dependencies
40
40
 
41
41
  This component requires:
42
- - `button`
43
- - `input`
44
- - `card`
45
42
  - `auth-core`
46
43
  - `api`
@@ -1,6 +1,6 @@
1
1
  # Reset Password Page Split
2
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. Uses customerClient for API calls.
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. Uses useAuth hook from auth-core for password reset.
4
4
 
5
5
  ## Files
6
6
 
@@ -30,16 +30,15 @@ import ResetPasswordPageSplit from '@/modules/reset-password-page-split';
30
30
 
31
31
  • Installed at: src/modules/reset-password-page-split/
32
32
  • Customize text: src/modules/reset-password-page-split/lang/*.json
33
- API Integration:
34
- - customerClient.auth.resetPassword() for resetting password
35
- - Expects ?code= and ?username= URL parameters from email link
33
+ Uses useAuth() hook from auth-core:
34
+ const { resetPassword } = useAuth();
35
+ await resetPassword(username, code, newPassword);
36
+ • Expects ?code= and ?username= URL parameters from email link
36
37
  • Add to your router as a page component
37
38
  ```
38
39
 
39
40
  ## Dependencies
40
41
 
41
42
  This component requires:
42
- - `button`
43
- - `input`
44
43
  - `auth-core`
45
44
  - `api`
@@ -2,14 +2,12 @@
2
2
  "name": "forgot-password-page-split",
3
3
  "type": "registry:page",
4
4
  "title": "Forgot Password Page Split",
5
- "description": "Split-screen password recovery page with form on the left and full-height image on the right. Features email form with success state after submission. Uses customerClient for API calls.",
5
+ "description": "Split-screen password recovery page with form on the left and full-height image on the right. Features username form with success state after submission. Uses useAuth hook from auth-core for password recovery.",
6
6
  "registryDependencies": [
7
- "button",
8
- "input",
9
7
  "auth-core",
10
8
  "api"
11
9
  ],
12
- "usage": "import ForgotPasswordPageSplit from '@/modules/forgot-password-page-split';\n\n<ForgotPasswordPageSplit\n image=\"/images/forgot-bg.jpg\"\n/>\n\n• Installed at: src/modules/forgot-password-page-split/\n• Customize text: src/modules/forgot-password-page-split/lang/*.json\n• API Integration:\n - customerClient.auth.forgotPassword() for sending reset email\n - Shows success state after email sent\n• Add to your router as a page component",
10
+ "usage": "import ForgotPasswordPageSplit from '@/modules/forgot-password-page-split';\n\n<ForgotPasswordPageSplit\n image=\"/images/forgot-bg.jpg\"\n/>\n\n• Installed at: src/modules/forgot-password-page-split/\n• Customize text: src/modules/forgot-password-page-split/lang/*.json\n• Uses useAuth() hook from auth-core:\n const { forgotPassword } = useAuth();\n await forgotPassword(username);\n• Shows success state after email sent\n• Add to your router as a page component",
13
11
  "route": {
14
12
  "path": "/forgot-password",
15
13
  "componentName": "ForgotPasswordPageSplit"
@@ -25,19 +23,19 @@
25
23
  "path": "forgot-password-page-split/forgot-password-page-split.tsx",
26
24
  "type": "registry:page",
27
25
  "target": "$modules$/forgot-password-page-split/forgot-password-page-split.tsx",
28
- "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Mail, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { customerClient } from \"@/modules/api/customer-client\";\r\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\r\n\r\ninterface ForgotPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ForgotPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ForgotPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"forgot-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n\r\n const [email, setEmail] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [isEmailSent, setIsEmailSent] = useState(false);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await customerClient.auth.forgotPassword({ username: email });\r\n\r\n setIsEmailSent(true);\r\n toast.success(t(\"emailSent\", \"Reset link sent!\"), {\r\n description: t(\"checkInbox\", \"Please check your email inbox.\"),\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"forgotPasswordError\", \"Failed to send reset link. Please try again.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Success state - email sent\r\n if (isEmailSent) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"checkYourEmail\", \"Check your email\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"emailSentTo\", \"We've sent a password reset link to\")}\r\n </p>\r\n <p className=\"text-sm font-medium mt-1\">{email}</p>\r\n </div>\r\n\r\n <div className=\"text-sm text-muted-foreground\">\r\n <p>{t(\"didntReceive\", \"Didn't receive the email?\")}</p>\r\n <Button\r\n variant=\"link\"\r\n className=\"p-0 h-auto\"\r\n onClick={() => {\r\n setIsEmailSent(false);\r\n setError(null);\r\n }}\r\n >\r\n {t(\"tryAgain\", \"Click here to try again\")}\r\n </Button>\r\n </div>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n }\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center\">\r\n <Mail className=\"w-6 h-6 text-primary\" />\r\n </div>\r\n </div>\r\n\r\n <div className=\"text-center\">\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"subtitle\", \"Enter your email to reset your password\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"email\">{t(\"email\", \"Email\")}</Label>\r\n <Input\r\n required\r\n id=\"email\"\r\n type=\"email\"\r\n autoComplete=\"email\"\r\n placeholder={t(\"emailPlaceholder\", \"you@example.com\")}\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendLink\", \"Send Reset Link\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPageSplit;\r\n"
26
+ "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Mail, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\n\r\ninterface ForgotPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ForgotPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ForgotPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"forgot-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n const { forgotPassword } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [isEmailSent, setIsEmailSent] = useState(false);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await forgotPassword(username);\r\n\r\n setIsEmailSent(true);\r\n toast.success(t(\"emailSent\", \"Reset link sent!\"), {\r\n description: t(\"checkInbox\", \"Please check your email inbox.\"),\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"forgotPasswordError\", \"Failed to send reset link. Please try again.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Success state - email sent\r\n if (isEmailSent) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"checkYourEmail\", \"Check your email\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"emailSentTo\", \"We've sent a password reset link to\")}\r\n </p>\r\n <p className=\"text-sm font-medium mt-1\">{username}</p>\r\n </div>\r\n\r\n <div className=\"text-sm text-muted-foreground\">\r\n <p>{t(\"didntReceive\", \"Didn't receive the email?\")}</p>\r\n <Button\r\n variant=\"link\"\r\n className=\"p-0 h-auto\"\r\n onClick={() => {\r\n setIsEmailSent(false);\r\n setError(null);\r\n }}\r\n >\r\n {t(\"tryAgain\", \"Click here to try again\")}\r\n </Button>\r\n </div>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n }\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center\">\r\n <Mail className=\"w-6 h-6 text-primary\" />\r\n </div>\r\n </div>\r\n\r\n <div className=\"text-center\">\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"subtitle\", \"Enter your username to reset your password\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"username\">{t(\"username\", \"Username\")}</Label>\r\n <Input\r\n required\r\n id=\"username\"\r\n type=\"text\"\r\n autoComplete=\"username\"\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendLink\", \"Send Reset Link\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPageSplit;\r\n"
29
27
  },
30
28
  {
31
29
  "path": "forgot-password-page-split/lang/en.json",
32
30
  "type": "registry:lang",
33
31
  "target": "$modules$/forgot-password-page-split/lang/en.json",
34
- "content": "{\r\n \"title\": \"Forgot Password\",\r\n \"subtitle\": \"Enter your email to reset your password\",\r\n \"email\": \"Email\",\r\n \"emailPlaceholder\": \"you@example.com\",\r\n \"sendLink\": \"Send Reset Link\",\r\n \"sending\": \"Sending...\",\r\n \"emailSent\": \"Reset link sent!\",\r\n \"checkInbox\": \"Please check your email inbox.\",\r\n \"forgotPasswordError\": \"Failed to send reset link. Please try again.\",\r\n \"checkYourEmail\": \"Check your email\",\r\n \"emailSentTo\": \"We've sent a password reset link to\",\r\n \"didntReceive\": \"Didn't receive the email?\",\r\n \"tryAgain\": \"Click here to try again\",\r\n \"backToLogin\": \"Back to login\",\r\n \"copyright\": \"All rights reserved.\",\r\n \"imageAlt\": \"Forgot password background\"\r\n}\r\n"
32
+ "content": "{\r\n \"title\": \"Forgot Password\",\r\n \"subtitle\": \"Enter your username to reset your password\",\r\n \"username\": \"Username\",\r\n \"usernamePlaceholder\": \"Enter your username\",\r\n \"email\": \"Email\",\r\n \"emailPlaceholder\": \"you@example.com\",\r\n \"sendLink\": \"Send Reset Link\",\r\n \"sending\": \"Sending...\",\r\n \"emailSent\": \"Reset link sent!\",\r\n \"checkInbox\": \"Please check your email inbox.\",\r\n \"forgotPasswordError\": \"Failed to send reset link. Please try again.\",\r\n \"checkYourEmail\": \"Check your email\",\r\n \"emailSentTo\": \"We've sent a password reset link to\",\r\n \"didntReceive\": \"Didn't receive the email?\",\r\n \"tryAgain\": \"Click here to try again\",\r\n \"backToLogin\": \"Back to login\",\r\n \"copyright\": \"All rights reserved.\",\r\n \"imageAlt\": \"Forgot password background\"\r\n}\r\n"
35
33
  },
36
34
  {
37
35
  "path": "forgot-password-page-split/lang/tr.json",
38
36
  "type": "registry:lang",
39
37
  "target": "$modules$/forgot-password-page-split/lang/tr.json",
40
- "content": "{\r\n \"title\": \"Şifremi Unuttum\",\r\n \"subtitle\": \"Şifrenizi sıfırlamak için e-postanızı girin\",\r\n \"email\": \"E-posta\",\r\n \"emailPlaceholder\": \"ornek@email.com\",\r\n \"sendLink\": \"Sıfırlama Linki Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"emailSent\": \"Sıfırlama linki gönderildi!\",\r\n \"checkInbox\": \"Lütfen e-posta gelen kutunuzu kontrol edin.\",\r\n \"forgotPasswordError\": \"Sıfırlama linki gönderilemedi. Lütfen tekrar deneyin.\",\r\n \"checkYourEmail\": \"E-postanızı kontrol edin\",\r\n \"emailSentTo\": \"Şifre sıfırlama linki gönderildi:\",\r\n \"didntReceive\": \"E-posta almadınız mı?\",\r\n \"tryAgain\": \"Tekrar denemek için tıklayın\",\r\n \"backToLogin\": \"Girişe dön\",\r\n \"copyright\": \"Tüm hakları saklıdır.\",\r\n \"imageAlt\": \"Şifre sıfırlama arka planı\"\r\n}\r\n"
38
+ "content": "{\r\n \"title\": \"Şifremi Unuttum\",\r\n \"subtitle\": \"Şifrenizi sıfırlamak için kullanıcı adınızı girin\",\r\n \"username\": \"Kullanıcı Adı\",\r\n \"usernamePlaceholder\": \"Kullanıcı adınızı girin\",\r\n \"email\": \"E-posta\",\r\n \"emailPlaceholder\": \"ornek@email.com\",\r\n \"sendLink\": \"Sıfırlama Linki Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"emailSent\": \"Sıfırlama linki gönderildi!\",\r\n \"checkInbox\": \"Lütfen e-posta gelen kutunuzu kontrol edin.\",\r\n \"forgotPasswordError\": \"Sıfırlama linki gönderilemedi. Lütfen tekrar deneyin.\",\r\n \"checkYourEmail\": \"E-postanızı kontrol edin\",\r\n \"emailSentTo\": \"Şifre sıfırlama linki gönderildi:\",\r\n \"didntReceive\": \"E-posta almadınız mı?\",\r\n \"tryAgain\": \"Tekrar denemek için tıklayın\",\r\n \"backToLogin\": \"Girişe dön\",\r\n \"copyright\": \"Tüm hakları saklıdır.\",\r\n \"imageAlt\": \"Şifre sıfırlama arka planı\"\r\n}\r\n"
41
39
  }
42
40
  ],
43
41
  "exports": {