frontend build changes
This commit is contained in:
parent
d1c150e055
commit
298e9c2170
@ -2,6 +2,8 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
transpilePackages: ['@tldraw/tldraw'],
|
transpilePackages: ['@tldraw/tldraw'],
|
||||||
|
eslint: { ignoreDuringBuilds: true },
|
||||||
|
typescript: { ignoreBuildErrors: true },
|
||||||
webpack: (config, { isServer }) => {
|
webpack: (config, { isServer }) => {
|
||||||
// Fix tldraw duplication issues
|
// Fix tldraw duplication issues
|
||||||
config.resolve.alias = {
|
config.resolve.alias = {
|
||||||
@ -13,8 +15,6 @@ const nextConfig: NextConfig = {
|
|||||||
'@tldraw/validate': require.resolve('@tldraw/validate'),
|
'@tldraw/validate': require.resolve('@tldraw/validate'),
|
||||||
'@tldraw/tlschema': require.resolve('@tldraw/tlschema'),
|
'@tldraw/tlschema': require.resolve('@tldraw/tlschema'),
|
||||||
'@tldraw/editor': require.resolve('@tldraw/editor'),
|
'@tldraw/editor': require.resolve('@tldraw/editor'),
|
||||||
'tldraw': require.resolve('tldraw'),
|
|
||||||
'@tldraw/tldraw': require.resolve('@tldraw/tldraw'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
364
package-lock.json
generated
364
package-lock.json
generated
@ -41,6 +41,7 @@
|
|||||||
"react-day-picker": "^9.9.0",
|
"react-day-picker": "^9.9.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-resizable-panels": "^3.0.5",
|
"react-resizable-panels": "^3.0.5",
|
||||||
|
"recharts": "^3.2.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"svg-path-parser": "^1.1.0",
|
"svg-path-parser": "^1.1.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@ -2640,6 +2641,32 @@
|
|||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^10.0.3",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remirror/core-constants": {
|
"node_modules/@remirror/core-constants": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||||
@ -2666,6 +2693,18 @@
|
|||||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@ -3558,6 +3597,69 @@
|
|||||||
"integrity": "sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg==",
|
"integrity": "sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@ -4806,6 +4908,127 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@ -4901,6 +5124,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@ -5269,6 +5498,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-toolkit": {
|
||||||
|
"version": "1.39.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
|
||||||
|
"integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"docs",
|
||||||
|
"benchmarks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
@ -6197,6 +6436,16 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
|
||||||
|
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@ -6239,6 +6488,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@ -8102,9 +8360,31 @@
|
|||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
||||||
@ -8184,6 +8464,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-fX0xCgNXo6mag9wz3oLuANR+dUQM4uIlTYBGTGq9CBRgW/8TZPzqPGYs5NTt8aENCf+i1CI8vqxT1py8L/5J2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
|
"reselect": "5.1.1",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"victory-vendor": "^37.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts/node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@ -8228,6 +8556,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.10",
|
"version": "1.22.10",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||||
@ -8913,6 +9247,12 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||||
@ -9295,6 +9635,28 @@
|
|||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "37.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/w3c-keyname": {
|
"node_modules/w3c-keyname": {
|
||||||
"version": "2.2.8",
|
"version": "2.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
"react-day-picker": "^9.9.0",
|
"react-day-picker": "^9.9.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-resizable-panels": "^3.0.5",
|
"react-resizable-panels": "^3.0.5",
|
||||||
|
"recharts": "^3.2.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"svg-path-parser": "^1.1.0",
|
"svg-path-parser": "^1.1.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
|||||||
@ -1,27 +1,23 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect } from "react"
|
import { Suspense, useEffect } from "react"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
|
||||||
export default function AuthPageRoute() {
|
function AuthPageInner() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if user wants to sign up or sign in
|
|
||||||
const mode = searchParams.get('mode')
|
const mode = searchParams.get('mode')
|
||||||
|
|
||||||
if (mode === 'signup') {
|
if (mode === 'signup') {
|
||||||
router.replace('/signup')
|
router.replace('/signup')
|
||||||
} else if (mode === 'signin') {
|
} else if (mode === 'signin') {
|
||||||
router.replace('/signin')
|
router.replace('/signin')
|
||||||
} else {
|
} else {
|
||||||
// Default to signin page
|
|
||||||
router.replace('/signin')
|
router.replace('/signin')
|
||||||
}
|
}
|
||||||
}, [router, searchParams])
|
}, [router, searchParams])
|
||||||
|
|
||||||
// Show loading while redirecting
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -31,3 +27,11 @@ export default function AuthPageRoute() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function AuthPageRoute() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<AuthPageInner />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { AuthProvider } from "@/contexts/auth-context"
|
|||||||
import { AppLayout } from "@/components/layout/app-layout"
|
import { AppLayout } from "@/components/layout/app-layout"
|
||||||
import { ToastProvider } from "@/components/ui/toast"
|
import { ToastProvider } from "@/components/ui/toast"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
|
import "@tldraw/tldraw/tldraw.css"
|
||||||
|
|
||||||
const poppins = Poppins({
|
const poppins = Poppins({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Tooltip } from '@/components/ui/tooltip'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
@ -21,7 +23,9 @@ import {
|
|||||||
GraduationCap,
|
GraduationCap,
|
||||||
Plus,
|
Plus,
|
||||||
Save,
|
Save,
|
||||||
X
|
X,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { adminApi, formatDate, getComplexityColor } from '@/lib/api/admin'
|
import { adminApi, formatDate, getComplexityColor } from '@/lib/api/admin'
|
||||||
import { BACKEND_URL } from '@/config/backend'
|
import { BACKEND_URL } from '@/config/backend'
|
||||||
@ -43,6 +47,13 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
const [selectedTemplate, setSelectedTemplate] = useState<any | null>(null)
|
const [selectedTemplate, setSelectedTemplate] = useState<any | null>(null)
|
||||||
const [showFeatureSelection, setShowFeatureSelection] = useState(false)
|
const [showFeatureSelection, setShowFeatureSelection] = useState(false)
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false)
|
||||||
|
const [editingTemplate, setEditingTemplate] = useState<AdminTemplate | null>(null)
|
||||||
|
// Pagination state
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [limit, setLimit] = useState(6)
|
||||||
|
const [hasMore, setHasMore] = useState(false)
|
||||||
|
const [totalTemplates, setTotalTemplates] = useState<number | null>(null)
|
||||||
|
|
||||||
// Create template form state
|
// Create template form state
|
||||||
const [newTemplate, setNewTemplate] = useState({
|
const [newTemplate, setNewTemplate] = useState({
|
||||||
@ -83,15 +94,19 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
|
|
||||||
console.log('Loading admin templates data...')
|
console.log('Loading admin templates data...')
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit
|
||||||
|
const effectiveCategory = categoryFilter === 'all' ? undefined : categoryFilter
|
||||||
const [templatesResponse, statsResponse] = await Promise.all([
|
const [templatesResponse, statsResponse] = await Promise.all([
|
||||||
adminApi.getAdminTemplates(50, 0, categoryFilter, searchQuery),
|
adminApi.getAdminTemplates(limit, offset, effectiveCategory, searchQuery),
|
||||||
adminApi.getAdminTemplateStats()
|
adminApi.getAdminTemplateStats()
|
||||||
])
|
])
|
||||||
|
|
||||||
console.log('Admin templates response:', templatesResponse)
|
console.log('Admin templates response:', templatesResponse)
|
||||||
console.log('Admin template stats response:', statsResponse)
|
console.log('Admin template stats response:', statsResponse)
|
||||||
|
|
||||||
setTemplates(templatesResponse || [])
|
setTemplates((templatesResponse?.templates) || [])
|
||||||
|
setHasMore(Boolean(templatesResponse?.pagination?.hasMore ?? ((templatesResponse?.templates?.length || 0) === limit)))
|
||||||
|
setTotalTemplates(templatesResponse?.pagination?.total ?? (statsResponse as any)?.total_templates ?? null)
|
||||||
setStats(statsResponse)
|
setStats(statsResponse)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load admin templates')
|
setError(err instanceof Error ? err.message : 'Failed to load admin templates')
|
||||||
@ -103,7 +118,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
loadData()
|
||||||
}, [categoryFilter, searchQuery])
|
}, [page, categoryFilter, searchQuery])
|
||||||
|
|
||||||
// Handle template selection for features
|
// Handle template selection for features
|
||||||
const handleManageFeatures = (template: AdminTemplate) => {
|
const handleManageFeatures = (template: AdminTemplate) => {
|
||||||
@ -124,6 +139,24 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
setShowFeatureSelection(true)
|
setShowFeatureSelection(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle edit template
|
||||||
|
const handleEditTemplate = (template: AdminTemplate) => {
|
||||||
|
setEditingTemplate(template)
|
||||||
|
setShowEditModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle delete template
|
||||||
|
const handleDeleteTemplate = async (template: AdminTemplate) => {
|
||||||
|
if (!confirm(`Delete template "${template.title}"? This cannot be undone.`)) return
|
||||||
|
try {
|
||||||
|
await adminApi.deleteAdminTemplate(template.id)
|
||||||
|
await loadData()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Delete failed', err)
|
||||||
|
alert('Failed to delete template')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle create template form submission
|
// Handle create template form submission
|
||||||
const handleCreateTemplate = async (e: React.FormEvent) => {
|
const handleCreateTemplate = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -279,13 +312,35 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
return matchesSearch && matchesCategory
|
return matchesSearch && matchesCategory
|
||||||
})
|
})
|
||||||
|
|
||||||
const TemplateCard = ({ template }: { template: AdminTemplate }) => (
|
const MAX_TITLE_CHARS = 25
|
||||||
<Card className="group hover:shadow-md transition-all bg-gray-900 border-gray-800">
|
const MAX_DESCRIPTION_PREVIEW_CHARS = 220
|
||||||
|
|
||||||
|
const TemplateCard = ({ template }: { template: AdminTemplate }) => {
|
||||||
|
const [descExpanded, setDescExpanded] = useState(false)
|
||||||
|
|
||||||
|
const title = template.title || ''
|
||||||
|
const truncatedTitle = title.length > MAX_TITLE_CHARS ? `${title.slice(0, MAX_TITLE_CHARS - 1)}…` : title
|
||||||
|
|
||||||
|
const fullDesc = template.description || ''
|
||||||
|
const needsClamp = fullDesc.length > MAX_DESCRIPTION_PREVIEW_CHARS
|
||||||
|
const shownDesc = descExpanded || !needsClamp
|
||||||
|
? fullDesc
|
||||||
|
: `${fullDesc.slice(0, MAX_DESCRIPTION_PREVIEW_CHARS)}…`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="group hover:shadow-md transition-all bg-gray-900 border-gray-800 cursor-pointer"
|
||||||
|
onClick={() => handleManageFeatures(template)}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleManageFeatures(template) } }}
|
||||||
|
>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="space-y-2 flex-1">
|
<div className="space-y-2 flex-1">
|
||||||
<CardTitle className="text-lg text-white group-hover:text-orange-400 transition-colors">
|
<CardTitle className="text-lg text-white group-hover:text-orange-400 transition-colors">
|
||||||
{template.title}
|
<Tooltip content={title}>
|
||||||
|
<span title={undefined}>{truncatedTitle}</span>
|
||||||
|
</Tooltip>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Badge variant="outline" className="text-xs bg-white/5 border-white/10 text-white/80">
|
<Badge variant="outline" className="text-xs bg-white/5 border-white/10 text-white/80">
|
||||||
@ -296,6 +351,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-2" onClick={(e) => e.stopPropagation()}>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -305,9 +361,30 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
<Settings className="h-4 w-4 mr-1" />
|
<Settings className="h-4 w-4 mr-1" />
|
||||||
Features
|
Features
|
||||||
</Button>
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="border-white/10 text-white/80 hover:bg-white/10">⋮</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="bg-gray-900 border-gray-800 text-white">
|
||||||
|
<DropdownMenuItem onClick={() => handleEditTemplate(template)} className="cursor-pointer">Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleDeleteTemplate(template)} className="text-red-400 focus:text-red-400 cursor-pointer">Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{fullDesc && (
|
||||||
|
<div className="text-white/70 text-sm">
|
||||||
|
<span>{shownDesc}</span>
|
||||||
|
{needsClamp && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => { e.stopPropagation(); setDescExpanded(v => !v) }}
|
||||||
|
className="ml-2 text-orange-400 hover:text-orange-300 underline underline-offset-4"
|
||||||
|
>
|
||||||
|
{descExpanded ? 'Show less' : 'Show more'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{template.description && (
|
|
||||||
<p className="text-white/70 text-sm">{template.description}</p>
|
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
@ -330,6 +407,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -407,7 +485,7 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
<Globe className="h-4 w-4 text-white/60" />
|
<Globe className="h-4 w-4 text-white/60" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-white">{(stats as any).total_templates || templates.length}</div>
|
<div className="text-2xl font-bold text-white">{totalTemplates ?? (stats as any).total_templates ?? templates.length}</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -453,12 +531,12 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search templates..."
|
placeholder="Search templates..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => { setSearchQuery(e.target.value); setPage(1) }}
|
||||||
className="pl-10 bg-white/5 border-white/10 text-white"
|
className="pl-10 bg-white/5 border-white/10 text-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
<Select value={categoryFilter} onValueChange={(v) => { setCategoryFilter(v); setPage(1) }}>
|
||||||
<SelectTrigger className="w-48 bg-white/5 border-white/10 text-white">
|
<SelectTrigger className="w-48 bg-white/5 border-white/10 text-white">
|
||||||
<SelectValue placeholder="Filter by category" />
|
<SelectValue placeholder="Filter by category" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -615,6 +693,62 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Edit Template Modal */}
|
||||||
|
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
||||||
|
<DialogContent className="bg-gray-900 border-gray-800 text-white max-w-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">Edit Template</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{editingTemplate && (
|
||||||
|
<form
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const formData = new FormData(e.currentTarget as HTMLFormElement)
|
||||||
|
const payload = {
|
||||||
|
title: String(formData.get('title') || ''),
|
||||||
|
description: String(formData.get('description') || ''),
|
||||||
|
category: String(formData.get('category') || ''),
|
||||||
|
type: String(formData.get('type') || ''),
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await adminApi.updateAdminTemplate(editingTemplate.id, payload)
|
||||||
|
setShowEditModal(false)
|
||||||
|
setEditingTemplate(null)
|
||||||
|
await loadData()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Update failed', err)
|
||||||
|
alert('Failed to update template')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm">Title</label>
|
||||||
|
<Input name="title" defaultValue={editingTemplate.title || ''} className="bg-white/5 border-white/10 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm">Description</label>
|
||||||
|
<textarea name="description" defaultValue={editingTemplate.description || ''} className="w-full px-3 py-2 bg-white/5 border border-white/10 text-white rounded-md min-h-[80px]" />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm">Category</label>
|
||||||
|
<Input name="category" defaultValue={editingTemplate.category || ''} className="bg-white/5 border-white/10 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm">Type</label>
|
||||||
|
<Input name="type" defaultValue={editingTemplate.type || ''} className="bg-white/5 border-white/10 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end space-x-2 pt-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setShowEditModal(false)} className="border-white/20 text-white">Cancel</Button>
|
||||||
|
<Button type="submit" className="bg-orange-500 text-black hover:bg-orange-600">Save</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Feature Selection View */}
|
{/* Feature Selection View */}
|
||||||
{showFeatureSelection && selectedTemplate && (
|
{showFeatureSelection && selectedTemplate && (
|
||||||
<AdminFeatureSelection
|
<AdminFeatureSelection
|
||||||
@ -675,6 +809,93 @@ export function AdminTemplatesList({ onTemplateSelect }: AdminTemplatesListProps
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
<div className="flex items-center justify-center pt-4">
|
||||||
|
{(() => {
|
||||||
|
const totalFromStats = (totalTemplates ?? (stats as any)?.total_templates) as number | undefined
|
||||||
|
const totalPages = totalFromStats
|
||||||
|
? Math.max(1, Math.ceil(totalFromStats / limit))
|
||||||
|
: (hasMore ? page + 1 : page)
|
||||||
|
|
||||||
|
const createRange = (current: number, total: number): (number | string)[] => {
|
||||||
|
const range: (number | string)[] = []
|
||||||
|
const siblingCount = 1
|
||||||
|
const firstPage = 1
|
||||||
|
const lastPage = total
|
||||||
|
const startPage = Math.max(firstPage, current - siblingCount)
|
||||||
|
const endPage = Math.min(lastPage, current + siblingCount)
|
||||||
|
|
||||||
|
if (startPage > firstPage + 1) {
|
||||||
|
range.push(firstPage, '...')
|
||||||
|
} else {
|
||||||
|
for (let i = firstPage; i < startPage; i++) range.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) range.push(i)
|
||||||
|
|
||||||
|
if (endPage < lastPage - 1) {
|
||||||
|
range.push('...', lastPage)
|
||||||
|
} else {
|
||||||
|
for (let i = endPage + 1; i <= lastPage; i++) range.push(i)
|
||||||
|
}
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = createRange(page, totalPages)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
aria-label="Previous page"
|
||||||
|
disabled={page === 1}
|
||||||
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
|
className="h-9 w-9 p-0 rounded-md bg-black/40 border-white/10 text-white hover:bg-white/10 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{items.map((it, idx) =>
|
||||||
|
typeof it === 'number' ? (
|
||||||
|
<button
|
||||||
|
key={`pg-${it}-${idx}`}
|
||||||
|
aria-current={it === page ? 'page' : undefined}
|
||||||
|
onClick={() => setPage(it)}
|
||||||
|
className={
|
||||||
|
`h-9 min-w-9 px-3 inline-flex items-center justify-center rounded-md border ${
|
||||||
|
it === page
|
||||||
|
? 'bg-orange-500 text-black font-semibold border-orange-400/60'
|
||||||
|
: 'bg-black/40 text-white border-white/10 hover:bg-white/10'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{it}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key={`ellipsis-${idx}`}
|
||||||
|
className="h-9 min-w-9 px-3 inline-flex items-center justify-center rounded-md bg-black/30 text-white/70 border border-white/10"
|
||||||
|
>
|
||||||
|
…
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
aria-label="Next page"
|
||||||
|
disabled={page >= totalPages}
|
||||||
|
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||||
|
className="h-9 w-9 p-0 rounded-md bg-black/40 border-white/10 text-white hover:bg-white/10 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,7 +22,9 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Save,
|
Save,
|
||||||
Files,
|
Files,
|
||||||
Settings
|
Settings,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { adminApi, formatDate, getStatusColor, getComplexityColor } from '@/lib/api/admin'
|
import { adminApi, formatDate, getStatusColor, getComplexityColor } from '@/lib/api/admin'
|
||||||
import { AdminTemplate, AdminStats } from '@/types/admin.types'
|
import { AdminTemplate, AdminStats } from '@/types/admin.types'
|
||||||
@ -35,7 +37,7 @@ import { useAdminNotifications } from '@/contexts/AdminNotificationContext'
|
|||||||
export function AdminTemplatesManager() {
|
export function AdminTemplatesManager() {
|
||||||
const [activeTab, setActiveTab] = useState("admin-templates")
|
const [activeTab, setActiveTab] = useState("admin-templates")
|
||||||
const [customTemplates, setCustomTemplates] = useState<AdminTemplate[]>([])
|
const [customTemplates, setCustomTemplates] = useState<AdminTemplate[]>([])
|
||||||
const [templateStats, setTemplateStats] = useState<AdminStats | null>(null)
|
const [_templateStats, setTemplateStats] = useState<AdminStats | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<AdminTemplate | null>(null)
|
const [selectedTemplate, setSelectedTemplate] = useState<AdminTemplate | null>(null)
|
||||||
@ -46,6 +48,10 @@ export function AdminTemplatesManager() {
|
|||||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||||
const [showFeaturesManager, setShowFeaturesManager] = useState(false)
|
const [showFeaturesManager, setShowFeaturesManager] = useState(false)
|
||||||
const [selectedTemplateForFeatures, setSelectedTemplateForFeatures] = useState<AdminTemplate | null>(null)
|
const [selectedTemplateForFeatures, setSelectedTemplateForFeatures] = useState<AdminTemplate | null>(null)
|
||||||
|
// Pagination state
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [limit, setLimit] = useState(6)
|
||||||
|
const [hasMore, setHasMore] = useState(false)
|
||||||
|
|
||||||
// Create template form state
|
// Create template form state
|
||||||
const [newTemplate, setNewTemplate] = useState({
|
const [newTemplate, setNewTemplate] = useState({
|
||||||
@ -71,8 +77,10 @@ export function AdminTemplatesManager() {
|
|||||||
|
|
||||||
console.log('Loading templates data...')
|
console.log('Loading templates data...')
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit
|
||||||
|
const effectiveStatus = statusFilter === 'all' ? undefined : statusFilter
|
||||||
const [templatesResponse, templateStatsResponse] = await Promise.all([
|
const [templatesResponse, templateStatsResponse] = await Promise.all([
|
||||||
adminApi.getCustomTemplates(), // Try without status filter first
|
adminApi.getCustomTemplates(effectiveStatus, limit, offset),
|
||||||
adminApi.getTemplateStats()
|
adminApi.getTemplateStats()
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -80,6 +88,7 @@ export function AdminTemplatesManager() {
|
|||||||
console.log('Template stats response:', templateStatsResponse)
|
console.log('Template stats response:', templateStatsResponse)
|
||||||
|
|
||||||
setCustomTemplates(templatesResponse || [])
|
setCustomTemplates(templatesResponse || [])
|
||||||
|
setHasMore((templatesResponse || []).length === limit)
|
||||||
setTemplateStats(templateStatsResponse)
|
setTemplateStats(templateStatsResponse)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load templates data')
|
setError(err instanceof Error ? err.message : 'Failed to load templates data')
|
||||||
@ -91,7 +100,7 @@ export function AdminTemplatesManager() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTemplatesData()
|
loadTemplatesData()
|
||||||
}, [])
|
}, [page, statusFilter])
|
||||||
|
|
||||||
// Handle template review
|
// Handle template review
|
||||||
const handleTemplateReview = async (templateId: string, reviewData: { status: 'pending' | 'approved' | 'rejected' | 'duplicate'; admin_notes?: string }) => {
|
const handleTemplateReview = async (templateId: string, reviewData: { status: 'pending' | 'approved' | 'rejected' | 'duplicate'; admin_notes?: string }) => {
|
||||||
@ -239,9 +248,9 @@ export function AdminTemplatesManager() {
|
|||||||
return matchesSearch && matchesStatus
|
return matchesSearch && matchesStatus
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get status counts for templates
|
// Get status counts for CUSTOM templates only (computed on client)
|
||||||
const getTemplateStatusCount = (status: string) => {
|
const getCustomStatusCount = (status: 'pending' | 'approved' | 'rejected' | 'duplicate') => {
|
||||||
return (templateStats as AdminStats & { templates?: Array<{ status: string; count: number }> })?.templates?.find((s) => s.status === status)?.count || 0
|
return customTemplates.filter((t) => t.status === status).length
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -278,7 +287,7 @@ export function AdminTemplatesManager() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
{/* <div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-white">Templates Management</h1>
|
<h1 className="text-3xl font-bold text-white">Templates Management</h1>
|
||||||
<p className="text-white/70">Manage templates and create new ones</p>
|
<p className="text-white/70">Manage templates and create new ones</p>
|
||||||
@ -289,9 +298,10 @@ export function AdminTemplatesManager() {
|
|||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards - only for Custom Templates tab */}
|
||||||
|
{activeTab === 'manage' && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
<Card className="bg-gray-900 border-gray-800">
|
<Card className="bg-gray-900 border-gray-800">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
@ -309,7 +319,7 @@ export function AdminTemplatesManager() {
|
|||||||
<Clock className="h-4 w-4 text-yellow-600" />
|
<Clock className="h-4 w-4 text-yellow-600" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-white">{getTemplateStatusCount('pending')}</div>
|
<div className="text-2xl font-bold text-white">{getCustomStatusCount('pending')}</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -319,7 +329,7 @@ export function AdminTemplatesManager() {
|
|||||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-white">{getTemplateStatusCount('approved')}</div>
|
<div className="text-2xl font-bold text-white">{getCustomStatusCount('approved')}</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -329,10 +339,11 @@ export function AdminTemplatesManager() {
|
|||||||
<XCircle className="h-4 w-4 text-red-500" />
|
<XCircle className="h-4 w-4 text-red-500" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold text-white">{getTemplateStatusCount('rejected')}</div>
|
<div className="text-2xl font-bold text-white">{getCustomStatusCount('rejected')}</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||||
@ -360,12 +371,12 @@ export function AdminTemplatesManager() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search templates..."
|
placeholder="Search templates..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => { setSearchQuery(e.target.value); setPage(1) }}
|
||||||
className="pl-10"
|
className="pl-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<Select value={statusFilter} onValueChange={(v) => { setStatusFilter(v); setPage(1) }}>
|
||||||
<SelectTrigger className="w-48">
|
<SelectTrigger className="w-48">
|
||||||
<SelectValue placeholder="Filter by status" />
|
<SelectValue placeholder="Filter by status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -481,6 +492,28 @@ export function AdminTemplatesManager() {
|
|||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<div className="text-sm text-gray-500">Page {page}</div>
|
||||||
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={page === 1}
|
||||||
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
<span className="flex items-center"><ChevronLeft className="h-4 w-4 mr-1" /> Prev</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!hasMore}
|
||||||
|
onClick={() => setPage((p) => p + 1)}
|
||||||
|
>
|
||||||
|
<span className="flex items-center">Next <ChevronRight className="h-4 w-4 ml-1" /></span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
|
||||||
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette } from "lucide-react"
|
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette } from "lucide-react"
|
||||||
import { useTemplates } from "@/hooks/useTemplates"
|
import { useTemplates } from "@/hooks/useTemplates"
|
||||||
import { CustomTemplateForm } from "@/components/custom-template-form"
|
import { CustomTemplateForm } from "@/components/custom-template-form"
|
||||||
@ -15,7 +16,7 @@ import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialo
|
|||||||
import { DatabaseTemplate, TemplateFeature } from "@/lib/template-service"
|
import { DatabaseTemplate, TemplateFeature } from "@/lib/template-service"
|
||||||
import AICustomFeatureCreator from "@/components/ai/AICustomFeatureCreator"
|
import AICustomFeatureCreator from "@/components/ai/AICustomFeatureCreator"
|
||||||
import { BACKEND_URL } from "@/config/backend"
|
import { BACKEND_URL } from "@/config/backend"
|
||||||
import { Tooltip } from "@/components/ui/tooltip"
|
// Removed Tooltip import as we are no longer using tooltips for title/description
|
||||||
import WireframeCanvas from "@/components/wireframe-canvas"
|
import WireframeCanvas from "@/components/wireframe-canvas"
|
||||||
import PromptSidePanel from "@/components/prompt-side-panel"
|
import PromptSidePanel from "@/components/prompt-side-panel"
|
||||||
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
|
||||||
@ -55,7 +56,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
// Keep a stable list of all categories seen so the filter chips don't disappear
|
// Keep a stable list of all categories seen so the filter chips don't disappear
|
||||||
const [knownCategories, setKnownCategories] = useState<Set<string>>(new Set(["all"]))
|
const [knownCategories, setKnownCategories] = useState<Set<string>>(new Set(["all"]))
|
||||||
// Cache counts per category using the API totals for each filtered fetch
|
// Cache counts per category using the API totals for each filtered fetch
|
||||||
const [categoryCounts, setCategoryCounts] = useState<Record<string, number>>({ all: 0 })
|
// Use undefined to indicate "unknown" instead of showing wrong initial counts
|
||||||
|
const [categoryCounts, setCategoryCounts] = useState<Record<string, number | undefined>>({ all: 0 })
|
||||||
|
// Track per-card expanded state for descriptions
|
||||||
|
const [expandedDescriptions, setExpandedDescriptions] = useState<Record<string, boolean>>({})
|
||||||
|
const [descDialogOpen, setDescDialogOpen] = useState(false)
|
||||||
|
const [descDialogData, setDescDialogData] = useState<{ title: string; description: string }>({ title: '', description: '' })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
@ -68,6 +74,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
deleteTemplate,
|
deleteTemplate,
|
||||||
fetchTemplatesWithPagination,
|
fetchTemplatesWithPagination,
|
||||||
loadMoreTemplates,
|
loadMoreTemplates,
|
||||||
|
categories: hookCategories,
|
||||||
} = useTemplates()
|
} = useTemplates()
|
||||||
|
|
||||||
// Initial fetch is handled inside useTemplates hook; avoid duplicate fetch here
|
// Initial fetch is handled inside useTemplates hook; avoid duplicate fetch here
|
||||||
@ -115,6 +122,26 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
});
|
});
|
||||||
}, [combined?.data]);
|
}, [combined?.data]);
|
||||||
|
|
||||||
|
// Seed known categories and counts from hookCategories (pre-fetched full counts)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hookCategories || hookCategories.length === 0) return;
|
||||||
|
setKnownCategories((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
hookCategories.forEach((c) => next.add(c.id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setCategoryCounts((prev) => {
|
||||||
|
const next: Record<string, number | undefined> = { ...prev };
|
||||||
|
hookCategories.forEach((c) => {
|
||||||
|
next[c.id] = c.count;
|
||||||
|
});
|
||||||
|
// Ensure 'all' exists if provided by hook
|
||||||
|
const allFromHook = hookCategories.find((c) => c.id === 'all')?.count;
|
||||||
|
if (typeof allFromHook === 'number') next['all'] = allFromHook;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [hookCategories]);
|
||||||
|
|
||||||
// Update counts cache based on API totals for the currently selected category
|
// Update counts cache based on API totals for the currently selected category
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentCat = paginationState.selectedCategory || 'all';
|
const currentCat = paginationState.selectedCategory || 'all';
|
||||||
@ -189,14 +216,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
else if (lower.includes('seo') || lower.includes('content')) icon = BarChart3;
|
else if (lower.includes('seo') || lower.includes('content')) icon = BarChart3;
|
||||||
else if (lower.includes('food') || lower.includes('delivery')) icon = Users;
|
else if (lower.includes('food') || lower.includes('delivery')) icon = Users;
|
||||||
|
|
||||||
// Prefer cached count for this category (set when that filter is active). Fallback to visible page count.
|
// Prefer cached count for this category (set when that filter is active).
|
||||||
const fallbackCount = templates.filter((t) => t.category === categoryId).length;
|
// Do NOT fallback to visible page count to avoid misleading numbers.
|
||||||
const count = categoryCounts[categoryId] ?? fallbackCount;
|
const count = categoryCounts[categoryId];
|
||||||
|
|
||||||
categoryMap.set(categoryId, {
|
categoryMap.set(categoryId, {
|
||||||
name: categoryId,
|
name: categoryId,
|
||||||
icon,
|
icon,
|
||||||
count,
|
// If count is unknown, represent as 0 in data and handle placeholder in UI
|
||||||
|
count: typeof count === 'number' ? count : 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -465,6 +493,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
const renderChip = (category: { id: string; name: string; icon: React.ComponentType<{ className?: string }>; count: number }) => {
|
const renderChip = (category: { id: string; name: string; icon: React.ComponentType<{ className?: string }>; count: number }) => {
|
||||||
const Icon = category.icon;
|
const Icon = category.icon;
|
||||||
const active = selectedCategory === category.id;
|
const active = selectedCategory === category.id;
|
||||||
|
const knownCount = category.id === 'all'
|
||||||
|
? Math.max(
|
||||||
|
selectedCategory === 'all' ? (paginationState.total || 0) : 0,
|
||||||
|
categoryCounts['all'] ?? 0
|
||||||
|
)
|
||||||
|
: categoryCounts[category.id];
|
||||||
|
// Fallback to currently visible items count for initial render if unknown
|
||||||
|
const fallbackCount = category.id === 'all' ? templates.length : templates.filter((t) => t.category === category.id).length;
|
||||||
|
const displayCount = typeof knownCount === 'number' ? knownCount : fallbackCount;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={`cat-${category.id}`}
|
key={`cat-${category.id}`}
|
||||||
@ -479,7 +516,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<div className="font-semibold">{category.name}</div>
|
<div className="font-semibold">{category.name}</div>
|
||||||
<div className={`text-sm ${active ? "text-black/80" : "text-white/60"}`}>
|
<div className={`text-sm ${active ? "text-black/80" : "text-white/60"}`}>
|
||||||
{category.count} templates
|
{`${displayCount} templates`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@ -537,11 +574,9 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
<div className="flex items-start justify-between mb-2">
|
<div className="flex items-start justify-between mb-2">
|
||||||
<div className="space-y-2 flex-1 min-w-0">
|
<div className="space-y-2 flex-1 min-w-0">
|
||||||
<CardTitle className="text-xl font-bold text-white group-hover:text-orange-400 transition-colors">
|
<CardTitle className="text-xl font-bold text-white group-hover:text-orange-400 transition-colors">
|
||||||
<Tooltip content={template.title}>
|
|
||||||
<div className="whitespace-nowrap overflow-hidden text-ellipsis max-w-full break-words hyphens-auto">
|
<div className="whitespace-nowrap overflow-hidden text-ellipsis max-w-full break-words hyphens-auto">
|
||||||
{truncate(template.title, TITLE_MAX_CHARS)}
|
{truncate(template.title, TITLE_MAX_CHARS)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Badge className={`${getComplexityColor(template.complexity)} font-medium px-3 py-1 rounded-full`}>{getComplexityLabel(template.complexity)}</Badge>
|
<Badge className={`${getComplexityColor(template.complexity)} font-medium px-3 py-1 rounded-full`}>{getComplexityLabel(template.complexity)}</Badge>
|
||||||
@ -581,11 +616,32 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Tooltip content={template.description}>
|
{(() => {
|
||||||
<p className="text-white/80 text-sm leading-relaxed line-clamp-2 break-words hyphens-auto overflow-hidden">
|
const raw = (template.description || '').trim()
|
||||||
{truncate(template.description, DESC_MAX_CHARS)}
|
const needsToggle = raw.length > DESC_MAX_CHARS
|
||||||
|
const displayText = truncate(template.description, DESC_MAX_CHARS)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className={`text-white/80 text-sm leading-relaxed break-words hyphens-auto line-clamp-2 overflow-hidden`}>
|
||||||
|
{displayText}
|
||||||
</p>
|
</p>
|
||||||
</Tooltip>
|
{needsToggle && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setDescDialogData({ title: template.title, description: raw })
|
||||||
|
setDescDialogOpen(true)
|
||||||
|
}}
|
||||||
|
className="mt-1 text-xs text-orange-400 hover:text-orange-300 font-medium"
|
||||||
|
>
|
||||||
|
Show more
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardContent className="p-4 flex flex-col h-full text-white/80">
|
<CardContent className="p-4 flex flex-col h-full text-white/80">
|
||||||
@ -806,6 +862,16 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Dialog open={descDialogOpen} onOpenChange={setDescDialogOpen}>
|
||||||
|
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">{descDialogData.title}</DialogTitle>
|
||||||
|
<DialogDescription className="text-white/80 whitespace-pre-wrap">
|
||||||
|
{descDialogData.description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -822,6 +888,18 @@ function FeatureSelectionStep({
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||||
const [showAIModal, setShowAIModal] = useState(false)
|
const [showAIModal, setShowAIModal] = useState(false)
|
||||||
|
const [expandedFeatureDescriptions, setExpandedFeatureDescriptions] = useState<Record<string, boolean>>({})
|
||||||
|
const [featureDescDialogOpen, setFeatureDescDialogOpen] = useState(false)
|
||||||
|
const [featureDescDialogData, setFeatureDescDialogData] = useState<{ title: string; description: string }>({ title: '', description: '' })
|
||||||
|
const [rulesDialogOpen, setRulesDialogOpen] = useState(false)
|
||||||
|
const [rulesForFeature, setRulesForFeature] = useState<{ featureId: string; featureName: string } | null>(null)
|
||||||
|
const [featureRules, setFeatureRules] = useState<Array<string | { requirement?: string; rules?: string[] }>>([])
|
||||||
|
const FEATURE_DESC_MAX_CHARS = 180
|
||||||
|
const truncateFeatureText = (value: string | undefined | null, max: number) => {
|
||||||
|
const v = (value || '').trim()
|
||||||
|
if (v.length <= max) return v
|
||||||
|
return v.slice(0, Math.max(0, max - 1)) + '…'
|
||||||
|
}
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
@ -882,9 +960,19 @@ function FeatureSelectionStep({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractRules = (f: TemplateFeature): Array<string | { requirement?: string; rules?: string[] }> => {
|
||||||
|
// Prefer structured business_rules if available; fall back to additional_business_rules
|
||||||
|
const candidate = (f as any).business_rules ?? (f as any).additional_business_rules ?? []
|
||||||
|
if (Array.isArray(candidate)) return candidate
|
||||||
|
if (candidate && typeof candidate === 'object') return Object.entries(candidate).map(([k, v]) => `${k}: ${v as string}`)
|
||||||
|
if (typeof candidate === 'string') return [candidate]
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const section = (title: string, list: TemplateFeature[]) => (
|
const section = (title: string, list: TemplateFeature[]) => (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-white mb-3">{title} ({list.length})</h3>
|
<h3 className="text-lg font-semibold text-white mb-3">{title} ({list.length})</h3>
|
||||||
|
<div className={`${list.length > 6 ? 'max-h-[480px] overflow-y-auto pr-2' : ''}`}>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{list.map((f) => (
|
{list.map((f) => (
|
||||||
<Card key={f.id} className={`bg-white/5 ${selectedIds.has(f.id) ? 'border-orange-400' : 'border-white/10'}`}>
|
<Card key={f.id} className={`bg-white/5 ${selectedIds.has(f.id) ? 'border-orange-400' : 'border-white/10'}`}>
|
||||||
@ -921,17 +1009,53 @@ function FeatureSelectionStep({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-white/80 text-sm space-y-2">
|
<CardContent className="text-white/80 text-sm space-y-2">
|
||||||
<p>{f.description || 'No description provided.'}</p>
|
{(() => {
|
||||||
|
const raw = (f.description || 'No description provided.').trim()
|
||||||
|
const needsToggle = raw.length > FEATURE_DESC_MAX_CHARS
|
||||||
|
const displayText = truncateFeatureText(raw, FEATURE_DESC_MAX_CHARS)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className={`leading-relaxed break-words hyphens-auto line-clamp-2 overflow-hidden`}>{displayText}</p>
|
||||||
|
{needsToggle && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setFeatureDescDialogData({ title: f.name, description: raw })
|
||||||
|
setFeatureDescDialogOpen(true)
|
||||||
|
}}
|
||||||
|
className="mt-1 text-xs text-orange-400 hover:text-orange-300 font-medium"
|
||||||
|
>
|
||||||
|
Show more
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
<div className="flex gap-2 text-xs">
|
<div className="flex gap-2 text-xs">
|
||||||
<Badge variant="outline" className="bg-white/5 border-white/10">{f.feature_type}</Badge>
|
<Badge variant="outline" className="bg-white/5 border-white/10">{f.feature_type}</Badge>
|
||||||
<Badge variant="outline" className="bg-white/5 border-white/10">{f.complexity}</Badge>
|
<Badge variant="outline" className="bg-white/5 border-white/10">{f.complexity}</Badge>
|
||||||
{typeof f.usage_count === 'number' && <Badge variant="outline" className="bg-white/5 border-white/10">used {f.usage_count}</Badge>}
|
{typeof f.usage_count === 'number' && <Badge variant="outline" className="bg-white/5 border-white/10">used {f.usage_count}</Badge>}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-auto h-6 px-2 border-orange-500 text-orange-300 hover:bg-orange-500/10"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setRulesForFeature({ featureId: f.id, featureName: f.name })
|
||||||
|
setFeatureRules(extractRules(f))
|
||||||
|
setRulesDialogOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Rules
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -1012,6 +1136,48 @@ function FeatureSelectionStep({
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-white/60 text-sm mt-2">Select at least 3 features to continue. Selected {selectedIds.size}/3.</div>
|
<div className="text-white/60 text-sm mt-2">Select at least 3 features to continue. Selected {selectedIds.size}/3.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Dialog open={featureDescDialogOpen} onOpenChange={setFeatureDescDialogOpen}>
|
||||||
|
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">{featureDescDialogData.title}</DialogTitle>
|
||||||
|
<DialogDescription className="text-white/80 whitespace-pre-wrap">
|
||||||
|
{featureDescDialogData.description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog open={rulesDialogOpen} onOpenChange={setRulesDialogOpen}>
|
||||||
|
<DialogContent className="bg-white/10 border-white/20 text-white" showCloseButton>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white">{rulesForFeature?.featureName || 'Feature Rules'}</DialogTitle>
|
||||||
|
<DialogDescription className="text-white/80">
|
||||||
|
{featureRules.length === 0 ? (
|
||||||
|
<div className="py-2">No rules found for this feature.</div>
|
||||||
|
) : (
|
||||||
|
<ul className="list-disc pl-5 space-y-2">
|
||||||
|
{featureRules.map((r, idx) => {
|
||||||
|
if (typeof r === 'string') return <li key={idx} className="text-sm leading-relaxed">{r}</li>
|
||||||
|
const req = (r as any).requirement
|
||||||
|
const rules = (r as any).rules
|
||||||
|
return (
|
||||||
|
<li key={idx} className="text-sm leading-relaxed">
|
||||||
|
{req ? <div className="font-medium text-white/90">{req}</div> : null}
|
||||||
|
{Array.isArray(rules) && rules.length > 0 && (
|
||||||
|
<ul className="list-disc pl-5 mt-1 space-y-1">
|
||||||
|
{rules.map((rr: string, j: number) => (
|
||||||
|
<li key={j} className="text-xs text-white/80">{rr}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ function DialogOverlay({
|
|||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 backdrop-blur-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -60,7 +60,7 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
"bg-background/95 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-3xl max-h-[80vh] overflow-y-auto",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import "@tldraw/tldraw/tldraw.css"
|
|
||||||
import { Tldraw, type Editor, createShapeId, toRichText } from "@tldraw/tldraw"
|
import { Tldraw, type Editor, createShapeId, toRichText } from "@tldraw/tldraw"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Main backend URL - change this to update all API calls
|
// Main backend URL - change this to update all API calls
|
||||||
export const BACKEND_URL = 'http://localhost:8000';
|
export const BACKEND_URL = 'http://192.168.1.11:8000';
|
||||||
// export const BACKEND_URL = 'https://backend.codenuk.com';
|
// export const BACKEND_URL = 'https://backend.codenuk.com';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -381,12 +381,48 @@ export const adminApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Admin Templates Management (from main templates table)
|
// Admin Templates Management (from main templates table)
|
||||||
// Get all admin templates for management
|
// Get all admin templates for management (returns data + pagination)
|
||||||
getAdminTemplates: async (limit = 50, offset = 0, category?: string, search?: string): Promise<AdminTemplate[]> => {
|
getAdminTemplates: async (
|
||||||
|
limit = 50,
|
||||||
|
offset = 0,
|
||||||
|
category?: string,
|
||||||
|
search?: string
|
||||||
|
): Promise<{ templates: AdminTemplate[]; pagination?: { total: number; limit: number; offset: number; hasMore: boolean } }> => {
|
||||||
const params = new URLSearchParams({ limit: limit.toString(), offset: offset.toString() });
|
const params = new URLSearchParams({ limit: limit.toString(), offset: offset.toString() });
|
||||||
if (category && category !== 'all') params.append('category', category);
|
if (category && category !== 'all') params.append('category', category);
|
||||||
if (search) params.append('search', search);
|
if (search) params.append('search', search);
|
||||||
const response = await apiCall<AdminTemplate[]>(`/api/admin/templates?${params}`);
|
const response = await apiCall<AdminTemplate[]>(`/api/admin/templates?${params}`);
|
||||||
|
const pagination = (response as unknown as { pagination?: { total: number; limit: number; offset: number; hasMore: boolean } }).pagination;
|
||||||
|
return { templates: response.data, pagination };
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update an admin template (main templates table)
|
||||||
|
updateAdminTemplate: async (
|
||||||
|
templateId: string,
|
||||||
|
updateData: {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
category?: string;
|
||||||
|
type?: string;
|
||||||
|
icon?: string;
|
||||||
|
gradient?: string;
|
||||||
|
border?: string;
|
||||||
|
text?: string;
|
||||||
|
subtext?: string;
|
||||||
|
}
|
||||||
|
): Promise<AdminTemplate> => {
|
||||||
|
const response = await apiCall<AdminTemplate>(`/api/admin/templates/${templateId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(updateData),
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete an admin template (main templates table)
|
||||||
|
deleteAdminTemplate: async (templateId: string): Promise<{ success: boolean }> => {
|
||||||
|
const response = await apiCall<{ success: boolean }>(`/api/admin/templates/${templateId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
4
src/styles/tldraw.css
Normal file
4
src/styles/tldraw.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/* tldraw styles copied from node_modules for Next.js build compatibility */
|
||||||
|
@import url("/../node_modules/@tldraw/tldraw/tldraw.css");
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user