diff --git a/.eslintrc.js b/.eslintrc.js index 37cf179..37e699b 100755 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,4 +40,4 @@ module.exports = { process: 'readonly', global: 'readonly', }, -}; +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffc7139..cf44c8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,9 @@ jobs: - name: Install dependencies run: yarn install + - name: Clear Jest Cache + run: yarn jest --clearCache + - name: Run ESLint run: yarn lint diff --git a/.prettierrc.js b/.prettierrc.js index 2ae7b38..e9666a3 100755 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,7 +1,8 @@ module.exports = { + semi: false, arrowParens: 'avoid', bracketSameLine: true, bracketSpacing: true, singleQuote: true, trailingComma: 'all', -}; +} diff --git a/.yarn/releases/yarn-3.6.4.cjs b/.yarn/releases/yarn-3.6.4.cjs index ebd9272..2910c98 100755 --- a/.yarn/releases/yarn-3.6.4.cjs +++ b/.yarn/releases/yarn-3.6.4.cjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /* eslint-disable */ //prettier-ignore -(()=>{var Dge=Object.create;var lS=Object.defineProperty;var kge=Object.getOwnPropertyDescriptor;var Rge=Object.getOwnPropertyNames;var Fge=Object.getPrototypeOf,Nge=Object.prototype.hasOwnProperty;var J=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+r+'" is not supported')});var Tge=(r,e)=>()=>(r&&(e=r(r=0)),e);var w=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),ut=(r,e)=>{for(var t in e)lS(r,t,{get:e[t],enumerable:!0})},Lge=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Rge(e))!Nge.call(r,n)&&n!==t&&lS(r,n,{get:()=>e[n],enumerable:!(i=kge(e,n))||i.enumerable});return r};var Pe=(r,e,t)=>(t=r!=null?Dge(Fge(r)):{},Lge(e||!r||!r.__esModule?lS(t,"default",{value:r,enumerable:!0}):t,r));var PK=w((zXe,xK)=>{xK.exports=vK;vK.sync=ife;var QK=J("fs");function rfe(r,e){var t=e.pathExt!==void 0?e.pathExt:process.env.PATHEXT;if(!t||(t=t.split(";"),t.indexOf("")!==-1))return!0;for(var i=0;i{FK.exports=kK;kK.sync=nfe;var DK=J("fs");function kK(r,e,t){DK.stat(r,function(i,n){t(i,i?!1:RK(n,e))})}function nfe(r,e){return RK(DK.statSync(r),e)}function RK(r,e){return r.isFile()&&sfe(r,e)}function sfe(r,e){var t=r.mode,i=r.uid,n=r.gid,s=e.uid!==void 0?e.uid:process.getuid&&process.getuid(),o=e.gid!==void 0?e.gid:process.getgid&&process.getgid(),a=parseInt("100",8),l=parseInt("010",8),c=parseInt("001",8),u=a|l,g=t&c||t&l&&n===o||t&a&&i===s||t&u&&s===0;return g}});var LK=w((ZXe,TK)=>{var XXe=J("fs"),lI;process.platform==="win32"||global.TESTING_WINDOWS?lI=PK():lI=NK();TK.exports=SS;SS.sync=ofe;function SS(r,e,t){if(typeof e=="function"&&(t=e,e={}),!t){if(typeof Promise!="function")throw new TypeError("callback not provided");return new Promise(function(i,n){SS(r,e||{},function(s,o){s?n(s):i(o)})})}lI(r,e||{},function(i,n){i&&(i.code==="EACCES"||e&&e.ignoreErrors)&&(i=null,n=!1),t(i,n)})}function ofe(r,e){try{return lI.sync(r,e||{})}catch(t){if(e&&e.ignoreErrors||t.code==="EACCES")return!1;throw t}}});var YK=w((_Xe,GK)=>{var Dg=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys",OK=J("path"),afe=Dg?";":":",MK=LK(),KK=r=>Object.assign(new Error(`not found: ${r}`),{code:"ENOENT"}),UK=(r,e)=>{let t=e.colon||afe,i=r.match(/\//)||Dg&&r.match(/\\/)?[""]:[...Dg?[process.cwd()]:[],...(e.path||process.env.PATH||"").split(t)],n=Dg?e.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",s=Dg?n.split(t):[""];return Dg&&r.indexOf(".")!==-1&&s[0]!==""&&s.unshift(""),{pathEnv:i,pathExt:s,pathExtExe:n}},HK=(r,e,t)=>{typeof e=="function"&&(t=e,e={}),e||(e={});let{pathEnv:i,pathExt:n,pathExtExe:s}=UK(r,e),o=[],a=c=>new Promise((u,g)=>{if(c===i.length)return e.all&&o.length?u(o):g(KK(r));let f=i[c],h=/^".*"$/.test(f)?f.slice(1,-1):f,p=OK.join(h,r),C=!h&&/^\.[\\\/]/.test(r)?r.slice(0,2)+p:p;u(l(C,c,0))}),l=(c,u,g)=>new Promise((f,h)=>{if(g===n.length)return f(a(u+1));let p=n[g];MK(c+p,{pathExt:s},(C,y)=>{if(!C&&y)if(e.all)o.push(c+p);else return f(c+p);return f(l(c,u,g+1))})});return t?a(0).then(c=>t(null,c),t):a(0)},Afe=(r,e)=>{e=e||{};let{pathEnv:t,pathExt:i,pathExtExe:n}=UK(r,e),s=[];for(let o=0;o{"use strict";var jK=(r={})=>{let e=r.env||process.env;return(r.platform||process.platform)!=="win32"?"PATH":Object.keys(e).reverse().find(i=>i.toUpperCase()==="PATH")||"Path"};vS.exports=jK;vS.exports.default=jK});var VK=w((eZe,zK)=>{"use strict";var JK=J("path"),lfe=YK(),cfe=qK();function WK(r,e){let t=r.options.env||process.env,i=process.cwd(),n=r.options.cwd!=null,s=n&&process.chdir!==void 0&&!process.chdir.disabled;if(s)try{process.chdir(r.options.cwd)}catch{}let o;try{o=lfe.sync(r.command,{path:t[cfe({env:t})],pathExt:e?JK.delimiter:void 0})}catch{}finally{s&&process.chdir(i)}return o&&(o=JK.resolve(n?r.options.cwd:"",o)),o}function ufe(r){return WK(r)||WK(r,!0)}zK.exports=ufe});var XK=w((tZe,PS)=>{"use strict";var xS=/([()\][%!^"`<>&|;, *?])/g;function gfe(r){return r=r.replace(xS,"^$1"),r}function ffe(r,e){return r=`${r}`,r=r.replace(/(\\*)"/g,'$1$1\\"'),r=r.replace(/(\\*)$/,"$1$1"),r=`"${r}"`,r=r.replace(xS,"^$1"),e&&(r=r.replace(xS,"^$1")),r}PS.exports.command=gfe;PS.exports.argument=ffe});var _K=w((rZe,ZK)=>{"use strict";ZK.exports=/^#!(.*)/});var eU=w((iZe,$K)=>{"use strict";var hfe=_K();$K.exports=(r="")=>{let e=r.match(hfe);if(!e)return null;let[t,i]=e[0].replace(/#! ?/,"").split(" "),n=t.split("/").pop();return n==="env"?i:i?`${n} ${i}`:n}});var rU=w((nZe,tU)=>{"use strict";var DS=J("fs"),pfe=eU();function dfe(r){let t=Buffer.alloc(150),i;try{i=DS.openSync(r,"r"),DS.readSync(i,t,0,150,0),DS.closeSync(i)}catch{}return pfe(t.toString())}tU.exports=dfe});var oU=w((sZe,sU)=>{"use strict";var Cfe=J("path"),iU=VK(),nU=XK(),mfe=rU(),Efe=process.platform==="win32",Ife=/\.(?:com|exe)$/i,yfe=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function wfe(r){r.file=iU(r);let e=r.file&&mfe(r.file);return e?(r.args.unshift(r.file),r.command=e,iU(r)):r.file}function Bfe(r){if(!Efe)return r;let e=wfe(r),t=!Ife.test(e);if(r.options.forceShell||t){let i=yfe.test(e);r.command=Cfe.normalize(r.command),r.command=nU.command(r.command),r.args=r.args.map(s=>nU.argument(s,i));let n=[r.command].concat(r.args).join(" ");r.args=["/d","/s","/c",`"${n}"`],r.command=process.env.comspec||"cmd.exe",r.options.windowsVerbatimArguments=!0}return r}function bfe(r,e,t){e&&!Array.isArray(e)&&(t=e,e=null),e=e?e.slice(0):[],t=Object.assign({},t);let i={command:r,args:e,options:t,file:void 0,original:{command:r,args:e}};return t.shell?i:Bfe(i)}sU.exports=bfe});var lU=w((oZe,AU)=>{"use strict";var kS=process.platform==="win32";function RS(r,e){return Object.assign(new Error(`${e} ${r.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${e} ${r.command}`,path:r.command,spawnargs:r.args})}function Qfe(r,e){if(!kS)return;let t=r.emit;r.emit=function(i,n){if(i==="exit"){let s=aU(n,e,"spawn");if(s)return t.call(r,"error",s)}return t.apply(r,arguments)}}function aU(r,e){return kS&&r===1&&!e.file?RS(e.original,"spawn"):null}function Sfe(r,e){return kS&&r===1&&!e.file?RS(e.original,"spawnSync"):null}AU.exports={hookChildProcess:Qfe,verifyENOENT:aU,verifyENOENTSync:Sfe,notFoundError:RS}});var TS=w((aZe,kg)=>{"use strict";var cU=J("child_process"),FS=oU(),NS=lU();function uU(r,e,t){let i=FS(r,e,t),n=cU.spawn(i.command,i.args,i.options);return NS.hookChildProcess(n,i),n}function vfe(r,e,t){let i=FS(r,e,t),n=cU.spawnSync(i.command,i.args,i.options);return n.error=n.error||NS.verifyENOENTSync(n.status,i),n}kg.exports=uU;kg.exports.spawn=uU;kg.exports.sync=vfe;kg.exports._parse=FS;kg.exports._enoent=NS});var fU=w((AZe,gU)=>{"use strict";function xfe(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function Zl(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Zl)}xfe(Zl,Error);Zl.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;g>",ie=me(">>",!1),de=">&",_e=me(">&",!1),Pt=">",It=me(">",!1),Or="<<<",ii=me("<<<",!1),gi="<&",hr=me("<&",!1),fi="<",ni=me("<",!1),Ks=function(m){return{type:"argument",segments:[].concat(...m)}},pr=function(m){return m},Ii="$'",rs=me("$'",!1),fa="'",dA=me("'",!1),cg=function(m){return[{type:"text",text:m}]},is='""',CA=me('""',!1),ha=function(){return{type:"text",text:""}},wp='"',mA=me('"',!1),EA=function(m){return m},wr=function(m){return{type:"arithmetic",arithmetic:m,quoted:!0}},Tl=function(m){return{type:"shell",shell:m,quoted:!0}},ug=function(m){return{type:"variable",...m,quoted:!0}},yo=function(m){return{type:"text",text:m}},gg=function(m){return{type:"arithmetic",arithmetic:m,quoted:!1}},Bp=function(m){return{type:"shell",shell:m,quoted:!1}},bp=function(m){return{type:"variable",...m,quoted:!1}},vr=function(m){return{type:"glob",pattern:m}},se=/^[^']/,wo=Je(["'"],!0,!1),Fn=function(m){return m.join("")},fg=/^[^$"]/,bt=Je(["$",'"'],!0,!1),Ll=`\\ +;(()=>{var Dge=Object.create;var lS=Object.defineProperty;var kge=Object.getOwnPropertyDescriptor;var Rge=Object.getOwnPropertyNames;var Fge=Object.getPrototypeOf,Nge=Object.prototype.hasOwnProperty;var J=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+r+'" is not supported')});var Tge=(r,e)=>()=>(r&&(e=r(r=0)),e);var w=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),ut=(r,e)=>{for(var t in e)lS(r,t,{get:e[t],enumerable:!0})},Lge=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Rge(e))!Nge.call(r,n)&&n!==t&&lS(r,n,{get:()=>e[n],enumerable:!(i=kge(e,n))||i.enumerable});return r};var Pe=(r,e,t)=>(t=r!=null?Dge(Fge(r)):{},Lge(e||!r||!r.__esModule?lS(t,"default",{value:r,enumerable:!0}):t,r));var PK=w((zXe,xK)=>{xK.exports=vK;vK.sync=ife;var QK=J("fs");function rfe(r,e){var t=e.pathExt!==void 0?e.pathExt:process.env.PATHEXT;if(!t||(t=t.split(";"),t.indexOf("")!==-1))return!0;for(var i=0;i{FK.exports=kK;kK.sync=nfe;var DK=J("fs");function kK(r,e,t){DK.stat(r,function(i,n){t(i,i?!1:RK(n,e))})}function nfe(r,e){return RK(DK.statSync(r),e)}function RK(r,e){return r.isFile()&&sfe(r,e)}function sfe(r,e){var t=r.mode,i=r.uid,n=r.gid,s=e.uid!==void 0?e.uid:process.getuid&&process.getuid(),o=e.gid!==void 0?e.gid:process.getgid&&process.getgid(),a=parseInt("100",8),l=parseInt("010",8),c=parseInt("001",8),u=a|l,g=t&c||t&l&&n===o||t&a&&i===s||t&u&&s===0;return g}});var LK=w((ZXe,TK)=>{var XXe=J("fs"),lI;process.platform==="win32"||global.TESTING_WINDOWS?lI=PK():lI=NK();TK.exports=SS;SS.sync=ofe;function SS(r,e,t){if(typeof e=="function"&&(t=e,e={}),!t){if(typeof Promise!="function")throw new TypeError("callback not provided");return new Promise(function(i,n){SS(r,e||{},function(s,o){s?n(s):i(o)})})}lI(r,e||{},function(i,n){i&&(i.code==="EACCES"||e&&e.ignoreErrors)&&(i=null,n=!1),t(i,n)})}function ofe(r,e){try{return lI.sync(r,e||{})}catch(t){if(e&&e.ignoreErrors||t.code==="EACCES")return!1;throw t}}});var YK=w((_Xe,GK)=>{var Dg=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys",OK=J("path"),afe=Dg?";":":",MK=LK(),KK=r=>Object.assign(new Error(`not found: ${r}`),{code:"ENOENT"}),UK=(r,e)=>{let t=e.colon||afe,i=r.match(/\//)||Dg&&r.match(/\\/)?[""]:[...Dg?[process.cwd()]:[],...(e.path||process.env.PATH||"").split(t)],n=Dg?e.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",s=Dg?n.split(t):[""];return Dg&&r.indexOf(".")!==-1&&s[0]!==""&&s.unshift(""),{pathEnv:i,pathExt:s,pathExtExe:n}},HK=(r,e,t)=>{typeof e=="function"&&(t=e,e={}),e||(e={});let{pathEnv:i,pathExt:n,pathExtExe:s}=UK(r,e),o=[],a=c=>new Promise((u,g)=>{if(c===i.length)return e.all&&o.length?u(o):g(KK(r));let f=i[c],h=/^".*"$/.test(f)?f.slice(1,-1):f,p=OK.join(h,r),C=!h&&/^\.[\\\/]/.test(r)?r.slice(0,2)+p:p;u(l(C,c,0))}),l=(c,u,g)=>new Promise((f,h)=>{if(g===n.length)return f(a(u+1));let p=n[g];MK(c+p,{pathExt:s},(C,y)=>{if(!C&&y)if(e.all)o.push(c+p);else return f(c+p);return f(l(c,u,g+1))})});return t?a(0).then(c=>t(null,c),t):a(0)},Afe=(r,e)=>{e=e||{};let{pathEnv:t,pathExt:i,pathExtExe:n}=UK(r,e),s=[];for(let o=0;o{"use strict";var jK=(r={})=>{let e=r.env||process.env;return(r.platform||process.platform)!=="win32"?"PATH":Object.keys(e).reverse().find(i=>i.toUpperCase()==="PATH")||"Path"};vS.exports=jK;vS.exports.default=jK});var VK=w((eZe,zK)=>{"use strict";var JK=J("path"),lfe=YK(),cfe=qK();function WK(r,e){let t=r.options.env||process.env,i=process.cwd(),n=r.options.cwd!=null,s=n&&process.chdir!==void 0&&!process.chdir.disabled;if(s)try{process.chdir(r.options.cwd)}catch{}let o;try{o=lfe.sync(r.command,{path:t[cfe({env:t})],pathExt:e?JK.delimiter:void 0})}catch{}finally{s&&process.chdir(i)}return o&&(o=JK.resolve(n?r.options.cwd:"",o)),o}function ufe(r){return WK(r)||WK(r,!0)}zK.exports=ufe});var XK=w((tZe,PS)=>{"use strict";var xS=/([()\][%!^"`<>&|;, *?])/g;function gfe(r){return r=r.replace(xS,"^$1"),r}function ffe(r,e){return r=`${r}`,r=r.replace(/(\\*)"/g,'$1$1\\"'),r=r.replace(/(\\*)$/,"$1$1"),r=`"${r}"`,r=r.replace(xS,"^$1"),e&&(r=r.replace(xS,"^$1")),r}PS.exports.command=gfe;PS.exports.argument=ffe});var _K=w((rZe,ZK)=>{"use strict";ZK.exports=/^#!(.*)/});var eU=w((iZe,$K)=>{"use strict";var hfe=_K();$K.exports=(r="")=>{let e=r.match(hfe);if(!e)return null;let[t,i]=e[0].replace(/#! ?/,"").split(" "),n=t.split("/").pop();return n==="env"?i:i?`${n} ${i}`:n}});var rU=w((nZe,tU)=>{"use strict";var DS=J("fs"),pfe=eU();function dfe(r){let t=Buffer.alloc(150),i;try{i=DS.openSync(r,"r"),DS.readSync(i,t,0,150,0),DS.closeSync(i)}catch{}return pfe(t.toString())}tU.exports=dfe});var oU=w((sZe,sU)=>{"use strict";var Cfe=J("path"),iU=VK(),nU=XK(),mfe=rU(),Efe=process.platform==="win32",Ife=/\.(?:com|exe)$/i,yfe=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function wfe(r){r.file=iU(r);let e=r.file&&mfe(r.file);return e?(r.args.unshift(r.file),r.command=e,iU(r)):r.file}function Bfe(r){if(!Efe)return r;let e=wfe(r),t=!Ife.test(e);if(r.options.forceShell||t){let i=yfe.test(e);r.command=Cfe.normalize(r.command),r.command=nU.command(r.command),r.args=r.args.map(s=>nU.argument(s,i));let n=[r.command].concat(r.args).join(" ");r.args=["/d","/s","/c",`"${n}"`],r.command=process.env.comspec||"cmd.exe",r.options.windowsVerbatimArguments=!0}return r}function bfe(r,e,t){e&&!Array.isArray(e)&&(t=e,e=null),e=e?e.slice(0):[],t=Object.assign({},t);let i={command:r,args:e,options:t,file:void 0,original:{command:r,args:e}};return t.shell?i:Bfe(i)}sU.exports=bfe});var lU=w((oZe,AU)=>{"use strict";var kS=process.platform==="win32";function RS(r,e){return Object.assign(new Error(`${e} ${r.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${e} ${r.command}`,path:r.command,spawnargs:r.args})}function Qfe(r,e){if(!kS)return;let t=r.emit;r.emit=function(i,n){if(i==="exit"){let s=aU(n,e,"spawn");if(s)return t.call(r,"error",s)}return t.apply(r,arguments)}}function aU(r,e){return kS&&r===1&&!e.file?RS(e.original,"spawn"):null}function Sfe(r,e){return kS&&r===1&&!e.file?RS(e.original,"spawnSync"):null}AU.exports={hookChildProcess:Qfe,verifyENOENT:aU,verifyENOENTSync:Sfe,notFoundError:RS}});var TS=w((aZe,kg)=>{"use strict";var cU=J("child_process"),FS=oU(),NS=lU();function uU(r,e,t){let i=FS(r,e,t),n=cU.spawn(i.command,i.args,i.options);return NS.hookChildProcess(n,i),n}function vfe(r,e,t){let i=FS(r,e,t),n=cU.spawnSync(i.command,i.args,i.options);return n.error=n.error||NS.verifyENOENTSync(n.status,i),n}kg.exports=uU;kg.exports.spawn=uU;kg.exports.sync=vfe;kg.exports._parse=FS;kg.exports._enoent=NS});var fU=w((AZe,gU)=>{"use strict";function xfe(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function Zl(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Zl)}xfe(Zl,Error);Zl.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;g>",ie=me(">>",!1),de=">&",_e=me(">&",!1),Pt=">",It=me(">",!1),Or="<<<",ii=me("<<<",!1),gi="<&",hr=me("<&",!1),fi="<",ni=me("<",!1),Ks=function(m){return{type:"argument",segments:[].concat(...m)}},pr=function(m){return m},Ii="$'",rs=me("$'",!1),fa="'",dA=me("'",!1),cg=function(m){return[{type:"text",text:m}]},is='""',CA=me('""',!1),ha=function(){return{type:"text",text:""}},wp='"',mA=me('"',!1),EA=function(m){return m},wr=function(m){return{type:"arithmetic",arithmetic:m,quoted:!0}},Tl=function(m){return{type:"shell",shell:m,quoted:!0}},ug=function(m){return{type:"variable",...m,quoted:!0}},yo=function(m){return{type:"text",text:m}},gg=function(m){return{type:"arithmetic",arithmetic:m,quoted:!1}},Bp=function(m){return{type:"shell",shell:m,quoted:!1}},bp=function(m){return{type:"variable",...m,quoted:!1}},vr=function(m){return{type:"glob",pattern:m}},se=/^[^']/,wo=Je(["'"],!0,!1),Fn=function(m){return m.join("")},fg=/^[^$"]/,bt=Je(["$",'"'],!0,!1),Ll=`\\ `,Nn=me(`\\ `,!1),ns=function(){return""},ss="\\",gt=me("\\",!1),Bo=/^[\\$"`]/,At=Je(["\\","$",'"',"`"],!1,!1),ln=function(m){return m},S="\\a",Lt=me("\\a",!1),hg=function(){return"a"},Ol="\\b",Qp=me("\\b",!1),Sp=function(){return"\b"},vp=/^[Ee]/,xp=Je(["E","e"],!1,!1),Pp=function(){return"\x1B"},G="\\f",yt=me("\\f",!1),IA=function(){return"\f"},zi="\\n",Ml=me("\\n",!1),Xe=function(){return` `},pa="\\r",pg=me("\\r",!1),OE=function(){return"\r"},Dp="\\t",ME=me("\\t",!1),ar=function(){return" "},Tn="\\v",Kl=me("\\v",!1),kp=function(){return"\v"},Us=/^[\\'"?]/,da=Je(["\\","'",'"',"?"],!1,!1),cn=function(m){return String.fromCharCode(parseInt(m,16))},Le="\\x",dg=me("\\x",!1),Ul="\\u",Hs=me("\\u",!1),Hl="\\U",yA=me("\\U",!1),Cg=function(m){return String.fromCodePoint(parseInt(m,16))},mg=/^[0-7]/,Ca=Je([["0","7"]],!1,!1),ma=/^[0-9a-fA-f]/,rt=Je([["0","9"],["a","f"],["A","f"]],!1,!1),bo=nt(),wA="-",Gl=me("-",!1),Gs="+",Yl=me("+",!1),KE=".",Rp=me(".",!1),Eg=function(m,Q,N){return{type:"number",value:(m==="-"?-1:1)*parseFloat(Q.join("")+"."+N.join(""))}},Fp=function(m,Q){return{type:"number",value:(m==="-"?-1:1)*parseInt(Q.join(""))}},UE=function(m){return{type:"variable",...m}},jl=function(m){return{type:"variable",name:m}},HE=function(m){return m},Ig="*",BA=me("*",!1),Rr="/",GE=me("/",!1),Ys=function(m,Q,N){return{type:Q==="*"?"multiplication":"division",right:N}},js=function(m,Q){return Q.reduce((N,U)=>({left:N,...U}),m)},yg=function(m,Q,N){return{type:Q==="+"?"addition":"subtraction",right:N}},bA="$((",R=me("$((",!1),q="))",Ce=me("))",!1),Ke=function(m){return m},Re="$(",ze=me("$(",!1),dt=function(m){return m},Ft="${",Ln=me("${",!1),JQ=":-",k1=me(":-",!1),R1=function(m,Q){return{name:m,defaultValue:Q}},WQ=":-}",F1=me(":-}",!1),N1=function(m){return{name:m,defaultValue:[]}},zQ=":+",T1=me(":+",!1),L1=function(m,Q){return{name:m,alternativeValue:Q}},VQ=":+}",O1=me(":+}",!1),M1=function(m){return{name:m,alternativeValue:[]}},XQ=function(m){return{name:m}},K1="$",U1=me("$",!1),H1=function(m){return e.isGlobPattern(m)},G1=function(m){return m},ZQ=/^[a-zA-Z0-9_]/,_Q=Je([["a","z"],["A","Z"],["0","9"],"_"],!1,!1),$Q=function(){return L()},eS=/^[$@*?#a-zA-Z0-9_\-]/,tS=Je(["$","@","*","?","#",["a","z"],["A","Z"],["0","9"],"_","-"],!1,!1),Y1=/^[(){}<>$|&; \t"']/,wg=Je(["(",")","{","}","<",">","$","|","&",";"," "," ",'"',"'"],!1,!1),rS=/^[<>&; \t"']/,iS=Je(["<",">","&",";"," "," ",'"',"'"],!1,!1),YE=/^[ \t]/,jE=Je([" "," "],!1,!1),b=0,Me=0,QA=[{line:1,column:1}],d=0,E=[],I=0,k;if("startRule"in e){if(!(e.startRule in i))throw new Error(`Can't start parsing from rule "`+e.startRule+'".');n=i[e.startRule]}function L(){return r.substring(Me,b)}function Z(){return Et(Me,b)}function te(m,Q){throw Q=Q!==void 0?Q:Et(Me,b),Ri([lt(m)],r.substring(Me,b),Q)}function we(m,Q){throw Q=Q!==void 0?Q:Et(Me,b),On(m,Q)}function me(m,Q){return{type:"literal",text:m,ignoreCase:Q}}function Je(m,Q,N){return{type:"class",parts:m,inverted:Q,ignoreCase:N}}function nt(){return{type:"any"}}function wt(){return{type:"end"}}function lt(m){return{type:"other",description:m}}function it(m){var Q=QA[m],N;if(Q)return Q;for(N=m-1;!QA[N];)N--;for(Q=QA[N],Q={line:Q.line,column:Q.column};Nd&&(d=b,E=[]),E.push(m))}function On(m,Q){return new Zl(m,null,null,Q)}function Ri(m,Q,N){return new Zl(Zl.buildMessage(m,Q),m,Q,N)}function SA(){var m,Q;return m=b,Q=Mr(),Q===t&&(Q=null),Q!==t&&(Me=m,Q=s(Q)),m=Q,m}function Mr(){var m,Q,N,U,ce;if(m=b,Q=Kr(),Q!==t){for(N=[],U=He();U!==t;)N.push(U),U=He();N!==t?(U=Ea(),U!==t?(ce=os(),ce===t&&(ce=null),ce!==t?(Me=m,Q=o(Q,U,ce),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t)}else b=m,m=t;if(m===t)if(m=b,Q=Kr(),Q!==t){for(N=[],U=He();U!==t;)N.push(U),U=He();N!==t?(U=Ea(),U===t&&(U=null),U!==t?(Me=m,Q=a(Q,U),m=Q):(b=m,m=t)):(b=m,m=t)}else b=m,m=t;return m}function os(){var m,Q,N,U,ce;for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t)if(N=Mr(),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();U!==t?(Me=m,Q=l(N),m=Q):(b=m,m=t)}else b=m,m=t;else b=m,m=t;return m}function Ea(){var m;return r.charCodeAt(b)===59?(m=c,b++):(m=t,I===0&&be(u)),m===t&&(r.charCodeAt(b)===38?(m=g,b++):(m=t,I===0&&be(f))),m}function Kr(){var m,Q,N;return m=b,Q=j1(),Q!==t?(N=fge(),N===t&&(N=null),N!==t?(Me=m,Q=h(Q,N),m=Q):(b=m,m=t)):(b=m,m=t),m}function fge(){var m,Q,N,U,ce,Se,ht;for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t)if(N=hge(),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t)if(ce=Kr(),ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();Se!==t?(Me=m,Q=p(N,ce),m=Q):(b=m,m=t)}else b=m,m=t;else b=m,m=t}else b=m,m=t;else b=m,m=t;return m}function hge(){var m;return r.substr(b,2)===C?(m=C,b+=2):(m=t,I===0&&be(y)),m===t&&(r.substr(b,2)===B?(m=B,b+=2):(m=t,I===0&&be(v))),m}function j1(){var m,Q,N;return m=b,Q=Cge(),Q!==t?(N=pge(),N===t&&(N=null),N!==t?(Me=m,Q=D(Q,N),m=Q):(b=m,m=t)):(b=m,m=t),m}function pge(){var m,Q,N,U,ce,Se,ht;for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t)if(N=dge(),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t)if(ce=j1(),ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();Se!==t?(Me=m,Q=T(N,ce),m=Q):(b=m,m=t)}else b=m,m=t;else b=m,m=t}else b=m,m=t;else b=m,m=t;return m}function dge(){var m;return r.substr(b,2)===H?(m=H,b+=2):(m=t,I===0&&be(j)),m===t&&(r.charCodeAt(b)===124?(m=$,b++):(m=t,I===0&&be(V))),m}function qE(){var m,Q,N,U,ce,Se;if(m=b,Q=rK(),Q!==t)if(r.charCodeAt(b)===61?(N=W,b++):(N=t,I===0&&be(_)),N!==t)if(U=W1(),U!==t){for(ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();ce!==t?(Me=m,Q=A(Q,U),m=Q):(b=m,m=t)}else b=m,m=t;else b=m,m=t;else b=m,m=t;if(m===t)if(m=b,Q=rK(),Q!==t)if(r.charCodeAt(b)===61?(N=W,b++):(N=t,I===0&&be(_)),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();U!==t?(Me=m,Q=Ae(Q),m=Q):(b=m,m=t)}else b=m,m=t;else b=m,m=t;return m}function Cge(){var m,Q,N,U,ce,Se,ht,Bt,qr,hi,as;for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t)if(r.charCodeAt(b)===40?(N=ge,b++):(N=t,I===0&&be(re)),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t)if(ce=Mr(),ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();if(Se!==t)if(r.charCodeAt(b)===41?(ht=O,b++):(ht=t,I===0&&be(F)),ht!==t){for(Bt=[],qr=He();qr!==t;)Bt.push(qr),qr=He();if(Bt!==t){for(qr=[],hi=Np();hi!==t;)qr.push(hi),hi=Np();if(qr!==t){for(hi=[],as=He();as!==t;)hi.push(as),as=He();hi!==t?(Me=m,Q=ue(ce,qr),m=Q):(b=m,m=t)}else b=m,m=t}else b=m,m=t}else b=m,m=t;else b=m,m=t}else b=m,m=t;else b=m,m=t}else b=m,m=t;else b=m,m=t;if(m===t){for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t)if(r.charCodeAt(b)===123?(N=pe,b++):(N=t,I===0&&be(ke)),N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t)if(ce=Mr(),ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();if(Se!==t)if(r.charCodeAt(b)===125?(ht=Fe,b++):(ht=t,I===0&&be(Ne)),ht!==t){for(Bt=[],qr=He();qr!==t;)Bt.push(qr),qr=He();if(Bt!==t){for(qr=[],hi=Np();hi!==t;)qr.push(hi),hi=Np();if(qr!==t){for(hi=[],as=He();as!==t;)hi.push(as),as=He();hi!==t?(Me=m,Q=oe(ce,qr),m=Q):(b=m,m=t)}else b=m,m=t}else b=m,m=t}else b=m,m=t;else b=m,m=t}else b=m,m=t;else b=m,m=t}else b=m,m=t;else b=m,m=t;if(m===t){for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t){for(N=[],U=qE();U!==t;)N.push(U),U=qE();if(N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();if(U!==t){if(ce=[],Se=J1(),Se!==t)for(;Se!==t;)ce.push(Se),Se=J1();else ce=t;if(ce!==t){for(Se=[],ht=He();ht!==t;)Se.push(ht),ht=He();Se!==t?(Me=m,Q=le(N,ce),m=Q):(b=m,m=t)}else b=m,m=t}else b=m,m=t}else b=m,m=t}else b=m,m=t;if(m===t){for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t){if(N=[],U=qE(),U!==t)for(;U!==t;)N.push(U),U=qE();else N=t;if(N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();U!==t?(Me=m,Q=Be(N),m=Q):(b=m,m=t)}else b=m,m=t}else b=m,m=t}}}return m}function q1(){var m,Q,N,U,ce;for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t){if(N=[],U=JE(),U!==t)for(;U!==t;)N.push(U),U=JE();else N=t;if(N!==t){for(U=[],ce=He();ce!==t;)U.push(ce),ce=He();U!==t?(Me=m,Q=fe(N),m=Q):(b=m,m=t)}else b=m,m=t}else b=m,m=t;return m}function J1(){var m,Q,N;for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();if(Q!==t?(N=Np(),N!==t?(Me=m,Q=ae(N),m=Q):(b=m,m=t)):(b=m,m=t),m===t){for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();Q!==t?(N=JE(),N!==t?(Me=m,Q=ae(N),m=Q):(b=m,m=t)):(b=m,m=t)}return m}function Np(){var m,Q,N,U,ce;for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();return Q!==t?(qe.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(ne)),N===t&&(N=null),N!==t?(U=mge(),U!==t?(ce=JE(),ce!==t?(Me=m,Q=Y(N,U,ce),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m}function mge(){var m;return r.substr(b,2)===he?(m=he,b+=2):(m=t,I===0&&be(ie)),m===t&&(r.substr(b,2)===de?(m=de,b+=2):(m=t,I===0&&be(_e)),m===t&&(r.charCodeAt(b)===62?(m=Pt,b++):(m=t,I===0&&be(It)),m===t&&(r.substr(b,3)===Or?(m=Or,b+=3):(m=t,I===0&&be(ii)),m===t&&(r.substr(b,2)===gi?(m=gi,b+=2):(m=t,I===0&&be(hr)),m===t&&(r.charCodeAt(b)===60?(m=fi,b++):(m=t,I===0&&be(ni))))))),m}function JE(){var m,Q,N;for(m=b,Q=[],N=He();N!==t;)Q.push(N),N=He();return Q!==t?(N=W1(),N!==t?(Me=m,Q=ae(N),m=Q):(b=m,m=t)):(b=m,m=t),m}function W1(){var m,Q,N;if(m=b,Q=[],N=z1(),N!==t)for(;N!==t;)Q.push(N),N=z1();else Q=t;return Q!==t&&(Me=m,Q=Ks(Q)),m=Q,m}function z1(){var m,Q;return m=b,Q=Ege(),Q!==t&&(Me=m,Q=pr(Q)),m=Q,m===t&&(m=b,Q=Ige(),Q!==t&&(Me=m,Q=pr(Q)),m=Q,m===t&&(m=b,Q=yge(),Q!==t&&(Me=m,Q=pr(Q)),m=Q,m===t&&(m=b,Q=wge(),Q!==t&&(Me=m,Q=pr(Q)),m=Q))),m}function Ege(){var m,Q,N,U;return m=b,r.substr(b,2)===Ii?(Q=Ii,b+=2):(Q=t,I===0&&be(rs)),Q!==t?(N=Qge(),N!==t?(r.charCodeAt(b)===39?(U=fa,b++):(U=t,I===0&&be(dA)),U!==t?(Me=m,Q=cg(N),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m}function Ige(){var m,Q,N,U;return m=b,r.charCodeAt(b)===39?(Q=fa,b++):(Q=t,I===0&&be(dA)),Q!==t?(N=Bge(),N!==t?(r.charCodeAt(b)===39?(U=fa,b++):(U=t,I===0&&be(dA)),U!==t?(Me=m,Q=cg(N),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m}function yge(){var m,Q,N,U;if(m=b,r.substr(b,2)===is?(Q=is,b+=2):(Q=t,I===0&&be(CA)),Q!==t&&(Me=m,Q=ha()),m=Q,m===t)if(m=b,r.charCodeAt(b)===34?(Q=wp,b++):(Q=t,I===0&&be(mA)),Q!==t){for(N=[],U=V1();U!==t;)N.push(U),U=V1();N!==t?(r.charCodeAt(b)===34?(U=wp,b++):(U=t,I===0&&be(mA)),U!==t?(Me=m,Q=EA(N),m=Q):(b=m,m=t)):(b=m,m=t)}else b=m,m=t;return m}function wge(){var m,Q,N;if(m=b,Q=[],N=X1(),N!==t)for(;N!==t;)Q.push(N),N=X1();else Q=t;return Q!==t&&(Me=m,Q=EA(Q)),m=Q,m}function V1(){var m,Q;return m=b,Q=eK(),Q!==t&&(Me=m,Q=wr(Q)),m=Q,m===t&&(m=b,Q=tK(),Q!==t&&(Me=m,Q=Tl(Q)),m=Q,m===t&&(m=b,Q=aS(),Q!==t&&(Me=m,Q=ug(Q)),m=Q,m===t&&(m=b,Q=bge(),Q!==t&&(Me=m,Q=yo(Q)),m=Q))),m}function X1(){var m,Q;return m=b,Q=eK(),Q!==t&&(Me=m,Q=gg(Q)),m=Q,m===t&&(m=b,Q=tK(),Q!==t&&(Me=m,Q=Bp(Q)),m=Q,m===t&&(m=b,Q=aS(),Q!==t&&(Me=m,Q=bp(Q)),m=Q,m===t&&(m=b,Q=xge(),Q!==t&&(Me=m,Q=vr(Q)),m=Q,m===t&&(m=b,Q=vge(),Q!==t&&(Me=m,Q=yo(Q)),m=Q)))),m}function Bge(){var m,Q,N;for(m=b,Q=[],se.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(wo));N!==t;)Q.push(N),se.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(wo));return Q!==t&&(Me=m,Q=Fn(Q)),m=Q,m}function bge(){var m,Q,N;if(m=b,Q=[],N=Z1(),N===t&&(fg.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(bt))),N!==t)for(;N!==t;)Q.push(N),N=Z1(),N===t&&(fg.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(bt)));else Q=t;return Q!==t&&(Me=m,Q=Fn(Q)),m=Q,m}function Z1(){var m,Q,N;return m=b,r.substr(b,2)===Ll?(Q=Ll,b+=2):(Q=t,I===0&&be(Nn)),Q!==t&&(Me=m,Q=ns()),m=Q,m===t&&(m=b,r.charCodeAt(b)===92?(Q=ss,b++):(Q=t,I===0&&be(gt)),Q!==t?(Bo.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(At)),N!==t?(Me=m,Q=ln(N),m=Q):(b=m,m=t)):(b=m,m=t)),m}function Qge(){var m,Q,N;for(m=b,Q=[],N=_1(),N===t&&(se.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(wo)));N!==t;)Q.push(N),N=_1(),N===t&&(se.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(wo)));return Q!==t&&(Me=m,Q=Fn(Q)),m=Q,m}function _1(){var m,Q,N;return m=b,r.substr(b,2)===S?(Q=S,b+=2):(Q=t,I===0&&be(Lt)),Q!==t&&(Me=m,Q=hg()),m=Q,m===t&&(m=b,r.substr(b,2)===Ol?(Q=Ol,b+=2):(Q=t,I===0&&be(Qp)),Q!==t&&(Me=m,Q=Sp()),m=Q,m===t&&(m=b,r.charCodeAt(b)===92?(Q=ss,b++):(Q=t,I===0&&be(gt)),Q!==t?(vp.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(xp)),N!==t?(Me=m,Q=Pp(),m=Q):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.substr(b,2)===G?(Q=G,b+=2):(Q=t,I===0&&be(yt)),Q!==t&&(Me=m,Q=IA()),m=Q,m===t&&(m=b,r.substr(b,2)===zi?(Q=zi,b+=2):(Q=t,I===0&&be(Ml)),Q!==t&&(Me=m,Q=Xe()),m=Q,m===t&&(m=b,r.substr(b,2)===pa?(Q=pa,b+=2):(Q=t,I===0&&be(pg)),Q!==t&&(Me=m,Q=OE()),m=Q,m===t&&(m=b,r.substr(b,2)===Dp?(Q=Dp,b+=2):(Q=t,I===0&&be(ME)),Q!==t&&(Me=m,Q=ar()),m=Q,m===t&&(m=b,r.substr(b,2)===Tn?(Q=Tn,b+=2):(Q=t,I===0&&be(Kl)),Q!==t&&(Me=m,Q=kp()),m=Q,m===t&&(m=b,r.charCodeAt(b)===92?(Q=ss,b++):(Q=t,I===0&&be(gt)),Q!==t?(Us.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(da)),N!==t?(Me=m,Q=ln(N),m=Q):(b=m,m=t)):(b=m,m=t),m===t&&(m=Sge()))))))))),m}function Sge(){var m,Q,N,U,ce,Se,ht,Bt,qr,hi,as,AS;return m=b,r.charCodeAt(b)===92?(Q=ss,b++):(Q=t,I===0&&be(gt)),Q!==t?(N=nS(),N!==t?(Me=m,Q=cn(N),m=Q):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.substr(b,2)===Le?(Q=Le,b+=2):(Q=t,I===0&&be(dg)),Q!==t?(N=b,U=b,ce=nS(),ce!==t?(Se=Mn(),Se!==t?(ce=[ce,Se],U=ce):(b=U,U=t)):(b=U,U=t),U===t&&(U=nS()),U!==t?N=r.substring(N,b):N=U,N!==t?(Me=m,Q=cn(N),m=Q):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.substr(b,2)===Ul?(Q=Ul,b+=2):(Q=t,I===0&&be(Hs)),Q!==t?(N=b,U=b,ce=Mn(),ce!==t?(Se=Mn(),Se!==t?(ht=Mn(),ht!==t?(Bt=Mn(),Bt!==t?(ce=[ce,Se,ht,Bt],U=ce):(b=U,U=t)):(b=U,U=t)):(b=U,U=t)):(b=U,U=t),U!==t?N=r.substring(N,b):N=U,N!==t?(Me=m,Q=cn(N),m=Q):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.substr(b,2)===Hl?(Q=Hl,b+=2):(Q=t,I===0&&be(yA)),Q!==t?(N=b,U=b,ce=Mn(),ce!==t?(Se=Mn(),Se!==t?(ht=Mn(),ht!==t?(Bt=Mn(),Bt!==t?(qr=Mn(),qr!==t?(hi=Mn(),hi!==t?(as=Mn(),as!==t?(AS=Mn(),AS!==t?(ce=[ce,Se,ht,Bt,qr,hi,as,AS],U=ce):(b=U,U=t)):(b=U,U=t)):(b=U,U=t)):(b=U,U=t)):(b=U,U=t)):(b=U,U=t)):(b=U,U=t)):(b=U,U=t),U!==t?N=r.substring(N,b):N=U,N!==t?(Me=m,Q=Cg(N),m=Q):(b=m,m=t)):(b=m,m=t)))),m}function nS(){var m;return mg.test(r.charAt(b))?(m=r.charAt(b),b++):(m=t,I===0&&be(Ca)),m}function Mn(){var m;return ma.test(r.charAt(b))?(m=r.charAt(b),b++):(m=t,I===0&&be(rt)),m}function vge(){var m,Q,N,U,ce;if(m=b,Q=[],N=b,r.charCodeAt(b)===92?(U=ss,b++):(U=t,I===0&&be(gt)),U!==t?(r.length>b?(ce=r.charAt(b),b++):(ce=t,I===0&&be(bo)),ce!==t?(Me=N,U=ln(ce),N=U):(b=N,N=t)):(b=N,N=t),N===t&&(N=b,U=b,I++,ce=iK(),I--,ce===t?U=void 0:(b=U,U=t),U!==t?(r.length>b?(ce=r.charAt(b),b++):(ce=t,I===0&&be(bo)),ce!==t?(Me=N,U=ln(ce),N=U):(b=N,N=t)):(b=N,N=t)),N!==t)for(;N!==t;)Q.push(N),N=b,r.charCodeAt(b)===92?(U=ss,b++):(U=t,I===0&&be(gt)),U!==t?(r.length>b?(ce=r.charAt(b),b++):(ce=t,I===0&&be(bo)),ce!==t?(Me=N,U=ln(ce),N=U):(b=N,N=t)):(b=N,N=t),N===t&&(N=b,U=b,I++,ce=iK(),I--,ce===t?U=void 0:(b=U,U=t),U!==t?(r.length>b?(ce=r.charAt(b),b++):(ce=t,I===0&&be(bo)),ce!==t?(Me=N,U=ln(ce),N=U):(b=N,N=t)):(b=N,N=t));else Q=t;return Q!==t&&(Me=m,Q=Fn(Q)),m=Q,m}function sS(){var m,Q,N,U,ce,Se;if(m=b,r.charCodeAt(b)===45?(Q=wA,b++):(Q=t,I===0&&be(Gl)),Q===t&&(r.charCodeAt(b)===43?(Q=Gs,b++):(Q=t,I===0&&be(Yl))),Q===t&&(Q=null),Q!==t){if(N=[],qe.test(r.charAt(b))?(U=r.charAt(b),b++):(U=t,I===0&&be(ne)),U!==t)for(;U!==t;)N.push(U),qe.test(r.charAt(b))?(U=r.charAt(b),b++):(U=t,I===0&&be(ne));else N=t;if(N!==t)if(r.charCodeAt(b)===46?(U=KE,b++):(U=t,I===0&&be(Rp)),U!==t){if(ce=[],qe.test(r.charAt(b))?(Se=r.charAt(b),b++):(Se=t,I===0&&be(ne)),Se!==t)for(;Se!==t;)ce.push(Se),qe.test(r.charAt(b))?(Se=r.charAt(b),b++):(Se=t,I===0&&be(ne));else ce=t;ce!==t?(Me=m,Q=Eg(Q,N,ce),m=Q):(b=m,m=t)}else b=m,m=t;else b=m,m=t}else b=m,m=t;if(m===t){if(m=b,r.charCodeAt(b)===45?(Q=wA,b++):(Q=t,I===0&&be(Gl)),Q===t&&(r.charCodeAt(b)===43?(Q=Gs,b++):(Q=t,I===0&&be(Yl))),Q===t&&(Q=null),Q!==t){if(N=[],qe.test(r.charAt(b))?(U=r.charAt(b),b++):(U=t,I===0&&be(ne)),U!==t)for(;U!==t;)N.push(U),qe.test(r.charAt(b))?(U=r.charAt(b),b++):(U=t,I===0&&be(ne));else N=t;N!==t?(Me=m,Q=Fp(Q,N),m=Q):(b=m,m=t)}else b=m,m=t;if(m===t&&(m=b,Q=aS(),Q!==t&&(Me=m,Q=UE(Q)),m=Q,m===t&&(m=b,Q=ql(),Q!==t&&(Me=m,Q=jl(Q)),m=Q,m===t)))if(m=b,r.charCodeAt(b)===40?(Q=ge,b++):(Q=t,I===0&&be(re)),Q!==t){for(N=[],U=He();U!==t;)N.push(U),U=He();if(N!==t)if(U=$1(),U!==t){for(ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();ce!==t?(r.charCodeAt(b)===41?(Se=O,b++):(Se=t,I===0&&be(F)),Se!==t?(Me=m,Q=HE(U),m=Q):(b=m,m=t)):(b=m,m=t)}else b=m,m=t;else b=m,m=t}else b=m,m=t}return m}function oS(){var m,Q,N,U,ce,Se,ht,Bt;if(m=b,Q=sS(),Q!==t){for(N=[],U=b,ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();if(ce!==t)if(r.charCodeAt(b)===42?(Se=Ig,b++):(Se=t,I===0&&be(BA)),Se===t&&(r.charCodeAt(b)===47?(Se=Rr,b++):(Se=t,I===0&&be(GE))),Se!==t){for(ht=[],Bt=He();Bt!==t;)ht.push(Bt),Bt=He();ht!==t?(Bt=sS(),Bt!==t?(Me=U,ce=Ys(Q,Se,Bt),U=ce):(b=U,U=t)):(b=U,U=t)}else b=U,U=t;else b=U,U=t;for(;U!==t;){for(N.push(U),U=b,ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();if(ce!==t)if(r.charCodeAt(b)===42?(Se=Ig,b++):(Se=t,I===0&&be(BA)),Se===t&&(r.charCodeAt(b)===47?(Se=Rr,b++):(Se=t,I===0&&be(GE))),Se!==t){for(ht=[],Bt=He();Bt!==t;)ht.push(Bt),Bt=He();ht!==t?(Bt=sS(),Bt!==t?(Me=U,ce=Ys(Q,Se,Bt),U=ce):(b=U,U=t)):(b=U,U=t)}else b=U,U=t;else b=U,U=t}N!==t?(Me=m,Q=js(Q,N),m=Q):(b=m,m=t)}else b=m,m=t;return m}function $1(){var m,Q,N,U,ce,Se,ht,Bt;if(m=b,Q=oS(),Q!==t){for(N=[],U=b,ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();if(ce!==t)if(r.charCodeAt(b)===43?(Se=Gs,b++):(Se=t,I===0&&be(Yl)),Se===t&&(r.charCodeAt(b)===45?(Se=wA,b++):(Se=t,I===0&&be(Gl))),Se!==t){for(ht=[],Bt=He();Bt!==t;)ht.push(Bt),Bt=He();ht!==t?(Bt=oS(),Bt!==t?(Me=U,ce=yg(Q,Se,Bt),U=ce):(b=U,U=t)):(b=U,U=t)}else b=U,U=t;else b=U,U=t;for(;U!==t;){for(N.push(U),U=b,ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();if(ce!==t)if(r.charCodeAt(b)===43?(Se=Gs,b++):(Se=t,I===0&&be(Yl)),Se===t&&(r.charCodeAt(b)===45?(Se=wA,b++):(Se=t,I===0&&be(Gl))),Se!==t){for(ht=[],Bt=He();Bt!==t;)ht.push(Bt),Bt=He();ht!==t?(Bt=oS(),Bt!==t?(Me=U,ce=yg(Q,Se,Bt),U=ce):(b=U,U=t)):(b=U,U=t)}else b=U,U=t;else b=U,U=t}N!==t?(Me=m,Q=js(Q,N),m=Q):(b=m,m=t)}else b=m,m=t;return m}function eK(){var m,Q,N,U,ce,Se;if(m=b,r.substr(b,3)===bA?(Q=bA,b+=3):(Q=t,I===0&&be(R)),Q!==t){for(N=[],U=He();U!==t;)N.push(U),U=He();if(N!==t)if(U=$1(),U!==t){for(ce=[],Se=He();Se!==t;)ce.push(Se),Se=He();ce!==t?(r.substr(b,2)===q?(Se=q,b+=2):(Se=t,I===0&&be(Ce)),Se!==t?(Me=m,Q=Ke(U),m=Q):(b=m,m=t)):(b=m,m=t)}else b=m,m=t;else b=m,m=t}else b=m,m=t;return m}function tK(){var m,Q,N,U;return m=b,r.substr(b,2)===Re?(Q=Re,b+=2):(Q=t,I===0&&be(ze)),Q!==t?(N=Mr(),N!==t?(r.charCodeAt(b)===41?(U=O,b++):(U=t,I===0&&be(F)),U!==t?(Me=m,Q=dt(N),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m}function aS(){var m,Q,N,U,ce,Se;return m=b,r.substr(b,2)===Ft?(Q=Ft,b+=2):(Q=t,I===0&&be(Ln)),Q!==t?(N=ql(),N!==t?(r.substr(b,2)===JQ?(U=JQ,b+=2):(U=t,I===0&&be(k1)),U!==t?(ce=q1(),ce!==t?(r.charCodeAt(b)===125?(Se=Fe,b++):(Se=t,I===0&&be(Ne)),Se!==t?(Me=m,Q=R1(N,ce),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.substr(b,2)===Ft?(Q=Ft,b+=2):(Q=t,I===0&&be(Ln)),Q!==t?(N=ql(),N!==t?(r.substr(b,3)===WQ?(U=WQ,b+=3):(U=t,I===0&&be(F1)),U!==t?(Me=m,Q=N1(N),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.substr(b,2)===Ft?(Q=Ft,b+=2):(Q=t,I===0&&be(Ln)),Q!==t?(N=ql(),N!==t?(r.substr(b,2)===zQ?(U=zQ,b+=2):(U=t,I===0&&be(T1)),U!==t?(ce=q1(),ce!==t?(r.charCodeAt(b)===125?(Se=Fe,b++):(Se=t,I===0&&be(Ne)),Se!==t?(Me=m,Q=L1(N,ce),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.substr(b,2)===Ft?(Q=Ft,b+=2):(Q=t,I===0&&be(Ln)),Q!==t?(N=ql(),N!==t?(r.substr(b,3)===VQ?(U=VQ,b+=3):(U=t,I===0&&be(O1)),U!==t?(Me=m,Q=M1(N),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.substr(b,2)===Ft?(Q=Ft,b+=2):(Q=t,I===0&&be(Ln)),Q!==t?(N=ql(),N!==t?(r.charCodeAt(b)===125?(U=Fe,b++):(U=t,I===0&&be(Ne)),U!==t?(Me=m,Q=XQ(N),m=Q):(b=m,m=t)):(b=m,m=t)):(b=m,m=t),m===t&&(m=b,r.charCodeAt(b)===36?(Q=K1,b++):(Q=t,I===0&&be(U1)),Q!==t?(N=ql(),N!==t?(Me=m,Q=XQ(N),m=Q):(b=m,m=t)):(b=m,m=t)))))),m}function xge(){var m,Q,N;return m=b,Q=Pge(),Q!==t?(Me=b,N=H1(Q),N?N=void 0:N=t,N!==t?(Me=m,Q=G1(Q),m=Q):(b=m,m=t)):(b=m,m=t),m}function Pge(){var m,Q,N,U,ce;if(m=b,Q=[],N=b,U=b,I++,ce=nK(),I--,ce===t?U=void 0:(b=U,U=t),U!==t?(r.length>b?(ce=r.charAt(b),b++):(ce=t,I===0&&be(bo)),ce!==t?(Me=N,U=ln(ce),N=U):(b=N,N=t)):(b=N,N=t),N!==t)for(;N!==t;)Q.push(N),N=b,U=b,I++,ce=nK(),I--,ce===t?U=void 0:(b=U,U=t),U!==t?(r.length>b?(ce=r.charAt(b),b++):(ce=t,I===0&&be(bo)),ce!==t?(Me=N,U=ln(ce),N=U):(b=N,N=t)):(b=N,N=t);else Q=t;return Q!==t&&(Me=m,Q=Fn(Q)),m=Q,m}function rK(){var m,Q,N;if(m=b,Q=[],ZQ.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(_Q)),N!==t)for(;N!==t;)Q.push(N),ZQ.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(_Q));else Q=t;return Q!==t&&(Me=m,Q=$Q()),m=Q,m}function ql(){var m,Q,N;if(m=b,Q=[],eS.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(tS)),N!==t)for(;N!==t;)Q.push(N),eS.test(r.charAt(b))?(N=r.charAt(b),b++):(N=t,I===0&&be(tS));else Q=t;return Q!==t&&(Me=m,Q=$Q()),m=Q,m}function iK(){var m;return Y1.test(r.charAt(b))?(m=r.charAt(b),b++):(m=t,I===0&&be(wg)),m}function nK(){var m;return rS.test(r.charAt(b))?(m=r.charAt(b),b++):(m=t,I===0&&be(iS)),m}function He(){var m,Q;if(m=[],YE.test(r.charAt(b))?(Q=r.charAt(b),b++):(Q=t,I===0&&be(jE)),Q!==t)for(;Q!==t;)m.push(Q),YE.test(r.charAt(b))?(Q=r.charAt(b),b++):(Q=t,I===0&&be(jE));else m=t;return m}if(k=n(),k!==t&&b===r.length)return k;throw k!==t&&b{"use strict";function Dfe(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function $l(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,$l)}Dfe($l,Error);$l.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;gH&&(H=v,j=[]),j.push(ne))}function Ne(ne,Y){return new $l(ne,null,null,Y)}function oe(ne,Y,he){return new $l($l.buildMessage(ne,Y),ne,Y,he)}function le(){var ne,Y,he,ie;return ne=v,Y=Be(),Y!==t?(r.charCodeAt(v)===47?(he=s,v++):(he=t,$===0&&Fe(o)),he!==t?(ie=Be(),ie!==t?(D=ne,Y=a(Y,ie),ne=Y):(v=ne,ne=t)):(v=ne,ne=t)):(v=ne,ne=t),ne===t&&(ne=v,Y=Be(),Y!==t&&(D=ne,Y=l(Y)),ne=Y),ne}function Be(){var ne,Y,he,ie;return ne=v,Y=fe(),Y!==t?(r.charCodeAt(v)===64?(he=c,v++):(he=t,$===0&&Fe(u)),he!==t?(ie=qe(),ie!==t?(D=ne,Y=g(Y,ie),ne=Y):(v=ne,ne=t)):(v=ne,ne=t)):(v=ne,ne=t),ne===t&&(ne=v,Y=fe(),Y!==t&&(D=ne,Y=f(Y)),ne=Y),ne}function fe(){var ne,Y,he,ie,de;return ne=v,r.charCodeAt(v)===64?(Y=c,v++):(Y=t,$===0&&Fe(u)),Y!==t?(he=ae(),he!==t?(r.charCodeAt(v)===47?(ie=s,v++):(ie=t,$===0&&Fe(o)),ie!==t?(de=ae(),de!==t?(D=ne,Y=h(),ne=Y):(v=ne,ne=t)):(v=ne,ne=t)):(v=ne,ne=t)):(v=ne,ne=t),ne===t&&(ne=v,Y=ae(),Y!==t&&(D=ne,Y=h()),ne=Y),ne}function ae(){var ne,Y,he;if(ne=v,Y=[],p.test(r.charAt(v))?(he=r.charAt(v),v++):(he=t,$===0&&Fe(C)),he!==t)for(;he!==t;)Y.push(he),p.test(r.charAt(v))?(he=r.charAt(v),v++):(he=t,$===0&&Fe(C));else Y=t;return Y!==t&&(D=ne,Y=h()),ne=Y,ne}function qe(){var ne,Y,he;if(ne=v,Y=[],y.test(r.charAt(v))?(he=r.charAt(v),v++):(he=t,$===0&&Fe(B)),he!==t)for(;he!==t;)Y.push(he),y.test(r.charAt(v))?(he=r.charAt(v),v++):(he=t,$===0&&Fe(B));else Y=t;return Y!==t&&(D=ne,Y=h()),ne=Y,ne}if(V=n(),V!==t&&v===r.length)return V;throw V!==t&&v{"use strict";function mU(r){return typeof r>"u"||r===null}function Rfe(r){return typeof r=="object"&&r!==null}function Ffe(r){return Array.isArray(r)?r:mU(r)?[]:[r]}function Nfe(r,e){var t,i,n,s;if(e)for(s=Object.keys(e),t=0,i=s.length;t{"use strict";function Vp(r,e){Error.call(this),this.name="YAMLException",this.reason=r,this.mark=e,this.message=(this.reason||"(unknown reason)")+(this.mark?" "+this.mark.toString():""),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack||""}Vp.prototype=Object.create(Error.prototype);Vp.prototype.constructor=Vp;Vp.prototype.toString=function(e){var t=this.name+": ";return t+=this.reason||"(unknown reason)",!e&&this.mark&&(t+=" "+this.mark.toString()),t};EU.exports=Vp});var wU=w((SZe,yU)=>{"use strict";var IU=tc();function HS(r,e,t,i,n){this.name=r,this.buffer=e,this.position=t,this.line=i,this.column=n}HS.prototype.getSnippet=function(e,t){var i,n,s,o,a;if(!this.buffer)return null;for(e=e||4,t=t||75,i="",n=this.position;n>0&&`\0\r diff --git a/App.tsx b/App.tsx index 385db25..c352bd9 100755 --- a/App.tsx +++ b/App.tsx @@ -1,19 +1,26 @@ -import React from 'react'; -import Toast from 'react-native-toast-message'; -import Navigation from '@navigation/index'; -import AppInitializer from '@root/AppInitializer'; -import toastConfig from '@config/toastConfig'; -import { Provider as ReduxProvider } from 'react-redux'; -import { store } from '@store/index'; +import React from 'react' +import { Provider as ReduxProvider } from 'react-redux' +import { SafeAreaProvider } from 'react-native-safe-area-context' +import Toast from 'react-native-toast-message' + +import Navigation from '@navigation/index' +import { FolderSelectionProvider } from '@context/folder-selection' +import { store } from '@store/index' +import toastConfig from '@config/toast.config' +import AppInitializer from '@root/app-initializer' const App: React.FC = () => { return ( - - - - - - ); -}; + + + + + + + + + + ) +} -export default App; +export default App diff --git a/AppInitializer.tsx b/AppInitializer.tsx deleted file mode 100644 index 1f41ea8..0000000 --- a/AppInitializer.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import { AppDispatch } from '@store/index'; -import { bootstrapAsync } from '@store/authSlice'; -import { setFolders } from '@store/foldersSlice'; -import foldersMockData from '@store/mockData/folders.mockData'; - -const AppInitializer: React.FC = () => { - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(bootstrapAsync()); - dispatch(setFolders(foldersMockData)); - }, [dispatch]); - - return null; -}; - -export default AppInitializer; diff --git a/__tests__/App.test.tsx b/__tests__/App.test.tsx index 68e10d1..acd5965 100644 --- a/__tests__/App.test.tsx +++ b/__tests__/App.test.tsx @@ -1,15 +1,15 @@ -import 'react-native'; -import React from 'react'; -import App from '../App'; -import { act } from 'react-test-renderer'; -import renderer from 'react-test-renderer'; +import 'react-native' +import React from 'react' +import App from '../App' +import { act } from 'react-test-renderer' +import renderer from 'react-test-renderer' it('renders correctly', async () => { - let tree; + let tree await act(async () => { - tree = renderer.create(); - }); + tree = renderer.create() + }) - expect(tree).toBeTruthy(); -}); + expect(tree).toBeTruthy() +}) diff --git a/__tests__/Header.test.tsx b/__tests__/Header.test.tsx deleted file mode 100644 index 309673d..0000000 --- a/__tests__/Header.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import Header from '@root/src/components/Header'; -import { render, fireEvent } from '@testing-library/react-native'; -import { useNavigation } from '@react-navigation/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -jest.mock('@react-navigation/native', () => ({ - useNavigation: jest.fn(), -})); - -jest.mock('react-native-safe-area-context', () => ({ - useSafeAreaInsets: jest.fn(), -})); - -const mockNavigation = { - goBack: jest.fn(), -}; - -describe('Header', () => { - beforeEach(() => { - (useNavigation as jest.Mock).mockReturnValue(mockNavigation); - (useSafeAreaInsets as jest.Mock).mockReturnValue({ - top: 10, - bottom: 0, - left: 0, - right: 0, - }); - jest.clearAllMocks(); - }); - - it('renders the back button correctly', () => { - const { getByTestId } = render(
); - const backButton = getByTestId('back-button'); - expect(backButton).toBeTruthy(); - }); - - it('triggers navigation.goBack when the back button is pressed', () => { - const { getByTestId } = render(
); - const backButton = getByTestId('back-button'); - - fireEvent.press(backButton); - expect(mockNavigation.goBack).toHaveBeenCalled(); - }); - - it('renders the title when provided', () => { - const { getByText } = render(
); - expect(getByText('Test Title')).toBeTruthy(); - }); - - it('does not render the title when not provided', () => { - const { queryByText } = render(
); - expect(queryByText('Test Title')).toBeNull(); - }); -}); diff --git a/__tests__/HomeScreen.test.tsx b/__tests__/HomeScreen.test.tsx deleted file mode 100644 index c831f98..0000000 --- a/__tests__/HomeScreen.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import { configureStore } from '@reduxjs/toolkit'; -import { Provider } from 'react-redux'; -import { NavigationContainer } from '@react-navigation/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { fireEvent, render } from '@testing-library/react-native'; - -import foldersReducer from '@store/foldersSlice'; -import foldersMockData from '@store/mockData/folders.mockData'; -import HomeScreen from '@screens/HomeScreen'; - -jest.mock('@screens/HomeScreen/components/FolderList', () => 'FolderList'); -jest.mock( - '@screens/HomeScreen/components/NotesResultsList', - () => 'NotesResultsList', -); -jest.mock( - '@screens/HomeScreen/components/FloatingButton', - () => 'FloatingButton', -); -jest.mock('@components/BackgroundLayers', () => 'BackgroundLayers'); - -jest.mock('react-native-safe-area-context', () => ({ - useSafeAreaInsets: jest.fn(), -})); - -describe('HomeScreen', () => { - const initialState = { - folders: foldersMockData, - }; - - beforeEach(() => { - (useSafeAreaInsets as jest.Mock).mockReturnValue({ - top: 20, - bottom: 0, - left: 0, - right: 0, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - const createTestStore = (preloadedState = initialState) => - configureStore({ - reducer: { - folders: foldersReducer, - }, - preloadedState, - }); - - const renderWithProviders = (component: React.ReactNode) => { - const store = createTestStore(initialState); - - return render( - - {component} - , - ); - }; - - it('renders the background layers', () => { - const { getByTestId } = renderWithProviders(); - expect(getByTestId('background-layers')).toBeTruthy(); - }); - - it('renders the container with correct insets', () => { - const { getByTestId } = renderWithProviders(); - const container = getByTestId('container'); - expect(container.props.style.paddingTop).toBe(20); - expect(container.props.style.paddingLeft).toBe(0); - expect(container.props.style.paddingRight).toBe(-2); - }); - - it('renders the folder list', () => { - const { getByTestId } = renderWithProviders(); - expect(getByTestId('folder-list')).toBeTruthy(); - }); - - it('renders the notes results list when there is a search query', () => { - const { getByTestId } = renderWithProviders(); - - const searchInput = getByTestId('search-input'); - fireEvent.changeText(searchInput, 'Sample Note'); - - expect(getByTestId('notes-results')).toBeTruthy(); - }); -}); diff --git a/__tests__/LoginScreen.test.tsx b/__tests__/LoginScreen.test.tsx deleted file mode 100644 index 0badb7b..0000000 --- a/__tests__/LoginScreen.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import LoginScreen from '../src/screens/LoginScreen'; -import { render, fireEvent } from '@testing-library/react-native'; -import { useDispatch } from 'react-redux'; -import { performSignIn } from '../src/store/authSlice'; - -jest.mock('react-redux', () => ({ - useDispatch: jest.fn(), -})); - -jest.mock('../src/store/authSlice', () => ({ - performSignIn: jest.fn(), -})); - -describe('LoginScreen', () => { - const mockDispatch = jest.fn(); - beforeEach(() => { - (useDispatch as unknown as jest.Mock).mockReturnValue(mockDispatch); - }); - - it('renders correctly', () => { - const { getByPlaceholderText, getByText } = render(); - - expect(getByPlaceholderText('Username')).toBeTruthy(); - expect(getByPlaceholderText('Password')).toBeTruthy(); - expect(getByText('Sign In')).toBeTruthy(); - }); - - it('dispatches performSignIn on button press', () => { - const { getByPlaceholderText, getByText } = render(); - - const usernameInput = getByPlaceholderText('Username'); - const passwordInput = getByPlaceholderText('Password'); - const signInButton = getByText('Sign In'); - - fireEvent.changeText(usernameInput, 'user'); - fireEvent.changeText(passwordInput, 'password'); - fireEvent.press(signInButton); - - expect(mockDispatch).toHaveBeenCalledWith( - performSignIn('user', 'password'), - ); - }); -}); diff --git a/__tests__/NoteDetailsScreen.test.tsx b/__tests__/NoteDetailsScreen.test.tsx deleted file mode 100644 index 21d3f59..0000000 --- a/__tests__/NoteDetailsScreen.test.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react-native'; -import { Provider } from 'react-redux'; -import { configureStore } from '@reduxjs/toolkit'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { - NavigationContainer, - RouteProp, - useNavigation, -} from '@react-navigation/native'; - -import foldersReducer from '@store/foldersSlice'; -import { - useFetchNoteDetailsQuery, - useUpdateNoteMutation, -} from '@store/queries/notes'; -import NoteDetailsScreen from '@screens/NoteDetailsScreen'; -import { RootStackParamList } from '@navigation/types'; - -jest.mock('@store/queries/notes', () => ({ - useFetchNoteDetailsQuery: jest.fn(), - useUpdateNoteMutation: jest.fn(() => [jest.fn()]), - useCreateNoteMutation: jest.fn(() => [jest.fn()]), -})); - -jest.mock('react-native-safe-area-context', () => ({ - useSafeAreaInsets: jest.fn(), -})); - -jest.mock('@store/queries/notes', () => ({ - useFetchNoteDetailsQuery: jest.fn(), - useUpdateNoteMutation: jest.fn(() => [jest.fn()]), - useCreateNoteMutation: jest.fn(() => [jest.fn().mockResolvedValue({})]), -})); - -jest.mock('@react-navigation/native', () => { - const actual = jest.requireActual('@react-navigation/native'); - return { - ...actual, - useNavigation: jest.fn(), - }; -}); - -const createTestStore = (preloadedState = {}) => { - return configureStore({ - reducer: { - folders: foldersReducer, - }, - preloadedState, - }); -}; - -const mockNavigate = jest.fn(); -const mockNavigation = { navigate: mockNavigate }; -const mockRoute: RouteProp = { - key: 'NoteDetailKey', - name: 'NoteDetails', - params: { noteId: 1, isNew: false }, -}; - -describe('NoteDetailsScreen', () => { - const mockedDate = new Date('2024-10-13T11:34:00').getTime(); - - beforeEach(() => { - jest.clearAllMocks(); - jest.spyOn(global.Date, 'now').mockImplementation(() => mockedDate); - (useSafeAreaInsets as jest.Mock).mockReturnValue({ - top: 10, - bottom: 10, - left: 0, - right: 0, - }); - }); - - (useNavigation as jest.Mock).mockReturnValue(mockNavigation); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('renders loading state initially', () => { - (useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ - data: null, - isLoading: true, - }); - - const store = createTestStore({ - folders: [ - { - id: 1, - folderName: 'Test Folder', - notes: [], - }, - ], - }); - - const { getByText } = render( - - - - - , - ); - - expect(getByText('Loading...')).toBeTruthy(); - }); - - it('renders the note details when loaded', () => { - (useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ - data: { - id: '1', - title: 'Test Note', - modifiedDate: mockedDate, - content: 'This is the content of the note', - }, - isLoading: false, - }); - - const store = createTestStore({ - folders: [ - { - id: 1, - folderName: 'Test Folder', - notes: [], - }, - ], - }); - - const { getByDisplayValue } = render( - - - , - ); - - expect(getByDisplayValue('Test Note')).toBeTruthy(); - expect(getByDisplayValue('This is the content of the note')).toBeTruthy(); - }); - - it('updates note on save button press', async () => { - const mockUpdateNote = jest.fn(); - (useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ - data: { - id: '1', - title: 'Test Note', - modifiedDate: mockedDate, - content: 'This is the content of the note', - }, - isLoading: false, - }); - - (useUpdateNoteMutation as jest.Mock).mockReturnValue([mockUpdateNote]); - - const store = createTestStore({ - folders: [ - { - id: 1, - folderName: 'Test Folder', - notes: [], - }, - ], - }); - - const { getByDisplayValue, getByTestId } = render( - - - , - ); - - fireEvent.changeText(getByDisplayValue('Test Note'), 'Updated Note Title'); - fireEvent.changeText( - getByDisplayValue('This is the content of the note'), - 'Updated Note Content', - ); - fireEvent.press(getByTestId('save-button')); - - await waitFor(() => { - expect(mockUpdateNote).toHaveBeenCalledWith({ - id: '1', - title: 'Updated Note Title', - content: 'Updated Note Content', - }); - }); - }); - - it('displays the date correctly', () => { - (useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ - data: { - id: '1', - title: 'Test Note', - modifiedDate: mockedDate, - content: 'This is the content of the note', - }, - isLoading: false, - }); - - const store = createTestStore({ - folders: [ - { - id: 1, - folderName: 'Test Folder', - notes: [], - }, - ], - }); - - const { getByText } = render( - - - , - ); - - expect(getByText('Oct 13, 2024 at 11:34 AM')).toBeTruthy(); - }); -}); diff --git a/__tests__/SettingsScreen.test.tsx b/__tests__/SettingsScreen.test.tsx deleted file mode 100644 index 1481d6f..0000000 --- a/__tests__/SettingsScreen.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import SettingsScreen from '../src/screens/SettingsScreen'; -import { render, fireEvent } from '@testing-library/react-native'; -import { useDispatch } from 'react-redux'; -import { performSignOut } from '../src/store/authSlice'; - -jest.mock('react-redux', () => ({ - useDispatch: jest.fn(), -})); - -jest.mock('../src/store/authSlice', () => ({ - performSignOut: jest.fn(), -})); - -describe('SettingsScreen', () => { - const mockDispatch = jest.fn(); - beforeEach(() => { - (useDispatch as jest.Mock).mockReturnValue(mockDispatch); - }); - - it('renders correctly', () => { - const { getByText } = render(); - - expect(getByText('Settings')).toBeTruthy(); - expect(getByText('Sign Out')).toBeTruthy(); - }); - - it('dispatches performSignOut on button press', () => { - const { getByText } = render(); - const signOutButton = getByText('Sign Out'); - - fireEvent.press(signOutButton); - - expect(mockDispatch).toHaveBeenCalledWith(performSignOut()); - }); -}); diff --git a/__tests__/FolderList.test.tsx b/__tests__/folder-list.test.tsx similarity index 56% rename from __tests__/FolderList.test.tsx rename to __tests__/folder-list.test.tsx index 8f358cd..6dc4acd 100644 --- a/__tests__/FolderList.test.tsx +++ b/__tests__/folder-list.test.tsx @@ -1,41 +1,42 @@ -import React from 'react'; -import FolderList from '@screens/HomeScreen/components/FolderList'; -import { render } from '@testing-library/react-native'; -import { useSelector } from 'react-redux'; +import React from 'react' +import { useSelector } from 'react-redux' +import { render } from '@testing-library/react-native' + +import FolderList from '@screens/home/components/folder-list' jest.mock('react-redux', () => ({ useSelector: jest.fn(), -})); +})) describe('FolderList', () => { const mockFolders = [ { id: 1, folderName: 'Folder 1', notes: [] }, { id: 2, folderName: 'Folder 2', notes: [] }, - ]; + ] beforeEach(() => { - (useSelector as unknown as jest.Mock).mockImplementation(callback => + ;(useSelector as unknown as jest.Mock).mockImplementation(callback => callback({ folders: { folders: mockFolders } }), - ); - }); + ) + }) afterEach(() => { - jest.clearAllMocks(); - }); + jest.clearAllMocks() + }) it('renders the correct number of folders', () => { - const { getAllByTestId } = render(); + const { getAllByTestId } = render() - expect(getAllByTestId('folder-component')).toHaveLength(mockFolders.length); - }); + expect(getAllByTestId('folder-component')).toHaveLength(mockFolders.length) + }) it('renders no folders when the list is empty', () => { - (useSelector as unknown as jest.Mock).mockImplementation(callback => + ;(useSelector as unknown as jest.Mock).mockImplementation(callback => callback({ folders: { folders: [] } }), - ); + ) - const { queryByTestId } = render(); + const { queryByTestId } = render() - expect(queryByTestId('folder-component')).toBeNull(); - }); -}); + expect(queryByTestId('folder-component')).toBeNull() + }) +}) diff --git a/__tests__/Folder.test.tsx b/__tests__/folder.test.tsx similarity index 51% rename from __tests__/Folder.test.tsx rename to __tests__/folder.test.tsx index 3c4d724..ecce362 100644 --- a/__tests__/Folder.test.tsx +++ b/__tests__/folder.test.tsx @@ -1,18 +1,19 @@ -import React from 'react'; -import Folder from '@screens/HomeScreen/components/Folder'; -import { render, fireEvent } from '@testing-library/react-native'; -import { FolderProps } from '@screens/HomeScreen/components/Folder/types'; +import React from 'react' +import { render, fireEvent } from '@testing-library/react-native' -jest.mock('react-native-vector-icons/FontAwesome', () => 'Icon'); +import Folder from '@screens/home/components/folder' +import { FolderProps } from '@screens/home/components/folder/types' + +jest.mock('react-native-vector-icons/FontAwesome', () => 'Icon') jest.mock( - '@screens/HomeScreen/components/Folder/components/GradientBackground', + '@screens/home/components/folder/components/gradient-background', () => 'GradientBackground', -); +) jest.mock( - '@screens/HomeScreen/components/Folder/components/NoteItem', + '@screens/home/components/folder/components/note-item', () => 'NoteItem', -); -jest.mock('@root/src/components/Divider', () => 'Divider'); +) +jest.mock('@components/divider', () => 'Divider') const folderData: FolderProps = { folder: { @@ -33,47 +34,47 @@ const folderData: FolderProps = { }, ], }, -}; +} describe('Folder', () => { it('renders the folder title correctly', () => { - const { getByText } = render(); + const { getByText } = render() - expect(getByText('Test Folder')).toBeTruthy(); - }); + expect(getByText('Test Folder')).toBeTruthy() + }) it('toggles the folder expansion when header is pressed', () => { const { getByText, queryByTestId } = render( , - ); + ) - expect(queryByTestId('divider')).toBeTruthy(); - expect(queryByTestId('flat-list')).toBeNull(); + expect(queryByTestId('divider')).toBeTruthy() + expect(queryByTestId('flat-list')).toBeNull() - fireEvent.press(getByText('Test Folder')); + fireEvent.press(getByText('Test Folder')) - expect(queryByTestId('divider')).toBeNull(); - expect(queryByTestId('flat-list')).toBeTruthy(); - }); + expect(queryByTestId('divider')).toBeNull() + expect(queryByTestId('flat-list')).toBeTruthy() + }) it('displays notes when the folder is expanded', () => { const { getByText, getByTestId } = render( , - ); + ) - fireEvent.press(getByText('Test Folder')); + fireEvent.press(getByText('Test Folder')) - const flatList = getByTestId('flat-list'); - expect(flatList).toBeTruthy(); - expect(flatList.props.data).toHaveLength(2); + const flatList = getByTestId('flat-list') + expect(flatList).toBeTruthy() + expect(flatList.props.data).toHaveLength(2) - expect(getByTestId('note-item-1')).toBeTruthy(); - expect(getByTestId('note-item-2')).toBeTruthy(); - }); + expect(getByTestId('note-item-1')).toBeTruthy() + expect(getByTestId('note-item-2')).toBeTruthy() + }) it('renders a divider when the folder is not expanded', () => { - const { queryByTestId } = render(); + const { queryByTestId } = render() - expect(queryByTestId('divider')).toBeTruthy(); - }); -}); + expect(queryByTestId('divider')).toBeTruthy() + }) +}) diff --git a/__tests__/header.test.tsx b/__tests__/header.test.tsx new file mode 100644 index 0000000..7559628 --- /dev/null +++ b/__tests__/header.test.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import { useNavigation } from '@react-navigation/native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { render, fireEvent } from '@testing-library/react-native' + +import Header from '@components/header' + +jest.mock('@react-navigation/native', () => ({ + useNavigation: jest.fn(), +})) + +jest.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: jest.fn(), +})) + +const mockNavigation = { + goBack: jest.fn(), +} + +describe('Header', () => { + beforeEach(() => { + ;(useNavigation as jest.Mock).mockReturnValue(mockNavigation) + ;(useSafeAreaInsets as jest.Mock).mockReturnValue({ + top: 10, + bottom: 0, + left: 0, + right: 0, + }) + jest.clearAllMocks() + }) + + it('renders the back button correctly', () => { + const { getByTestId } = render(
) + const backButton = getByTestId('back-button') + expect(backButton).toBeTruthy() + }) + + it('triggers navigation.goBack when the back button is pressed', () => { + const { getByTestId } = render(
) + const backButton = getByTestId('back-button') + + fireEvent.press(backButton) + expect(mockNavigation.goBack).toHaveBeenCalled() + }) + + it('renders the title when provided', () => { + const { getByText } = render(
) + expect(getByText('Test Title')).toBeTruthy() + }) + + it('does not render the title when not provided', () => { + const { queryByText } = render(
) + expect(queryByText('Test Title')).toBeNull() + }) +}) diff --git a/__tests__/home.test.tsx b/__tests__/home.test.tsx new file mode 100644 index 0000000..016d94b --- /dev/null +++ b/__tests__/home.test.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import { Provider } from 'react-redux' +import { configureStore } from '@reduxjs/toolkit' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { NavigationContainer } from '@react-navigation/native' +import { fireEvent, render } from '@testing-library/react-native' + +import foldersReducer from '@store/folders.slice' +import foldersMockData from '@store/mocks/folders.mocks' +import HomeScreen from '@screens/home' + +jest.mock('@screens/home/components/folder-list', () => 'FolderList') +jest.mock( + '@screens/home/components/notes-results-list', + () => 'NotesResultsList', +) +jest.mock('@screens/home/components/floating-button', () => 'FloatingButton') +jest.mock('@components/background-layers', () => 'BackgroundLayers') + +jest.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: jest.fn(), +})) + +describe('HomeScreen', () => { + const initialState = { + folders: foldersMockData, + } + + beforeEach(() => { + ;(useSafeAreaInsets as jest.Mock).mockReturnValue({ + top: 20, + bottom: 0, + left: 0, + right: 0, + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + const createTestStore = (preloadedState = initialState) => + configureStore({ + reducer: { + folders: foldersReducer, + }, + preloadedState, + }) + + const renderWithProviders = (component: React.ReactNode) => { + const store = createTestStore(initialState) + + return render( + + {component} + , + ) + } + + it('renders the background layers', () => { + const { getByTestId } = renderWithProviders() + expect(getByTestId('background-layers')).toBeTruthy() + }) + + it('renders the container with correct insets', () => { + const { getByTestId } = renderWithProviders() + const container = getByTestId('container') + expect(container.props.style.paddingTop).toBe(20) + expect(container.props.style.paddingLeft).toBe(0) + expect(container.props.style.paddingRight).toBe(-2) + }) + + it('renders the folder list', () => { + const { getByTestId } = renderWithProviders() + expect(getByTestId('folder-list')).toBeTruthy() + }) + + it('renders the notes results list when there is a search query', () => { + const { getByTestId } = renderWithProviders() + + const searchInput = getByTestId('search-input') + fireEvent.changeText(searchInput, 'Sample Note') + + expect(getByTestId('notes-results')).toBeTruthy() + }) +}) diff --git a/__tests__/login.test.tsx b/__tests__/login.test.tsx new file mode 100644 index 0000000..ce1eb2f --- /dev/null +++ b/__tests__/login.test.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import { useDispatch } from 'react-redux' +import { render, fireEvent } from '@testing-library/react-native' + +import LoginScreen from '../src/screens/login' +import { performSignIn } from '../src/store/auth.slice' + +jest.mock('react-redux', () => ({ + useDispatch: jest.fn(), +})) + +jest.mock('../src/store/auth.slice', () => ({ + performSignIn: jest.fn(), +})) + +describe('LoginScreen', () => { + const mockDispatch = jest.fn() + beforeEach(() => { + ;(useDispatch as unknown as jest.Mock).mockReturnValue(mockDispatch) + }) + + it('renders correctly', () => { + const { getByPlaceholderText, getByText } = render() + + expect(getByPlaceholderText('Username')).toBeTruthy() + expect(getByPlaceholderText('Password')).toBeTruthy() + expect(getByText('Sign In')).toBeTruthy() + }) + + it('dispatches performSignIn on button press', () => { + const { getByPlaceholderText, getByText } = render() + + const usernameInput = getByPlaceholderText('Username') + const passwordInput = getByPlaceholderText('Password') + const signInButton = getByText('Sign In') + + fireEvent.changeText(usernameInput, 'user') + fireEvent.changeText(passwordInput, 'password') + fireEvent.press(signInButton) + + expect(mockDispatch).toHaveBeenCalledWith(performSignIn('user', 'password')) + }) +}) diff --git a/__tests__/note-details.test.tsx b/__tests__/note-details.test.tsx new file mode 100644 index 0000000..10cf4b0 --- /dev/null +++ b/__tests__/note-details.test.tsx @@ -0,0 +1,189 @@ +import React from 'react' +import { Provider } from 'react-redux' +import { configureStore } from '@reduxjs/toolkit' +import { render, fireEvent, waitFor } from '@testing-library/react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { + NavigationContainer, + RouteProp, + useNavigation, +} from '@react-navigation/native' + +import foldersReducer from '@store/folders.slice' +import { + useFetchNoteDetailsQuery, + useUpdateNoteMutation, +} from '@store/queries/notes' +import NoteDetailsScreen from '@screens/note-details' +import { RootStackParamList } from '@navigation/types' +import { FolderSelectionProvider } from '@context/folder-selection' + +jest.mock('@store/queries/notes', () => ({ + useFetchNoteDetailsQuery: jest.fn(), + useUpdateNoteMutation: jest.fn(() => [jest.fn()]), + useCreateNoteMutation: jest.fn(() => [jest.fn().mockResolvedValue({})]), +})) + +jest.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: jest.fn(), +})) + +jest.mock('@react-navigation/native', () => { + const actual = jest.requireActual('@react-navigation/native') + return { + ...actual, + useNavigation: jest.fn(), + } +}) + +const createTestStore = (preloadedState = {}) => { + return configureStore({ + reducer: { + folders: foldersReducer, + }, + preloadedState, + }) +} + +const mockNavigate = jest.fn() +const mockNavigation = { navigate: mockNavigate } +const mockRoute: RouteProp = { + key: 'NoteDetailKey', + name: 'NoteDetails', + params: { noteId: 1, isNew: false }, +} + +describe('NoteDetailsScreen', () => { + const mockedDate = new Date('2024-10-13T11:34:00').getTime() + + beforeEach(() => { + jest.clearAllMocks() + jest.spyOn(global.Date, 'now').mockImplementation(() => mockedDate) + ;(useSafeAreaInsets as jest.Mock).mockReturnValue({ + top: 10, + bottom: 10, + left: 0, + right: 0, + }) + }) + ;(useNavigation as jest.Mock).mockReturnValue(mockNavigation) + + afterEach(() => { + jest.restoreAllMocks() + }) + + const renderWithProviders = (component: React.ReactNode) => { + const store = createTestStore({ + folders: [ + { + id: 1, + folderName: 'Test Folder', + notes: [], + }, + ], + }) + + return render( + + + {component} + + , + ) + } + + it('renders loading state initially', () => { + ;(useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ + data: null, + isLoading: true, + }) + + const { getByText } = renderWithProviders( + , + ) + + expect(getByText('Loading...')).toBeTruthy() + }) + + it('renders the note details when loaded', () => { + ;(useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ + data: { + id: '1', + title: 'Test Note', + modifiedDate: mockedDate, + content: 'This is the content of the note', + }, + isLoading: false, + }) + + const { getByDisplayValue } = renderWithProviders( + , + ) + + expect(getByDisplayValue('Test Note')).toBeTruthy() + expect(getByDisplayValue('This is the content of the note')).toBeTruthy() + }) + + it('updates note on save button press', async () => { + const mockUpdateNote = jest.fn() + ;(useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ + data: { + id: '1', + title: 'Test Note', + modifiedDate: mockedDate, + content: 'This is the content of the note', + }, + isLoading: false, + }) + ;(useUpdateNoteMutation as jest.Mock).mockReturnValue([mockUpdateNote]) + + const { getByDisplayValue, getByTestId } = renderWithProviders( + , + ) + + fireEvent.changeText(getByDisplayValue('Test Note'), 'Updated Note Title') + fireEvent.changeText( + getByDisplayValue('This is the content of the note'), + 'Updated Note Content', + ) + fireEvent.press(getByTestId('save-button')) + + await waitFor(() => { + expect(mockUpdateNote).toHaveBeenCalledWith({ + id: '1', + title: 'Updated Note Title', + content: 'Updated Note Content', + }) + }) + }) + + it('displays the date correctly', () => { + ;(useFetchNoteDetailsQuery as jest.Mock).mockReturnValue({ + data: { + id: '1', + title: 'Test Note', + modifiedDate: mockedDate, + content: 'This is the content of the note', + }, + isLoading: false, + }) + + const { getByText } = renderWithProviders( + , + ) + + expect(getByText('Oct 13, 2024 at 11:34 AM')).toBeTruthy() + }) +}) diff --git a/__tests__/NoteItem.test.tsx b/__tests__/note-item.test.tsx similarity index 55% rename from __tests__/NoteItem.test.tsx rename to __tests__/note-item.test.tsx index 35f73f2..69a3e8f 100644 --- a/__tests__/NoteItem.test.tsx +++ b/__tests__/note-item.test.tsx @@ -1,78 +1,79 @@ -import React from 'react'; -import NoteItem from '@screens/HomeScreen/components/Folder/components/NoteItem'; -import { render, fireEvent } from '@testing-library/react-native'; -import { formatTimestampToDate } from '@root/src/utils'; -import { useNavigation } from '@react-navigation/native'; -import { NoteListItem } from '@screens/HomeScreen/components/Folder/types'; +import React from 'react' +import { useNavigation } from '@react-navigation/native' +import { render, fireEvent } from '@testing-library/react-native' + +import { formatTimestampToDate } from '@root/src/utils' +import NoteItem from '@screens/home/components/folder/components/note-item' +import { NoteListItem } from '@screens/home/components/folder/types' jest.mock('@react-navigation/native', () => ({ useNavigation: jest.fn(), -})); +})) const mockNavigation = { navigate: jest.fn(), -}; +} describe('NoteItem', () => { - const mockedDate = new Date('2024-10-13T11:34:00').getTime(); + const mockedDate = new Date('2024-10-13T11:34:00').getTime() beforeAll(() => { - process.env.TZ = 'UTC'; - jest.useFakeTimers(); - jest.setSystemTime(new Date('2024-10-13T11:34:00')); - }); + process.env.TZ = 'UTC' + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-10-13T11:34:00')) + }) afterAll(() => { - jest.useRealTimers(); - }); + jest.useRealTimers() + }) beforeEach(() => { - (useNavigation as jest.Mock).mockReturnValue(mockNavigation); - jest.clearAllMocks(); - }); + ;(useNavigation as jest.Mock).mockReturnValue(mockNavigation) + jest.clearAllMocks() + }) const item: NoteListItem = { id: 1, title: 'Test Note', snippet: 'This is a snippet', modifiedDate: mockedDate, - }; + } it('renders the note item with correct title, snippet, and date', () => { const { getByText } = render( , - ); + ) - expect(getByText('Test Note: This is a snippet')).toBeTruthy(); + expect(getByText('Test Note: This is a snippet')).toBeTruthy() - const formattedDate = formatTimestampToDate(mockedDate); + const formattedDate = formatTimestampToDate(mockedDate) - expect(getByText(formattedDate)).toBeTruthy(); - }); + expect(getByText(formattedDate)).toBeTruthy() + }) it('navigates to the note details screen when pressed', () => { const { getByTestId } = render( , - ); + ) - fireEvent.press(getByTestId('note-item-container')); + fireEvent.press(getByTestId('note-item-container')) expect(mockNavigation.navigate).toHaveBeenCalledWith('NoteDetails', { noteId: item.id, - }); - }); + }) + }) it('renders the divider for all but the last item', () => { const { getByTestId } = render( , - ); + ) - expect(getByTestId('divider')).toBeTruthy(); + expect(getByTestId('divider')).toBeTruthy() const { queryByTestId: queryByTestIdLast } = render( , - ); + ) - expect(queryByTestIdLast('divider')).toBeNull(); - }); -}); + expect(queryByTestIdLast('divider')).toBeNull() + }) +}) diff --git a/__tests__/settings.test.tsx b/__tests__/settings.test.tsx new file mode 100644 index 0000000..b011037 --- /dev/null +++ b/__tests__/settings.test.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { useDispatch } from 'react-redux' +import { render, fireEvent } from '@testing-library/react-native' + +import SettingsScreen from '../src/screens/settings' +import { performSignOut } from '../src/store/auth.slice' + +jest.mock('react-redux', () => ({ + useDispatch: jest.fn(), +})) + +jest.mock('../src/store/auth.slice', () => ({ + performSignOut: jest.fn(), +})) + +describe('SettingsScreen', () => { + const mockDispatch = jest.fn() + beforeEach(() => { + // FIXME: + ;(useDispatch as jest.Mock).mockReturnValue(mockDispatch) + }) + + it('renders correctly', () => { + const { getByText } = render() + + expect(getByText('Settings')).toBeTruthy() + expect(getByText('Sign Out')).toBeTruthy() + }) + + it('dispatches performSignOut on button press', () => { + const { getByText } = render() + const signOutButton = getByText('Sign Out') + + fireEvent.press(signOutButton) + + expect(mockDispatch).toHaveBeenCalledWith(performSignOut()) + }) +}) diff --git a/app-initializer.tsx b/app-initializer.tsx new file mode 100644 index 0000000..dafea3f --- /dev/null +++ b/app-initializer.tsx @@ -0,0 +1,19 @@ +import React, { useEffect } from 'react' +import { useDispatch } from 'react-redux' +import { AppDispatch } from '@store/index' +import { bootstrapAsync } from '@store/auth.slice' +import { setFolders } from '@store/folders.slice' +import foldersMockData from '@store/mocks/folders.mocks' + +const AppInitializer: React.FC = () => { + const dispatch = useDispatch() + + useEffect(() => { + dispatch(bootstrapAsync()) + dispatch(setFolders(foldersMockData)) + }, [dispatch]) + + return null +} + +export default AppInitializer diff --git a/babel.config.js b/babel.config.js index f50eda6..8ba690f 100755 --- a/babel.config.js +++ b/babel.config.js @@ -10,6 +10,9 @@ module.exports = { '@components': './src/components', '@screens': './src/screens', '@theme': './src/theme', + '@hooks': './src/hooks', + '@utils': './src/utils', + '@context': './src/context', '@navigation': './src/navigation', '@store': './src/store', '@config': './src/config', @@ -18,4 +21,4 @@ module.exports = { }, ], ], -}; +} diff --git a/index.js b/index.js index 9b73932..cbef63e 100755 --- a/index.js +++ b/index.js @@ -2,8 +2,8 @@ * @format */ -import { AppRegistry } from 'react-native'; -import App from './App'; -import { name as appName } from './app.json'; +import { AppRegistry } from 'react-native' +import App from './App' +import { name as appName } from './app.json' -AppRegistry.registerComponent(appName, () => App); +AppRegistry.registerComponent(appName, () => App) diff --git a/ios/kiByte/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/kiByte/Images.xcassets/AppIcon.appiconset/Contents.json index 8121323..ddd7fca 100755 --- a/ios/kiByte/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/kiByte/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,53 +1,53 @@ { - "images" : [ + "images": [ { - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" + "idiom": "iphone", + "scale": "2x", + "size": "20x20" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" + "idiom": "iphone", + "scale": "3x", + "size": "20x20" }, { - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" + "idiom": "iphone", + "scale": "2x", + "size": "29x29" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" + "idiom": "iphone", + "scale": "3x", + "size": "29x29" }, { - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" + "idiom": "iphone", + "scale": "2x", + "size": "40x40" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" + "idiom": "iphone", + "scale": "3x", + "size": "40x40" }, { - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" + "idiom": "iphone", + "scale": "2x", + "size": "60x60" }, { - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" + "idiom": "iphone", + "scale": "3x", + "size": "60x60" }, { - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/ios/kiByte/Images.xcassets/Contents.json b/ios/kiByte/Images.xcassets/Contents.json index 2d92bd5..97a8662 100755 --- a/ios/kiByte/Images.xcassets/Contents.json +++ b/ios/kiByte/Images.xcassets/Contents.json @@ -1,6 +1,6 @@ { - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } } diff --git a/jest.config.js b/jest.config.js index d572ced..18e636a 100755 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,9 @@ module.exports = { preset: 'react-native', - setupFilesAfterEnv: ['/src/setupTests.ts'], + setupFilesAfterEnv: ['/src/tests-setup.ts'], transformIgnorePatterns: [], transform: { '^.+\\.[t|j]sx?$': 'babel-jest', }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], -}; +} diff --git a/metro.config.js b/metro.config.js index 5386492..ebdf6c4 100755 --- a/metro.config.js +++ b/metro.config.js @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-require-imports */ -const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config') /** * Metro configuration @@ -7,6 +7,6 @@ const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); * * @type {import('metro-config').MetroConfig} */ -const config = {}; +const config = {} -module.exports = mergeConfig(getDefaultConfig(__dirname), config); +module.exports = mergeConfig(getDefaultConfig(__dirname), config) diff --git a/src/components/BackgroundLayers/index.tsx b/src/components/background-layers/index.tsx similarity index 79% rename from src/components/BackgroundLayers/index.tsx rename to src/components/background-layers/index.tsx index 1318508..ee8fa41 100644 --- a/src/components/BackgroundLayers/index.tsx +++ b/src/components/background-layers/index.tsx @@ -1,12 +1,14 @@ -import React from 'react'; -import { StyleSheet } from 'react-native'; -import { BlurView } from '@react-native-community/blur'; -import { Defs, LinearGradient, Rect, Stop, Svg } from 'react-native-svg'; -import NoiseLayer from '../NoiseLayer'; -import colors from '@theme/colors'; +import React from 'react' +import { StyleSheet } from 'react-native' +import { BlurView } from '@react-native-community/blur' +import { Defs, LinearGradient, Rect, Stop, Svg } from 'react-native-svg' + +import colors from '@theme/colors' + +import NoiseLayer from '../noise-layer' interface BackgroundLayersProps { - testID?: string; + testID?: string } const BackgroundLayers: React.FC = ({ @@ -37,8 +39,8 @@ const BackgroundLayers: React.FC = ({ - ); -}; + ) +} const styles = StyleSheet.create({ blurView: { @@ -49,6 +51,6 @@ const styles = StyleSheet.create({ right: 0, opacity: 0.5, }, -}); +}) -export default BackgroundLayers; +export default BackgroundLayers diff --git a/src/components/Divider/index.tsx b/src/components/divider/index.tsx similarity index 71% rename from src/components/Divider/index.tsx rename to src/components/divider/index.tsx index 0879741..956afce 100644 --- a/src/components/Divider/index.tsx +++ b/src/components/divider/index.tsx @@ -1,14 +1,15 @@ -import colors from '@theme/colors'; -import React from 'react'; -import styled from 'styled-components/native'; +import React from 'react' +import styled from 'styled-components/native' + +import colors from '@theme/colors' interface DividerProps { - color?: string; - opacity?: number; - height?: number; - marginHorizontal?: number; - marginVertical?: number; - testID?: string; + color?: string + opacity?: number + height?: number + marginHorizontal?: number + marginVertical?: number + testID?: string } const Divider: React.FC = ({ @@ -28,8 +29,8 @@ const Divider: React.FC = ({ marginHorizontal={marginHorizontal} marginVertical={marginVertical} /> - ); -}; + ) +} const DividerLine = styled.View` height: ${({ height }) => height}px; @@ -37,6 +38,6 @@ const DividerLine = styled.View` background-color: ${({ color }) => color}; margin: ${({ marginVertical }) => marginVertical}px ${({ marginHorizontal }) => marginHorizontal}px; -`; +` -export default Divider; +export default Divider diff --git a/src/components/Header/index.tsx b/src/components/header/index.tsx similarity index 69% rename from src/components/Header/index.tsx rename to src/components/header/index.tsx index 69dc0b5..8a6adff 100644 --- a/src/components/Header/index.tsx +++ b/src/components/header/index.tsx @@ -1,26 +1,28 @@ -import React from 'react'; -import BackButtonIcon from 'react-native-vector-icons/FontAwesome'; -import colors from '@theme/colors'; -import { useNavigation } from '@react-navigation/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import React from 'react' +import { useNavigation } from '@react-navigation/native' +import BackButtonIcon from 'react-native-vector-icons/FontAwesome' +import { useSafeAreaInsets } from 'react-native-safe-area-context' + +import colors from '@theme/colors' + import { BackButton, HeaderContainer, HeaderTitle, TitleContainer, -} from './styles'; +} from './styles' const Header: React.FC<{ - title?: string; - showBackButton?: boolean; - rightColumnContent?: any; + title?: string + showBackButton?: boolean + rightColumnContent?: any }> = ({ title, rightColumnContent = true, showBackButton = true }) => { - const insets = useSafeAreaInsets(); - const navigation = useNavigation(); + const insets = useSafeAreaInsets() + const navigation = useNavigation() const handleBackPress = () => { - navigation.goBack(); - }; + navigation.goBack() + } return ( @@ -38,7 +40,7 @@ const Header: React.FC<{ {rightColumnContent && rightColumnContent} - ); -}; + ) +} -export default Header; +export default Header diff --git a/src/components/Header/styles.ts b/src/components/header/styles.ts similarity index 67% rename from src/components/Header/styles.ts rename to src/components/header/styles.ts index 272c409..bab10c5 100644 --- a/src/components/Header/styles.ts +++ b/src/components/header/styles.ts @@ -1,26 +1,27 @@ -import styled from 'styled-components/native'; -import colors from '@theme/colors'; -import { TouchableOpacity } from 'react-native'; +import { TouchableOpacity } from 'react-native' +import styled from 'styled-components/native' + +import colors from '@theme/colors' const HeaderContainer = styled.View<{ - insets: { top: number; bottom: number; left: number; right: number }; + insets: { top: number; bottom: number; left: number; right: number } }>` height: 22px; margin-bottom: 20px; flex-direction: row; align-items: center; -`; +` -const BackButton = styled(TouchableOpacity)``; +const BackButton = styled(TouchableOpacity)`` const TitleContainer = styled.View` flex: 1; -`; +` const HeaderTitle = styled.Text` font-size: 18px; color: ${colors.common.offWhite}; text-align: center; -`; +` -export { HeaderContainer, BackButton, TitleContainer, HeaderTitle }; +export { HeaderContainer, BackButton, TitleContainer, HeaderTitle } diff --git a/src/components/NoiseLayer/index.tsx b/src/components/noise-layer/index.tsx similarity index 75% rename from src/components/NoiseLayer/index.tsx rename to src/components/noise-layer/index.tsx index 176324c..5822c67 100644 --- a/src/components/NoiseLayer/index.tsx +++ b/src/components/noise-layer/index.tsx @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-require-imports */ -import React from 'react'; -import { Image, StyleSheet } from 'react-native'; +import React from 'react' +import { Image, StyleSheet } from 'react-native' interface NoiseLayerProps { - opacity?: number; + opacity?: number } const NoiseLayer: React.FC = ({ opacity = 0.1 }) => ( @@ -12,7 +12,7 @@ const NoiseLayer: React.FC = ({ opacity = 0.1 }) => ( style={[styles.noiseImage, { opacity }]} resizeMode="repeat" /> -); +) const styles = StyleSheet.create({ noiseImage: { @@ -20,6 +20,6 @@ const styles = StyleSheet.create({ height: '100%', position: 'absolute', }, -}); +}) -export default NoiseLayer; +export default NoiseLayer diff --git a/src/config/toastConfig.tsx b/src/config/toast.config.tsx similarity index 89% rename from src/config/toastConfig.tsx rename to src/config/toast.config.tsx index 5f67ebe..4cca0c4 100644 --- a/src/config/toastConfig.tsx +++ b/src/config/toast.config.tsx @@ -1,6 +1,7 @@ -import colors from '@theme/colors'; -import React from 'react'; -import { BaseToast, ErrorToast, ToastProps } from 'react-native-toast-message'; +import React from 'react' +import { BaseToast, ErrorToast, ToastProps } from 'react-native-toast-message' + +import colors from '@theme/colors' const toastConfig = { success: (props: ToastProps) => ( @@ -37,6 +38,6 @@ const toastConfig = { }} /> ), -}; +} -export default toastConfig; +export default toastConfig diff --git a/src/context/folder-selection.tsx b/src/context/folder-selection.tsx new file mode 100644 index 0000000..448949f --- /dev/null +++ b/src/context/folder-selection.tsx @@ -0,0 +1,94 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react' +import { useSelector } from 'react-redux' + +import FolderSelectionModal from '@screens/note-details/components/folder-selection-modal' +import { RootState } from '@store/index' + +interface FolderSelectionContextProps { + isModalVisible: boolean + selectedFolderId: number | null + showModal: ( + onConfirm: (folderId: number, title?: string) => void, + options?: { allowTitleEdit?: boolean }, + ) => void + hideModal: () => void + handleFolderSelect: (folderId: number | null) => void +} + +const FolderSelectionContext = + createContext(null) + +export const useFolderSelection = () => { + const context = useContext(FolderSelectionContext) + if (!context) { + throw new Error( + 'useFolderSelection must be used within FolderSelectionProvider', + ) + } + return context +} + +export const FolderSelectionProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [isModalVisible, setIsModalVisible] = useState(false) + const [selectedFolderId, setSelectedFolderId] = useState(null) + const [allowTitleEdit, setAllowTitleEdit] = useState(false) + const [noteTitle, setNoteTitle] = useState() + const [onConfirmHandler, setOnConfirmHandler] = useState< + ((folderId: number, noteTitle?: string) => void) | null + >(null) + + const folders = useSelector((state: RootState) => state.folders) + + const showModal = ( + onConfirm: (folderId: number, noteTitle?: string) => void, + options: { allowTitleEdit?: boolean } = {}, + ) => { + setOnConfirmHandler(() => onConfirm) + setAllowTitleEdit(!!options.allowTitleEdit) + setIsModalVisible(true) + } + + const hideModal = () => { + setIsModalVisible(false) + setSelectedFolderId(null) + setNoteTitle('') + } + + const handleFolderSelect = (folderId: number | null) => + setSelectedFolderId(folderId) + + const handleConfirm = () => { + if (onConfirmHandler && selectedFolderId !== null) { + onConfirmHandler(selectedFolderId, noteTitle) + } + hideModal() + } + + return ( + + {children} + {isModalVisible && ( + + )} + + ) +} diff --git a/src/hooks/use-note-operations.ts b/src/hooks/use-note-operations.ts new file mode 100644 index 0000000..761a80d --- /dev/null +++ b/src/hooks/use-note-operations.ts @@ -0,0 +1,89 @@ +import { useDispatch, useSelector } from 'react-redux' + +import { + useCreateNoteMutation, + useUpdateNoteMutation, +} from '@store/queries/notes' +import { RootState } from '@store/index' +import { addNoteToFolder, setFolders } from '@store/folders.slice' + +export const useNoteOperations = () => { + const dispatch = useDispatch() + const folders = useSelector((state: RootState) => state.folders) + + const [createNote] = useCreateNoteMutation() + const [updateNoteMutation] = useUpdateNoteMutation() + + const createNewNote = async ({ + title, + content, + audioUrl, + folderId, + }: { + title: string + content: string + audioUrl?: string + folderId: number | null + }) => { + if (!folderId) return + const createdNote = await createNote({ + title, + content, + audioUrl, + folderId, + }).unwrap() + + dispatch( + addNoteToFolder({ + folderId, + note: { + id: createdNote.id, + title: createdNote.title, + snippet: createdNote.content ? createdNote.content.slice(0, 50) : '', + modifiedDate: createdNote.modifiedDate, + }, + }), + ) + + return createdNote + } + + const updateNote = async ({ + id, + title, + content, + }: { + id: number + title: string + content: string + }) => { + if (!id) return + const updatedNote = await updateNoteMutation({ + id, + title, + content, + }).unwrap() + + const updatedFolders = folders.map(folder => ({ + ...folder, + notes: folder.notes.map(note => + note.id === id + ? { + ...note, + title: updatedNote.title, + snippet: updatedNote.content + ? updatedNote.content.slice(0, 50) + : '', + modifiedDate: updatedNote.modifiedDate, + } + : note, + ), + })) + + dispatch(setFolders(updatedFolders)) + + return updatedNote + } + + return { createNewNote, updateNote } +} diff --git a/src/utils/hooks/usePermissions.ts b/src/hooks/use-permissions.ts similarity index 74% rename from src/utils/hooks/usePermissions.ts rename to src/hooks/use-permissions.ts index 2f7feb5..a1c6b3b 100644 --- a/src/utils/hooks/usePermissions.ts +++ b/src/hooks/use-permissions.ts @@ -1,4 +1,4 @@ -import { PermissionsAndroid, Platform } from 'react-native'; +import { PermissionsAndroid, Platform } from 'react-native' export const usePermissions = () => { const requestAudioPermissions = async (): Promise => { @@ -8,9 +8,9 @@ export const usePermissions = () => { PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, - ]); + ]) - console.log('Permissions status:', grants); + console.log('Permissions status:', grants) return ( grants['android.permission.WRITE_EXTERNAL_STORAGE'] === @@ -19,15 +19,15 @@ export const usePermissions = () => { PermissionsAndroid.RESULTS.GRANTED && grants['android.permission.RECORD_AUDIO'] === PermissionsAndroid.RESULTS.GRANTED - ); + ) } catch (err) { - console.warn('Permission request failed:', err); - return false; + console.warn('Permission request failed:', err) + return false } } - return true; - }; + return true + } - return { requestAudioPermissions }; -}; + return { requestAudioPermissions } +} diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts new file mode 100644 index 0000000..b456a7c --- /dev/null +++ b/src/hooks/use-toast.ts @@ -0,0 +1,23 @@ +import Toast from 'react-native-toast-message' +import { useSafeAreaInsets } from 'react-native-safe-area-context' + +type ToastOptions = { + isSuccess: boolean + message: string +} + +export const useToast = () => { + const insets = useSafeAreaInsets() + + const showToast = ({ isSuccess, message }: ToastOptions) => { + Toast.show({ + type: isSuccess ? 'success' : 'error', + text1: message, + position: 'bottom', + bottomOffset: insets.bottom * 3, + visibilityTime: 1750, + }) + } + + return { showToast } +} diff --git a/src/navigation/index.tsx b/src/navigation/index.tsx index 56c4ab0..ba6c53f 100755 --- a/src/navigation/index.tsx +++ b/src/navigation/index.tsx @@ -1,24 +1,24 @@ -import React, { useEffect } from 'react'; -import { NavigationContainer } from '@react-navigation/native'; -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { createStackNavigator } from '@react-navigation/stack'; -import Icon from 'react-native-vector-icons/FontAwesome'; - -// Screens -import LoginScreen from '@screens/LoginScreen'; -import HomeScreen from '@screens/HomeScreen'; -import SettingsScreen from '@screens/SettingsScreen'; -import NoteDetailsScreen from '@screens/NoteDetailsScreen'; +import React, { useEffect } from 'react' +import { NavigationContainer } from '@react-navigation/native' +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' +import { createStackNavigator } from '@react-navigation/stack' +import Icon from 'react-native-vector-icons/FontAwesome' // Store -import { useSelector, useDispatch } from 'react-redux'; -import { AppDispatch, RootState } from '@store/index'; -import { bootstrapAsync } from '@store/authSlice'; -import { tabNavigatorStyles } from '@navigation/tabNavigatorStyles'; -import { RootStackParamList } from './types'; +import { useSelector, useDispatch } from 'react-redux' +import { AppDispatch, RootState } from '@store/index' +import { bootstrapAsync } from '@store/auth.slice' +import { tabNavigatorStyles } from '@navigation/tab-navigator.styles' +import { RootStackParamList } from './types' + +// Screens +import LoginScreen from '@screens/login' +import HomeScreen from '@screens/home' +import SettingsScreen from '@screens/settings' +import NoteDetailsScreen from '@screens/note-details' -const Stack = createStackNavigator(); -const Tab = createBottomTabNavigator(); +const Stack = createStackNavigator() +const Tab = createBottomTabNavigator() function HomeStack() { return ( @@ -26,7 +26,7 @@ function HomeStack() { - ); + ) } function HomeTabs() { @@ -54,21 +54,19 @@ function HomeTabs() { }} /> - ); + ) } export default function Navigation() { - const dispatch = useDispatch(); - const { isLoading, userToken } = useSelector( - (state: RootState) => state.auth, - ); + const dispatch = useDispatch() + const { isLoading, userToken } = useSelector((state: RootState) => state.auth) useEffect(() => { - dispatch(bootstrapAsync()); - }, [dispatch]); + dispatch(bootstrapAsync()) + }, [dispatch]) if (isLoading) { - return <>; + return <> } return ( @@ -85,5 +83,5 @@ export default function Navigation() { )} - ); + ) } diff --git a/src/navigation/tabNavigatorStyles.ts b/src/navigation/tab-navigator.styles.ts similarity index 91% rename from src/navigation/tabNavigatorStyles.ts rename to src/navigation/tab-navigator.styles.ts index 33d86d5..92d15ed 100755 --- a/src/navigation/tabNavigatorStyles.ts +++ b/src/navigation/tab-navigator.styles.ts @@ -1,5 +1,6 @@ -import { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs'; -import colors from '@theme/colors'; +import { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs' + +import colors from '@theme/colors' export const tabNavigatorStyles = (): BottomTabNavigationOptions => ({ headerShown: false, @@ -21,4 +22,4 @@ export const tabNavigatorStyles = (): BottomTabNavigationOptions => ({ tabBarIconStyle: { marginTop: 4, }, -}); +}) diff --git a/src/navigation/types.ts b/src/navigation/types.ts index 33a2371..28c8df7 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -1,19 +1,27 @@ -import { RouteProp } from '@react-navigation/native'; -import { StackNavigationProp } from '@react-navigation/stack'; +import { RouteProp } from '@react-navigation/native' +import { StackNavigationProp } from '@react-navigation/stack' export type RootStackParamList = { - Home: undefined; - SignIn: undefined; - AuthStack: undefined; - NoteDetails: { noteId?: number; isNew?: boolean }; -}; + Home: undefined + SignIn: undefined + AuthStack: undefined + NoteDetails: { + noteId?: number + isNew?: boolean + preFilledData?: { + title: string + content: string + audioUrl?: string + } + } +} export type NoteDetailScreenNavigationProp = StackNavigationProp< RootStackParamList, 'NoteDetails' ->; +> export type NoteDetailScreenRouteProp = RouteProp< RootStackParamList, 'NoteDetails' ->; +> diff --git a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts b/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts deleted file mode 100644 index 8e4eefa..0000000 --- a/src/screens/HomeScreen/components/FloatingButton/hooks/useAudioRecorder.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import AudioRecorderPlayer from 'react-native-audio-recorder-player'; - -import { usePermissions } from '@root/src/utils/hooks/usePermissions'; - -export const useAudioRecorder = () => { - const audioRecorderPlayer = useMemo(() => new AudioRecorderPlayer(), []); - const [isRecording, setIsRecording] = useState(false); - const [recordingResult, setRecordingResult] = useState(null); - const { requestAudioPermissions } = usePermissions(); - - useEffect(() => { - return () => { - audioRecorderPlayer.removeRecordBackListener(); - }; - }, [audioRecorderPlayer]); - - const startRecording = async () => { - try { - const hasPermissions = await requestAudioPermissions(); - if (!hasPermissions) { - console.log('Permissions not granted'); - return; - } - - setIsRecording(true); - const result = await audioRecorderPlayer.startRecorder(); - - audioRecorderPlayer.addRecordBackListener(e => { - console.log(e); - return; - }); - - console.log('🚀 ~ startRecording ~ result:', result); - } catch (error) { - console.error('Failed to start recording:', error); - setIsRecording(false); - } - }; - - const stopRecording = async () => { - if (!isRecording) return; - - try { - const result = await audioRecorderPlayer.stopRecorder(); - audioRecorderPlayer.removeRecordBackListener(); - - setIsRecording(false); - setRecordingResult(result); - console.log('🚀 ~ stopRecording ~ result:', result); - } catch (error) { - console.error('Failed to stop recording:', error); - } - }; - - return { startRecording, stopRecording, isRecording, recordingResult }; -}; diff --git a/src/screens/HomeScreen/components/FloatingButton/hooks/useFloatingButtonHandlers.ts b/src/screens/HomeScreen/components/FloatingButton/hooks/useFloatingButtonHandlers.ts deleted file mode 100644 index 7e6b410..0000000 --- a/src/screens/HomeScreen/components/FloatingButton/hooks/useFloatingButtonHandlers.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { useRef, useState } from 'react'; -import { Animated } from 'react-native'; -import { useAudioRecorder } from './useAudioRecorder'; - -const pressAnimationDuration = 190; -const releaseAnimationDuration = 150; - -interface UseFloatingButtonHandlersProps { - onPress: () => void; - onLongPress: () => void; -} - -export const useFloatingButtonHandlers = ({ - onPress, - onLongPress, -}: UseFloatingButtonHandlersProps) => { - const { startRecording, stopRecording, isRecording } = useAudioRecorder(); - - const [isLongPressed, setIsLongPressed] = useState(false); - - const bottomPositionAnim = useRef(new Animated.Value(25)).current; - const opacityAnim = useRef(new Animated.Value(0.9)).current; - const heightAnim = useRef(new Animated.Value(50)).current; - const translateYAnim = useRef(new Animated.Value(0)).current; - - const handlePressIn = () => { - Animated.timing(opacityAnim, { - toValue: 0.2, - duration: 100, - useNativeDriver: false, - }).start(); - }; - - const handleLongPress = async () => { - setIsLongPressed(true); - Animated.parallel([ - Animated.timing(bottomPositionAnim, { - toValue: 25, - duration: pressAnimationDuration, - useNativeDriver: false, - }), - Animated.timing(translateYAnim, { - toValue: -5, - duration: pressAnimationDuration, - useNativeDriver: false, - }), - Animated.timing(opacityAnim, { - toValue: 0.9, - duration: pressAnimationDuration, - useNativeDriver: false, - }), - Animated.timing(heightAnim, { - toValue: 70, - duration: pressAnimationDuration, - useNativeDriver: false, - }), - ]).start(); - - if (onLongPress) onLongPress(); - await startRecording(); - }; - - const handlePressOut = async () => { - if (isLongPressed) { - await stopRecording(); - } else onPress(); - - Animated.parallel([ - Animated.timing(bottomPositionAnim, { - toValue: 25, - duration: releaseAnimationDuration, - useNativeDriver: false, - }), - Animated.timing(translateYAnim, { - toValue: 0, - duration: pressAnimationDuration, - useNativeDriver: false, - }), - Animated.timing(opacityAnim, { - toValue: 0.9, - duration: releaseAnimationDuration, - useNativeDriver: false, - }), - Animated.timing(heightAnim, { - toValue: 50, - duration: releaseAnimationDuration, - useNativeDriver: false, - }), - ]).start(); - - setIsLongPressed(false); - }; - - const animatedStyles = { - height: heightAnim, - bottom: bottomPositionAnim, - opacity: opacityAnim, - transform: [{ translateY: translateYAnim }, { translateX: -35 }], - }; - - return { - isLongPressed, - isRecording, - animatedStyles, - handlePressIn, - handlePressOut, - handleLongPress, - }; -}; diff --git a/src/screens/HomeScreen/components/FloatingButton/index.tsx b/src/screens/HomeScreen/components/FloatingButton/index.tsx deleted file mode 100644 index a80cab3..0000000 --- a/src/screens/HomeScreen/components/FloatingButton/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import colors from '@theme/colors'; -import { TouchableWithoutFeedback } from 'react-native'; -import { useFloatingButtonHandlers } from './hooks/useFloatingButtonHandlers'; -import { AnimatedButtonContainer, FullCircleIcon, PlusIcon } from './styles'; - -interface FloatingButtonProps { - onPress: () => void; - onLongPress: () => void; - testID?: string; -} - -const FloatingButton: React.FC = ({ - onPress, - onLongPress, - testID, -}) => { - const { - isLongPressed, - animatedStyles, - handlePressIn, - handlePressOut, - handleLongPress, - } = useFloatingButtonHandlers({ onPress, onLongPress }); - - return ( - - - {isLongPressed ? ( - - ) : ( - - )} - - - ); -}; - -export default FloatingButton; diff --git a/src/screens/HomeScreen/components/Folder/types.ts b/src/screens/HomeScreen/components/Folder/types.ts deleted file mode 100644 index e8179c8..0000000 --- a/src/screens/HomeScreen/components/Folder/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -type NoteListItem = { - id: number; - title: string; - snippet: string; - modifiedDate: number; -}; - -type Folder = { - id: number; - folderName: string; - notes: NoteListItem[]; -}; - -interface FolderProps { - folder: Folder; -} - -export { NoteListItem, Folder, FolderProps }; diff --git a/src/screens/HomeScreen/index.tsx b/src/screens/HomeScreen/index.tsx deleted file mode 100755 index 9dae78b..0000000 --- a/src/screens/HomeScreen/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useState } from 'react'; -import styled from 'styled-components/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useSelector } from 'react-redux'; -import { RootState } from '@store/index'; -import { useNavigation } from '@react-navigation/native'; -import { NoteDetailScreenNavigationProp } from '@navigation/types'; -import BackgroundLayers from '../../components/BackgroundLayers'; -import FolderList from './components/FolderList'; -import SearchBox from './components/SearchBox'; -import NotesResultsList from './components/NotesResultsList'; -import FloatingButton from './components/FloatingButton'; - -const HomeScreen: React.FC = () => { - const insets = useSafeAreaInsets(); - const folders = useSelector((state: RootState) => state.folders); - const navigation = useNavigation(); - const [searchQuery, setSearchQuery] = useState(''); - - /* - *TODO: should I combine local filtering with the query? - */ - const allNotes = folders.flatMap(folder => folder.notes); - const filteredNotes = allNotes.filter(note => - note.title.toLowerCase().includes(searchQuery.toLowerCase()), - ); - - const handleRecordVoiceNote = () => { - console.log('recording voice note'); - }; - - const handleNewNote = () => { - navigation.navigate('NoteDetails', { isNew: true }); - }; - - return ( - <> - - - - {searchQuery ? ( - - ) : ( - - )} - - - - ); -}; - -const Container = styled.View<{ - insets: { top: number; bottom: number; left: number; right: number }; -}>` - flex: 1; - justify-content: center; - padding-top: ${props => props.insets.top}px; - padding-left: ${props => props.insets.left}px; - padding-right: ${props => props.insets.right - 2}px; -`; - -export default HomeScreen; diff --git a/src/screens/NoteDetailsScreen/components/SaveButton.tsx b/src/screens/NoteDetailsScreen/components/SaveButton.tsx deleted file mode 100644 index df363de..0000000 --- a/src/screens/NoteDetailsScreen/components/SaveButton.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import Icon from 'react-native-vector-icons/Ionicons'; -import styled from 'styled-components/native'; -import colors from '@theme/colors'; -import { TouchableOpacity } from 'react-native'; - -const SaveButton: React.FC<{ handleSave: any }> = ({ handleSave }) => { - return ( - - - - ); -}; - -const ButtonContainer = styled(TouchableOpacity)``; - -export default SaveButton; diff --git a/src/screens/NoteDetailsScreen/index.tsx b/src/screens/NoteDetailsScreen/index.tsx deleted file mode 100644 index b35fa52..0000000 --- a/src/screens/NoteDetailsScreen/index.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import Toast from 'react-native-toast-message'; -import BackgroundLayers from '@root/src/components/BackgroundLayers'; -import Header from '@root/src/components/Header'; -import SaveButton from './components/SaveButton'; -import FolderSelectionModal from './components/FolderSelectionModal'; -import { RootStackParamList } from '@navigation/types'; -import { StackScreenProps } from '@react-navigation/stack'; -import { - useCreateNoteMutation, - useFetchNoteDetailsQuery, - useUpdateNoteMutation, -} from '@store/queries/notes'; -import { - Container, - StyledText, - NoteTitleInput, - TextArea, - DateText, - TitleContainer, -} from './styles'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { formatTimestampToDateTime } from '@root/src/utils'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '@store/index'; -import { addNoteToFolder } from '@store/foldersSlice'; - -type NoteDetailsScreenProps = StackScreenProps< - RootStackParamList, - 'NoteDetails' ->; - -const NoteDetailsScreen: React.FC = ({ route }) => { - const { noteId = 0, isNew } = route.params; - const insets = useSafeAreaInsets(); - const folders = useSelector((state: RootState) => state.folders); - const dispatch = useDispatch(); - - const { data: note, isLoading } = useFetchNoteDetailsQuery(noteId, { - skip: isNew, - }); - - const [updateNote] = useUpdateNoteMutation(); - const [createNote] = useCreateNoteMutation(); - - const [title, setTitle] = useState(''); - const [modifiedDate, setModifiedDate] = useState(''); - const [content, setContent] = useState(''); - const [isModalVisible, setIsModalVisible] = useState(false); - const [selectedFolderId, setSelectedFolderId] = useState(null); - - const handleSave = async () => { - if (isNew) { - setIsModalVisible(true); - } else if (note) { - try { - await updateNote({ id: note.id, title, content }); - showToast(true); - } catch (error) { - console.error(error); - showToast(false); - } - } - }; - - const showToast = (isSuccess: boolean) => { - Toast.show({ - type: isSuccess ? 'success' : 'error', - text1: isSuccess ? 'Saved successfully!' : 'Something went wrong', - position: 'bottom', - bottomOffset: insets.bottom * 3, - visibilityTime: 1750, - }); - }; - - useEffect(() => { - if (note) { - setTitle(note.title); - setModifiedDate(formatTimestampToDateTime(note.modifiedDate)); - setContent(note.content || ''); - } else if (isNew) { - setTitle(''); - setContent(''); - setModifiedDate(''); - } - }, [note, isNew]); - - const handleFolderSelect = (folderId: number | null) => { - setSelectedFolderId(folderId); - }; - - const handleConfirmFolderSelection = async () => { - if (selectedFolderId) { - setIsModalVisible(false); - try { - const createdNote = await createNote({ - title, - content, - folderId: selectedFolderId, - }).unwrap(); - - dispatch( - addNoteToFolder({ - folderId: selectedFolderId, - note: { - id: createdNote.id, - title: createdNote.title, - snippet: createdNote.content - ? createdNote.content.slice(0, 50) - : '', - modifiedDate: createdNote.modifiedDate, - }, - }), - ); - - showToast(true); - } catch (error) { - console.error(error); - showToast(false); - } - } - }; - - if (!isNew && isLoading) { - return Loading...; - } - - return ( - <> - - -
} /> - - - {modifiedDate} - - -