startx 1.1.35 → 1.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_gitignore +3 -0
- package/apps/core-server/Dockerfile +2 -2
- package/apps/core-server/package.json +1 -2
- package/apps/core-server/src/middlewares/logger-middleware.ts +32 -71
- package/apps/queue-worker/Dockerfile +2 -2
- package/apps/startx-cli/dist/index.mjs +1 -1
- package/apps/web-client/Dockerfile +3 -3
- package/apps/web-client/package.json +2 -0
- package/apps/web-client/src/components/utils/navigation-loader.tsx +106 -0
- package/apps/web-client/src/components/utils/pagination-footer.tsx +205 -0
- package/apps/web-client/src/hooks/pagination/usePagination.tsx +22 -0
- package/apps/web-client/src/hooks/utils/use-url-params.ts +95 -0
- package/apps/web-client/src/root.tsx +2 -0
- package/apps/web-client/tsconfig.json +1 -1
- package/apps/web-client/vite.config.ts +3 -0
- package/package.json +1 -1
- package/packages/ui/package.json +1 -0
- package/packages/ui/src/api/use-api/react-query/use-api.ts +22 -69
- package/packages/ui/src/components/custom/form-wrapper.tsx +11 -4
- package/pnpm-workspace.yaml +11 -1
package/_gitignore
CHANGED
|
@@ -14,7 +14,7 @@ COPY --parents packages/*/package.json ./
|
|
|
14
14
|
COPY --parents packages/*/*/package.json ./
|
|
15
15
|
COPY --parents configs/*/package.json ./
|
|
16
16
|
|
|
17
|
-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
|
17
|
+
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store,sharing=locked \
|
|
18
18
|
pnpm install --frozen-lockfile
|
|
19
19
|
|
|
20
20
|
COPY apps/core-server/ ./apps/core-server/
|
|
@@ -24,7 +24,7 @@ COPY assets ./assets
|
|
|
24
24
|
COPY turbo.json ./
|
|
25
25
|
|
|
26
26
|
# Build the required packages with Turbo cache mounted
|
|
27
|
-
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache \
|
|
27
|
+
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache,sharing=locked \
|
|
28
28
|
pnpm build --filter=core-server
|
|
29
29
|
|
|
30
30
|
# --- Final production image ---
|
|
@@ -32,8 +32,7 @@
|
|
|
32
32
|
"country-state-city": "catalog:",
|
|
33
33
|
"express": "catalog:",
|
|
34
34
|
"express-fileupload": "catalog:",
|
|
35
|
-
"express-list-endpoints": "catalog:"
|
|
36
|
-
"morgan": "catalog:"
|
|
35
|
+
"express-list-endpoints": "catalog:"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
39
38
|
"@types/cookie-parser": "catalog:",
|
|
@@ -1,81 +1,42 @@
|
|
|
1
|
+
import { ENV } from "@repo/env";
|
|
1
2
|
import { logger } from "@repo/logger";
|
|
2
|
-
import type { Request, Response } from "express";
|
|
3
|
-
import morgan from "morgan";
|
|
3
|
+
import type { NextFunction, Request, Response } from "express";
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const method = tokens.method(req, res) || "-";
|
|
7
|
-
const url = tokens.url(req, res) || "-";
|
|
8
|
-
const status = Number(tokens.status(req, res) || 0);
|
|
9
|
-
const responseTime = tokens["response-time"](req, res) ?? "-";
|
|
10
|
-
const ip =
|
|
11
|
-
(req.ip as string) || (req.headers["x-forwarded-for"] as string) || req.socket?.remoteAddress;
|
|
12
|
-
const user = req.user ? { id: req.user.id, email: req.user.email } : null;
|
|
5
|
+
const SKIPPED_PREFIXES = ["/static", "/_next"];
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
method,
|
|
16
|
-
url,
|
|
17
|
-
status,
|
|
18
|
-
responseTimeMs: responseTime,
|
|
19
|
-
user,
|
|
20
|
-
ip,
|
|
21
|
-
ts: new Date().toISOString(),
|
|
22
|
-
});
|
|
23
|
-
};
|
|
7
|
+
const shouldSkip = (path: string) => path === "/health" || SKIPPED_PREFIXES.some(prefix => path.startsWith(prefix));
|
|
24
8
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const fn = lg[level];
|
|
36
|
-
if (typeof fn === "function") {
|
|
37
|
-
fn(message, meta);
|
|
9
|
+
const resolveIp = (req: Request) =>
|
|
10
|
+
req.ip || (req.headers["x-forwarded-for"] as string | undefined) || req.socket?.remoteAddress || "-";
|
|
11
|
+
|
|
12
|
+
const levelForStatus = (status: number) => (status >= 500 ? "error" : status >= 400 ? "warn" : "http");
|
|
13
|
+
|
|
14
|
+
const loggerMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
|
15
|
+
if (shouldSkip(req.path) || ENV.LOG_LEVEL !== "debug") {
|
|
16
|
+
next();
|
|
38
17
|
return;
|
|
39
18
|
}
|
|
40
|
-
// fallback
|
|
41
|
-
if (level && console[level]) {
|
|
42
|
-
console[level]?.(message, meta ?? {});
|
|
43
|
-
} else console.warn(message, meta ?? {});
|
|
44
|
-
};
|
|
45
19
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
url: string;
|
|
54
|
-
status: number;
|
|
55
|
-
responseTimeMs: string | number;
|
|
56
|
-
user?: { id: string; email: string } | null;
|
|
57
|
-
ip?: string;
|
|
58
|
-
ts?: string;
|
|
59
|
-
};
|
|
60
|
-
const level = meta.status >= 500 ? "error" : meta.status >= 400 ? "warn" : "info";
|
|
61
|
-
safeLog(level, "http_request", {
|
|
62
|
-
...meta,
|
|
63
|
-
logType: "routeInfo",
|
|
64
|
-
});
|
|
65
|
-
} catch (err) {
|
|
66
|
-
console.error(err);
|
|
67
|
-
safeLog("info", "http_request_parse_error", { raw: trimmed, logType: "routeInfo" });
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
};
|
|
20
|
+
const startedAt = process.hrtime.bigint();
|
|
21
|
+
|
|
22
|
+
res.on("finish", () => {
|
|
23
|
+
const { method, originalUrl: url } = req;
|
|
24
|
+
const { statusCode: status } = res;
|
|
25
|
+
const responseTimeMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
|
|
26
|
+
const user = req.user ? { id: req.user.id, email: req.user.email } : null;
|
|
71
27
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
28
|
+
logger.log(levelForStatus(status), `${method} ${url} ${status} - ${responseTimeMs.toFixed(1)}ms`, {
|
|
29
|
+
logType: "routeInfo",
|
|
30
|
+
method,
|
|
31
|
+
url,
|
|
32
|
+
status,
|
|
33
|
+
responseTimeMs: Number(responseTimeMs.toFixed(1)),
|
|
34
|
+
ip: resolveIp(req),
|
|
35
|
+
user,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
next();
|
|
40
|
+
};
|
|
80
41
|
|
|
81
42
|
export { loggerMiddleware };
|
|
@@ -14,7 +14,7 @@ COPY --parents packages/*/package.json ./
|
|
|
14
14
|
COPY --parents packages/*/*/package.json ./
|
|
15
15
|
COPY --parents configs/*/package.json ./
|
|
16
16
|
|
|
17
|
-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
|
17
|
+
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store,sharing=locked \
|
|
18
18
|
pnpm install --frozen-lockfile
|
|
19
19
|
|
|
20
20
|
COPY apps/queue-worker/ ./apps/queue-worker/
|
|
@@ -24,7 +24,7 @@ COPY assets ./assets
|
|
|
24
24
|
COPY turbo.json ./
|
|
25
25
|
|
|
26
26
|
# Build the required packages with Turbo cache mounted
|
|
27
|
-
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache \
|
|
27
|
+
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache,sharing=locked \
|
|
28
28
|
pnpm build --filter=queue-worker
|
|
29
29
|
|
|
30
30
|
# --- Final production image ---
|
|
@@ -223,4 +223,4 @@ export default extend(baseConfig);
|
|
|
223
223
|
`),l&&await o.writeFile(t.join(a,`vitest.config.ts`),`import vitestConfig from "vitest-config/node";
|
|
224
224
|
|
|
225
225
|
export default vitestConfig;
|
|
226
|
-
`),Z_.info(`Created package ${r} at ${t.relative(i.workspace,a)}`),Z_.info("Run `pnpm install` to link the new package.")}static async resolveEslintPreference(e){if(e.eslint===!1)return!1;let n=Iy.getDirectory(),r=await this.readRootPackage(n.workspace);return this.hasDependency(r,`eslint`)?e.eslint??!0:e.eslint===!0||await Xx.confirm({message:`ESLint is not installed in this monorepo. Install and enable it?`,default:!0})?(r.devDependencies={...r.devDependencies,eslint:await this.resolveDependencyVersion(n.workspace,`eslint`)},await this.writeJson(t.join(n.workspace,`package.json`),r),Z_.info(`Added eslint to the root devDependencies.`),e.install!==!1&&await this.installRootDependencies(n.workspace),!0):!1}static async getInstallTags(e){let t=new Set([`common`,`node`]),n=await this.readRootPackage(e.workspace);e.eslintEnabled&&t.add(`eslint`),this.hasDependency(n,`@biomejs/biome`)&&t.add(`biome`),this.hasDependency(n,`prettier`)&&t.add(`prettier`),this.hasDependency(n,`vitest`)&&t.add(`vitest`),this.hasDependency(n,`tsdown`)&&t.add(`tsdown`);for(let n of e.packages)n.packageJson?.startx?.tags?.forEach(e=>t.add(e)),n.packageJson?.startx?.iTags?.forEach(e=>t.add(e)),n.packageJson?.startx?.gTags?.forEach(e=>t.add(e)),(n.type===`apps`||n.packageJson?.startx?.mode===`standalone`)&&t.add(`runnable`);return Array.from(t)}static resolvePackageClosure(e){let t=new Map,n=[e.selectedPackage],r=r=>{let i=this.findPackage(e.packages,r);i&&!t.has(i.name)&&n.push(i)};for(e.includeEslintConfig&&r(`eslint-config`);n.length>0;){let e=n.shift();if(!t.has(e.name)){t.set(e.name,e);for(let t of[...e.packageJson?.startx?.requiredDeps??[],...e.packageJson?.startx?.requiredDevDeps??[]])r(t)}}return Array.from(t.values())}static async ensureTemplatePackage(e){let t=this.findPackage(e.packages,e.name);if(!t){Z_.warn(`Could not find template package ${e.name}; skipping.`);return}await this.installTemplatePackage({pkg:t,directory:e.directory,tags:e.tags})}static async installTemplatePackage(e){if(!e.pkg.packageJson)throw Error(`Missing package.json for ${e.pkg.name}`);let n=e.overrideRelativePath??e.pkg.relativePath,r=t.join(e.directory.workspace,n);if(await this.pathExists(t.join(r,`package.json`))&&!await Xx.confirm({message:`"${n}" already exists. Overwrite?`,default:!1})){Z_.info(`Skipping ${e.pkg.name}.`);return}let i=new Set([...e.tags,...e.pkg.packageJson.startx?.tags??[]]),a=e.pkg.packageJson.startx?.ignore??[];a.includes(`eslint-config`)&&i.delete(`eslint`),a.includes(`vitest-config`)&&i.delete(`vitest`);let{packageJson:o,isWorkspace:s}=By.handlePackageJson({app:e.pkg.packageJson,tags:Array.from(i),name:e.overrideName??e.pkg.packageJson.name??e.pkg.name});if(s)throw Error(`Cannot install workspace as a package: ${e.pkg.name}`);await this.syncDepsWithCatalog({workspace:e.directory.workspace,templateDir:e.directory.template,packageJson:o}),await Ny.writeJSONFile({dir:r,file:`package`,content:o}),await this.copyValidatedFilesFromFolder(e.pkg.path,r,i),await Ny.copyDirectory({from:t.join(e.pkg.path,`src`),to:t.join(r,`src`),exclude:i.has(`vitest`)?void 0:/\.test\.tsx?$/}),Z_.info(`Installed ${e.overrideName??e.pkg.name} at ${n}`)}static async copyValidatedFilesFromFolder(e,n,r){let i=await Ny.listFiles({dir:e}).catch(()=>[]);for(let a of i){let i=Py[a];i&&!i.tags.every(e=>r.has(e))||a!==`package.json`&&await Ny.copyFile({from:t.join(e,a),to:t.join(n,a)})}}static createPackageJson(e){let t={typecheck:`tsc --noEmit`,clean:`rimraf dist .turbo`},n={"typescript-config":`workspace:*`},r=[];return e.eslintEnabled?(t.lint=`eslint .`,t[`lint:fix`]=`eslint . --fix`,n[`eslint-config`]=`workspace:*`):r.push(`eslint-config`),e.vitestEnabled?(t.test=`vitest run`,n[`vitest-config`]=`workspace:*`):r.push(`vitest-config`),{name:e.name,version:`1.0.0`,type:`module`,scripts:t,exports:`./src/index.ts`,devDependencies:n,startx:{iTags:[`node`],requiredDevDeps:[`typescript-config`],...r.length>0?{ignore:r}:{}}}}static async checkAndInstallMissingDeps(e){let n=await this.readRootPackage(e.directory.workspace),r=await Iy.parsePnpmWorkspace({dir:e.directory.workspace}),i=[],a=[];for(let[t,o]of Object.entries(Ly))if(o.tags.every(t=>e.tags.includes(t))&&!o.tags.includes(`root`))if(o.version.startsWith(`workspace:`))await this.workspacePackageExists(e.directory.workspace,t)||a.push(t);else{if(this.hasDependency(n,t))continue;let e=r?.catalog?.[t]?`catalog:`:o.version;i.push({name:t,version:e,isDev:o.isDevDependency??!0})}if(a.length>0){Z_.warn(`The following workspace packages are missing from this monorepo:`);for(let e of a)Z_.warn(` - ${e} → run: startx package add ${e}`)}if(i.length!==0){Z_.warn(`The following npm dependencies are required but not installed:`);for(let e of i)Z_.warn(` - ${e.name}`);if(!await Xx.confirm({message:`Add them to the workspace root package.json?`,default:!0})){Z_.warn(`Skipping. Some features may not work correctly without these dependencies.`);return}n.devDependencies??={},n.dependencies??={};for(let e of i)e.isDev?n.devDependencies[e.name]=e.version:n.dependencies[e.name]=e.version;await this.writeJson(t.join(e.directory.workspace,`package.json`),n),Z_.info(`Added missing dependencies to root package.json.`),e.install!==!1&&await this.installRootDependencies(e.directory.workspace)}}static async workspacePackageExists(e,n){let r=n.startsWith(`@`)?t.join(...n.split(`/`)):n,i=[t.join(e,`configs`,r,`package.json`),t.join(e,`packages`,r,`package.json`),t.join(e,`apps`,r,`package.json`)];for(let e of i)if(await this.pathExists(e))return!0;return!1}static getDestinationPath(e,n){let r=t.dirname(e),i=n.includes(`/`)?n.split(`/`).pop():n;return t.join(r,i)}static getDefaultPackagePath(e){if(e.startsWith(`@`)){let[n,r]=e.split(`/`);return t.join(`packages`,n,r)}return t.join(`packages`,e)}static findPackage(e,t){return e.find(e=>e.name===t||e.packageJson?.name===t)}static hasDependency(e,t){return!!(e.dependencies?.[t]||e.devDependencies?.[t]||e.peerDependencies?.[t])}static async readRootPackage(e){let n=await o.readFile(t.join(e,`package.json`),`utf-8`);return JSON.parse(n)}static async resolveDependencyVersion(e,t){return(await Iy.parsePnpmWorkspace({dir:e}))?.catalog?.[t]?`catalog:`:t===`eslint`?`^9.0.0`:`latest`}static async installRootDependencies(e){let t=(await this.readRootPackage(e)).packageManager?.split(`@`)[0]||`pnpm`,r=t===`yarn`?`yarn`:t,i=[`install`];Z_.info(`Running ${r} ${i.join(` `)} to install ESLint...`),await new Promise((t,a)=>{let o=n(r,i,{cwd:e,stdio:`inherit`,shell:process.platform===`win32`});o.on(`error`,a),o.on(`close`,e=>{if(e===0){t();return}a(Error(`${r} ${i.join(` `)} exited with code ${e}`))})}).catch(t=>{Z_.warn(`Could not install dependencies automatically: ${t instanceof Error?t.message:t}`),Z_.warn(`Run "${r} ${i.join(` `)}" manually in ${e}.`)})}static async syncDepsWithCatalog(e){let n=t.join(e.workspace,`pnpm-workspace.yaml`),r;try{r=await o.readFile(n,`utf-8`)}catch{Z_.warn(`Could not find pnpm workspace file at ${n}.`);return}let i=jy.parseDocument(r);i.has(`catalog`)||i.set(`catalog`,{});let a=await this.loadTemplateCatalog(e.templateDir),s=e.packageJson.dependencies,c=e.packageJson.devDependencies,l={},u=e=>{if(e)for(let[t,n]of Object.entries(e)){if(n.startsWith(`workspace:`))continue;let r=i.hasIn([`catalog`,t]);if(n===`catalog:`){if(!r){let e=a[t];e&&(l[t]=e)}}else e[t]=`catalog:`,r||(l[t]=n)}};if(u(s),u(c),Object.keys(l).length!==0){for(let[e,t]of Object.entries(l))i.setIn([`catalog`,e],t);await o.writeFile(n,i.toString()),Z_.info(`Added to pnpm-workspace.yaml catalog:`);for(let[e,t]of Object.entries(l))Z_.info(` + ${e}: ${t}`)}}static async loadTemplateCatalog(e){try{let n=await o.readFile(t.join(e,`pnpm-workspace.yaml`),`utf-8`),r=jy.parseDocument(n).get(`catalog`);return r?r.toJSON():{}}catch{return Z_.warn(`Could not find pnpm-workspace.yaml template in ${e}.`),{}}}static async writeJson(e,t){await o.writeFile(e,`${JSON.stringify(t,null,2)}\n`)}static async pathExists(e){try{return await o.access(e),!0}catch{return!1}}},eS=`1.1.
|
|
226
|
+
`),Z_.info(`Created package ${r} at ${t.relative(i.workspace,a)}`),Z_.info("Run `pnpm install` to link the new package.")}static async resolveEslintPreference(e){if(e.eslint===!1)return!1;let n=Iy.getDirectory(),r=await this.readRootPackage(n.workspace);return this.hasDependency(r,`eslint`)?e.eslint??!0:e.eslint===!0||await Xx.confirm({message:`ESLint is not installed in this monorepo. Install and enable it?`,default:!0})?(r.devDependencies={...r.devDependencies,eslint:await this.resolveDependencyVersion(n.workspace,`eslint`)},await this.writeJson(t.join(n.workspace,`package.json`),r),Z_.info(`Added eslint to the root devDependencies.`),e.install!==!1&&await this.installRootDependencies(n.workspace),!0):!1}static async getInstallTags(e){let t=new Set([`common`,`node`]),n=await this.readRootPackage(e.workspace);e.eslintEnabled&&t.add(`eslint`),this.hasDependency(n,`@biomejs/biome`)&&t.add(`biome`),this.hasDependency(n,`prettier`)&&t.add(`prettier`),this.hasDependency(n,`vitest`)&&t.add(`vitest`),this.hasDependency(n,`tsdown`)&&t.add(`tsdown`);for(let n of e.packages)n.packageJson?.startx?.tags?.forEach(e=>t.add(e)),n.packageJson?.startx?.iTags?.forEach(e=>t.add(e)),n.packageJson?.startx?.gTags?.forEach(e=>t.add(e)),(n.type===`apps`||n.packageJson?.startx?.mode===`standalone`)&&t.add(`runnable`);return Array.from(t)}static resolvePackageClosure(e){let t=new Map,n=[e.selectedPackage],r=r=>{let i=this.findPackage(e.packages,r);i&&!t.has(i.name)&&n.push(i)};for(e.includeEslintConfig&&r(`eslint-config`);n.length>0;){let e=n.shift();if(!t.has(e.name)){t.set(e.name,e);for(let t of[...e.packageJson?.startx?.requiredDeps??[],...e.packageJson?.startx?.requiredDevDeps??[]])r(t)}}return Array.from(t.values())}static async ensureTemplatePackage(e){let t=this.findPackage(e.packages,e.name);if(!t){Z_.warn(`Could not find template package ${e.name}; skipping.`);return}await this.installTemplatePackage({pkg:t,directory:e.directory,tags:e.tags})}static async installTemplatePackage(e){if(!e.pkg.packageJson)throw Error(`Missing package.json for ${e.pkg.name}`);let n=e.overrideRelativePath??e.pkg.relativePath,r=t.join(e.directory.workspace,n);if(await this.pathExists(t.join(r,`package.json`))&&!await Xx.confirm({message:`"${n}" already exists. Overwrite?`,default:!1})){Z_.info(`Skipping ${e.pkg.name}.`);return}let i=new Set([...e.tags,...e.pkg.packageJson.startx?.tags??[]]),a=e.pkg.packageJson.startx?.ignore??[];a.includes(`eslint-config`)&&i.delete(`eslint`),a.includes(`vitest-config`)&&i.delete(`vitest`);let{packageJson:o,isWorkspace:s}=By.handlePackageJson({app:e.pkg.packageJson,tags:Array.from(i),name:e.overrideName??e.pkg.packageJson.name??e.pkg.name});if(s)throw Error(`Cannot install workspace as a package: ${e.pkg.name}`);await this.syncDepsWithCatalog({workspace:e.directory.workspace,templateDir:e.directory.template,packageJson:o}),await Ny.writeJSONFile({dir:r,file:`package`,content:o}),await this.copyValidatedFilesFromFolder(e.pkg.path,r,i),await Ny.copyDirectory({from:t.join(e.pkg.path,`src`),to:t.join(r,`src`),exclude:i.has(`vitest`)?void 0:/\.test\.tsx?$/}),Z_.info(`Installed ${e.overrideName??e.pkg.name} at ${n}`)}static async copyValidatedFilesFromFolder(e,n,r){let i=await Ny.listFiles({dir:e}).catch(()=>[]);for(let a of i){let i=Py[a];i&&!i.tags.every(e=>r.has(e))||a!==`package.json`&&await Ny.copyFile({from:t.join(e,a),to:t.join(n,a)})}}static createPackageJson(e){let t={typecheck:`tsc --noEmit`,clean:`rimraf dist .turbo`},n={"typescript-config":`workspace:*`},r=[];return e.eslintEnabled?(t.lint=`eslint .`,t[`lint:fix`]=`eslint . --fix`,n[`eslint-config`]=`workspace:*`):r.push(`eslint-config`),e.vitestEnabled?(t.test=`vitest run`,n[`vitest-config`]=`workspace:*`):r.push(`vitest-config`),{name:e.name,version:`1.0.0`,type:`module`,scripts:t,exports:`./src/index.ts`,devDependencies:n,startx:{iTags:[`node`],requiredDevDeps:[`typescript-config`],...r.length>0?{ignore:r}:{}}}}static async checkAndInstallMissingDeps(e){let n=await this.readRootPackage(e.directory.workspace),r=await Iy.parsePnpmWorkspace({dir:e.directory.workspace}),i=[],a=[];for(let[t,o]of Object.entries(Ly))if(o.tags.every(t=>e.tags.includes(t))&&!o.tags.includes(`root`))if(o.version.startsWith(`workspace:`))await this.workspacePackageExists(e.directory.workspace,t)||a.push(t);else{if(this.hasDependency(n,t))continue;let e=r?.catalog?.[t]?`catalog:`:o.version;i.push({name:t,version:e,isDev:o.isDevDependency??!0})}if(a.length>0){Z_.warn(`The following workspace packages are missing from this monorepo:`);for(let e of a)Z_.warn(` - ${e} → run: startx package add ${e}`)}if(i.length!==0){Z_.warn(`The following npm dependencies are required but not installed:`);for(let e of i)Z_.warn(` - ${e.name}`);if(!await Xx.confirm({message:`Add them to the workspace root package.json?`,default:!0})){Z_.warn(`Skipping. Some features may not work correctly without these dependencies.`);return}n.devDependencies??={},n.dependencies??={};for(let e of i)e.isDev?n.devDependencies[e.name]=e.version:n.dependencies[e.name]=e.version;await this.writeJson(t.join(e.directory.workspace,`package.json`),n),Z_.info(`Added missing dependencies to root package.json.`),e.install!==!1&&await this.installRootDependencies(e.directory.workspace)}}static async workspacePackageExists(e,n){let r=n.startsWith(`@`)?t.join(...n.split(`/`)):n,i=[t.join(e,`configs`,r,`package.json`),t.join(e,`packages`,r,`package.json`),t.join(e,`apps`,r,`package.json`)];for(let e of i)if(await this.pathExists(e))return!0;return!1}static getDestinationPath(e,n){let r=t.dirname(e),i=n.includes(`/`)?n.split(`/`).pop():n;return t.join(r,i)}static getDefaultPackagePath(e){if(e.startsWith(`@`)){let[n,r]=e.split(`/`);return t.join(`packages`,n,r)}return t.join(`packages`,e)}static findPackage(e,t){return e.find(e=>e.name===t||e.packageJson?.name===t)}static hasDependency(e,t){return!!(e.dependencies?.[t]||e.devDependencies?.[t]||e.peerDependencies?.[t])}static async readRootPackage(e){let n=await o.readFile(t.join(e,`package.json`),`utf-8`);return JSON.parse(n)}static async resolveDependencyVersion(e,t){return(await Iy.parsePnpmWorkspace({dir:e}))?.catalog?.[t]?`catalog:`:t===`eslint`?`^9.0.0`:`latest`}static async installRootDependencies(e){let t=(await this.readRootPackage(e)).packageManager?.split(`@`)[0]||`pnpm`,r=t===`yarn`?`yarn`:t,i=[`install`];Z_.info(`Running ${r} ${i.join(` `)} to install ESLint...`),await new Promise((t,a)=>{let o=n(r,i,{cwd:e,stdio:`inherit`,shell:process.platform===`win32`});o.on(`error`,a),o.on(`close`,e=>{if(e===0){t();return}a(Error(`${r} ${i.join(` `)} exited with code ${e}`))})}).catch(t=>{Z_.warn(`Could not install dependencies automatically: ${t instanceof Error?t.message:t}`),Z_.warn(`Run "${r} ${i.join(` `)}" manually in ${e}.`)})}static async syncDepsWithCatalog(e){let n=t.join(e.workspace,`pnpm-workspace.yaml`),r;try{r=await o.readFile(n,`utf-8`)}catch{Z_.warn(`Could not find pnpm workspace file at ${n}.`);return}let i=jy.parseDocument(r);i.has(`catalog`)||i.set(`catalog`,{});let a=await this.loadTemplateCatalog(e.templateDir),s=e.packageJson.dependencies,c=e.packageJson.devDependencies,l={},u=e=>{if(e)for(let[t,n]of Object.entries(e)){if(n.startsWith(`workspace:`))continue;let r=i.hasIn([`catalog`,t]);if(n===`catalog:`){if(!r){let e=a[t];e&&(l[t]=e)}}else e[t]=`catalog:`,r||(l[t]=n)}};if(u(s),u(c),Object.keys(l).length!==0){for(let[e,t]of Object.entries(l))i.setIn([`catalog`,e],t);await o.writeFile(n,i.toString()),Z_.info(`Added to pnpm-workspace.yaml catalog:`);for(let[e,t]of Object.entries(l))Z_.info(` + ${e}: ${t}`)}}static async loadTemplateCatalog(e){try{let n=await o.readFile(t.join(e,`pnpm-workspace.yaml`),`utf-8`),r=jy.parseDocument(n).get(`catalog`);return r?r.toJSON():{}}catch{return Z_.warn(`Could not find pnpm-workspace.yaml template in ${e}.`),{}}}static async writeJson(e,t){await o.writeFile(e,`${JSON.stringify(t,null,2)}\n`)}static async pathExists(e){try{return await o.access(e),!0}catch{return!1}}},eS=`1.1.52`;const tS=new dv;tS.name(`startx`).description(`StartX CLI - Your all in one monorepo startup tool.`).version(eS),tS.command(`ping`).action(()=>{Z_.info(`pong`)}),tS.addCommand(Zx.command),tS.addCommand($x.command),tS.parse(process.argv);export{};
|
|
@@ -14,8 +14,8 @@ COPY --parents packages/common/package.json ./
|
|
|
14
14
|
COPY --parents packages/ui/package.json ./
|
|
15
15
|
COPY --parents configs/*/package.json ./
|
|
16
16
|
|
|
17
|
-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
|
18
|
-
pnpm install --frozen-lockfile
|
|
17
|
+
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store,sharing=locked \
|
|
18
|
+
pnpm install --frozen-lockfile --ignore-scripts
|
|
19
19
|
|
|
20
20
|
COPY apps/web-client/ ./apps/web-client/
|
|
21
21
|
COPY packages/common ./packages/common
|
|
@@ -24,7 +24,7 @@ COPY configs ./configs
|
|
|
24
24
|
COPY turbo.json ./
|
|
25
25
|
|
|
26
26
|
# Build the required packages with Turbo cache mounted
|
|
27
|
-
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache \
|
|
27
|
+
RUN --mount=type=cache,id=turbo,target=/app/.turbo/cache,sharing=locked \
|
|
28
28
|
pnpm build --filter=web-client
|
|
29
29
|
|
|
30
30
|
# --- Final production image ---
|
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
"@dotenvx/dotenvx": "catalog:",
|
|
19
19
|
"@react-router/node": "catalog:",
|
|
20
20
|
"@react-router/serve": "catalog:",
|
|
21
|
+
"@hookform/resolvers": "catalog:",
|
|
21
22
|
"@repo/ui": "workspace:*",
|
|
22
23
|
"isbot": "catalog:",
|
|
23
24
|
"react": "catalog:",
|
|
24
25
|
"react-dom": "catalog:",
|
|
26
|
+
"motion": "catalog:",
|
|
25
27
|
"react-router": "catalog:",
|
|
26
28
|
"zustand": "catalog:",
|
|
27
29
|
"@repo/common": "workspace:*"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { cn } from "@repo/ui/lib/utils";
|
|
2
|
+
import { motion, useReducedMotion, AnimatePresence } from "motion/react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useNavigation } from "react-router";
|
|
5
|
+
|
|
6
|
+
type LoaderState = "idle" | "loading" | "complete";
|
|
7
|
+
|
|
8
|
+
export function NavigationLoader({ className }: { className?: string }) {
|
|
9
|
+
const navigation = useNavigation();
|
|
10
|
+
const prefersReducedMotion = useReducedMotion();
|
|
11
|
+
const [loaderState, setLoaderState] = React.useState<LoaderState>("idle");
|
|
12
|
+
|
|
13
|
+
const stateRef = React.useRef<LoaderState>(loaderState);
|
|
14
|
+
stateRef.current = loaderState;
|
|
15
|
+
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
18
|
+
|
|
19
|
+
const isNavigating = navigation.state === "loading" || navigation.state === "submitting";
|
|
20
|
+
|
|
21
|
+
if (isNavigating) {
|
|
22
|
+
if (stateRef.current === "idle") {
|
|
23
|
+
timer = setTimeout(() => {
|
|
24
|
+
setLoaderState("loading");
|
|
25
|
+
}, 150);
|
|
26
|
+
}
|
|
27
|
+
} else if (navigation.state === "idle") {
|
|
28
|
+
if (stateRef.current === "loading") {
|
|
29
|
+
setLoaderState("complete");
|
|
30
|
+
timer = setTimeout(() => {
|
|
31
|
+
setLoaderState("idle");
|
|
32
|
+
}, 500);
|
|
33
|
+
} else {
|
|
34
|
+
setLoaderState("idle");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return () => clearTimeout(timer);
|
|
39
|
+
}, [navigation.state]);
|
|
40
|
+
|
|
41
|
+
if (prefersReducedMotion) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const progressVariants = {
|
|
46
|
+
idle: {
|
|
47
|
+
scaleX: 0,
|
|
48
|
+
opacity: 0,
|
|
49
|
+
transition: { duration: 0 },
|
|
50
|
+
},
|
|
51
|
+
loading: {
|
|
52
|
+
scaleX: 0.85,
|
|
53
|
+
opacity: 1,
|
|
54
|
+
transition: {
|
|
55
|
+
duration: 3.5,
|
|
56
|
+
ease: [0.08, 0.82, 0.17, 1],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
complete: {
|
|
60
|
+
scaleX: 1,
|
|
61
|
+
opacity: 0,
|
|
62
|
+
transition: {
|
|
63
|
+
scaleX: { duration: 0.3, ease: [0.22, 1, 0.36, 1] },
|
|
64
|
+
opacity: { duration: 0.3, delay: 0.1, ease: "linear" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<AnimatePresence>
|
|
71
|
+
{loaderState !== "idle" && (
|
|
72
|
+
<div
|
|
73
|
+
role="progressbar"
|
|
74
|
+
aria-hidden={loaderState === "complete"}
|
|
75
|
+
aria-valuemin={0}
|
|
76
|
+
aria-valuemax={100}
|
|
77
|
+
className="pointer-events-none fixed inset-x-0 top-0 z-[9999] h-[3px] w-full bg-transparent"
|
|
78
|
+
>
|
|
79
|
+
<motion.div
|
|
80
|
+
className={cn("relative h-full w-full origin-left bg-primary/50", className)}
|
|
81
|
+
initial="idle"
|
|
82
|
+
animate={loaderState}
|
|
83
|
+
exit="idle"
|
|
84
|
+
variants={progressVariants}
|
|
85
|
+
style={{ willChange: "transform, opacity" }}
|
|
86
|
+
>
|
|
87
|
+
<motion.div
|
|
88
|
+
className="absolute inset-0 w-full bg-gradient-to-r from-transparent via-white/50 to-transparent dark:via-white/30"
|
|
89
|
+
initial={{ x: "-100%" }}
|
|
90
|
+
animate={{ x: "100%" }}
|
|
91
|
+
transition={{
|
|
92
|
+
repeat: Infinity,
|
|
93
|
+
duration: 1.2,
|
|
94
|
+
ease: "linear",
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<div className="absolute right-0 top-0 h-full w-64 bg-gradient-to-r from-transparent to-primary opacity-100" />
|
|
99
|
+
|
|
100
|
+
<div className="absolute right-0 top-1/2 h-[5px] w-[15px] -translate-y-1/2 rounded-full bg-primary shadow-[0_0_12px_4px_hsl(var(--primary))]" />
|
|
101
|
+
</motion.div>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</AnimatePresence>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { IPaginatedData } from "@repo/ui/api/use-api/api-types";
|
|
2
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@repo/ui/components/ui/select";
|
|
3
|
+
import { cn } from "@repo/ui/lib/utils";
|
|
4
|
+
import { ChevronFirst, ChevronLast, ChevronLeft, ChevronRight } from "@repo/ui/lucide";
|
|
5
|
+
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
|
|
6
|
+
import React, { forwardRef, useMemo, type ButtonHTMLAttributes } from "react";
|
|
7
|
+
import usePagination from "~/hooks/pagination/usePagination";
|
|
8
|
+
|
|
9
|
+
export type PaginationVariant = "default" | "compact" | "ghost";
|
|
10
|
+
|
|
11
|
+
interface PaginationSectionProps<T> {
|
|
12
|
+
pagination?: IPaginatedData<T>["pagination"];
|
|
13
|
+
pageKey?: string;
|
|
14
|
+
limitKey?: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
variant?: PaginationVariant;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const emptyPagination: IPaginatedData<unknown>["pagination"] = {
|
|
21
|
+
total: 0,
|
|
22
|
+
totalPages: 0,
|
|
23
|
+
currentPage: 0,
|
|
24
|
+
pageSize: 0,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const DEFAULT_LIMIT_LIST = [5, 10, 20, 30, 40, 50];
|
|
28
|
+
|
|
29
|
+
const SPRING_TRANSITION = {
|
|
30
|
+
type: "spring",
|
|
31
|
+
stiffness: 500,
|
|
32
|
+
damping: 30,
|
|
33
|
+
mass: 1,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
interface PaginationNavButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
37
|
+
icon: React.ElementType;
|
|
38
|
+
label: string;
|
|
39
|
+
isHiddenOnMobile?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PaginationNavButton = forwardRef<HTMLButtonElement, PaginationNavButtonProps>(
|
|
43
|
+
({ icon: Icon, label, disabled, isHiddenOnMobile, onClick, ...props }, ref) => {
|
|
44
|
+
const shouldReduceMotion = useReducedMotion();
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<motion.button
|
|
48
|
+
ref={ref}
|
|
49
|
+
aria-label={label}
|
|
50
|
+
aria-disabled={disabled}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
onClick={onClick}
|
|
53
|
+
whileHover={!disabled && !shouldReduceMotion ? { scale: 1.05 } : undefined}
|
|
54
|
+
whileTap={!disabled && !shouldReduceMotion ? { scale: 0.95 } : undefined}
|
|
55
|
+
transition={SPRING_TRANSITION}
|
|
56
|
+
className={cn(
|
|
57
|
+
"relative flex h-9 w-9 md:h-8 md:w-8 items-center justify-center rounded-md border border-transparent bg-transparent text-muted-foreground outline-none transition-colors",
|
|
58
|
+
"focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
59
|
+
"hover:bg-secondary/80 hover:text-foreground",
|
|
60
|
+
"disabled:pointer-events-none disabled:opacity-40",
|
|
61
|
+
isHiddenOnMobile && "hidden sm:flex",
|
|
62
|
+
props.className
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<Icon className="h-4 w-4" aria-hidden="true" />
|
|
66
|
+
</motion.button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
PaginationNavButton.displayName = "PaginationNavButton";
|
|
71
|
+
|
|
72
|
+
function PaginationFooter<T>({
|
|
73
|
+
pagination,
|
|
74
|
+
pageKey,
|
|
75
|
+
limitKey,
|
|
76
|
+
limit,
|
|
77
|
+
variant = "default",
|
|
78
|
+
className,
|
|
79
|
+
}: PaginationSectionProps<T>) {
|
|
80
|
+
const prefersReducedMotion = useReducedMotion();
|
|
81
|
+
const { next, prev, setLimit, setPage } = usePagination({
|
|
82
|
+
pageKey,
|
|
83
|
+
limitKey,
|
|
84
|
+
limit,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const data = pagination || emptyPagination;
|
|
88
|
+
|
|
89
|
+
const limitList = useMemo(() => {
|
|
90
|
+
const list = [...DEFAULT_LIMIT_LIST];
|
|
91
|
+
if (data.pageSize > 0 && !list.includes(data.pageSize)) {
|
|
92
|
+
list.push(data.pageSize);
|
|
93
|
+
list.sort((a, b) => a - b);
|
|
94
|
+
}
|
|
95
|
+
return list;
|
|
96
|
+
}, [data.pageSize]);
|
|
97
|
+
|
|
98
|
+
if (data.total === 0) return null;
|
|
99
|
+
|
|
100
|
+
const isFirstPage = data.currentPage <= 1;
|
|
101
|
+
const isLastPage = data.currentPage >= data.totalPages;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<nav
|
|
105
|
+
aria-label="Pagination Navigation"
|
|
106
|
+
className={cn(
|
|
107
|
+
"flex w-full flex-col-reverse items-center justify-between gap-4 sm:flex-row",
|
|
108
|
+
variant === "default" && "rounded-xl border bg-background/50 p-2 shadow-sm backdrop-blur-sm",
|
|
109
|
+
variant === "compact" && "rounded-lg border border-transparent py-1",
|
|
110
|
+
variant === "ghost" && "py-2",
|
|
111
|
+
className
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<div className="flex w-full items-center justify-between sm:w-auto sm:justify-start gap-4">
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<label
|
|
117
|
+
htmlFor="rows-per-page-select"
|
|
118
|
+
className={cn(
|
|
119
|
+
"whitespace-nowrap font-medium text-muted-foreground",
|
|
120
|
+
variant === "compact" ? "text-xs" : "text-sm"
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
Rows per page
|
|
124
|
+
</label>
|
|
125
|
+
<Select value={`${data.pageSize}`} onValueChange={value => setLimit(parseInt(value, 10))}>
|
|
126
|
+
<SelectTrigger
|
|
127
|
+
id="rows-per-page-select"
|
|
128
|
+
className={cn(
|
|
129
|
+
"h-8 w-[72px] transition-colors hover:bg-secondary/50 focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
130
|
+
variant === "compact" && "h-7 text-xs"
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
<SelectValue placeholder={`${data.pageSize}`} />
|
|
134
|
+
</SelectTrigger>
|
|
135
|
+
<SelectContent side="top" className="min-w-[72px]">
|
|
136
|
+
{limitList.map(pageSize => (
|
|
137
|
+
<SelectItem key={pageSize} value={`${pageSize}`} className="cursor-pointer transition-colors">
|
|
138
|
+
{pageSize}
|
|
139
|
+
</SelectItem>
|
|
140
|
+
))}
|
|
141
|
+
</SelectContent>
|
|
142
|
+
</Select>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div className="flex w-full items-center justify-between sm:w-auto gap-4 sm:gap-6">
|
|
147
|
+
<div
|
|
148
|
+
className={cn(
|
|
149
|
+
"flex flex-1 items-center justify-center font-medium tabular-nums text-muted-foreground sm:justify-end",
|
|
150
|
+
variant === "compact" ? "text-xs" : "text-sm"
|
|
151
|
+
)}
|
|
152
|
+
aria-live="polite"
|
|
153
|
+
>
|
|
154
|
+
<span className="mr-1">Page</span>
|
|
155
|
+
<div className="relative flex min-w-[1ch] items-center justify-center overflow-hidden">
|
|
156
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
157
|
+
<motion.span
|
|
158
|
+
key={data.currentPage}
|
|
159
|
+
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 10, filter: "blur(4px)" }}
|
|
160
|
+
animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0, filter: "blur(0px)" }}
|
|
161
|
+
exit={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: -10, filter: "blur(4px)" }}
|
|
162
|
+
transition={SPRING_TRANSITION}
|
|
163
|
+
className="inline-block text-foreground"
|
|
164
|
+
>
|
|
165
|
+
{data.currentPage}
|
|
166
|
+
</motion.span>
|
|
167
|
+
</AnimatePresence>
|
|
168
|
+
</div>
|
|
169
|
+
<span className="ml-1">of {data.totalPages}</span>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="flex items-center gap-1 sm:gap-1.5">
|
|
173
|
+
<PaginationNavButton
|
|
174
|
+
icon={ChevronFirst}
|
|
175
|
+
label="Go to first page"
|
|
176
|
+
onClick={() => setPage(1)}
|
|
177
|
+
disabled={isFirstPage}
|
|
178
|
+
isHiddenOnMobile
|
|
179
|
+
/>
|
|
180
|
+
<PaginationNavButton
|
|
181
|
+
icon={ChevronLeft}
|
|
182
|
+
label="Go to previous page"
|
|
183
|
+
onClick={() => prev()}
|
|
184
|
+
disabled={isFirstPage}
|
|
185
|
+
/>
|
|
186
|
+
<PaginationNavButton
|
|
187
|
+
icon={ChevronRight}
|
|
188
|
+
label="Go to next page"
|
|
189
|
+
onClick={() => next()}
|
|
190
|
+
disabled={isLastPage}
|
|
191
|
+
/>
|
|
192
|
+
<PaginationNavButton
|
|
193
|
+
icon={ChevronLast}
|
|
194
|
+
label="Go to last page"
|
|
195
|
+
onClick={() => setPage(data.totalPages)}
|
|
196
|
+
disabled={isLastPage}
|
|
197
|
+
isHiddenOnMobile
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</nav>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default PaginationFooter;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useUrlParams } from "../utils/use-url-params";
|
|
2
|
+
|
|
3
|
+
const usePagination = (options?: { limit?: number; pageKey?: string; limitKey?: string }) => {
|
|
4
|
+
const pageKey = options?.pageKey ?? "page";
|
|
5
|
+
const limitKey = options?.limitKey ?? "limit";
|
|
6
|
+
|
|
7
|
+
const { params, setParams } = useUrlParams({
|
|
8
|
+
[pageKey]: 1,
|
|
9
|
+
[limitKey]: 10,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
limit: params[limitKey],
|
|
14
|
+
page: params[pageKey],
|
|
15
|
+
setPage: (page: number) => setParams({ [pageKey]: page }),
|
|
16
|
+
setLimit: (limit: number) => setParams({ [limitKey]: limit }),
|
|
17
|
+
next: () => setParams({ [pageKey]: Number(params[pageKey]) + 1 }),
|
|
18
|
+
prev: () => setParams({ [pageKey]: Number(params[pageKey]) - 1 }),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default usePagination;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
|
+
|
|
4
|
+
type StringRecord = Record<string, string | number>;
|
|
5
|
+
|
|
6
|
+
export type UseUrlParamsReturn<T extends StringRecord> = {
|
|
7
|
+
params: T;
|
|
8
|
+
setParam: <K extends keyof T>(key: K, value: T[K]) => void;
|
|
9
|
+
setParams: (values: Partial<T>) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getParams<T extends StringRecord>(searchParams: URLSearchParams, defaults: T): T {
|
|
13
|
+
const result = {} as T;
|
|
14
|
+
|
|
15
|
+
for (const key of Object.keys(defaults) as Array<keyof T>) {
|
|
16
|
+
result[key] = (searchParams.get(key as string) ?? defaults[key]) as T[keyof T];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useUrlParams<T extends StringRecord>(defaults: T): UseUrlParamsReturn<T> {
|
|
23
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
24
|
+
|
|
25
|
+
const params = getParams(searchParams, defaults);
|
|
26
|
+
|
|
27
|
+
// Ensure non-empty defaults exist in URL
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let changed = false;
|
|
30
|
+
const next = new URLSearchParams(searchParams);
|
|
31
|
+
|
|
32
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
33
|
+
if (value === "") continue;
|
|
34
|
+
|
|
35
|
+
if (!next.has(key)) {
|
|
36
|
+
if (typeof value === "string") next.set(key, value);
|
|
37
|
+
else next.set(key, String(value));
|
|
38
|
+
changed = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (changed) {
|
|
43
|
+
setSearchParams(next, { replace: true });
|
|
44
|
+
}
|
|
45
|
+
}, [searchParams, defaults, setSearchParams]);
|
|
46
|
+
|
|
47
|
+
const setParam = useCallback(
|
|
48
|
+
<K extends keyof T>(key: K, value: T[K]) => {
|
|
49
|
+
setSearchParams((current) => {
|
|
50
|
+
const next = new URLSearchParams(current);
|
|
51
|
+
|
|
52
|
+
if (value === "") {
|
|
53
|
+
next.delete(key as string);
|
|
54
|
+
} else {
|
|
55
|
+
if (typeof value === "string") next.set(key as string, value);
|
|
56
|
+
else next.set(key as string, String(value));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
[setSearchParams],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const setParams = useCallback(
|
|
66
|
+
(values: Partial<T>) => {
|
|
67
|
+
setSearchParams((current) => {
|
|
68
|
+
const next = new URLSearchParams(current);
|
|
69
|
+
|
|
70
|
+
for (const [key, value] of Object.entries(values)) {
|
|
71
|
+
if (value == null || value === "") {
|
|
72
|
+
next.delete(key);
|
|
73
|
+
} else {
|
|
74
|
+
next.set(key, value as string);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return next;
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
[setSearchParams],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
params,
|
|
86
|
+
setParam,
|
|
87
|
+
setParams,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function useUrlParamsValue<T extends StringRecord>(defaults: T): T {
|
|
92
|
+
const [searchParams] = useSearchParams();
|
|
93
|
+
|
|
94
|
+
return getParams(searchParams, defaults);
|
|
95
|
+
}
|
|
@@ -4,6 +4,7 @@ import { ThemeProvider } from "@repo/ui/components/custom/theme-provider";
|
|
|
4
4
|
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
|
5
5
|
import type { Route } from "./+types/root";
|
|
6
6
|
import "./app.css";
|
|
7
|
+
import { NavigationLoader } from "./components/utils/navigation-loader";
|
|
7
8
|
import { AuthStartup } from "./config/auth/auth-state";
|
|
8
9
|
import { ENV } from "./config/env";
|
|
9
10
|
|
|
@@ -31,6 +32,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|
|
31
32
|
<Links />
|
|
32
33
|
</head>
|
|
33
34
|
<body>
|
|
35
|
+
<NavigationLoader />
|
|
34
36
|
{children}
|
|
35
37
|
<ScrollRestoration />
|
|
36
38
|
<Scripts />
|
package/package.json
CHANGED
package/packages/ui/package.json
CHANGED
|
@@ -1,62 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useQuery,
|
|
3
|
-
useMutation,
|
|
4
|
-
useQueryClient,
|
|
5
|
-
type UseQueryOptions,
|
|
6
|
-
type UseQueryResult,
|
|
7
|
-
type UseMutationResult,
|
|
8
|
-
} from "@tanstack/react-query";
|
|
1
|
+
import { useQuery, useMutation, useQueryClient, type UseQueryOptions, type UseQueryResult, type UseMutationResult } from "@tanstack/react-query";
|
|
9
2
|
import { type AxiosError, type AxiosInstance, type AxiosRequestConfig } from "axios";
|
|
10
3
|
import { useMemo, useRef } from "react";
|
|
11
|
-
import { toast } from "sonner";
|
|
12
4
|
import { z } from "zod";
|
|
13
5
|
import { FormUtils } from "@repo/ui/lib/utils";
|
|
6
|
+
import { toast } from "@repo/ui/sonner";
|
|
14
7
|
import { ApiHelper } from "../api-helpers";
|
|
15
|
-
import type {
|
|
16
|
-
IFetchOptions,
|
|
17
|
-
IPaginatedFetchOptions,
|
|
18
|
-
IFetchMutationOptions,
|
|
19
|
-
IPaginatedData,
|
|
20
|
-
QueryEvent,
|
|
21
|
-
CbAction,
|
|
22
|
-
RawSchema,
|
|
23
|
-
QueryKey,
|
|
24
|
-
ZQuery,
|
|
25
|
-
ZParams,
|
|
26
|
-
} from "../api-types";
|
|
8
|
+
import type { IFetchOptions, IPaginatedFetchOptions, IFetchMutationOptions, IPaginatedData, QueryEvent, CbAction, RawSchema, QueryKey, ZQuery, ZParams } from "../api-types";
|
|
27
9
|
import { createQueryKeysProxy } from "../query-factory";
|
|
28
10
|
|
|
29
|
-
import type {
|
|
30
|
-
UseApiOptions,
|
|
31
|
-
UseApiReturn,
|
|
32
|
-
MutationVariables,
|
|
33
|
-
ExtractData,
|
|
34
|
-
FetchOptions,
|
|
35
|
-
PaginatedFetchOptions,
|
|
36
|
-
MutationOptions,
|
|
37
|
-
} from "./types";
|
|
11
|
+
import type { UseApiOptions, UseApiReturn, MutationVariables, ExtractData, FetchOptions, PaginatedFetchOptions, MutationOptions } from "./types";
|
|
38
12
|
|
|
39
|
-
async function processEvents<
|
|
40
|
-
Schema extends RawSchema,
|
|
41
|
-
IK extends string,
|
|
42
|
-
IQ extends z.output<ZQuery>,
|
|
43
|
-
IP extends z.output<ZParams>,
|
|
44
|
-
IB,
|
|
45
|
-
ID,
|
|
46
|
-
>(
|
|
13
|
+
async function processEvents<Schema extends RawSchema, IK extends string, IQ extends z.output<ZQuery>, IP extends z.output<ZParams>, IB, ID>(
|
|
47
14
|
events: QueryEvent<IK, IQ, IP, IB, ID, Schema> | undefined,
|
|
48
15
|
data: ID,
|
|
49
16
|
variables: { query?: IQ; params?: IP; body?: IB } | undefined,
|
|
50
17
|
schemaProxy: ReturnType<typeof createQueryKeysProxy<Schema>>,
|
|
51
18
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
52
|
-
override?: boolean
|
|
19
|
+
override?: boolean,
|
|
53
20
|
) {
|
|
54
21
|
if (!events) return;
|
|
55
22
|
const payload = { data, ...variables };
|
|
56
|
-
const runAction = async (
|
|
57
|
-
action: CbAction<Schema, IP, IQ, IB, ID, IK, Array<QueryKey<IK>>> | undefined,
|
|
58
|
-
exec: (key: QueryKey<IK>) => Promise<void>
|
|
59
|
-
) => {
|
|
23
|
+
const runAction = async (action: CbAction<Schema, IP, IQ, IB, ID, IK, Array<QueryKey<IK>>> | undefined, exec: (key: QueryKey<IK>) => Promise<void>) => {
|
|
60
24
|
if (!action) return;
|
|
61
25
|
const keys: Array<QueryKey<IK>> = typeof action === "function" ? action(payload, schemaProxy) : action;
|
|
62
26
|
for (const key of keys) {
|
|
@@ -64,9 +28,9 @@ async function processEvents<
|
|
|
64
28
|
}
|
|
65
29
|
};
|
|
66
30
|
|
|
67
|
-
await runAction(events.invalidateQuery, key => queryClient.invalidateQueries({ queryKey: key }));
|
|
68
|
-
await runAction(events.refetchQuery, key => queryClient.refetchQueries({ queryKey: key }));
|
|
69
|
-
await runAction(events.clearQuery, key => queryClient.resetQueries({ queryKey: key }));
|
|
31
|
+
await runAction(events.invalidateQuery, (key) => queryClient.invalidateQueries({ queryKey: key }));
|
|
32
|
+
await runAction(events.refetchQuery, (key) => queryClient.refetchQueries({ queryKey: key }));
|
|
33
|
+
await runAction(events.clearQuery, (key) => queryClient.resetQueries({ queryKey: key }));
|
|
70
34
|
|
|
71
35
|
if (!override) {
|
|
72
36
|
if (typeof events.fn === "function") await events.fn?.(payload, schemaProxy);
|
|
@@ -82,7 +46,7 @@ function useFetchApi<ID, ZQ extends ZQuery, ZP extends ZParams>(
|
|
|
82
46
|
params?: z.output<ZP>;
|
|
83
47
|
staleTime?: number;
|
|
84
48
|
enabled?: boolean;
|
|
85
|
-
}
|
|
49
|
+
},
|
|
86
50
|
): UseQueryResult<ID> & { abort: () => void } {
|
|
87
51
|
const queryClient = useQueryClient();
|
|
88
52
|
const queryKey = useMemo(
|
|
@@ -91,7 +55,7 @@ function useFetchApi<ID, ZQ extends ZQuery, ZP extends ZParams>(
|
|
|
91
55
|
params: options.params,
|
|
92
56
|
query: options.query,
|
|
93
57
|
}),
|
|
94
|
-
[options.query, options.params]
|
|
58
|
+
[options.query, options.params],
|
|
95
59
|
);
|
|
96
60
|
const staleTime = ApiHelper.parseTime(ApiHelper.merge(options.staleTime, endpoint.staleTime));
|
|
97
61
|
|
|
@@ -105,7 +69,6 @@ function useFetchApi<ID, ZQ extends ZQuery, ZP extends ZParams>(
|
|
|
105
69
|
params: options.params,
|
|
106
70
|
searchParams: options.query,
|
|
107
71
|
}),
|
|
108
|
-
params: options.query,
|
|
109
72
|
signal,
|
|
110
73
|
};
|
|
111
74
|
const resp = await axiosClient.request<ID>(config);
|
|
@@ -127,6 +90,7 @@ function useFetchApi<ID, ZQ extends ZQuery, ZP extends ZParams>(
|
|
|
127
90
|
abort: () => queryClient.cancelQueries({ queryKey }),
|
|
128
91
|
};
|
|
129
92
|
}
|
|
93
|
+
|
|
130
94
|
function usePaginatedFetchApi<ID, IO, ZQ extends ZQuery, ZP extends ZParams>(
|
|
131
95
|
key: keyof RawSchema,
|
|
132
96
|
endpoint: IPaginatedFetchOptions<ID, IO, ZQ, ZP>,
|
|
@@ -138,7 +102,7 @@ function usePaginatedFetchApi<ID, IO, ZQ extends ZQuery, ZP extends ZParams>(
|
|
|
138
102
|
limit?: number;
|
|
139
103
|
staleTime?: number;
|
|
140
104
|
enabled?: boolean;
|
|
141
|
-
}
|
|
105
|
+
},
|
|
142
106
|
): UseQueryResult<IPaginatedData<ID, IO>> & { abort: () => void } {
|
|
143
107
|
const queryClient = useQueryClient();
|
|
144
108
|
const mergedQuery = useMemo(
|
|
@@ -147,7 +111,7 @@ function usePaginatedFetchApi<ID, IO, ZQ extends ZQuery, ZP extends ZParams>(
|
|
|
147
111
|
page: options.page,
|
|
148
112
|
limit: options.limit,
|
|
149
113
|
}),
|
|
150
|
-
[options.query, options.page, options.limit]
|
|
114
|
+
[options.query, options.page, options.limit],
|
|
151
115
|
);
|
|
152
116
|
|
|
153
117
|
const queryKey = useMemo(
|
|
@@ -156,7 +120,7 @@ function usePaginatedFetchApi<ID, IO, ZQ extends ZQuery, ZP extends ZParams>(
|
|
|
156
120
|
params: options.params,
|
|
157
121
|
query: mergedQuery,
|
|
158
122
|
}),
|
|
159
|
-
[key, endpoint.key, options.params, mergedQuery]
|
|
123
|
+
[key, endpoint.key, options.params, mergedQuery],
|
|
160
124
|
);
|
|
161
125
|
|
|
162
126
|
const staleTime = ApiHelper.parseTime(ApiHelper.merge(options.staleTime, endpoint.staleTime));
|
|
@@ -199,7 +163,7 @@ function useMutationApi<Schema extends RawSchema, K extends keyof Schema & strin
|
|
|
199
163
|
endpoint: IFetchMutationOptions<K>,
|
|
200
164
|
axiosClient: AxiosInstance,
|
|
201
165
|
proxy: ReturnType<typeof createQueryKeysProxy<Schema>>,
|
|
202
|
-
options?: MutationOptions<Schema[K]
|
|
166
|
+
options?: MutationOptions<Schema[K]>,
|
|
203
167
|
): UseMutationResult<ExtractData<Schema[K]>, Error, MutationVariables<Schema[K]>> & { abort: () => void } {
|
|
204
168
|
const queryClient = useQueryClient();
|
|
205
169
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
@@ -222,10 +186,9 @@ function useMutationApi<Schema extends RawSchema, K extends keyof Schema & strin
|
|
|
222
186
|
url: ApiHelper.buildUrl(
|
|
223
187
|
ApiHelper.merge(
|
|
224
188
|
{ route: endpoint.route, params: variables.params, searchParams: variables.query },
|
|
225
|
-
{ route: endpoint.route, params: options?.params, searchParams: options?.query }
|
|
226
|
-
)
|
|
189
|
+
{ route: endpoint.route, params: options?.params, searchParams: options?.query },
|
|
190
|
+
),
|
|
227
191
|
),
|
|
228
|
-
params: ApiHelper.merge(variables.query, options?.query),
|
|
229
192
|
data: isFormData ? FormUtils.getFormData(body!) : body,
|
|
230
193
|
...(isFormData && { headers: { "Content-Type": "multipart/form-data" } }),
|
|
231
194
|
signal: abortControllerRef.current.signal,
|
|
@@ -243,20 +206,9 @@ function useMutationApi<Schema extends RawSchema, K extends keyof Schema & strin
|
|
|
243
206
|
},
|
|
244
207
|
onError: async (error: Error, variables) => {
|
|
245
208
|
options?.onError?.(error, variables);
|
|
246
|
-
await processEvents(
|
|
247
|
-
endpoint.onError,
|
|
248
|
-
error as AxiosError<any>,
|
|
249
|
-
variables,
|
|
250
|
-
proxy,
|
|
251
|
-
queryClient,
|
|
252
|
-
options?.overwriteEvents
|
|
253
|
-
);
|
|
209
|
+
await processEvents(endpoint.onError, error as AxiosError<any>, variables, proxy, queryClient, options?.overwriteEvents);
|
|
254
210
|
if (!options?.onError && !endpoint.onError) {
|
|
255
|
-
|
|
256
|
-
toast.error((error as any)?.response?.data?.message ?? error.message);
|
|
257
|
-
} catch (error) {
|
|
258
|
-
console.error(error);
|
|
259
|
-
}
|
|
211
|
+
toast.error((error as any)?.response?.data?.message ?? error.message);
|
|
260
212
|
}
|
|
261
213
|
},
|
|
262
214
|
onMutate: options?.onMutate,
|
|
@@ -267,6 +219,7 @@ function useMutationApi<Schema extends RawSchema, K extends keyof Schema & strin
|
|
|
267
219
|
abort: () => abortControllerRef.current?.abort(),
|
|
268
220
|
};
|
|
269
221
|
}
|
|
222
|
+
|
|
270
223
|
export function useApi<Schema extends RawSchema>(schema: Schema, axiosClient: AxiosInstance) {
|
|
271
224
|
return <K extends keyof Schema & string>(key: K, options?: UseApiOptions<Schema, K>): UseApiReturn<Schema, K> => {
|
|
272
225
|
const endpoint = schema[key];
|
|
@@ -103,7 +103,7 @@ export const FormSelectField = <
|
|
|
103
103
|
}}
|
|
104
104
|
value={field.value ?? defaultValueProp ?? ""}
|
|
105
105
|
>
|
|
106
|
-
<SelectTrigger className={cn("bg-background w-full", inputClassName)}>
|
|
106
|
+
<SelectTrigger aria-invalid={!!fieldState.error} className={cn("bg-background w-full", inputClassName)}>
|
|
107
107
|
<SelectValue placeholder={placeholder ?? "Select"} />
|
|
108
108
|
</SelectTrigger>
|
|
109
109
|
<SelectContent>
|
|
@@ -155,7 +155,7 @@ export const FormTextField = <
|
|
|
155
155
|
<Field data-invalid={!!fieldState.error} className="flex flex-col">
|
|
156
156
|
{label ? <FieldLabel className="self-start">{label}</FieldLabel> : null}
|
|
157
157
|
<FieldContent>
|
|
158
|
-
<Input {...field} {...rest} />
|
|
158
|
+
<Input aria-invalid={!!fieldState.error} {...field} {...rest} />
|
|
159
159
|
</FieldContent>
|
|
160
160
|
{description ? <FieldDescription>{description}</FieldDescription> : null}
|
|
161
161
|
<FieldError errors={[fieldState.error]} />
|
|
@@ -187,7 +187,7 @@ export const FormTextAreaField = <
|
|
|
187
187
|
<Field data-invalid={!!fieldState.error} className="flex flex-col">
|
|
188
188
|
{label ? <FieldLabel className="self-start">{label}</FieldLabel> : null}
|
|
189
189
|
<FieldContent>
|
|
190
|
-
<Textarea {...field} {...rest} ref={field.ref} />
|
|
190
|
+
<Textarea aria-invalid={!!fieldState.error} {...field} {...rest} ref={field.ref} />
|
|
191
191
|
</FieldContent>
|
|
192
192
|
{description ? <FieldDescription>{description}</FieldDescription> : null}
|
|
193
193
|
<FieldError errors={[fieldState.error]} />
|
|
@@ -218,6 +218,7 @@ export const FormNumberField = <
|
|
|
218
218
|
<FieldContent>
|
|
219
219
|
<Input
|
|
220
220
|
inputMode="numeric"
|
|
221
|
+
aria-invalid={!!fieldState.error}
|
|
221
222
|
{...field}
|
|
222
223
|
onChange={e => {
|
|
223
224
|
if (!isNaN(Number(e.currentTarget.value))) field.onChange(Number(e.currentTarget.value));
|
|
@@ -363,6 +364,7 @@ export const FormDefaultDateField = <
|
|
|
363
364
|
{label ? <FieldLabel className="self-start">{label}</FieldLabel> : null}
|
|
364
365
|
<FieldContent>
|
|
365
366
|
<Input
|
|
367
|
+
aria-invalid={!!fieldState.error}
|
|
366
368
|
{...field}
|
|
367
369
|
{...rest}
|
|
368
370
|
value={field.value || ""}
|
|
@@ -430,7 +432,12 @@ export function FormMultiSelectField<
|
|
|
430
432
|
<FieldContent className="w-full">
|
|
431
433
|
<Popover open={open} onOpenChange={setOpen}>
|
|
432
434
|
<PopoverTrigger asChild disabled={disabled}>
|
|
433
|
-
<Button
|
|
435
|
+
<Button
|
|
436
|
+
aria-invalid={!!fieldState.error}
|
|
437
|
+
variant="outline"
|
|
438
|
+
size={"sm"}
|
|
439
|
+
className={cn("w-full justify-between", inputClassName)}
|
|
440
|
+
>
|
|
434
441
|
{selected.length > 0 ? `${selected.length} selected` : (placeholder ?? "Select")}
|
|
435
442
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
436
443
|
</Button>
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -4,6 +4,15 @@ packages:
|
|
|
4
4
|
- "packages/*/*"
|
|
5
5
|
- "configs/*"
|
|
6
6
|
|
|
7
|
+
allowBuilds:
|
|
8
|
+
"@firebase/util": false
|
|
9
|
+
better-sqlite3: false
|
|
10
|
+
esbuild: false
|
|
11
|
+
msgpackr-extract: false
|
|
12
|
+
protobufjs: false
|
|
13
|
+
sharp: false
|
|
14
|
+
unrs-resolver: false
|
|
15
|
+
|
|
7
16
|
catalog:
|
|
8
17
|
# configs
|
|
9
18
|
typescript: "^5.9.2"
|
|
@@ -75,6 +84,7 @@ catalog:
|
|
|
75
84
|
tailwindcss: "^4.2.2"
|
|
76
85
|
"@tailwindcss/vite": "^4.2.2"
|
|
77
86
|
zustand: "^5.0.13"
|
|
87
|
+
motion: "^12.40.0"
|
|
78
88
|
|
|
79
89
|
# email
|
|
80
90
|
react-email: "^5.2.11"
|
|
@@ -88,7 +98,7 @@ catalog:
|
|
|
88
98
|
"@tanstack/react-query-devtools": "^5.100.9"
|
|
89
99
|
"@fontsource-variable/eb-garamond": "^5.2.7"
|
|
90
100
|
"@fontsource-variable/inter": "^5.2.8"
|
|
91
|
-
"@hookform/resolvers": "^
|
|
101
|
+
"@hookform/resolvers": "^5.4.0"
|
|
92
102
|
"@phosphor-icons/react": "^2.1.10"
|
|
93
103
|
"@tailwindcss/postcss": "^4"
|
|
94
104
|
"@tailwindcss/typography": "^0.5.19"
|