@promakeai/cli 0.2.9 → 0.2.11
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
|
@@ -492,7 +492,7 @@ constants.json (full)`)),console.log(C.dim(JSON.stringify(U.constantsConfig,null
|
|
|
492
492
|
`)),e8("promake create <name>","Create new project"),e8("promake add <modules...>","Add modules"),e8("promake add <page> --module-name <name>","Add page with custom name"),e8("promake remove <modules...>","Remove modules"),e8("promake sync","Sync from promake.json"),e8("promake theme --list","List theme options"),e8("promake theme --preset <name>","Apply color preset"),e8("promake list","List available modules"),e8("promake doctor","Check project health"),e8("promake doctor --runtime","Include runtime checks"),e8("promake discover --json","Project info for AI"),e8("promake seo","Sync SEO meta tags"),e8("promake usage","Show this guide"),e8("promake <command> --help","Command-specific help"),console.log()}var Rr=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 R4(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 F91 from"path";var kK=b1(o8(),1);import{execSync as dh0}from"child_process";import wq from"path";async function cE(D,X=!1){let Z=Date.now(),J=[],Y=wq.join(D,"tsconfig.json");if(!await kK.default.pathExists(Y))return{name:"TypeScript",status:"skip",severity:"info",duration:Date.now()-Z,items:[],summary:"No tsconfig.json found"};let Q=wq.join(D,"package.json"),$=!1;if(await kK.default.pathExists(Q))$=!!(await kK.default.readJson(Q)).scripts?.check;let K=tQ(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=dh0(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(...lh0(W,D)),{name:"TypeScript",status:"fail",severity:"error",duration:Date.now()-Z,items:J,summary:`${J.length} type error(s)`}}}function lh0(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:wq.isAbsolute(G)?wq.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 pD=b1(o8(),1);import{execSync as ph0}from"child_process";import hK from"path";var ih0=["eslint.config.js","eslint.config.mjs","eslint.config.cjs",".eslintrc.js",".eslintrc.cjs",".eslintrc.json",".eslintrc.yml",".eslintrc.yaml",".eslintrc"];async function nh0(D){for(let Z of ih0)if(await pD.default.pathExists(hK.join(D,Z)))return!0;let X=hK.join(D,"package.json");if(await pD.default.pathExists(X))try{if((await pD.default.readJson(X)).eslintConfig)return!0}catch{}return!1}async function dE(D,X=!1,Z=!1){let J=Date.now();if(!await nh0(D))return{name:"ESLint",status:"skip",severity:"warning",duration:Date.now()-J,items:[],summary:"No ESLint config found"};let Y=hK.join(D,"package.json"),Q=!1;if(await pD.default.pathExists(Y))Q=!!(await pD.default.readJson(Y)).scripts?.lint;let $=tQ(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=ph0(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=oh0(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 oh0(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(`
|
|
493
493
|
`);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:hK.isAbsolute(Q)?hK.relative(X,Q):Q,line:parseInt(F[1]),column:parseInt(F[2]),code:F[5]||void 0,message:F[4].trim()})}return Z}var Sq=b1(o8(),1);import{execSync as rh0}from"child_process";import xK from"path";async function lE(D){let X=Date.now(),Z=["vite.config.ts","vite.config.js","vite.config.mjs"],J=!1;for(let Y of Z)if(await Sq.default.pathExists(xK.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{rh0("npx vite build 2>&1",{cwd:D,encoding:"utf-8",stdio:"pipe",env:{...process.env,NODE_ENV:"production"}});let Y=xK.join(D,"dist");if(await Sq.default.pathExists(Y))await Sq.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||"",$=sh0(Q,D);return{name:"Build",status:"fail",severity:"error",duration:Date.now()-X,items:$.length>0?$:[{message:"Build failed"}],summary:"Build failed"}}}function sh0(D,X){let Z=[],J=D.split(`
|
|
494
494
|
`),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:xK.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:xK.isAbsolute(F[1]||F[2])?xK.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 bS=b1(o8(),1);import sW from"path";var s41=[process.env["PROGRAMFILES(X86)"]&&sW.join(process.env["PROGRAMFILES(X86)"],"Google","Chrome","Application","chrome.exe"),process.env.PROGRAMFILES&&sW.join(process.env.PROGRAMFILES,"Google","Chrome","Application","chrome.exe"),process.env.LOCALAPPDATA&&sW.join(process.env.LOCALAPPDATA,"Google","Chrome","Application","chrome.exe"),process.env["PROGRAMFILES(X86)"]&&sW.join(process.env["PROGRAMFILES(X86)"],"Microsoft","Edge","Application","msedge.exe"),process.env.PROGRAMFILES&&sW.join(process.env.PROGRAMFILES,"Microsoft","Edge","Application","msedge.exe")].filter(Boolean),a41=["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge","/Applications/Chromium.app/Contents/MacOS/Chromium"],t41=["/usr/bin/google-chrome","/usr/bin/google-chrome-stable","/usr/bin/chromium-browser","/usr/bin/chromium","/snap/bin/chromium"];async function A$0(){let D=await e41();if(!D)return null;try{return{type:"chrome",browser:await(await Promise.resolve().then(() => (M$0(),L$0))).default.launch({executablePath:D,headless:!0,args:["--no-sandbox","--disable-setuid-sandbox","--disable-gpu"]})}}catch{return null}}async function e41(){if(process.env.CHROME_PATH){if(await bS.default.pathExists(process.env.CHROME_PATH))return process.env.CHROME_PATH}let D;switch(process.platform){case"win32":D=s41;break;case"darwin":D=a41;break;default:D=t41}for(let X of D)if(await bS.default.pathExists(X))return X;return null}async function N$0(D){try{await D.browser.close()}catch{}}async function fS(D,X,Z="/"){let J=Date.now();if(!await D91(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 A$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 B=H.location();$.push({type:q==="warning"?"warning":"console",message:H.text(),fileName:B?.url,lineNumber:B?.lineNumber,columnNumber:B?.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 X91(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 N$0(Q)}let F=Z91($);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 D91(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 X91(D){return new Promise((X)=>setTimeout(X,D))}function Z91(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(`
|
|
495
|
-
`)}})}function gS(D,X){console.log(""),console.log(C.bold("Health Check")+C.dim(` (${W91(D.duration)})`)),console.log(C.dim("─".repeat(40)));for(let Z of D.checks)J91(Z,X);console.log(C.dim("─".repeat(40))),G91(D)}function J91(D,X){let Z=$91(D.status),J=K91(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 $)Y91(G,D.severity);if(K>0)console.log(C.dim(` ... +${K} more (--verbose)`))}function Y91(D,X){let Z=X==="error"?C.red:C.yellow,J=Q91(D),Y=J?C.cyan(J)+" ":"";console.log(` ${Z("›")} ${Y}${D.message}`)}function Q91(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 $91(D){switch(D){case"pass":return C.green("✓");case"fail":return C.red("✗");case"skip":return C.dim("○");default:return C.dim("?")}}function K91(D,X){if(D==="fail")return X==="error"?C.red:C.yellow;if(D==="pass")return C.green;return C.dim}function G91(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 W91(D){if(D<1000)return`${D}ms`;return`${(D/1000).toFixed(1)}s`}var R$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(()=>cE(X,$));if(D.lint!==!1)F.push(()=>dE(X,D.fix,$));if(D.build!==!1)F.push(()=>lE(X));if(D.runtime)F.push(()=>fS(Z,J,Y));let W=[];try{if(W=await V91(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:F91.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 gS(V,D.verbose||!1);if(V.summary.errors>0)process.exit(1)});async function V91(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 E$0={name:"@promakeai/cli",version:"0.2.
|
|
495
|
+
`)}})}function gS(D,X){console.log(""),console.log(C.bold("Health Check")+C.dim(` (${W91(D.duration)})`)),console.log(C.dim("─".repeat(40)));for(let Z of D.checks)J91(Z,X);console.log(C.dim("─".repeat(40))),G91(D)}function J91(D,X){let Z=$91(D.status),J=K91(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 $)Y91(G,D.severity);if(K>0)console.log(C.dim(` ... +${K} more (--verbose)`))}function Y91(D,X){let Z=X==="error"?C.red:C.yellow,J=Q91(D),Y=J?C.cyan(J)+" ":"";console.log(` ${Z("›")} ${Y}${D.message}`)}function Q91(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 $91(D){switch(D){case"pass":return C.green("✓");case"fail":return C.red("✗");case"skip":return C.dim("○");default:return C.dim("?")}}function K91(D,X){if(D==="fail")return X==="error"?C.red:C.yellow;if(D==="pass")return C.green;return C.dim}function G91(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 W91(D){if(D<1000)return`${D}ms`;return`${(D/1000).toFixed(1)}s`}var R$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(()=>cE(X,$));if(D.lint!==!1)F.push(()=>dE(X,D.fix,$));if(D.build!==!1)F.push(()=>lE(X));if(D.runtime)F.push(()=>fS(Z,J,Y));let W=[];try{if(W=await V91(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:F91.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 gS(V,D.verbose||!1);if(V.summary.errors>0)process.exit(1)});async function V91(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 E$0={name:"@promakeai/cli",version:"0.2.11",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","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 C8=new $5;C8.name("promake").description(`Modular React template CLI - Build React apps with pre-built components
|
|
496
496
|
|
|
497
497
|
Quick Start:
|
|
498
498
|
$ promake create my-app
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"path": "blog-list-page/blog-list-page.tsx",
|
|
25
25
|
"type": "registry:page",
|
|
26
26
|
"target": "$modules$/blog-list-page/blog-list-page.tsx",
|
|
27
|
-
"content": "import { useState, useEffect } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { Search, Filter } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { PostCard } from \"@/modules/post-card/post-card\";\nimport { usePosts, useBlogCategories, type BlogCategory } from \"@/modules/blog-core\";\n\ninterface FilterSectionProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: (key: string, options?: any) => string;\n searchTerm: string;\n setSearchTerm: (term: string) => void;\n categories: BlogCategory[];\n selectedCategories: string[];\n handleCategoryChange: (slug: string, checked: boolean) => void;\n allTags: string[];\n selectedTags: string[];\n handleTagChange: (tag: string, checked: boolean) => void;\n clearFilters: () => void;\n}\n\nfunction FilterSection({\n t,\n searchTerm,\n setSearchTerm,\n categories,\n selectedCategories,\n handleCategoryChange,\n allTags,\n selectedTags,\n handleTagChange,\n clearFilters,\n}: FilterSectionProps) {\n return (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-3 flex items-center gap-2\">\n <Search className=\"h-4 w-4\" />\n {t(\"search\")}\n </h3>\n <Input\n placeholder={t(\"searchPlaceholder\")}\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n />\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-3\">{t(\"categories\")}</h3>\n <div className=\"space-y-2\">\n {categories.map((category) => (\n <div key={category.slug} className=\"flex items-center space-x-2\">\n <Checkbox\n id={`category-${category.slug}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n />\n <label\n htmlFor={`category-${category.slug}`}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n {allTags.length > 0 && (\n <div>\n <h3 className=\"font-semibold mb-3\">{t(\"tags\")}</h3>\n <div className=\"space-y-2 max-h-48 overflow-y-auto\">\n {allTags.slice(0, 20).map((tag) => (\n <div key={tag} className=\"flex items-center space-x-2\">\n <Checkbox\n id={`tag-${tag}`}\n checked={selectedTags.includes(tag)}\n onCheckedChange={(checked) =>\n handleTagChange(tag, checked as boolean)\n }\n />\n <label\n htmlFor={`tag-${tag}`}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n >\n {tag}\n </label>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {(searchTerm ||\n selectedCategories.length > 0 ||\n selectedTags.length > 0) && (\n <Button variant=\"outline\" onClick={clearFilters} className=\"w-full\">\n {t(\"clearFilters\")}\n </Button>\n )}\n </div>\n );\n}\n\nexport function BlogListPage() {\n const { t } = useTranslation(\"blog-list-page\");\n usePageTitle({ title: t(\"title\") });\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [searchTerm, setSearchTerm] = useState(\n searchParams.get(\"search\") || \"\"\n );\n const [selectedCategories, setSelectedCategories] = useState<string[]>(\n searchParams.get(\"categories\")?.split(\",\").filter(Boolean) || []\n );\n const [selectedTags, setSelectedTags] = useState<string[]>(\n searchParams.get(\"tags\")?.split(\",\").filter(Boolean) || []\n );\n const [sortBy, setSortBy] = useState(searchParams.get(\"sort\") || \"newest\");\n const [viewMode, _setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n\n const { posts, loading, error } = usePosts();\n const { categories } = useBlogCategories();\n\n const filteredPosts = posts.filter((post) => {\n if (searchTerm) {\n const searchLower = searchTerm.toLowerCase();\n if (\n !post.title.toLowerCase().includes(searchLower) &&\n !post.excerpt.toLowerCase().includes(searchLower) &&\n !post.content.toLowerCase().includes(searchLower)\n ) {\n return false;\n }\n }\n\n if (selectedCategories.length > 0) {\n const hasMatchingCategory = selectedCategories.some(\n (categorySlug) =>\n post.category === categorySlug ||\n post.categories?.some((cat) => cat.slug === categorySlug)\n );\n if (!hasMatchingCategory) return false;\n }\n\n if (selectedTags.length > 0) {\n const hasMatchingTag = selectedTags.some((tag) =>\n post.tags.includes(tag)\n );\n if (!hasMatchingTag) return false;\n }\n\n return true;\n });\n\n const sortedPosts = [...filteredPosts].sort((a, b) => {\n switch (sortBy) {\n case \"oldest\":\n return (\n new Date(a.published_at).getTime() -\n new Date(b.published_at).getTime()\n );\n case \"popular\":\n return (b.view_count || 0) - (a.view_count || 0);\n case \"reading-time\":\n return (a.read_time || 0) - (b.read_time || 0);\n case \"newest\":\n default:\n return (\n new Date(b.published_at).getTime() -\n new Date(a.published_at).getTime()\n );\n }\n });\n\n useEffect(() => {\n const params = new URLSearchParams();\n if (searchTerm) params.set(\"search\", searchTerm);\n if (selectedCategories.length)\n params.set(\"categories\", selectedCategories.join(\",\"));\n if (selectedTags.length) params.set(\"tags\", selectedTags.join(\",\"));\n if (sortBy !== \"newest\") params.set(\"sort\", sortBy);\n\n setSearchParams(params);\n }, [searchTerm, selectedCategories, selectedTags, sortBy, setSearchParams]);\n\n const handleCategoryChange = (categorySlug: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories([...selectedCategories, categorySlug]);\n } else {\n setSelectedCategories(\n selectedCategories.filter((c) => c !== categorySlug)\n );\n }\n };\n\n const handleTagChange = (tag: string, checked: boolean) => {\n if (checked) {\n setSelectedTags([...selectedTags, tag]);\n } else {\n setSelectedTags(selectedTags.filter((t) => t !== tag));\n }\n };\n\n const allTags = Array.from(new Set(posts.flatMap((post) => post.tags)));\n\n const clearFilters = () => {\n setSearchTerm(\"\");\n setSelectedCategories([]);\n setSelectedTags([]);\n setSortBy(\"newest\");\n };\n\n const filterProps: FilterSectionProps = {\n t,\n searchTerm,\n setSearchTerm,\n categories,\n selectedCategories,\n handleCategoryChange,\n allTags,\n selectedTags,\n handleTagChange,\n clearFilters,\n };\n\n if (loading) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"animate-pulse space-y-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className=\"h-48 bg-muted rounded-lg\"></div>\n ))}\n </div>\n </div>\n </Layout>\n );\n }\n\n if (error) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\n <p className=\"text-destructive\">{t(\"error\")}</p>\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 flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8\">\n <div>\n <h1 className=\"text-3xl font-bold mb-2\">{t(\"title\")}</h1>\n <p className=\"text-muted-foreground\">{t(\"subtitle\")}</p>\n </div>\n\n <div className=\"flex items-center gap-4\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-[180px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"newest\">{t(\"sortNewest\")}</SelectItem>\n <SelectItem value=\"oldest\">{t(\"sortOldest\")}</SelectItem>\n <SelectItem value=\"popular\">{t(\"sortPopular\")}</SelectItem>\n <SelectItem value=\"reading-time\">\n {t(\"sortReadingTime\")}\n </SelectItem>\n </SelectContent>\n </Select>\n\n <Sheet>\n <SheetTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"lg:hidden\">\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent>\n <SheetHeader>\n <SheetTitle>{t(\"filters\")}</SheetTitle>\n <SheetDescription>{t(\"filterDescription\")}</SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSection {...filterProps} />\n </div>\n </SheetContent>\n </Sheet>\n </div>\n </FadeIn>\n\n <div className=\"flex flex-col lg:flex-row gap-8\">\n <div className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-4\">\n <FilterSection {...filterProps} />\n </div>\n </div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between mb-6\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\"showing\")} {sortedPosts.length} {t(\"of\")} {posts.length}{\" \"}\n {t(\"posts\")}\n {searchTerm && (\n <span className=\"ml-1\">\n {t(\"for\")} \"<strong>{searchTerm}</strong>\"\n </span>\n )}\n </p>\n </div>\n\n {sortedPosts.length > 0 ? (\n <div\n className={`grid gap-6 ${\n viewMode === \"grid\"\n ? \"grid-cols-1 md:grid-cols-2 xl:grid-cols-3\"\n : \"grid-cols-1\"\n }`}\n >\n {sortedPosts.map((post) => (\n <PostCard key={post.id} post={post} layout={viewMode} />\n ))}\n </div>\n ) : (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground mb-4\">\n {t(\"noPostsFound\")}\n </p>\n <Button onClick={clearFilters} variant=\"outline\">\n {t(\"clearFilters\")}\n </Button>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default BlogListPage;\n"
|
|
27
|
+
"content": "import { useState, useEffect } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { Search, Filter } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { PostCard } from \"@/modules/post-card/post-card\";\nimport { usePosts, useBlogCategories, type BlogCategory } from \"@/modules/blog-core\";\n\ninterface FilterSectionProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: any;\n searchTerm: string;\n setSearchTerm: (term: string) => void;\n categories: BlogCategory[];\n selectedCategories: string[];\n handleCategoryChange: (slug: string, checked: boolean) => void;\n allTags: string[];\n selectedTags: string[];\n handleTagChange: (tag: string, checked: boolean) => void;\n clearFilters: () => void;\n}\n\nfunction FilterSection({\n t,\n searchTerm,\n setSearchTerm,\n categories,\n selectedCategories,\n handleCategoryChange,\n allTags,\n selectedTags,\n handleTagChange,\n clearFilters,\n}: FilterSectionProps) {\n return (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-3 flex items-center gap-2\">\n <Search className=\"h-4 w-4\" />\n {t(\"search\")}\n </h3>\n <Input\n placeholder={t(\"searchPlaceholder\")}\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n />\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-3\">{t(\"categories\")}</h3>\n <div className=\"space-y-2\">\n {categories.map((category) => (\n <div key={category.slug} className=\"flex items-center space-x-2\">\n <Checkbox\n id={`category-${category.slug}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n />\n <label\n htmlFor={`category-${category.slug}`}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n {allTags.length > 0 && (\n <div>\n <h3 className=\"font-semibold mb-3\">{t(\"tags\")}</h3>\n <div className=\"space-y-2 max-h-48 overflow-y-auto\">\n {allTags.slice(0, 20).map((tag) => (\n <div key={tag} className=\"flex items-center space-x-2\">\n <Checkbox\n id={`tag-${tag}`}\n checked={selectedTags.includes(tag)}\n onCheckedChange={(checked) =>\n handleTagChange(tag, checked as boolean)\n }\n />\n <label\n htmlFor={`tag-${tag}`}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n >\n {tag}\n </label>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {(searchTerm ||\n selectedCategories.length > 0 ||\n selectedTags.length > 0) && (\n <Button variant=\"outline\" onClick={clearFilters} className=\"w-full\">\n {t(\"clearFilters\")}\n </Button>\n )}\n </div>\n );\n}\n\nexport function BlogListPage() {\n const { t } = useTranslation(\"blog-list-page\");\n usePageTitle({ title: t(\"title\") });\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [searchTerm, setSearchTerm] = useState(\n searchParams.get(\"search\") || \"\"\n );\n const [selectedCategories, setSelectedCategories] = useState<string[]>(\n searchParams.get(\"categories\")?.split(\",\").filter(Boolean) || []\n );\n const [selectedTags, setSelectedTags] = useState<string[]>(\n searchParams.get(\"tags\")?.split(\",\").filter(Boolean) || []\n );\n const [sortBy, setSortBy] = useState(searchParams.get(\"sort\") || \"newest\");\n const [viewMode, _setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n\n const { posts, loading, error } = usePosts();\n const { categories } = useBlogCategories();\n\n const filteredPosts = posts.filter((post) => {\n if (searchTerm) {\n const searchLower = searchTerm.toLowerCase();\n if (\n !post.title.toLowerCase().includes(searchLower) &&\n !post.excerpt.toLowerCase().includes(searchLower) &&\n !post.content.toLowerCase().includes(searchLower)\n ) {\n return false;\n }\n }\n\n if (selectedCategories.length > 0) {\n const hasMatchingCategory = selectedCategories.some(\n (categorySlug) =>\n post.category === categorySlug ||\n post.categories?.some((cat) => cat.slug === categorySlug)\n );\n if (!hasMatchingCategory) return false;\n }\n\n if (selectedTags.length > 0) {\n const hasMatchingTag = selectedTags.some((tag) =>\n post.tags.includes(tag)\n );\n if (!hasMatchingTag) return false;\n }\n\n return true;\n });\n\n const sortedPosts = [...filteredPosts].sort((a, b) => {\n switch (sortBy) {\n case \"oldest\":\n return (\n new Date(a.published_at).getTime() -\n new Date(b.published_at).getTime()\n );\n case \"popular\":\n return (b.view_count || 0) - (a.view_count || 0);\n case \"reading-time\":\n return (a.read_time || 0) - (b.read_time || 0);\n case \"newest\":\n default:\n return (\n new Date(b.published_at).getTime() -\n new Date(a.published_at).getTime()\n );\n }\n });\n\n useEffect(() => {\n const params = new URLSearchParams();\n if (searchTerm) params.set(\"search\", searchTerm);\n if (selectedCategories.length)\n params.set(\"categories\", selectedCategories.join(\",\"));\n if (selectedTags.length) params.set(\"tags\", selectedTags.join(\",\"));\n if (sortBy !== \"newest\") params.set(\"sort\", sortBy);\n\n setSearchParams(params);\n }, [searchTerm, selectedCategories, selectedTags, sortBy, setSearchParams]);\n\n const handleCategoryChange = (categorySlug: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories([...selectedCategories, categorySlug]);\n } else {\n setSelectedCategories(\n selectedCategories.filter((c) => c !== categorySlug)\n );\n }\n };\n\n const handleTagChange = (tag: string, checked: boolean) => {\n if (checked) {\n setSelectedTags([...selectedTags, tag]);\n } else {\n setSelectedTags(selectedTags.filter((t) => t !== tag));\n }\n };\n\n const allTags = Array.from(new Set(posts.flatMap((post) => post.tags)));\n\n const clearFilters = () => {\n setSearchTerm(\"\");\n setSelectedCategories([]);\n setSelectedTags([]);\n setSortBy(\"newest\");\n };\n\n const filterProps: FilterSectionProps = {\n t,\n searchTerm,\n setSearchTerm,\n categories,\n selectedCategories,\n handleCategoryChange,\n allTags,\n selectedTags,\n handleTagChange,\n clearFilters,\n };\n\n if (loading) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"animate-pulse space-y-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className=\"h-48 bg-muted rounded-lg\"></div>\n ))}\n </div>\n </div>\n </Layout>\n );\n }\n\n if (error) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\n <p className=\"text-destructive\">{t(\"error\")}</p>\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 flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8\">\n <div>\n <h1 className=\"text-3xl font-bold mb-2\">{t(\"title\")}</h1>\n <p className=\"text-muted-foreground\">{t(\"subtitle\")}</p>\n </div>\n\n <div className=\"flex items-center gap-4\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-[180px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"newest\">{t(\"sortNewest\")}</SelectItem>\n <SelectItem value=\"oldest\">{t(\"sortOldest\")}</SelectItem>\n <SelectItem value=\"popular\">{t(\"sortPopular\")}</SelectItem>\n <SelectItem value=\"reading-time\">\n {t(\"sortReadingTime\")}\n </SelectItem>\n </SelectContent>\n </Select>\n\n <Sheet>\n <SheetTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"lg:hidden\">\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent>\n <SheetHeader>\n <SheetTitle>{t(\"filters\")}</SheetTitle>\n <SheetDescription>{t(\"filterDescription\")}</SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSection {...filterProps} />\n </div>\n </SheetContent>\n </Sheet>\n </div>\n </FadeIn>\n\n <div className=\"flex flex-col lg:flex-row gap-8\">\n <div className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-4\">\n <FilterSection {...filterProps} />\n </div>\n </div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between mb-6\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\"showing\")} {sortedPosts.length} {t(\"of\")} {posts.length}{\" \"}\n {t(\"posts\")}\n {searchTerm && (\n <span className=\"ml-1\">\n {t(\"for\")} \"<strong>{searchTerm}</strong>\"\n </span>\n )}\n </p>\n </div>\n\n {sortedPosts.length > 0 ? (\n <div\n className={`grid gap-6 ${\n viewMode === \"grid\"\n ? \"grid-cols-1 md:grid-cols-2 xl:grid-cols-3\"\n : \"grid-cols-1\"\n }`}\n >\n {sortedPosts.map((post) => (\n <PostCard key={post.id} post={post} layout={viewMode} />\n ))}\n </div>\n ) : (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground mb-4\">\n {t(\"noPostsFound\")}\n </p>\n <Button onClick={clearFilters} variant=\"outline\">\n {t(\"clearFilters\")}\n </Button>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default BlogListPage;\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "blog-list-page/lang/en.json",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"path": "cart-drawer/cart-drawer.tsx",
|
|
21
21
|
"type": "registry:component",
|
|
22
22
|
"target": "$modules$/cart-drawer/cart-drawer.tsx",
|
|
23
|
-
"content": "import { Link } from \"react-router\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { useTranslation } from \"react-i18next\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport constants from \"@/constants/constants.json\";\n\ninterface CartDrawerProps {\n checkoutHref?: string;\n className?: string;\n}\n\nexport function CartDrawer({\n checkoutHref = \"/checkout\",\n className,\n}: CartDrawerProps) {\n const { t } = useTranslation(\"cart-drawer\");\n const {\n state,\n removeItem,\n updateQuantity,\n isDrawerOpen,\n setDrawerOpen,\n } = useCart();\n const { items, total } = state;\n const currency = (constants.site as any).currency || \"USD\";\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 handleQuantityChange = (id: string | number, newQuantity: number) => {\n if (newQuantity <= 0) {\n removeItem(id);\n } else {\n updateQuantity(id, newQuantity);\n }\n };\n\n return (\n <Sheet open={isDrawerOpen} onOpenChange={setDrawerOpen} className={className}>\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6 pb-8\">\n <SheetHeader>\n <SheetTitle>{t(\"title\", \"Shopping cart\")}</SheetTitle>\n </SheetHeader>\n\n <div className=\"flex-1 overflow-y-auto mt-8\">\n {items.length === 0 ? (\n <p className=\"text-center text-muted-foreground py-8\">\n {t(\"empty\", \"Your cart is empty\")}\n </p>\n ) : (\n <ul className=\"-my-6 divide-y divide-border\">\n {items.map((item) => (\n <li key={item.id} className=\"flex py-6\">\n <div className=\"size-24 shrink-0 overflow-hidden rounded-md border border-border\">\n <img\n alt={item.product.name}\n src={item.product.images[0] || \"/images/placeholder.png\"}\n className=\"size-full object-cover\"\n />\n </div>\n\n <div className=\"ml-4 flex flex-1 flex-col\">\n <div>\n <div className=\"flex justify-between text-base font-medium\">\n <h3>\n <Link\n to={`/products/${item.product.slug}`}\n onClick={() => setDrawerOpen(false)}\n >\n {item.product.name}\n </Link>\n </h3>\n <p className=\"ml-4\">\n {formatPrice(getProductPrice(item.product), currency)}\n </p>\n </div>\n {item.product.category_name && (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {item.product.category_name}\n </p>\n )}\n </div>\n <div className=\"flex flex-1 items-end justify-between text-sm\">\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() =>\n handleQuantityChange(item.id, item.quantity - 1)\n }\n >\n <Minus className=\"h-3 w-3\" />\n </Button>\n <span className=\"w-8 text-center text-sm\">\n {item.quantity}\n </span>\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() =>\n handleQuantityChange(item.id, item.quantity + 1)\n }\n >\n <Plus className=\"h-3 w-3\" />\n </Button>\n </div>\n\n <button\n type=\"button\"\n onClick={() => removeItem(item.id)}\n className=\"font-medium text-primary hover:text-primary/80\"\n >\n {t(\"remove\", \"Remove\")}\n </button>\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n <div className=\"border-t border-border pt-6 mt-6\">\n <div className=\"flex justify-between text-base font-medium\">\n <p>{t(\"subtotal\", \"Subtotal\")}</p>\n <p>{formatPrice(total, currency)}</p>\n </div>\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {t(\"shippingNote\", \"Shipping and taxes calculated at checkout.\")}\n </p>\n <div className=\"mt-6\">\n <Button asChild className=\"w-full\" disabled={items.length === 0}>\n <Link to={checkoutHref} onClick={() => setDrawerOpen(false)}>\n {t(\"checkout\", \"Checkout\")}\n </Link>\n </Button>\n </div>\n </div>\n </SheetContent>\n </Sheet>\n );\n}\n"
|
|
23
|
+
"content": "import { Link } from \"react-router\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { useTranslation } from \"react-i18next\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport constants from \"@/constants/constants.json\";\n\ninterface CartDrawerProps {\n checkoutHref?: string;\n className?: string;\n showTrigger?: boolean;\n}\n\nexport function CartDrawer({\n checkoutHref = \"/checkout\",\n className,\n showTrigger = true,\n}: CartDrawerProps) {\n const { t } = useTranslation(\"cart-drawer\");\n const {\n state,\n removeItem,\n updateQuantity,\n isDrawerOpen,\n setDrawerOpen,\n } = useCart();\n const { items, total } = state;\n const currency = (constants.site as any).currency || \"USD\";\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 handleQuantityChange = (id: string | number, newQuantity: number) => {\n if (newQuantity <= 0) {\n removeItem(id);\n } else {\n updateQuantity(id, newQuantity);\n }\n };\n\n return (\n <Sheet open={isDrawerOpen} onOpenChange={setDrawerOpen} className={className}>\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6 pb-8\">\n <SheetHeader>\n <SheetTitle>{t(\"title\", \"Shopping cart\")}</SheetTitle>\n </SheetHeader>\n\n <div className=\"flex-1 overflow-y-auto mt-8\">\n {items.length === 0 ? (\n <p className=\"text-center text-muted-foreground py-8\">\n {t(\"empty\", \"Your cart is empty\")}\n </p>\n ) : (\n <ul className=\"-my-6 divide-y divide-border\">\n {items.map((item) => (\n <li key={item.id} className=\"flex py-6\">\n <div className=\"size-24 shrink-0 overflow-hidden rounded-md border border-border\">\n <img\n alt={item.product.name}\n src={item.product.images[0] || \"/images/placeholder.png\"}\n className=\"size-full object-cover\"\n />\n </div>\n\n <div className=\"ml-4 flex flex-1 flex-col\">\n <div>\n <div className=\"flex justify-between text-base font-medium\">\n <h3>\n <Link\n to={`/products/${item.product.slug}`}\n onClick={() => setDrawerOpen(false)}\n >\n {item.product.name}\n </Link>\n </h3>\n <p className=\"ml-4\">\n {formatPrice(getProductPrice(item.product), currency)}\n </p>\n </div>\n {item.product.category_name && (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {item.product.category_name}\n </p>\n )}\n </div>\n <div className=\"flex flex-1 items-end justify-between text-sm\">\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() =>\n handleQuantityChange(item.id, item.quantity - 1)\n }\n >\n <Minus className=\"h-3 w-3\" />\n </Button>\n <span className=\"w-8 text-center text-sm\">\n {item.quantity}\n </span>\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() =>\n handleQuantityChange(item.id, item.quantity + 1)\n }\n >\n <Plus className=\"h-3 w-3\" />\n </Button>\n </div>\n\n <button\n type=\"button\"\n onClick={() => removeItem(item.id)}\n className=\"font-medium text-primary hover:text-primary/80\"\n >\n {t(\"remove\", \"Remove\")}\n </button>\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n <div className=\"border-t border-border pt-6 mt-6\">\n <div className=\"flex justify-between text-base font-medium\">\n <p>{t(\"subtotal\", \"Subtotal\")}</p>\n <p>{formatPrice(total, currency)}</p>\n </div>\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {t(\"shippingNote\", \"Shipping and taxes calculated at checkout.\")}\n </p>\n <div className=\"mt-6\">\n <Button asChild className=\"w-full\" disabled={items.length === 0}>\n <Link to={checkoutHref} onClick={() => setDrawerOpen(false)}>\n {t(\"checkout\", \"Checkout\")}\n </Link>\n </Button>\n </div>\n </div>\n </SheetContent>\n </Sheet>\n );\n}\n"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "cart-drawer/lang/en.json",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"path": "products-page/products-page.tsx",
|
|
25
25
|
"type": "registry:page",
|
|
26
26
|
"target": "$modules$/products-page/products-page.tsx",
|
|
27
|
-
"content": "import { useState, useRef, useCallback, useMemo } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Filter, Grid, List } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useProducts, useCategories } from \"@/modules/ecommerce-core\";\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\n\ninterface FilterSidebarProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: (key: string, options?: any) => string;\n categories: Category[];\n selectedCategories: string[];\n handleCategoryChange: (category: string, checked: boolean) => void;\n selectedFeatures: string[];\n handleFeatureChange: (feature: string, checked: boolean) => void;\n minPriceRef: React.RefObject<HTMLInputElement | null>;\n maxPriceRef: React.RefObject<HTMLInputElement | null>;\n searchParams: URLSearchParams;\n handlePriceFilter: () => void;\n}\n\nfunction FilterSidebar({\n t,\n categories,\n selectedCategories,\n handleCategoryChange,\n selectedFeatures,\n handleFeatureChange,\n minPriceRef,\n maxPriceRef,\n searchParams,\n handlePriceFilter,\n}: FilterSidebarProps) {\n return (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"categories\", \"Categories\")}\n </h3>\n <div className=\"space-y-3\">\n {categories.map((category) => (\n <div\n key={category.id}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={`category-${category.id}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={`category-${category.id}`}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"priceRange\", \"Price Range\")}\n </h3>\n <div className=\"space-y-3 p-3 bg-muted/30 rounded-lg\">\n <div className=\"grid grid-cols-2 gap-3\">\n <input\n ref={minPriceRef}\n type=\"number\"\n placeholder={t(\"minPrice\", \"Min\")}\n defaultValue={searchParams.get(\"minPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n <input\n ref={maxPriceRef}\n type=\"number\"\n placeholder={t(\"maxPrice\", \"Max\")}\n defaultValue={searchParams.get(\"maxPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n </div>\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"features\", \"Features\")}\n </h3>\n <div className=\"space-y-3\">\n {[\n { key: \"on_sale\", label: t(\"onSale\", \"On Sale\") },\n { key: \"is_new\", label: t(\"newArrivals\", \"New Arrivals\") },\n { key: \"featured\", label: t(\"featuredLabel\", \"Featured\") },\n { key: \"in_stock\", label: t(\"inStock\", \"In Stock\") },\n ].map((feature) => (\n <div\n key={feature.key}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={feature.key}\n checked={selectedFeatures.includes(feature.key)}\n onCheckedChange={(checked) =>\n handleFeatureChange(feature.key, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={feature.key}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {feature.label}\n </label>\n </div>\n ))}\n </div>\n </div>\n </div>\n );\n}\n\nexport function ProductsPage() {\n const { t } = useTranslation(\"products-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\n const { products, loading: productsLoading } = useProducts();\n const { categories, loading: categoriesLoading } = useCategories();\n const loading = productsLoading || categoriesLoading;\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n const [sortBy, setSortBy] = useState(\"featured\");\n const [selectedCategories, setSelectedCategories] = useState<string[]>(() => {\n const categorySlug = searchParams.get(\"category\");\n return categorySlug ? [categorySlug] : [];\n });\n const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\n const searchQuery = searchParams.get(\"search\") || \"\";\n const minPriceRef = useRef<HTMLInputElement>(null);\n const maxPriceRef = useRef<HTMLInputElement>(null);\n\n const filteredProducts = useMemo(() => {\n const minPrice = parseFloat(searchParams.get(\"minPrice\") || \"0\") || 0;\n const maxPrice =\n parseFloat(searchParams.get(\"maxPrice\") || \"999999\") || 999999;\n\n let filtered = products.filter((product) => {\n const currentPrice =\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n return currentPrice >= minPrice && currentPrice <= maxPrice;\n });\n\n if (selectedCategories.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedCategories.some((selectedCategory) => {\n if (product.category === selectedCategory) return true;\n return product.categories?.some(\n (cat) => cat.slug === selectedCategory\n );\n });\n });\n }\n\n if (selectedFeatures.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedFeatures.every((feature) => {\n switch (feature) {\n case \"on_sale\":\n return product.on_sale;\n case \"is_new\":\n return product.is_new;\n case \"featured\":\n return product.featured;\n case \"in_stock\":\n return product.stock > 0;\n default:\n return true;\n }\n });\n });\n }\n\n // Apply sorting\n return [...filtered].sort((a, b) => {\n switch (sortBy) {\n case \"price-low\":\n return (\n (a.on_sale ? a.sale_price || a.price : a.price) -\n (b.on_sale ? b.sale_price || b.price : b.price)\n );\n case \"price-high\":\n return (\n (b.on_sale ? b.sale_price || b.price : b.price) -\n (a.on_sale ? a.sale_price || a.price : a.price)\n );\n case \"newest\":\n return (\n new Date(b.created_at || 0).getTime() -\n new Date(a.created_at || 0).getTime()\n );\n case \"featured\":\n default:\n return (b.featured ? 1 : 0) - (a.featured ? 1 : 0);\n }\n });\n }, [products, searchParams, selectedFeatures, selectedCategories, sortBy]);\n\n const handlePriceFilter = useCallback(() => {\n const minPrice = minPriceRef.current?.value || \"\";\n const maxPrice = maxPriceRef.current?.value || \"\";\n const params = new URLSearchParams(searchParams);\n if (minPrice) params.set(\"minPrice\", minPrice);\n else params.delete(\"minPrice\");\n if (maxPrice) params.set(\"maxPrice\", maxPrice);\n else params.delete(\"maxPrice\");\n setSearchParams(params);\n }, [searchParams, setSearchParams]);\n\n const handleCategoryChange = useCallback(\n (category: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories((prev) => [...prev, category]);\n } else {\n setSelectedCategories((prev) => prev.filter((c) => c !== category));\n }\n },\n []\n );\n\n const handleFeatureChange = useCallback(\n (feature: string, checked: boolean) => {\n if (checked) {\n setSelectedFeatures((prev) => [...prev, feature]);\n } else {\n setSelectedFeatures((prev) => prev.filter((f) => f !== feature));\n }\n },\n []\n );\n\n const sortOptions = [\n { value: \"featured\", label: t(\"featured\", \"Featured\") },\n { value: \"price-low\", label: t(\"sortPriceLow\", \"Price: Low to High\") },\n { value: \"price-high\", label: t(\"sortPriceHigh\", \"Price: High to Low\") },\n { value: \"newest\", label: t(\"sortNewest\", \"Newest\") },\n ];\n\n const filterSidebarProps: FilterSidebarProps = {\n t,\n categories,\n selectedCategories,\n handleCategoryChange,\n selectedFeatures,\n handleFeatureChange,\n minPriceRef,\n maxPriceRef,\n searchParams,\n handlePriceFilter,\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=\"mb-8\">\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6\">\n <div className=\"space-y-1\">\n <h1 className=\"text-2xl lg:text-3xl font-bold\">\n {searchQuery\n ? t(\"searchResultsFor\", `Search Results for \"${searchQuery}\"`)\n : t(\"allProducts\", \"All Products\")}\n </h1>\n <p className=\"text-sm lg:text-base text-muted-foreground\">\n {t(\"showing\", \"Showing\")} {filteredProducts.length}{\" \"}\n {t(\"of\", \"of\")} {products.length} {t(\"products\", \"products\")}\n </p>\n </div>\n {searchQuery && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setSearchParams({})}\n className=\"w-fit\"\n >\n {t(\"clearSearch\", \"Clear Search\")}\n </Button>\n )}\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between\">\n <Sheet>\n <SheetTrigger asChild>\n <Button\n variant=\"outline\"\n className=\"lg:hidden w-full sm:w-auto\"\n >\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\", \"Filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"w-[300px]\">\n <SheetHeader>\n <SheetTitle>{t(\"filters\", \"Filters\")}</SheetTitle>\n <SheetDescription>\n {t(\"refineSearch\", \"Refine your product search\")}\n </SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSidebar {...filterSidebarProps} />\n </div>\n </SheetContent>\n </Sheet>\n\n <div className=\"flex flex-col sm:flex-row items-stretch sm:items-center gap-3\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-full sm:w-[160px]\">\n <SelectValue placeholder={t(\"sortBy\", \"Sort by\")} />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n\n <div className=\"flex border rounded-lg p-1 w-full sm:w-auto\">\n <Button\n variant={viewMode === \"grid\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"grid\")}\n className=\"flex-1 sm:flex-none\"\n >\n <Grid className=\"h-4 w-4\" />\n </Button>\n <Button\n variant={viewMode === \"list\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"list\")}\n className=\"flex-1 sm:flex-none\"\n >\n <List className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n </div>\n </FadeIn>\n\n <div className=\"flex gap-8\">\n <aside className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-24\">\n <FilterSidebar {...filterSidebarProps} />\n </div>\n </aside>\n\n <div className=\"flex-1\">\n {loading ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {[...Array(6)].map((_, i) => (\n <div\n key={i}\n className=\"animate-pulse bg-card rounded-lg shadow-md overflow-hidden\"\n >\n <div className=\"aspect-square bg-muted mb-4\"></div>\n <div className=\"p-4\">\n <div className=\"h-4 bg-muted rounded w-3/4 mb-2\"></div>\n <div className=\"h-3 bg-muted rounded w-1/2 mb-3\"></div>\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\n </div>\n </div>\n ))}\n </div>\n ) : viewMode === \"grid\" ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"grid\"\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-6\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"list\"\n />\n ))}\n </div>\n )}\n\n {!loading && filteredProducts.length === 0 && (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground\">\n {t(\n \"noProductsFound\",\n \"No products found matching your criteria.\"\n )}\n </p>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ProductsPage;\n"
|
|
27
|
+
"content": "import { useState, useRef, useCallback, useMemo } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Filter, Grid, List } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useProducts, useCategories } from \"@/modules/ecommerce-core\";\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\n\ninterface FilterSidebarProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: any;\n categories: Category[];\n selectedCategories: string[];\n handleCategoryChange: (category: string, checked: boolean) => void;\n selectedFeatures: string[];\n handleFeatureChange: (feature: string, checked: boolean) => void;\n minPriceRef: React.RefObject<HTMLInputElement | null>;\n maxPriceRef: React.RefObject<HTMLInputElement | null>;\n searchParams: URLSearchParams;\n handlePriceFilter: () => void;\n}\n\nfunction FilterSidebar({\n t,\n categories,\n selectedCategories,\n handleCategoryChange,\n selectedFeatures,\n handleFeatureChange,\n minPriceRef,\n maxPriceRef,\n searchParams,\n handlePriceFilter,\n}: FilterSidebarProps) {\n return (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"categories\", \"Categories\")}\n </h3>\n <div className=\"space-y-3\">\n {categories.map((category) => (\n <div\n key={category.id}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={`category-${category.id}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={`category-${category.id}`}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"priceRange\", \"Price Range\")}\n </h3>\n <div className=\"space-y-3 p-3 bg-muted/30 rounded-lg\">\n <div className=\"grid grid-cols-2 gap-3\">\n <input\n ref={minPriceRef}\n type=\"number\"\n placeholder={t(\"minPrice\", \"Min\")}\n defaultValue={searchParams.get(\"minPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n <input\n ref={maxPriceRef}\n type=\"number\"\n placeholder={t(\"maxPrice\", \"Max\")}\n defaultValue={searchParams.get(\"maxPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n </div>\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"features\", \"Features\")}\n </h3>\n <div className=\"space-y-3\">\n {[\n { key: \"on_sale\", label: t(\"onSale\", \"On Sale\") },\n { key: \"is_new\", label: t(\"newArrivals\", \"New Arrivals\") },\n { key: \"featured\", label: t(\"featuredLabel\", \"Featured\") },\n { key: \"in_stock\", label: t(\"inStock\", \"In Stock\") },\n ].map((feature) => (\n <div\n key={feature.key}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={feature.key}\n checked={selectedFeatures.includes(feature.key)}\n onCheckedChange={(checked) =>\n handleFeatureChange(feature.key, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={feature.key}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {feature.label}\n </label>\n </div>\n ))}\n </div>\n </div>\n </div>\n );\n}\n\nexport function ProductsPage() {\n const { t } = useTranslation(\"products-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\n const { products, loading: productsLoading } = useProducts();\n const { categories, loading: categoriesLoading } = useCategories();\n const loading = productsLoading || categoriesLoading;\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n const [sortBy, setSortBy] = useState(\"featured\");\n const [selectedCategories, setSelectedCategories] = useState<string[]>(() => {\n const categorySlug = searchParams.get(\"category\");\n return categorySlug ? [categorySlug] : [];\n });\n const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\n const searchQuery = searchParams.get(\"search\") || \"\";\n const minPriceRef = useRef<HTMLInputElement>(null);\n const maxPriceRef = useRef<HTMLInputElement>(null);\n\n const filteredProducts = useMemo(() => {\n const minPrice = parseFloat(searchParams.get(\"minPrice\") || \"0\") || 0;\n const maxPrice =\n parseFloat(searchParams.get(\"maxPrice\") || \"999999\") || 999999;\n\n let filtered = products.filter((product) => {\n const currentPrice =\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n return currentPrice >= minPrice && currentPrice <= maxPrice;\n });\n\n if (selectedCategories.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedCategories.some((selectedCategory) => {\n if (product.category === selectedCategory) return true;\n return product.categories?.some(\n (cat) => cat.slug === selectedCategory\n );\n });\n });\n }\n\n if (selectedFeatures.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedFeatures.every((feature) => {\n switch (feature) {\n case \"on_sale\":\n return product.on_sale;\n case \"is_new\":\n return product.is_new;\n case \"featured\":\n return product.featured;\n case \"in_stock\":\n return product.stock > 0;\n default:\n return true;\n }\n });\n });\n }\n\n // Apply sorting\n return [...filtered].sort((a, b) => {\n switch (sortBy) {\n case \"price-low\":\n return (\n (a.on_sale ? a.sale_price || a.price : a.price) -\n (b.on_sale ? b.sale_price || b.price : b.price)\n );\n case \"price-high\":\n return (\n (b.on_sale ? b.sale_price || b.price : b.price) -\n (a.on_sale ? a.sale_price || a.price : a.price)\n );\n case \"newest\":\n return (\n new Date(b.created_at || 0).getTime() -\n new Date(a.created_at || 0).getTime()\n );\n case \"featured\":\n default:\n return (b.featured ? 1 : 0) - (a.featured ? 1 : 0);\n }\n });\n }, [products, searchParams, selectedFeatures, selectedCategories, sortBy]);\n\n const handlePriceFilter = useCallback(() => {\n const minPrice = minPriceRef.current?.value || \"\";\n const maxPrice = maxPriceRef.current?.value || \"\";\n const params = new URLSearchParams(searchParams);\n if (minPrice) params.set(\"minPrice\", minPrice);\n else params.delete(\"minPrice\");\n if (maxPrice) params.set(\"maxPrice\", maxPrice);\n else params.delete(\"maxPrice\");\n setSearchParams(params);\n }, [searchParams, setSearchParams]);\n\n const handleCategoryChange = useCallback(\n (category: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories((prev) => [...prev, category]);\n } else {\n setSelectedCategories((prev) => prev.filter((c) => c !== category));\n }\n },\n []\n );\n\n const handleFeatureChange = useCallback(\n (feature: string, checked: boolean) => {\n if (checked) {\n setSelectedFeatures((prev) => [...prev, feature]);\n } else {\n setSelectedFeatures((prev) => prev.filter((f) => f !== feature));\n }\n },\n []\n );\n\n const sortOptions = [\n { value: \"featured\", label: t(\"featured\", \"Featured\") },\n { value: \"price-low\", label: t(\"sortPriceLow\", \"Price: Low to High\") },\n { value: \"price-high\", label: t(\"sortPriceHigh\", \"Price: High to Low\") },\n { value: \"newest\", label: t(\"sortNewest\", \"Newest\") },\n ];\n\n const filterSidebarProps: FilterSidebarProps = {\n t,\n categories,\n selectedCategories,\n handleCategoryChange,\n selectedFeatures,\n handleFeatureChange,\n minPriceRef,\n maxPriceRef,\n searchParams,\n handlePriceFilter,\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=\"mb-8\">\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6\">\n <div className=\"space-y-1\">\n <h1 className=\"text-2xl lg:text-3xl font-bold\">\n {searchQuery\n ? t(\"searchResultsFor\", `Search Results for \"${searchQuery}\"`)\n : t(\"allProducts\", \"All Products\")}\n </h1>\n <p className=\"text-sm lg:text-base text-muted-foreground\">\n {t(\"showing\", \"Showing\")} {filteredProducts.length}{\" \"}\n {t(\"of\", \"of\")} {products.length} {t(\"products\", \"products\")}\n </p>\n </div>\n {searchQuery && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setSearchParams({})}\n className=\"w-fit\"\n >\n {t(\"clearSearch\", \"Clear Search\")}\n </Button>\n )}\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between\">\n <Sheet>\n <SheetTrigger asChild>\n <Button\n variant=\"outline\"\n className=\"lg:hidden w-full sm:w-auto\"\n >\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\", \"Filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"w-[300px]\">\n <SheetHeader>\n <SheetTitle>{t(\"filters\", \"Filters\")}</SheetTitle>\n <SheetDescription>\n {t(\"refineSearch\", \"Refine your product search\")}\n </SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSidebar {...filterSidebarProps} />\n </div>\n </SheetContent>\n </Sheet>\n\n <div className=\"flex flex-col sm:flex-row items-stretch sm:items-center gap-3\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-full sm:w-[160px]\">\n <SelectValue placeholder={t(\"sortBy\", \"Sort by\")} />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n\n <div className=\"flex border rounded-lg p-1 w-full sm:w-auto\">\n <Button\n variant={viewMode === \"grid\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"grid\")}\n className=\"flex-1 sm:flex-none\"\n >\n <Grid className=\"h-4 w-4\" />\n </Button>\n <Button\n variant={viewMode === \"list\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"list\")}\n className=\"flex-1 sm:flex-none\"\n >\n <List className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n </div>\n </FadeIn>\n\n <div className=\"flex gap-8\">\n <aside className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-24\">\n <FilterSidebar {...filterSidebarProps} />\n </div>\n </aside>\n\n <div className=\"flex-1\">\n {loading ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {[...Array(6)].map((_, i) => (\n <div\n key={i}\n className=\"animate-pulse bg-card rounded-lg shadow-md overflow-hidden\"\n >\n <div className=\"aspect-square bg-muted mb-4\"></div>\n <div className=\"p-4\">\n <div className=\"h-4 bg-muted rounded w-3/4 mb-2\"></div>\n <div className=\"h-3 bg-muted rounded w-1/2 mb-3\"></div>\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\n </div>\n </div>\n ))}\n </div>\n ) : viewMode === \"grid\" ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"grid\"\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-6\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"list\"\n />\n ))}\n </div>\n )}\n\n {!loading && filteredProducts.length === 0 && (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground\">\n {t(\n \"noProductsFound\",\n \"No products found matching your criteria.\"\n )}\n </p>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ProductsPage;\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "products-page/lang/en.json",
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import js from
|
|
2
|
-
import globals from
|
|
3
|
-
import reactHooks from
|
|
4
|
-
import reactRefresh from
|
|
5
|
-
import tseslint from
|
|
6
|
-
import { defineConfig, globalIgnores } from
|
|
7
|
-
import eslintConfigPrettier from
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
5
|
+
import tseslint from 'typescript-eslint';
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config';
|
|
7
|
+
import eslintConfigPrettier from 'eslint-config-prettier/flat';
|
|
8
8
|
|
|
9
9
|
export default defineConfig([
|
|
10
|
-
globalIgnores([
|
|
10
|
+
globalIgnores(['dist']),
|
|
11
11
|
{
|
|
12
|
-
files: [
|
|
12
|
+
files: ['**/*.{ts,tsx}'],
|
|
13
13
|
extends: [
|
|
14
14
|
js.configs.recommended,
|
|
15
15
|
tseslint.configs.recommended,
|
|
@@ -22,12 +22,15 @@ export default defineConfig([
|
|
|
22
22
|
globals: globals.browser,
|
|
23
23
|
},
|
|
24
24
|
rules: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
26
|
+
'@typescript-eslint/no-unused-vars': 'off',
|
|
27
|
+
'@typescript-eslint/no-unused-expressions': 'off',
|
|
28
|
+
'react-refresh/only-export-components': 'off',
|
|
29
|
+
'react-hooks/purity': 'off',
|
|
30
|
+
'react-hooks/exhaustive-deps': 'off',
|
|
31
|
+
'react-hooks/set-state-in-effect': 'off',
|
|
32
|
+
'no-unused-vars': 'off',
|
|
33
|
+
'prefer-const': 'warn',
|
|
31
34
|
},
|
|
32
35
|
},
|
|
33
36
|
]);
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
/* Linting */
|
|
20
20
|
"strict": true,
|
|
21
|
-
"noUnusedLocals":
|
|
22
|
-
"noUnusedParameters":
|
|
21
|
+
"noUnusedLocals": false,
|
|
22
|
+
"noUnusedParameters": false,
|
|
23
23
|
"erasableSyntaxOnly": true,
|
|
24
24
|
"noFallthroughCasesInSwitch": true,
|
|
25
|
-
"noUncheckedSideEffectImports":
|
|
25
|
+
"noUncheckedSideEffectImports": false,
|
|
26
26
|
"baseUrl": ".",
|
|
27
27
|
"paths": {
|
|
28
28
|
"@/*": ["./src/*"]
|