@qlover/create-app 0.7.8 → 0.7.9
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/CHANGELOG.md +30 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/config/Identifier/page.home.ts +7 -0
- package/dist/templates/next-app/config/i18n/HomeI18n .ts +22 -0
- package/dist/templates/next-app/config/theme.ts +1 -0
- package/dist/templates/next-app/package.json +4 -4
- package/dist/templates/next-app/public/locales/en.json +2 -1
- package/dist/templates/next-app/public/locales/zh.json +2 -1
- package/dist/templates/next-app/src/app/[locale]/layout.tsx +1 -7
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +3 -11
- package/dist/templates/next-app/src/app/[locale]/login/page.tsx +5 -14
- package/dist/templates/next-app/src/app/[locale]/page.tsx +92 -100
- package/dist/templates/next-app/src/styles/css/antd-themes/_default.css +12 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/dark.css +26 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/pink.css +16 -0
- package/dist/templates/next-app/src/styles/css/page.css +4 -3
- package/dist/templates/next-app/src/styles/css/themes/_default.css +1 -0
- package/dist/templates/next-app/src/styles/css/themes/dark.css +1 -0
- package/dist/templates/next-app/src/styles/css/themes/pink.css +1 -0
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +6 -11
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +27 -0
- package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +49 -21
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +92 -48
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @qlover/create-app
|
|
2
2
|
|
|
3
|
+
## 0.7.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
#### ✨ Features
|
|
8
|
+
|
|
9
|
+
- **next-app:** enhance home page internationalization and SEO support ([54d7714](https://github.com/qlover/fe-base/commit/54d77146f26ce132ddd20edace52a1a10eb03149)) ([#508](https://github.com/qlover/fe-base/pull/508))
|
|
10
|
+
- Added PAGE_HOME_KEYWORDS constant to improve SEO with relevant keywords for the home page.
|
|
11
|
+
- Created HomeI18nInterface to manage internationalization for the home page, incorporating keywords and welcome messages.
|
|
12
|
+
- Updated English and Chinese localization files to include new keywords for the home page, enhancing multilingual support.
|
|
13
|
+
- Refactored Home component to utilize the new i18n structure for better localization handling.
|
|
14
|
+
|
|
15
|
+
These changes aim to improve the user experience by providing comprehensive localization and SEO enhancements for the home page.
|
|
16
|
+
|
|
17
|
+
- **next-app:** enhance login components and improve styling consistency ([59b8b85](https://github.com/qlover/fe-base/commit/59b8b85179e78f29f1eca5b8e7e375b6b8c660eb)) ([#508](https://github.com/qlover/fe-base/pull/508))
|
|
18
|
+
- Updated LoginForm component to use new border color variable for improved styling consistency.
|
|
19
|
+
- Simplified LocaleLink components in LoginForm for cleaner code and better readability.
|
|
20
|
+
- Refactored LoginPage to remove unnecessary elements, streamlining the layout.
|
|
21
|
+
- Enhanced CSS variables in page.css for better color management across the application.
|
|
22
|
+
|
|
23
|
+
These changes aim to improve the user interface and maintain a consistent design across the login components.
|
|
24
|
+
|
|
25
|
+
- **next-app:** update scripts, enhance theme support, and improve styling ([aebcdfa](https://github.com/qlover/fe-base/commit/aebcdfaeb5bae706ac96dc410056f3064eb1e8e9)) ([#508](https://github.com/qlover/fe-base/pull/508))
|
|
26
|
+
- Updated package.json scripts to specify ports for development and production environments.
|
|
27
|
+
- Added new CSS variables for hover states and improved theme management in various CSS files.
|
|
28
|
+
- Enhanced the LanguageSwitcher and ThemeSwitcher components to utilize dropdowns for better user experience.
|
|
29
|
+
- Refactored BaseLayout to include a background color for the main content area.
|
|
30
|
+
|
|
31
|
+
These changes aim to improve the application's usability and maintainability by enhancing the theme management and user interface components.
|
|
32
|
+
|
|
3
33
|
## 0.7.8
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -8,4 +8,4 @@ ${t}`,dn=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),_n=Objec
|
|
|
8
8
|
`))this.#e+=Math.max(1,Math.ceil(Ye(D,{countAnsiEscapeCodes:!0})/t))}get isEnabled(){return this.#a&&!this.#F}set isEnabled(t){if(typeof t!="boolean")throw new TypeError("The `isEnabled` option must be a boolean");this.#a=t}get isSilent(){return this.#F}set isSilent(t){if(typeof t!="boolean")throw new TypeError("The `isSilent` option must be a boolean");this.#F=t}frame(){let t=Date.now();(this.#i===-1||t-this.#c>=this.interval)&&(this.#i=++this.#i%this.#D.frames.length,this.#c=t);let{frames:r}=this.#D,u=r[this.#i];this.color&&(u=d[this.color](u));let i=typeof this.#s=="string"&&this.#s!==""?this.#s+" ":"",D=typeof this.text=="string"?" "+this.text:"",o=typeof this.#o=="string"&&this.#o!==""?" "+this.#o:"";return i+u+D+o}clear(){if(!this.#a||!this.#u.isTTY)return this;this.#u.cursorTo(0);for(let t=0;t<this.#n;t++)t>0&&this.#u.moveCursor(0,-1),this.#u.clearLine(1);return(this.#l||this.lastIndent!==this.#l)&&this.#u.cursorTo(this.#l),this.lastIndent=this.#l,this.#n=0,this}render(){return this.#F?this:(this.clear(),this.#u.write(this.frame()),this.#n=this.#e,this)}start(t){return t&&(this.text=t),this.#F?this:this.#a?this.isSpinning?this:(this.#t.hideCursor&&$e.hide(this.#u),this.#t.discardStdin&&V.default.stdin.isTTY&&(this.#r=!0,Je.start()),this.render(),this.#f=setInterval(this.render.bind(this),this.interval),this):(this.text&&this.#u.write(`- ${this.text}
|
|
9
9
|
`),this)}stop(){return this.#a?(clearInterval(this.#f),this.#f=void 0,this.#i=0,this.clear(),this.#t.hideCursor&&$e.show(this.#u),this.#t.discardStdin&&V.default.stdin.isTTY&&this.#r&&(Je.stop(),this.#r=!1),this):this}succeed(t){return this.stopAndPersist({symbol:H.success,text:t})}fail(t){return this.stopAndPersist({symbol:H.error,text:t})}warn(t){return this.stopAndPersist({symbol:H.warning,text:t})}info(t){return this.stopAndPersist({symbol:H.info,text:t})}stopAndPersist(t={}){if(this.#F)return this;let r=t.prefixText??this.#s,u=this.#E(r," "),i=t.symbol??" ",D=t.text??this.text,s=typeof D=="string"?(i?" ":"")+D:"",a=t.suffixText??this.#o,f=this.#g(a," "),p=u+i+s+f+`
|
|
10
10
|
`;return this.stop(),this.#u.write(p),this}};function tr(e){return new Ze(e)}async function rr(e,t){let r=typeof e=="function",u=typeof e.then=="function";if(!r&&!u)throw new TypeError("Parameter `action` must be a Function or a Promise");let{successText:i,failText:D}=typeof t=="object"?t:{successText:void 0,failText:void 0},o=tr(t).start();try{let a=await(r?e(o):e);return o.succeed(i===void 0?void 0:typeof i=="string"?i:i(a)),a}catch(s){throw o.fail(D===void 0?void 0:typeof D=="string"?D:D(s)),s}}var HD=require("fs");var P=require("path"),me=require("fs"),cr=h(lr(),1),Dt=require("fs");var de=require("fs"),X=class{static ensureDir(t){(0,de.existsSync)(t)||(0,de.mkdirSync)(t,{recursive:!0})}};var{copyFile:rs,stat:us}=Dt.promises,_e=class e{constructor(t,r=e.IGNORE_FILE){this.ignoreTargetPath=t;this.ignoreFile=r}static IGNORE_FILE=".gitignore.template";getIg(t=this.ignoreTargetPath){let r=(0,P.join)(t,this.ignoreFile);if(!(0,me.existsSync)(r))return;let D=(0,me.readFileSync)(r,"utf8").split(`
|
|
11
|
-
`).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,cr.default)().add(D)}async copyFiles(t,r,u,i){let D=await Dt.promises.readdir(t);await Promise.all(D.map(async o=>{let s=(0,P.join)(t,o),a=(0,P.join)(r,o);if(u&&u.ignores(o))return;if(X.ensureDir((0,P.dirname)(a)),(await us(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await rs(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){X.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var L=require("fs"),kD=h(LD(),1),xe=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return(0,L.readFileSync)(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){(0,L.writeFileSync)(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,kD.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return(0,L.existsSync)(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var $D=["pack-app"],ye=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!(0,HD.existsSync)(r))throw new Error("template path not exit");this.ora=rr,this.context=new UD.FeScriptContext(t),this.subPackages=["node-lib","react-app"],this.copyer=new _e((0,v.join)(this.context.options.configsRootPath,"_common")),this.compose=new xe}get logger(){return this.context.logger}async steps(t){try{return await WD.default.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return $D.includes(t)}async getGeneratorContext(){let t=Ot(this.subPackages,$D),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=wt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=(0,v.join)(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:(0,v.join)(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:(0,v.join)(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=(0,v.join)(D,o),a=(0,v.join)(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var bt={name:"@qlover/create-app",version:"0.7.
|
|
11
|
+
`).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,cr.default)().add(D)}async copyFiles(t,r,u,i){let D=await Dt.promises.readdir(t);await Promise.all(D.map(async o=>{let s=(0,P.join)(t,o),a=(0,P.join)(r,o);if(u&&u.ignores(o))return;if(X.ensureDir((0,P.dirname)(a)),(await us(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await rs(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){X.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var L=require("fs"),kD=h(LD(),1),xe=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return(0,L.readFileSync)(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){(0,L.writeFileSync)(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,kD.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return(0,L.existsSync)(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var $D=["pack-app"],ye=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!(0,HD.existsSync)(r))throw new Error("template path not exit");this.ora=rr,this.context=new UD.FeScriptContext(t),this.subPackages=["node-lib","react-app"],this.copyer=new _e((0,v.join)(this.context.options.configsRootPath,"_common")),this.compose=new xe}get logger(){return this.context.logger}async steps(t){try{return await WD.default.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return $D.includes(t)}async getGeneratorContext(){let t=Ot(this.subPackages,$D),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=wt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=(0,v.join)(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:(0,v.join)(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:(0,v.join)(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=(0,v.join)(D,o),a=(0,v.join)(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var bt={name:"@qlover/create-app",version:"0.7.9",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function Ul(){let e=new YD.Command;return e.version(bt.version,"-v, --version","Show version").description(bt.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Wl(){let{dryRun:e,verbose:t,...r}=Ul(),u=(0,vt.resolve)("./templates"),i=(0,vt.resolve)("./configs");(0,xt.existsSync)(u)||(console.error("Template is empty!"),process.exit(1)),(0,xt.existsSync)(i)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Wl().catch(e=>{console.error(e),process.exit(1)});
|
package/dist/index.js
CHANGED
|
@@ -8,4 +8,4 @@ ${t}`,cn=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),fn=Objec
|
|
|
8
8
|
`))this.#e+=Math.max(1,Math.ceil(Le(D,{countAnsiEscapeCodes:!0})/t))}get isEnabled(){return this.#a&&!this.#F}set isEnabled(t){if(typeof t!="boolean")throw new TypeError("The `isEnabled` option must be a boolean");this.#a=t}get isSilent(){return this.#F}set isSilent(t){if(typeof t!="boolean")throw new TypeError("The `isSilent` option must be a boolean");this.#F=t}frame(){let t=Date.now();(this.#i===-1||t-this.#c>=this.interval)&&(this.#i=++this.#i%this.#D.frames.length,this.#c=t);let{frames:r}=this.#D,u=r[this.#i];this.color&&(u=E[this.color](u));let i=typeof this.#s=="string"&&this.#s!==""?this.#s+" ":"",D=typeof this.text=="string"?" "+this.text:"",o=typeof this.#o=="string"&&this.#o!==""?" "+this.#o:"";return i+u+D+o}clear(){if(!this.#a||!this.#u.isTTY)return this;this.#u.cursorTo(0);for(let t=0;t<this.#n;t++)t>0&&this.#u.moveCursor(0,-1),this.#u.clearLine(1);return(this.#l||this.lastIndent!==this.#l)&&this.#u.cursorTo(this.#l),this.lastIndent=this.#l,this.#n=0,this}render(){return this.#F?this:(this.clear(),this.#u.write(this.frame()),this.#n=this.#e,this)}start(t){return t&&(this.text=t),this.#F?this:this.#a?this.isSpinning?this:(this.#t.hideCursor&&je.hide(this.#u),this.#t.discardStdin&&ce.stdin.isTTY&&(this.#r=!0,We.start()),this.render(),this.#f=setInterval(this.render.bind(this),this.interval),this):(this.text&&this.#u.write(`- ${this.text}
|
|
9
9
|
`),this)}stop(){return this.#a?(clearInterval(this.#f),this.#f=void 0,this.#i=0,this.clear(),this.#t.hideCursor&&je.show(this.#u),this.#t.discardStdin&&ce.stdin.isTTY&&this.#r&&(We.stop(),this.#r=!1),this):this}succeed(t){return this.stopAndPersist({symbol:k.success,text:t})}fail(t){return this.stopAndPersist({symbol:k.error,text:t})}warn(t){return this.stopAndPersist({symbol:k.warning,text:t})}info(t){return this.stopAndPersist({symbol:k.info,text:t})}stopAndPersist(t={}){if(this.#F)return this;let r=t.prefixText??this.#s,u=this.#E(r," "),i=t.symbol??" ",D=t.text??this.text,s=typeof D=="string"?(i?" ":"")+D:"",a=t.suffixText??this.#o,f=this.#g(a," "),p=u+i+s+f+`
|
|
10
10
|
`;return this.stop(),this.#u.write(p),this}};function zt(e){return new He(e)}async function Kt(e,t){let r=typeof e=="function",u=typeof e.then=="function";if(!r&&!u)throw new TypeError("Parameter `action` must be a Function or a Promise");let{successText:i,failText:D}=typeof t=="object"?t:{successText:void 0,failText:void 0},o=zt(t).start();try{let a=await(r?e(o):e);return o.succeed(i===void 0?void 0:typeof i=="string"?i:i(a)),a}catch(s){throw o.fail(D===void 0?void 0:typeof D=="string"?D:D(s)),s}}import{existsSync as Hl}from"fs";var Dr=ue(ir(),1);import{dirname as Jn,join as Ze}from"path";import{existsSync as Zn,readFileSync as Qn}from"fs";import{promises as nr}from"fs";import{existsSync as Kn,mkdirSync as Xn}from"fs";var H=class{static ensureDir(t){Kn(t)||Xn(t,{recursive:!0})}};var{copyFile:es,stat:ts}=nr,Ce=class e{constructor(t,r=e.IGNORE_FILE){this.ignoreTargetPath=t;this.ignoreFile=r}static IGNORE_FILE=".gitignore.template";getIg(t=this.ignoreTargetPath){let r=Ze(t,this.ignoreFile);if(!Zn(r))return;let D=Qn(r,"utf8").split(`
|
|
11
|
-
`).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,Dr.default)().add(D)}async copyFiles(t,r,u,i){let D=await nr.readdir(t);await Promise.all(D.map(async o=>{let s=Ze(t,o),a=Ze(r,o);if(u&&u.ignores(o))return;if(H.ensureDir(Jn(a)),(await ts(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await es(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){H.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var ID=ue(qD(),1);import{readFileSync as Ll,writeFileSync as kl,existsSync as $l}from"fs";var me=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return Ll(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){kl(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,ID.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return $l(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var jD=["pack-app"],Be=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!Hl(r))throw new Error("template path not exit");this.ora=Kt,this.context=new Ul(t),this.subPackages=["node-lib","react-app"],this.copyer=new Ce(N(this.context.options.configsRootPath,"_common")),this.compose=new me}get logger(){return this.context.logger}async steps(t){try{return await Wl.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return jD.includes(t)}async getGeneratorContext(){let t=_t(this.subPackages,jD),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=mt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=N(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:N(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:N(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=N(D,o),a=N(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var Et={name:"@qlover/create-app",version:"0.7.
|
|
11
|
+
`).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,Dr.default)().add(D)}async copyFiles(t,r,u,i){let D=await nr.readdir(t);await Promise.all(D.map(async o=>{let s=Ze(t,o),a=Ze(r,o);if(u&&u.ignores(o))return;if(H.ensureDir(Jn(a)),(await ts(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await es(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){H.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var ID=ue(qD(),1);import{readFileSync as Ll,writeFileSync as kl,existsSync as $l}from"fs";var me=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return Ll(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){kl(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,ID.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return $l(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var jD=["pack-app"],Be=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!Hl(r))throw new Error("template path not exit");this.ora=Kt,this.context=new Ul(t),this.subPackages=["node-lib","react-app"],this.copyer=new Ce(N(this.context.options.configsRootPath,"_common")),this.compose=new me}get logger(){return this.context.logger}async steps(t){try{return await Wl.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return jD.includes(t)}async getGeneratorContext(){let t=_t(this.subPackages,jD),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=mt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=N(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:N(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:N(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=N(D,o),a=N(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var Et={name:"@qlover/create-app",version:"0.7.9",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function zl(){let e=new Vl;return e.version(Et.version,"-v, --version","Show version").description(Et.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Kl(){let{dryRun:e,verbose:t,...r}=zl(),u=ND("./templates"),i=ND("./configs");GD(u)||(console.error("Template is empty!"),process.exit(1)),GD(i)||(console.error("Configs is empty!"),process.exit(1)),await new Be({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Kl().catch(e=>{console.error(e),process.exit(1)});
|
|
@@ -12,6 +12,13 @@ export const PAGE_HOME_TITLE = 'page__home__title';
|
|
|
12
12
|
*/
|
|
13
13
|
export const PAGE_HOME_DESCRIPTION = 'page__home__description';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @description Home page keywords
|
|
17
|
+
* @localZh 现代前端实用库, 实用工具, 组件
|
|
18
|
+
* @localEn Modern frontend utility library, practical tools, components
|
|
19
|
+
*/
|
|
20
|
+
export const PAGE_HOME_KEYWORDS = 'page__home__keywords';
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* @description Home page welcome message
|
|
17
24
|
* @localZh 欢迎来到主页
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as i18nKeys from '../Identifier/page.home';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Home page i18n interface
|
|
5
|
+
*
|
|
6
|
+
* @description
|
|
7
|
+
* - welcome: welcome message
|
|
8
|
+
*/
|
|
9
|
+
export type HomeI18nInterface = typeof homeI18n;
|
|
10
|
+
|
|
11
|
+
export const homeI18n = Object.freeze({
|
|
12
|
+
// basic meta properties
|
|
13
|
+
title: i18nKeys.PAGE_HOME_TITLE,
|
|
14
|
+
description: i18nKeys.PAGE_HOME_DESCRIPTION,
|
|
15
|
+
content: i18nKeys.PAGE_HOME_DESCRIPTION,
|
|
16
|
+
keywords: i18nKeys.PAGE_HOME_KEYWORDS,
|
|
17
|
+
|
|
18
|
+
welcome: i18nKeys.HOME_WELCOME,
|
|
19
|
+
getStartedTitle: i18nKeys.HOME_GET_STARTED_TITLE,
|
|
20
|
+
getStartedDescription: i18nKeys.HOME_GET_STARTED_DESCRIPTION,
|
|
21
|
+
getStartedButton: i18nKeys.HOME_GET_STARTED_BUTTON
|
|
22
|
+
});
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "cross-env APP_ENV=localhost next dev --turbopack",
|
|
7
|
-
"dev:staging": "cross-env APP_ENV=staging next dev --turbopack",
|
|
8
|
-
"dev:prod": "cross-env APP_ENV=production next dev --turbopack",
|
|
6
|
+
"dev": "cross-env APP_ENV=localhost next dev --turbopack --port 3100",
|
|
7
|
+
"dev:staging": "cross-env APP_ENV=staging next dev --turbopack --port 3100",
|
|
8
|
+
"dev:prod": "cross-env APP_ENV=production next dev --turbopack --port 3100",
|
|
9
9
|
"build": "cross-env APP_ENV=localhost next build --turbopack",
|
|
10
10
|
"build:staging": "cross-env APP_ENV=staging next build --turbopack",
|
|
11
11
|
"build:prod": "cross-env APP_ENV=production next build --turbopack",
|
|
12
|
-
"start": "next start",
|
|
12
|
+
"start": "next start --port 3101",
|
|
13
13
|
"lint": "eslint .",
|
|
14
14
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
|
15
15
|
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
|
@@ -180,5 +180,6 @@
|
|
|
180
180
|
"page__request__stop_api_catch": "Stop API Catch Result",
|
|
181
181
|
"page__login__content": "Login Page Content",
|
|
182
182
|
"page__login__keywords": "Login Page Keywords",
|
|
183
|
-
"err__server__auth__error": "Server auth error"
|
|
183
|
+
"err__server__auth__error": "Server auth error",
|
|
184
|
+
"page__home__keywords": "Modern frontend utility library, practical tools, components"
|
|
184
185
|
}
|
|
@@ -180,5 +180,6 @@
|
|
|
180
180
|
"page__request__stop_api_catch": "停止 API 捕获结果",
|
|
181
181
|
"page__login__content": "登录页面内容",
|
|
182
182
|
"page__login__keywords": "登录页面关键词",
|
|
183
|
-
"err__server__auth__error": "服务器认证错误"
|
|
183
|
+
"err__server__auth__error": "服务器认证错误",
|
|
184
|
+
"page__home__keywords": "现代前端实用库, 实用工具, 组件"
|
|
184
185
|
}
|
|
@@ -2,7 +2,6 @@ import { NextIntlClientProvider } from 'next-intl';
|
|
|
2
2
|
import { themeConfig } from '@config/theme';
|
|
3
3
|
import { PageParams } from '@/base/cases/PageParams';
|
|
4
4
|
import type { PageLayoutProps } from '@/base/types/PageProps';
|
|
5
|
-
import { BaseHeader } from '@/uikit/components/BaseHeader';
|
|
6
5
|
import { ComboProvider } from '@/uikit/components/ComboProvider';
|
|
7
6
|
import '@/styles/css/index.css';
|
|
8
7
|
|
|
@@ -19,12 +18,7 @@ export default async function RootLayout({
|
|
|
19
18
|
<html data-testid="RootLayout" lang={locale} suppressHydrationWarning>
|
|
20
19
|
<body>
|
|
21
20
|
<NextIntlClientProvider locale={locale} messages={messages}>
|
|
22
|
-
<ComboProvider themeConfig={themeConfig}>
|
|
23
|
-
<div className="flex flex-col min-h-screen">
|
|
24
|
-
<BaseHeader showLogoutButton />
|
|
25
|
-
<div className="flex flex-col">{children}</div>
|
|
26
|
-
</div>
|
|
27
|
-
</ComboProvider>
|
|
21
|
+
<ComboProvider themeConfig={themeConfig}>{children}</ComboProvider>
|
|
28
22
|
</NextIntlClientProvider>
|
|
29
23
|
</body>
|
|
30
24
|
</html>
|
|
@@ -47,7 +47,7 @@ export function LoginForm(props: { tt: LoginI18nInterface }) {
|
|
|
47
47
|
prefix={<UserOutlined className="text-text-tertiary" />}
|
|
48
48
|
placeholder={tt.email}
|
|
49
49
|
title={tt.emailTitle}
|
|
50
|
-
className="h-12 text-base bg-secondary border-border"
|
|
50
|
+
className="h-12 text-base bg-secondary border-c-border"
|
|
51
51
|
autoComplete="off"
|
|
52
52
|
/>
|
|
53
53
|
</Form.Item>
|
|
@@ -66,11 +66,7 @@ export function LoginForm(props: { tt: LoginI18nInterface }) {
|
|
|
66
66
|
</Form.Item>
|
|
67
67
|
|
|
68
68
|
<div className="flex justify-end">
|
|
69
|
-
<LocaleLink
|
|
70
|
-
href="#"
|
|
71
|
-
className="text-brand hover:text-brand-hover"
|
|
72
|
-
title={tt.forgotPasswordTitle}
|
|
73
|
-
>
|
|
69
|
+
<LocaleLink href="#" title={tt.forgotPasswordTitle}>
|
|
74
70
|
{tt.forgotPassword}
|
|
75
71
|
</LocaleLink>
|
|
76
72
|
</div>
|
|
@@ -101,11 +97,7 @@ export function LoginForm(props: { tt: LoginI18nInterface }) {
|
|
|
101
97
|
|
|
102
98
|
<div className="text-center mt-6">
|
|
103
99
|
<span className="text-text-tertiary">{tt.noAccount} </span>
|
|
104
|
-
<LocaleLink
|
|
105
|
-
href="/register"
|
|
106
|
-
className="text-brand hover:text-brand-hover"
|
|
107
|
-
title={tt.createAccountTitle}
|
|
108
|
-
>
|
|
100
|
+
<LocaleLink href="/register" title={tt.createAccountTitle}>
|
|
109
101
|
{tt.createAccount}
|
|
110
102
|
</LocaleLink>
|
|
111
103
|
</div>
|
|
@@ -5,6 +5,7 @@ import { ServerAuth } from '@/base/cases/ServerAuth';
|
|
|
5
5
|
import type { PageParamsProps } from '@/base/types/PageProps';
|
|
6
6
|
import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
|
|
7
7
|
import { redirect } from '@/i18n/routing';
|
|
8
|
+
import { BaseLayout } from '@/uikit/components/BaseLayout';
|
|
8
9
|
import { FeatureItem } from './FeatureItem';
|
|
9
10
|
import { LoginForm } from './LoginForm';
|
|
10
11
|
import type { Metadata } from 'next';
|
|
@@ -29,9 +30,7 @@ export async function generateMetadata({
|
|
|
29
30
|
}): Promise<Metadata> {
|
|
30
31
|
const pageParams = new PageParams(await params);
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return tt;
|
|
33
|
+
return await pageParams.getI18nInterface(loginI18n);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export default async function LoginPage(props: PageParamsProps) {
|
|
@@ -51,18 +50,11 @@ export default async function LoginPage(props: PageParamsProps) {
|
|
|
51
50
|
const tt = await pageParams.getI18nInterface(loginI18n);
|
|
52
51
|
|
|
53
52
|
return (
|
|
54
|
-
<
|
|
53
|
+
<BaseLayout
|
|
55
54
|
data-testid="LoginPage"
|
|
56
|
-
className="
|
|
55
|
+
className="text-xs1 bg-primary flex flex-col min-h-screen"
|
|
57
56
|
>
|
|
58
|
-
{/* Left side - Brand section */}
|
|
59
57
|
<div className="hidden lg:flex bg-secondary lg:w-1/2 p-12 flex-col">
|
|
60
|
-
<div className="flex items-center gap-3 mb-12">
|
|
61
|
-
<div className="w-10 h-10 bg-brand rounded-lg"></div>
|
|
62
|
-
<span className="text-2xl font-semibold text-text">
|
|
63
|
-
{'AppConfig.appName'}
|
|
64
|
-
</span>
|
|
65
|
-
</div>
|
|
66
58
|
<h1 className="text-4xl font-bold text-text mb-4">{tt.welcome}</h1>
|
|
67
59
|
<p className="text-text-secondary text-lg mb-8">{tt.subtitle}</p>
|
|
68
60
|
<div className="space-y-4">
|
|
@@ -72,7 +64,6 @@ export default async function LoginPage(props: PageParamsProps) {
|
|
|
72
64
|
</div>
|
|
73
65
|
</div>
|
|
74
66
|
|
|
75
|
-
{/* Right side - Login form */}
|
|
76
67
|
<div className="w-full lg:w-1/2 p-8 sm:p-12 flex items-center justify-center">
|
|
77
68
|
<div className="w-full max-w-[420px]">
|
|
78
69
|
<h2 className="text-2xl font-semibold mb-2 text-text">{tt.title}</h2>
|
|
@@ -81,6 +72,6 @@ export default async function LoginPage(props: PageParamsProps) {
|
|
|
81
72
|
<LoginForm tt={tt} />
|
|
82
73
|
</div>
|
|
83
74
|
</div>
|
|
84
|
-
</
|
|
75
|
+
</BaseLayout>
|
|
85
76
|
);
|
|
86
77
|
}
|
|
@@ -1,119 +1,111 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { Button } from 'antd';
|
|
2
|
+
import { i18nConfig } from '@config/i18n';
|
|
3
|
+
import { homeI18n } from '@config/i18n/HomeI18n ';
|
|
4
|
+
import { PageParams, type PageParamsType } from '@/base/cases/PageParams';
|
|
3
5
|
import { ServerAuth } from '@/base/cases/ServerAuth';
|
|
4
6
|
import type { PageParamsProps } from '@/base/types/PageProps';
|
|
5
7
|
import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
|
|
6
8
|
import { redirect } from '@/i18n/routing';
|
|
9
|
+
import { BaseLayout } from '@/uikit/components/BaseLayout';
|
|
10
|
+
import type { Metadata } from 'next';
|
|
11
|
+
|
|
12
|
+
// const navigationItems = [
|
|
13
|
+
// {
|
|
14
|
+
// href: '/identifier',
|
|
15
|
+
// titleKey: 'HOME_IDENTIFIER',
|
|
16
|
+
// descriptionKey: 'HOME_IDENTIFIER_DESCRIPTION'
|
|
17
|
+
// }
|
|
18
|
+
// ];
|
|
19
|
+
|
|
20
|
+
// Generate static params for all supported locales (used for SSG)
|
|
21
|
+
export async function generateStaticParams() {
|
|
22
|
+
// Return one entry for each supported locale
|
|
23
|
+
return i18nConfig.supportedLngs.map((locale) => ({ locale }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Allow Next.js to statically generate this page if possible (default behavior)
|
|
27
|
+
export const dynamic = 'auto'; // Enable static generation when possible, fallback to dynamic if needed
|
|
28
|
+
|
|
29
|
+
// Optional: Use revalidate if you want ISR (Incremental Static Regeneration)
|
|
30
|
+
// export const revalidate = 3600; // Rebuild every hour (optional)
|
|
31
|
+
|
|
32
|
+
// Generate localized SEO metadata per locale (Next.js 15+ best practice)
|
|
33
|
+
export async function generateMetadata({
|
|
34
|
+
params
|
|
35
|
+
}: {
|
|
36
|
+
params: Promise<PageParamsType>;
|
|
37
|
+
}): Promise<Metadata> {
|
|
38
|
+
const pageParams = new PageParams(await params);
|
|
39
|
+
return await pageParams.getI18nInterface(homeI18n);
|
|
40
|
+
}
|
|
7
41
|
|
|
8
42
|
export default async function Home({ params }: PageParamsProps) {
|
|
9
43
|
const server = new BootstrapServer();
|
|
10
44
|
const pageParams = new PageParams(await params!);
|
|
11
45
|
const locale = pageParams.getLocale();
|
|
46
|
+
const tt = await pageParams.getI18nInterface(homeI18n);
|
|
12
47
|
|
|
13
48
|
if (!(await new ServerAuth(server).hasAuth())) {
|
|
14
49
|
return redirect({ href: '/login', locale });
|
|
15
50
|
}
|
|
16
51
|
|
|
17
52
|
return (
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
className="
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
<BaseLayout data-testid="HomePage">
|
|
54
|
+
{/* Hero Section */}
|
|
55
|
+
<section className="py-16 px-4">
|
|
56
|
+
<div className="max-w-4xl mx-auto text-center">
|
|
57
|
+
<h1 className="text-4xl md:text-5xl font-bold mb-6 text-text">
|
|
58
|
+
{tt.welcome}
|
|
59
|
+
</h1>
|
|
60
|
+
<p className="text-xl text-text-secondary mb-8">{tt.description}</p>
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
{/* Navigation Grid */}
|
|
65
|
+
<section className="max-w-6xl mx-auto px-4 py-12">
|
|
66
|
+
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
67
|
+
{/* {navigationItems.map((item) => (
|
|
68
|
+
<LocaleLink
|
|
69
|
+
data-testid={`HomePage-navigation-${item.href}`}
|
|
70
|
+
key={item.href}
|
|
71
|
+
title={item.titleKey}
|
|
72
|
+
className={clsx(
|
|
73
|
+
href={item.href}
|
|
74
|
+
'block rounded-lg p-6',
|
|
75
|
+
'bg-secondary',
|
|
76
|
+
'border border-border',
|
|
77
|
+
'hover:bg-elevated',
|
|
78
|
+
'transition-colors duration-200'
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<h3 className={`text-xl font-semibold mb-3 text-text`}>
|
|
82
|
+
{t(item.titleKey)}
|
|
83
|
+
</h3>
|
|
84
|
+
<p className="text-text-secondary mb-4">
|
|
85
|
+
{t(item.descriptionKey)}
|
|
86
|
+
</p>
|
|
87
|
+
<Button type="primary" className="w-full">
|
|
88
|
+
{t(i18nKeys.HOME_EXPLORE)}
|
|
89
|
+
</Button>
|
|
90
|
+
</LocaleLink>
|
|
91
|
+
))} */}
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
43
94
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
height={20}
|
|
57
|
-
/>
|
|
58
|
-
Deploy now
|
|
59
|
-
</a>
|
|
60
|
-
<a
|
|
61
|
-
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
|
62
|
-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
63
|
-
target="_blank"
|
|
64
|
-
rel="noopener noreferrer"
|
|
65
|
-
>
|
|
66
|
-
Read our docs
|
|
67
|
-
</a>
|
|
95
|
+
{/* Call to Action Section */}
|
|
96
|
+
<section className="py-16 px-4 bg-elevated">
|
|
97
|
+
<div className="max-w-4xl mx-auto text-center">
|
|
98
|
+
<h2 className="text-3xl font-bold mb-4 text-text">
|
|
99
|
+
{tt.getStartedTitle}
|
|
100
|
+
</h2>
|
|
101
|
+
<p className="text-lg text-text-secondary mb-8">
|
|
102
|
+
{tt.getStartedDescription}
|
|
103
|
+
</p>
|
|
104
|
+
<Button type="primary" size="large" className="px-8">
|
|
105
|
+
{tt.getStartedButton}
|
|
106
|
+
</Button>
|
|
68
107
|
</div>
|
|
69
|
-
</
|
|
70
|
-
|
|
71
|
-
<a
|
|
72
|
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
73
|
-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
74
|
-
target="_blank"
|
|
75
|
-
rel="noopener noreferrer"
|
|
76
|
-
>
|
|
77
|
-
<Image
|
|
78
|
-
aria-hidden
|
|
79
|
-
src="/file.svg"
|
|
80
|
-
alt="File icon"
|
|
81
|
-
width={16}
|
|
82
|
-
height={16}
|
|
83
|
-
/>
|
|
84
|
-
Learn
|
|
85
|
-
</a>
|
|
86
|
-
<a
|
|
87
|
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
88
|
-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
89
|
-
target="_blank"
|
|
90
|
-
rel="noopener noreferrer"
|
|
91
|
-
>
|
|
92
|
-
<Image
|
|
93
|
-
aria-hidden
|
|
94
|
-
src="/window.svg"
|
|
95
|
-
alt="Window icon"
|
|
96
|
-
width={16}
|
|
97
|
-
height={16}
|
|
98
|
-
/>
|
|
99
|
-
Examples
|
|
100
|
-
</a>
|
|
101
|
-
<a
|
|
102
|
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
103
|
-
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
104
|
-
target="_blank"
|
|
105
|
-
rel="noopener noreferrer"
|
|
106
|
-
>
|
|
107
|
-
<Image
|
|
108
|
-
aria-hidden
|
|
109
|
-
src="/globe.svg"
|
|
110
|
-
alt="Globe icon"
|
|
111
|
-
width={16}
|
|
112
|
-
height={16}
|
|
113
|
-
/>
|
|
114
|
-
Go to nextjs.org →
|
|
115
|
-
</a>
|
|
116
|
-
</footer>
|
|
117
|
-
</div>
|
|
108
|
+
</section>
|
|
109
|
+
</BaseLayout>
|
|
118
110
|
);
|
|
119
111
|
}
|
|
@@ -236,4 +236,16 @@ html,
|
|
|
236
236
|
--fe-message-info-color: var(--fe-color-primary);
|
|
237
237
|
--fe-message-loading-color: var(--fe-color-primary);
|
|
238
238
|
}
|
|
239
|
+
|
|
240
|
+
.ant-dropdown-css-var {
|
|
241
|
+
/* Control Variables - Default Theme */
|
|
242
|
+
--fe-control-outline-width: 2px;
|
|
243
|
+
--fe-control-interactive-size: 16px;
|
|
244
|
+
--fe-control-item-bg-hover: rgba(0, 0, 0, 0.04);
|
|
245
|
+
--fe-control-item-bg-active: #e6f4ff;
|
|
246
|
+
--fe-control-item-bg-active-hover: #bae0ff;
|
|
247
|
+
--fe-control-item-bg-active-disabled: rgba(0, 0, 0, 0.15);
|
|
248
|
+
--fe-control-tmp-outline: rgba(0, 0, 0, 0.02);
|
|
249
|
+
--fe-control-outline: rgba(96, 165, 250, 0.1); /* blue-400 with 0.1 opacity */
|
|
250
|
+
}
|
|
239
251
|
}
|
|
@@ -175,4 +175,30 @@
|
|
|
175
175
|
0.85
|
|
176
176
|
); /* 确保文字在深色背景上清晰可见 */
|
|
177
177
|
}
|
|
178
|
+
.ant-dropdown-css-var {
|
|
179
|
+
/* Control Variables - Dark Theme */
|
|
180
|
+
--fe-control-outline-width: 2px;
|
|
181
|
+
--fe-control-interactive-size: 16px;
|
|
182
|
+
--fe-control-item-bg-hover: rgba(255, 255, 255, 0.08);
|
|
183
|
+
--fe-control-item-bg-active: rgba(
|
|
184
|
+
96,
|
|
185
|
+
165,
|
|
186
|
+
250,
|
|
187
|
+
0.2
|
|
188
|
+
); /* blue-400 with 0.2 opacity */
|
|
189
|
+
--fe-control-item-bg-active-hover: rgba(
|
|
190
|
+
96,
|
|
191
|
+
165,
|
|
192
|
+
250,
|
|
193
|
+
0.3
|
|
194
|
+
); /* blue-400 with 0.3 opacity */
|
|
195
|
+
--fe-control-item-bg-active-disabled: rgba(255, 255, 255, 0.15);
|
|
196
|
+
--fe-control-tmp-outline: rgba(255, 255, 255, 0.02);
|
|
197
|
+
--fe-control-outline: rgba(
|
|
198
|
+
96,
|
|
199
|
+
165,
|
|
200
|
+
250,
|
|
201
|
+
0.1
|
|
202
|
+
); /* blue-400 with 0.1 opacity */
|
|
203
|
+
}
|
|
178
204
|
}
|
|
@@ -201,4 +201,20 @@
|
|
|
201
201
|
0 9px 28px 8px rgba(244, 114, 182, 0.05);
|
|
202
202
|
--fe-modal-mask-bg: rgba(244, 114, 182, 0.45);
|
|
203
203
|
}
|
|
204
|
+
.ant-dropdown-css-var {
|
|
205
|
+
/* Control Variables - Pink Theme */
|
|
206
|
+
--fe-control-outline-width: 2px;
|
|
207
|
+
--fe-control-interactive-size: 16px;
|
|
208
|
+
--fe-control-item-bg-hover: rgba(0, 0, 0, 0.04);
|
|
209
|
+
--fe-control-item-bg-active: #fce7f3; /* pink-100 */
|
|
210
|
+
--fe-control-item-bg-active-hover: #fbcfe8; /* pink-200 */
|
|
211
|
+
--fe-control-item-bg-active-disabled: rgba(0, 0, 0, 0.15);
|
|
212
|
+
--fe-control-tmp-outline: rgba(0, 0, 0, 0.02);
|
|
213
|
+
--fe-control-outline: rgba(
|
|
214
|
+
244,
|
|
215
|
+
114,
|
|
216
|
+
182,
|
|
217
|
+
0.1
|
|
218
|
+
); /* pink-400 with 0.1 opacity */
|
|
219
|
+
}
|
|
204
220
|
}
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
--color-secondary: rgba(var(--color-bg-secondary));
|
|
12
12
|
--color-elevated: rgba(var(--color-bg-elevated));
|
|
13
13
|
--color-text: rgba(var(--text-primary));
|
|
14
|
+
--color-text-hover: rgba(var(--text-primary-hover));
|
|
14
15
|
--color-text-secondary: rgba(var(--text-secondary));
|
|
15
16
|
--color-text-tertiary: rgba(var(--text-tertiary));
|
|
16
|
-
--color-border: rgba(var(--color-border));
|
|
17
|
-
--color-brand: rgba(var(--color-brand));
|
|
18
|
-
--color-brand-hover: rgba(var(--color-brand-hover));
|
|
17
|
+
--color-c-border: rgba(var(--color-border));
|
|
18
|
+
--color-c-brand: rgba(var(--color-brand));
|
|
19
|
+
--color-c-brand-hover: rgba(var(--color-brand-hover));
|
|
19
20
|
}
|
|
@@ -10,10 +10,12 @@ import { ThemeSwitcher } from './ThemeSwitcher';
|
|
|
10
10
|
export function BaseHeader(props: { showLogoutButton?: boolean }) {
|
|
11
11
|
const { showLogoutButton } = props;
|
|
12
12
|
const appConfig = useIOC(IOCIdentifier.AppConfig);
|
|
13
|
+
const i18nService = useIOC(IOCIdentifier.I18nServiceInterface);
|
|
14
|
+
|
|
13
15
|
return (
|
|
14
16
|
<header
|
|
15
|
-
data-testid="
|
|
16
|
-
className="h-14 bg-secondary border-b border-border sticky top-0 z-50"
|
|
17
|
+
data-testid="BaseHeader"
|
|
18
|
+
className="h-14 bg-secondary border-b border-c-border sticky top-0 z-50"
|
|
17
19
|
>
|
|
18
20
|
<div className="flex items-center justify-between h-full px-4 mx-auto max-w-7xl">
|
|
19
21
|
<div className="flex items-center">
|
|
@@ -21,12 +23,6 @@ export function BaseHeader(props: { showLogoutButton?: boolean }) {
|
|
|
21
23
|
href="/"
|
|
22
24
|
className="flex items-center hover:opacity-80 transition-opacity"
|
|
23
25
|
>
|
|
24
|
-
{/* <img
|
|
25
|
-
data-testid="base-header-logo"
|
|
26
|
-
src={IOC(PublicAssetsPath).getPath('/logo.svg')}
|
|
27
|
-
alt="logo"
|
|
28
|
-
className="h-8 w-auto"
|
|
29
|
-
/> */}
|
|
30
26
|
<span
|
|
31
27
|
data-testid="base-header-app-name"
|
|
32
28
|
className="ml-2 text-lg font-semibold text-text"
|
|
@@ -35,10 +31,9 @@ export function BaseHeader(props: { showLogoutButton?: boolean }) {
|
|
|
35
31
|
</span>
|
|
36
32
|
</Link>
|
|
37
33
|
</div>
|
|
38
|
-
<div className="flex items-center gap-
|
|
39
|
-
<LanguageSwitcher />
|
|
34
|
+
<div className="flex items-center gap-2">
|
|
35
|
+
<LanguageSwitcher i18nService={i18nService} />
|
|
40
36
|
<ThemeSwitcher />
|
|
41
|
-
|
|
42
37
|
{showLogoutButton && <LogoutButton />}
|
|
43
38
|
</div>
|
|
44
39
|
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BaseHeader } from './BaseHeader';
|
|
2
|
+
import type { HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface BaseLayoutProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
showLogoutButton?: boolean;
|
|
6
|
+
mainProps?: HTMLAttributes<HTMLElement>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function BaseLayout({
|
|
10
|
+
children,
|
|
11
|
+
showLogoutButton,
|
|
12
|
+
mainProps,
|
|
13
|
+
...props
|
|
14
|
+
}: BaseLayoutProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="BaseLayout"
|
|
18
|
+
className="flex flex-col min-h-screen"
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<BaseHeader showLogoutButton={showLogoutButton} />
|
|
22
|
+
<main className="flex flex-1 flex-col bg-primary" {...mainProps}>
|
|
23
|
+
{children}
|
|
24
|
+
</main>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { TranslationOutlined } from '@ant-design/icons';
|
|
4
|
+
import { Dropdown } from 'antd';
|
|
4
5
|
import { useLocale } from 'next-intl';
|
|
5
|
-
import { useCallback } from 'react';
|
|
6
|
+
import { useCallback, useMemo } from 'react';
|
|
6
7
|
import { i18nConfig } from '@config/i18n';
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import type {
|
|
9
|
+
I18nServiceInterface,
|
|
10
|
+
I18nServiceLocale
|
|
11
|
+
} from '@/base/port/I18nServiceInterface';
|
|
9
12
|
import { usePathname, useRouter } from '@/i18n/routing';
|
|
10
|
-
import { useIOC } from '../hook/useIOC';
|
|
11
|
-
import { useStore } from '../hook/useStore';
|
|
12
13
|
import type { LocaleType } from '@config/i18n';
|
|
14
|
+
import type { ItemType } from 'antd/es/menu/interface';
|
|
13
15
|
|
|
14
|
-
export function LanguageSwitcher() {
|
|
15
|
-
const i18nService =
|
|
16
|
-
const { loading } = useStore(i18nService);
|
|
16
|
+
export function LanguageSwitcher(props: { i18nService: I18nServiceInterface }) {
|
|
17
|
+
const { i18nService } = props;
|
|
17
18
|
const pathname = usePathname(); // current pathname, aware of i18n
|
|
18
19
|
|
|
19
20
|
const router = useRouter(); // i18n-aware router instance
|
|
20
21
|
const currentLocale = useLocale() as LocaleType; // currently active locale
|
|
21
22
|
|
|
23
|
+
const options: ItemType[] = useMemo(() => {
|
|
24
|
+
return i18nConfig.supportedLngs.map(
|
|
25
|
+
(lang) =>
|
|
26
|
+
({
|
|
27
|
+
type: 'item',
|
|
28
|
+
key: lang,
|
|
29
|
+
value: lang,
|
|
30
|
+
label:
|
|
31
|
+
i18nConfig.localeNames[lang as keyof typeof i18nConfig.localeNames]
|
|
32
|
+
}) as ItemType
|
|
33
|
+
);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
22
36
|
const handleLanguageChange = useCallback(
|
|
23
37
|
async (value: string) => {
|
|
24
38
|
// Set a persistent cookie with the user's preferred locale (valid for 1 year)
|
|
@@ -35,18 +49,32 @@ export function LanguageSwitcher() {
|
|
|
35
49
|
[i18nService, pathname, router]
|
|
36
50
|
);
|
|
37
51
|
|
|
52
|
+
const nextLocale = useMemo(() => {
|
|
53
|
+
const targetIndex = i18nConfig.supportedLngs.indexOf(currentLocale) + 1;
|
|
54
|
+
return i18nConfig.supportedLngs[
|
|
55
|
+
targetIndex % i18nConfig.supportedLngs.length
|
|
56
|
+
];
|
|
57
|
+
}, [currentLocale]);
|
|
58
|
+
|
|
38
59
|
return (
|
|
39
|
-
<
|
|
40
|
-
data-testid="
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
<Dropdown
|
|
61
|
+
data-testid="LanguageSwitcherDropdown"
|
|
62
|
+
trigger={['hover']}
|
|
63
|
+
menu={{
|
|
64
|
+
selectedKeys: [currentLocale],
|
|
65
|
+
items: options,
|
|
66
|
+
onClick: ({ key }) => {
|
|
67
|
+
handleLanguageChange(key);
|
|
68
|
+
}
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<span
|
|
72
|
+
data-testid="LanguageSwitcher"
|
|
73
|
+
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
74
|
+
onClick={() => handleLanguageChange(nextLocale)}
|
|
75
|
+
>
|
|
76
|
+
<TranslationOutlined />
|
|
77
|
+
</span>
|
|
78
|
+
</Dropdown>
|
|
51
79
|
);
|
|
52
80
|
}
|
|
@@ -1,73 +1,84 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
BulbOutlined,
|
|
5
|
-
BulbFilled,
|
|
6
4
|
HeartFilled,
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
SettingOutlined,
|
|
6
|
+
SunOutlined,
|
|
7
|
+
MoonOutlined,
|
|
8
|
+
SettingFilled,
|
|
9
|
+
SunFilled,
|
|
10
|
+
MoonFilled,
|
|
11
|
+
HeartOutlined
|
|
9
12
|
} from '@ant-design/icons';
|
|
10
|
-
import {
|
|
13
|
+
import { Dropdown } from 'antd';
|
|
11
14
|
import { clsx } from 'clsx';
|
|
12
15
|
import { useTheme } from 'next-themes';
|
|
13
|
-
import {
|
|
16
|
+
import { useMemo } from 'react';
|
|
17
|
+
import { type SupportedTheme, themeConfig } from '@config/theme';
|
|
14
18
|
import { useMountedClient } from '../hook/useMountedClient';
|
|
19
|
+
import type { ItemType } from 'antd/es/menu/interface';
|
|
15
20
|
|
|
16
21
|
const { supportedThemes } = themeConfig;
|
|
17
22
|
|
|
23
|
+
const defaultTheme = supportedThemes[0] || 'system';
|
|
24
|
+
const themesList = ['system', ...supportedThemes];
|
|
25
|
+
|
|
18
26
|
const colorMap: Record<
|
|
19
27
|
string,
|
|
20
|
-
{
|
|
28
|
+
{
|
|
29
|
+
i18nkey: string;
|
|
30
|
+
selectedColor: string;
|
|
31
|
+
normalColor: string;
|
|
32
|
+
Icon: React.ElementType;
|
|
33
|
+
SelectedIcon: React.ElementType;
|
|
34
|
+
TriggerIcon: React.ElementType;
|
|
35
|
+
}
|
|
21
36
|
> = {
|
|
22
37
|
system: {
|
|
23
38
|
i18nkey: 'System',
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
selectedColor: 'text-text',
|
|
40
|
+
normalColor: 'text-text-secondary',
|
|
41
|
+
Icon: SettingOutlined,
|
|
42
|
+
SelectedIcon: SettingFilled,
|
|
43
|
+
TriggerIcon: SettingOutlined
|
|
26
44
|
},
|
|
27
45
|
light: {
|
|
28
46
|
i18nkey: 'Light',
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
selectedColor: 'text-text',
|
|
48
|
+
normalColor: 'text-text-secondary',
|
|
49
|
+
Icon: SunOutlined,
|
|
50
|
+
SelectedIcon: SunFilled,
|
|
51
|
+
TriggerIcon: SunOutlined
|
|
31
52
|
},
|
|
32
53
|
dark: {
|
|
33
54
|
i18nkey: 'Dark',
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
selectedColor: 'text-[#9333ea]',
|
|
56
|
+
normalColor: 'text-[#a855f7]',
|
|
57
|
+
Icon: MoonOutlined,
|
|
58
|
+
SelectedIcon: MoonFilled,
|
|
59
|
+
TriggerIcon: MoonOutlined
|
|
36
60
|
},
|
|
37
61
|
pink: {
|
|
38
62
|
i18nkey: 'Pink',
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
selectedColor: 'text-[#f472b6]',
|
|
64
|
+
normalColor: 'text-[#ec4899]',
|
|
65
|
+
Icon: HeartOutlined,
|
|
66
|
+
SelectedIcon: HeartFilled,
|
|
67
|
+
TriggerIcon: HeartOutlined
|
|
41
68
|
}
|
|
42
69
|
};
|
|
43
70
|
|
|
44
71
|
export function ThemeSwitcher() {
|
|
45
|
-
const { theme, resolvedTheme, setTheme } = useTheme();
|
|
72
|
+
const { theme: currentTheme, resolvedTheme, setTheme } = useTheme();
|
|
46
73
|
const mounted = useMountedClient();
|
|
47
74
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<Select
|
|
52
|
-
data-testid="ThemeSwitcher"
|
|
53
|
-
loading
|
|
54
|
-
value="system"
|
|
55
|
-
options={[]}
|
|
56
|
-
style={{ width: 120 }}
|
|
57
|
-
className="min-w-40 max-w-full"
|
|
58
|
-
disabled
|
|
59
|
-
/>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const themeOptions = ['system', ...supportedThemes!].map((themeName) => {
|
|
64
|
-
const { i18nkey, colors, icons } = colorMap[themeName] || colorMap.light;
|
|
65
|
-
const [currentColor, normalColor] = colors;
|
|
66
|
-
const [CurrentIcon, NormalIcon] = icons;
|
|
75
|
+
const themeOptions = themesList.map((themeName) => {
|
|
76
|
+
const { i18nkey, selectedColor, normalColor, Icon, SelectedIcon } =
|
|
77
|
+
colorMap[themeName] || colorMap.light;
|
|
67
78
|
|
|
68
79
|
const isCurrentTheme =
|
|
69
|
-
|
|
70
|
-
(themeName === resolvedTheme &&
|
|
80
|
+
currentTheme === themeName ||
|
|
81
|
+
(themeName === resolvedTheme && currentTheme === 'system');
|
|
71
82
|
|
|
72
83
|
return {
|
|
73
84
|
key: themeName,
|
|
@@ -76,24 +87,57 @@ export function ThemeSwitcher() {
|
|
|
76
87
|
<div
|
|
77
88
|
className={clsx(
|
|
78
89
|
'flex items-center gap-2',
|
|
79
|
-
isCurrentTheme ?
|
|
90
|
+
isCurrentTheme ? selectedColor : normalColor
|
|
80
91
|
)}
|
|
81
92
|
>
|
|
82
|
-
{isCurrentTheme ? <
|
|
93
|
+
{isCurrentTheme ? <SelectedIcon /> : <Icon />}
|
|
83
94
|
<span>{i18nkey}</span>
|
|
84
95
|
</div>
|
|
85
96
|
)
|
|
86
|
-
};
|
|
97
|
+
} as ItemType;
|
|
87
98
|
});
|
|
88
99
|
|
|
100
|
+
const nextTheme = useMemo(() => {
|
|
101
|
+
if (!currentTheme) {
|
|
102
|
+
return defaultTheme;
|
|
103
|
+
}
|
|
104
|
+
const targetIndex =
|
|
105
|
+
supportedThemes.indexOf(currentTheme as SupportedTheme) + 1;
|
|
106
|
+
return supportedThemes[targetIndex % supportedThemes.length];
|
|
107
|
+
}, [currentTheme]);
|
|
108
|
+
|
|
109
|
+
const TriggerIcon = colorMap[currentTheme || defaultTheme].TriggerIcon;
|
|
110
|
+
|
|
111
|
+
if (!mounted) {
|
|
112
|
+
return (
|
|
113
|
+
<span
|
|
114
|
+
data-testid="ThemeSwitcher"
|
|
115
|
+
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
116
|
+
>
|
|
117
|
+
<SettingOutlined />
|
|
118
|
+
</span>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
89
122
|
return (
|
|
90
|
-
<
|
|
91
|
-
data-testid="
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
<Dropdown
|
|
124
|
+
data-testid="ThemeSwitcherDropdown"
|
|
125
|
+
trigger={['hover']}
|
|
126
|
+
menu={{
|
|
127
|
+
items: themeOptions,
|
|
128
|
+
selectedKeys: [currentTheme!],
|
|
129
|
+
onClick: ({ key }) => {
|
|
130
|
+
setTheme(key);
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
<span
|
|
135
|
+
data-testid="ThemeSwitcher"
|
|
136
|
+
className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
|
|
137
|
+
onClick={() => setTheme(nextTheme)}
|
|
138
|
+
>
|
|
139
|
+
<TriggerIcon />
|
|
140
|
+
</span>
|
|
141
|
+
</Dropdown>
|
|
98
142
|
);
|
|
99
143
|
}
|