Compare commits

..
2 Commits
Author SHA1 Message Date
dimitar 19ec020046 db backup and restore implemented 2025-12-12 02:08:03 +01:00
dimitar 8353a24f6b cleanup
and AI recommnedation fix
2025-12-12 01:32:52 +01:00
34 changed files with 980 additions and 255 deletions

No files matched your search

Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+477 -1
View File
@@ -12,6 +12,7 @@
"@fitai/database": "file:../../packages/database", "@fitai/database": "file:../../packages/database",
"@fitai/shared": "file:../../packages/shared", "@fitai/shared": "file:../../packages/shared",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.90.7", "@tanstack/react-query": "^5.90.7",
"@types/bcryptjs": "^3.0.0", "@types/bcryptjs": "^3.0.0",
@@ -76,6 +77,8 @@
"name": "@fitai/shared", "name": "@fitai/shared",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
@@ -2402,6 +2405,12 @@
"url": "https://opencollective.com/pkgr" "url": "https://opencollective.com/pkgr"
} }
}, },
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-compose-refs": { "node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@@ -2417,6 +2426,249 @@
} }
} }
}, },
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
@@ -2435,6 +2687,91 @@
} }
} }
}, },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@reduxjs/toolkit": { "node_modules/@reduxjs/toolkit": {
"version": "2.10.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.1.tgz",
@@ -2899,7 +3236,7 @@
"version": "19.2.3", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"peerDependencies": { "peerDependencies": {
@@ -3777,6 +4114,18 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aria-query": { "node_modules/aria-query": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
@@ -5255,6 +5604,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -6696,6 +7051,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-package-type": { "node_modules/get-package-type": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -10826,6 +11190,75 @@
} }
} }
}, },
"node_modules/react-remove-scroll": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
"integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.3",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
"react-style-singleton": "^2.2.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
"get-nonce": "^1.0.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -12900,6 +13333,49 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"license": "MIT",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sync-external-store": { "node_modules/use-sync-external-store": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+1
View File
@@ -15,6 +15,7 @@
"@fitai/database": "file:../../packages/database", "@fitai/database": "file:../../packages/database",
"@fitai/shared": "file:../../packages/shared", "@fitai/shared": "file:../../packages/shared",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.90.7", "@tanstack/react-query": "^5.90.7",
"@types/bcryptjs": "^3.0.0", "@types/bcryptjs": "^3.0.0",
+2 -1
View File
@@ -1,5 +1,5 @@
import { getDatabase } from '../src/lib/database/index.ts'; import { getDatabase } from '../src/lib/database/index';
async function verifyDatabase() { async function verifyDatabase() {
console.log('Starting database verification...'); console.log('Starting database verification...');
@@ -34,6 +34,7 @@ async function verifyDatabase() {
// 3. Create Fitness Profile // 3. Create Fitness Profile
console.log('Creating fitness profile...'); console.log('Creating fitness profile...');
const profile = await db.createFitnessProfile({ const profile = await db.createFitnessProfile({
id: 'test-profile-id',
userId: user.id, userId: user.id,
height: '180', height: '180',
weight: '75', weight: '75',
@@ -0,0 +1,47 @@
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
import { getDatabase, DatabaseFactory } from '@/lib/database'
import { ensureUserSynced } from '@/lib/sync-user'
import fs from 'fs'
import path from 'path'
const BACKUP_DIR = path.join(process.cwd(), 'backups')
const DB_PATH = path.join(process.cwd(), 'data', 'fitai.db')
export async function POST(req: Request) {
try {
const { userId } = await auth()
if (!userId) return new NextResponse('Unauthorized', { status: 401 })
const db = await getDatabase()
const user = await ensureUserSynced(userId, db)
if (!user || (user.role !== 'admin' && user.role !== 'superAdmin')) {
return new NextResponse('Forbidden', { status: 403 })
}
const { filename } = await req.json()
if (!filename) {
return new NextResponse('Filename is required', { status: 400 })
}
const backupPath = path.join(BACKUP_DIR, filename)
if (!fs.existsSync(backupPath)) {
return new NextResponse('Backup file not found', { status: 404 })
}
// Close existing connection
await DatabaseFactory.disconnect()
// Restore file
fs.copyFileSync(backupPath, DB_PATH)
// Re-initialize connection (will happen automatically on next getDatabase call, but good to verify)
await getDatabase()
return NextResponse.json({ success: true })
} catch (error) {
console.error('Restore backup error:', error)
return new NextResponse('Internal Server Error', { status: 500 })
}
}
@@ -0,0 +1,78 @@
import { auth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
import { getDatabase } from '@/lib/database'
import { ensureUserSynced } from '@/lib/sync-user'
import fs from 'fs'
import path from 'path'
const BACKUP_DIR = path.join(process.cwd(), 'backups')
const DB_PATH = path.join(process.cwd(), 'data', 'fitai.db')
export async function GET() {
try {
const { userId } = await auth()
if (!userId) return new NextResponse('Unauthorized', { status: 401 })
const db = await getDatabase()
const user = await ensureUserSynced(userId, db)
if (!user || (user.role !== 'admin' && user.role !== 'superAdmin')) {
return new NextResponse('Forbidden', { status: 403 })
}
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true })
}
const files = fs.readdirSync(BACKUP_DIR)
.filter(file => file.endsWith('.db'))
.map(file => {
const stats = fs.statSync(path.join(BACKUP_DIR, file))
return {
name: file,
size: stats.size,
createdAt: stats.birthtime
}
})
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
return NextResponse.json(files)
} catch (error) {
console.error('List backups error:', error)
return new NextResponse('Internal Server Error', { status: 500 })
}
}
export async function POST() {
try {
const { userId } = await auth()
if (!userId) return new NextResponse('Unauthorized', { status: 401 })
const db = await getDatabase()
const user = await ensureUserSynced(userId, db)
if (!user || (user.role !== 'admin' && user.role !== 'superAdmin')) {
return new NextResponse('Forbidden', { status: 403 })
}
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true })
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const backupPath = path.join(BACKUP_DIR, `backup-${timestamp}.db`)
// Ensure source DB exists
if (!fs.existsSync(DB_PATH)) {
return new NextResponse('Database file not found', { status: 404 })
}
// Copy file
fs.copyFileSync(DB_PATH, backupPath)
return NextResponse.json({ success: true, filename: `backup-${timestamp}.db` })
} catch (error) {
console.error('Create backup error:', error)
return new NextResponse('Internal Server Error', { status: 500 })
}
}
@@ -5,7 +5,7 @@ import { getDatabase } from '@/lib/database';
// POST - Mark goal as complete // POST - Mark goal as complete
export async function POST( export async function POST(
req: NextRequest, req: NextRequest,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
@@ -5,7 +5,7 @@ import { getDatabase } from '@/lib/database';
// GET - Get specific goal // GET - Get specific goal
export async function GET( export async function GET(
req: NextRequest, req: NextRequest,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
@@ -40,7 +40,7 @@ export async function GET(
// PUT - Update goal // PUT - Update goal
export async function PUT( export async function PUT(
req: NextRequest, req: NextRequest,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
@@ -82,7 +82,7 @@ export async function PUT(
// DELETE - Delete goal // DELETE - Delete goal
export async function DELETE( export async function DELETE(
req: NextRequest, req: NextRequest,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { userId } = await auth(); const { userId } = await auth();
@@ -172,9 +172,9 @@ export async function POST(req: Request) {
const recommendation = await db.createRecommendation({ const recommendation = await db.createRecommendation({
id: crypto.randomUUID(), id: crypto.randomUUID(),
userId, userId,
fitnessProfileId: profile.userId, fitnessProfileId: profile.id,
type: 'ai_plan', type: 'ai_plan',
content: parsedResponse.recommendationText, recommendationText: parsedResponse.recommendationText,
activityPlan: parsedResponse.activityPlan, activityPlan: parsedResponse.activityPlan,
dietPlan: parsedResponse.dietPlan, dietPlan: parsedResponse.dietPlan,
status: 'pending' status: 'pending'
@@ -76,7 +76,7 @@ export async function POST(request: Request) {
userId, userId,
fitnessProfileId, fitnessProfileId,
type: 'ai_plan', type: 'ai_plan',
content: recommendationText, recommendationText: recommendationText,
activityPlan, activityPlan,
dietPlan, dietPlan,
status: status || 'pending' status: status || 'pending'
@@ -90,7 +90,7 @@ export async function POST(request: Request) {
id: crypto.randomUUID(), id: crypto.randomUUID(),
userId, userId,
type, type,
content, recommendationText: content,
status: status || 'pending' status: status || 'pending'
}) })
return NextResponse.json(recommendation) return NextResponse.json(recommendation)
@@ -122,8 +122,8 @@ export async function PUT(request: Request) {
const updated = await db.updateRecommendation(id, { const updated = await db.updateRecommendation(id, {
...(status && { status }), ...(status && { status }),
...(recommendationText && { content: recommendationText }), // Map legacy field ...(recommendationText && { recommendationText }),
...(content && { content }), ...(content && { recommendationText: content }),
...(activityPlan && { activityPlan }), ...(activityPlan && { activityPlan }),
...(dietPlan && { dietPlan }) ...(dietPlan && { dietPlan })
}) })
+1 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useUser } from "@clerk/nextjs"; import { useUser } from "@clerk/nextjs";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
interface UserProfile { interface UserProfile {
+1 -1
View File
@@ -219,7 +219,7 @@ export default function RecommendationsPage() {
</div> </div>
<div className="space-y-2 text-sm mb-4"> <div className="space-y-2 text-sm mb-4">
<div> <div>
<span className="font-semibold">Advice:</span> {rec.content} <span className="font-semibold">Advice:</span> {rec.recommendationText}
</div> </div>
<div> <div>
<span className="font-semibold">Activity:</span> {rec.activityPlan} <span className="font-semibold">Activity:</span> {rec.activityPlan}
+175
View File
@@ -0,0 +1,175 @@
"use client";
import { useEffect, useState } from "react";
import axios from "axios";
import { Database, Download, RefreshCw, AlertTriangle, Check, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
interface Backup {
name: string;
size: number;
createdAt: string;
}
export default function SettingsPage() {
const [backups, setBackups] = useState<Backup[]>([]);
const [loading, setLoading] = useState(true);
const [creatingBackup, setCreatingBackup] = useState(false);
const [restoring, setRestoring] = useState<string | null>(null);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const fetchBackups = async () => {
try {
const response = await axios.get("/api/admin/backups");
setBackups(response.data);
} catch (error) {
console.error("Failed to fetch backups:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchBackups();
}, []);
const handleCreateBackup = async () => {
setCreatingBackup(true);
setMessage(null);
try {
await axios.post("/api/admin/backups");
await fetchBackups();
setMessage({ type: 'success', text: 'Backup created successfully' });
} catch (error) {
console.error("Failed to create backup:", error);
setMessage({ type: 'error', text: 'Failed to create backup' });
} finally {
setCreatingBackup(false);
}
};
const handleRestore = async (filename: string) => {
if (!window.confirm(`Are you sure you want to restore from ${filename}? This will overwrite the current database.`)) {
return;
}
setRestoring(filename);
setMessage(null);
try {
await axios.post("/api/admin/backups/restore", { filename });
setMessage({ type: 'success', text: 'Database restored successfully' });
// Optional: Refresh page or force re-login if session is invalidated
} catch (error) {
console.error("Failed to restore backup:", error);
setMessage({ type: 'error', text: 'Failed to restore backup' });
} finally {
setRestoring(null);
}
};
const formatSize = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
return (
<div className="space-y-8 p-8">
<div>
<h2 className="text-3xl font-bold text-slate-900">Settings</h2>
<p className="text-slate-500 mt-2">Manage your application settings and database.</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-50 rounded-lg">
<Database className="w-6 h-6 text-blue-600" />
</div>
<div>
<h3 className="text-xl font-bold text-slate-900">Database Management</h3>
<p className="text-sm text-slate-500">Create backups and restore your database</p>
</div>
</div>
<Button
onClick={handleCreateBackup}
disabled={creatingBackup}
className="flex items-center gap-2"
>
{creatingBackup ? <Loader2 className="w-4 h-4 animate-spin" /> : <Download className="w-4 h-4" />}
Create Backup
</Button>
</div>
{message && (
<div className={`p-4 rounded-lg mb-6 flex items-center gap-2 ${message.type === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{message.type === 'success' ? <Check className="w-5 h-5" /> : <AlertTriangle className="w-5 h-5" />}
{message.text}
</div>
)}
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-left text-sm">
<thead className="bg-slate-50 border-b">
<tr>
<th className="px-6 py-4 font-semibold text-slate-900">Filename</th>
<th className="px-6 py-4 font-semibold text-slate-900">Size</th>
<th className="px-6 py-4 font-semibold text-slate-900">Created At</th>
<th className="px-6 py-4 font-semibold text-slate-900 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y">
{loading ? (
<tr>
<td colSpan={4} className="px-6 py-8 text-center text-slate-500">
Loading backups...
</td>
</tr>
) : backups.length === 0 ? (
<tr>
<td colSpan={4} className="px-6 py-8 text-center text-slate-500">
No backups found
</td>
</tr>
) : (
backups.map((backup) => (
<tr key={backup.name} className="hover:bg-slate-50 transition-colors">
<td className="px-6 py-4 font-medium text-slate-900">{backup.name}</td>
<td className="px-6 py-4 text-slate-600">{formatSize(backup.size)}</td>
<td className="px-6 py-4 text-slate-600">{formatDate(backup.createdAt)}</td>
<td className="px-6 py-4 text-right">
<Button
variant="ghost"
size="sm"
onClick={() => handleRestore(backup.name)}
disabled={!!restoring}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
{restoring === backup.name ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<RefreshCw className="w-4 h-4 mr-2" />
)}
Restore
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
+7 -2
View File
@@ -3,10 +3,10 @@
import { type ReactElement } from "react"; import { type ReactElement } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { Home, Users, BarChart3, User, Brain } from "lucide-react"; import { Home, Users, BarChart3, User, Brain, Settings } from "lucide-react";
import { SignedIn, UserButton } from "@clerk/nextjs"; import { SignedIn, UserButton } from "@clerk/nextjs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/button";
interface NavItem { interface NavItem {
href: string; href: string;
@@ -40,6 +40,11 @@ const navItems: NavItem[] = [
label: "Profile", label: "Profile",
icon: User, icon: User,
}, },
{
href: "/settings",
label: "Settings",
icon: Settings,
},
]; ];
export function Navigation(): ReactElement { export function Navigation(): ReactElement {
@@ -132,7 +132,13 @@ export function AnalyticsDashboard() {
<h3 className="text-lg font-semibold">Monthly Revenue</h3> <h3 className="text-lg font-semibold">Monthly Revenue</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<RevenueChart data={revenueData} /> <RevenueChart
data={revenueData.map(item => ({
category: item.label,
value: item.value,
color: item.color
}))}
/>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
-23
View File
@@ -1,23 +0,0 @@
import React from 'react'
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary'
children: React.ReactNode
}
export function Button({ variant = 'primary', children, className = '', ...props }: ButtonProps) {
const baseClasses = 'px-4 py-2 rounded-md font-medium transition-colors'
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
}
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
{...props}
>
{children}
</button>
)
}
-30
View File
@@ -1,30 +0,0 @@
import React from 'react'
interface CardProps {
children: React.ReactNode
className?: string
}
export function Card({ children, className = '' }: CardProps) {
return (
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
{children}
</div>
)
}
export function CardHeader({ children, className = '' }: CardProps) {
return (
<div className={`mb-4 ${className}`}>
{children}
</div>
)
}
export function CardContent({ children, className = '' }: CardProps) {
return (
<div className={className}>
{children}
</div>
)
}
+45 -52
View File
@@ -1,56 +1,49 @@
import * as React from "react"; import React from 'react'
import { Slot } from "@radix-ui/react-slot"; import { cn } from '@/lib/utils'
import { cva, type VariantProps } from "class-variance-authority"; import { Loader2 } from 'lucide-react'
import { cn } from "@/lib/utils"; interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'outline' | 'default'
const buttonVariants = cva( size?: 'default' | 'sm' | 'lg' | 'icon'
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", isLoading?: boolean
{ children: React.ReactNode
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( export function Button({
({ className, variant, size, asChild = false, ...props }, ref) => { variant = 'primary',
const Comp = asChild ? Slot : "button"; size = 'default',
return ( isLoading = false,
<Comp children,
className={cn(buttonVariants({ variant, size, className }))} className = '',
ref={ref} disabled,
{...props} ...props
/> }: ButtonProps) {
); const baseClasses = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50'
},
);
Button.displayName = "Button";
export { Button, buttonVariants }; const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 shadow hover:bg-blue-700/90',
default: 'bg-blue-600 text-white hover:bg-blue-700 shadow hover:bg-blue-700/90',
secondary: 'bg-slate-100 text-slate-900 hover:bg-slate-200/80',
ghost: 'hover:bg-slate-100 hover:text-slate-900',
destructive: 'bg-red-500 text-white hover:bg-red-600/90',
outline: 'border border-input bg-transparent shadow-sm hover:bg-slate-100 hover:text-slate-900'
}
const sizeClasses = {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9'
}
return (
<button
className={cn(baseClasses, variantClasses[variant], sizeClasses[size], className)}
disabled={disabled || isLoading}
{...props}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</button>
)
}
@@ -1,14 +1,14 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardContent } from "@/components/ui/card";
interface Recommendation { interface Recommendation {
id: string; id: string;
userId: string; userId: string;
type: "short_term" | "medium_term" | "long_term" | "ai_plan"; type: "short_term" | "medium_term" | "long_term" | "ai_plan";
content: string; recommendationText: string;
activityPlan?: string; activityPlan?: string;
dietPlan?: string; dietPlan?: string;
status: "pending" | "completed" | "approved" | "rejected"; status: "pending" | "completed" | "approved" | "rejected";
@@ -97,7 +97,7 @@ export function Recommendations({ userId }: RecommendationsProps) {
}`} }`}
> >
<div className="w-full"> <div className="w-full">
<p className="text-sm font-medium">{rec.content}</p> <p className="text-gray-700">{rec.recommendationText}</p>
{rec.type === 'ai_plan' && ( {rec.type === 'ai_plan' && (
<div className="mt-2 text-xs text-gray-600 space-y-1"> <div className="mt-2 text-xs text-gray-600 space-y-1">
{rec.activityPlan && <p><span className="font-semibold">Activity:</span> {rec.activityPlan}</p>} {rec.activityPlan && <p><span className="font-semibold">Activity:</span> {rec.activityPlan}</p>}
+3 -3
View File
@@ -224,14 +224,14 @@ export function UserGrid({
const gridRef = React.useRef<AgGridReact<User>>(null); const gridRef = React.useRef<AgGridReact<User>>(null);
const gridOptions = { const gridOptions = {
theme: "legacy", theme: "legacy" as const,
columnDefs, columnDefs,
defaultColDef, defaultColDef,
rowData: users, rowData: users,
rowSelection: "multiple", rowSelection: "multiple" as const,
onSelectionChanged: () => { onSelectionChanged: () => {
const selectedNodes = gridRef.current?.api.getSelectedNodes(); const selectedNodes = gridRef.current?.api.getSelectedNodes();
const selectedData = selectedNodes?.map((node) => node.data) || []; const selectedData = selectedNodes?.map((node) => node.data).filter((u): u is User => !!u) || [];
setSelectedUsers(selectedData); setSelectedUsers(selectedData);
if (selectedData.length === 1 && onUserSelect) { if (selectedData.length === 1 && onUserSelect) {
onUserSelect(selectedData[0]); onUserSelect(selectedData[0]);
@@ -2,7 +2,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { UserGrid } from "@/components/users/UserGrid"; import { UserGrid } from "@/components/users/UserGrid";
// import { Button } from "@/components/ui/Button"; // import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardContent } from "@/components/ui/card"; import { Card, CardHeader, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
+3 -9
View File
@@ -1,17 +1,11 @@
import { getDatabase } from '@/lib/database'; import { getDatabase } from '@/lib/database';
import type { FitnessGoal } from '@/lib/database/types'; import type { FitnessGoal, FitnessProfile, Recommendation } from '@/lib/database/types';
// Import types from database package
import type {
FitnessProfile as DBFitnessProfile,
Recommendation as DBRecommendation,
} from '@fitai/database';
export interface AIContext { export interface AIContext {
profile: DBFitnessProfile; profile: FitnessProfile;
activeGoals: FitnessGoal[]; activeGoals: FitnessGoal[];
completedGoals: FitnessGoal[]; completedGoals: FitnessGoal[];
recentRecommendations: DBRecommendation[]; recentRecommendations: Recommendation[];
progressSummary: { progressSummary: {
goalsCompleted: number; goalsCompleted: number;
goalsActive: number; goalsActive: number;
+2 -2
View File
@@ -32,7 +32,7 @@ export function buildEnhancedPrompt(context: AIContext): string {
- Weight: ${profile.weight || 'Not specified'} kg - Weight: ${profile.weight || 'Not specified'} kg
- Age: ${profile.age || 'Not specified'} - Age: ${profile.age || 'Not specified'}
- Gender: ${profile.gender || 'Not specified'} - Gender: ${profile.gender || 'Not specified'}
- Primary Goal: ${profile.fitnessGoal || 'General fitness'} - Primary Goal: ${profile.fitnessGoals?.[0] || 'General fitness'}
- Activity Level: ${profile.activityLevel || 'Not specified'} - Activity Level: ${profile.activityLevel || 'Not specified'}
- Medical Conditions: ${profile.medicalConditions || 'None'} - Medical Conditions: ${profile.medicalConditions || 'None'}
- Allergies: ${profile.allergies || 'None'} - Allergies: ${profile.allergies || 'None'}
@@ -77,7 +77,7 @@ Generate a detailed daily recommendation for a user with the following profile:
- Weight: ${profile.weight} kg - Weight: ${profile.weight} kg
- Age: ${profile.age} - Age: ${profile.age}
- Gender: ${profile.gender} - Gender: ${profile.gender}
- Goal: ${profile.fitnessGoal} - Goal: ${profile.fitnessGoals?.[0]}
- Activity Level: ${profile.activityLevel} - Activity Level: ${profile.activityLevel}
- Medical Conditions: ${profile.medicalConditions || "None"} - Medical Conditions: ${profile.medicalConditions || "None"}
- Injuries: ${profile.injuries || "None"} - Injuries: ${profile.injuries || "None"}
+2 -2
View File
@@ -246,7 +246,7 @@ export class DrizzleDatabase implements IDatabase {
} }
// Fitness Profile operations // Fitness Profile operations
async createFitnessProfile(profileData: Omit<FitnessProfile, 'createdAt' | 'updatedAt'>): Promise<FitnessProfile> { async createFitnessProfile(profileData: Omit<FitnessProfile, 'id' | 'createdAt' | 'updatedAt'>): Promise<FitnessProfile> {
const now = new Date() const now = new Date()
const id = Math.random().toString(36).substr(2, 9) const id = Math.random().toString(36).substr(2, 9)
const newProfile = { const newProfile = {
@@ -257,7 +257,7 @@ export class DrizzleDatabase implements IDatabase {
} }
await this.db.insert(fitnessProfiles).values(newProfile as any) await this.db.insert(fitnessProfiles).values(newProfile as any)
return { ...profileData, createdAt: now, updatedAt: now } return { id, ...profileData, createdAt: now, updatedAt: now }
} }
async getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null> { async getFitnessProfileByUserId(userId: string): Promise<FitnessProfile | null> {
+4 -78
View File
@@ -1,86 +1,12 @@
import { User as SharedUser, Client, FitnessProfile, Attendance, Recommendation, FitnessGoal } from "@fitai/shared";
// Database Entity Types // Database Entity Types
export interface User { export interface User extends SharedUser {
id: string;
email: string;
firstName: string;
lastName: string;
password: string; password: string;
phone?: string;
role: "superAdmin" | "admin" | "trainer" | "client";
createdAt: Date;
updatedAt: Date;
} }
export interface Client { export type { Client, FitnessProfile, Attendance, Recommendation, FitnessGoal };
id: string;
userId: string;
membershipType: "basic" | "premium" | "vip";
membershipStatus: "active" | "inactive" | "expired";
joinDate: Date;
lastVisit?: Date;
}
export interface FitnessProfile {
userId: string;
height: string;
weight: string;
age: string;
gender: "male" | "female" | "other";
activityLevel: "sedentary" | "lightly_active" | "moderately_active" | "very_active" | "extremely_active";
fitnessGoals: string[];
exerciseHabits: string;
dietHabits: string;
medicalConditions: string;
allergies?: string;
injuries?: string;
createdAt: Date;
updatedAt: Date;
}
export interface Attendance {
id: string;
userId: string;
checkInTime: Date;
checkOutTime?: Date;
type: "gym" | "class" | "personal_training";
notes?: string;
createdAt: Date;
}
export interface Recommendation {
id: string;
userId: string;
fitnessProfileId?: string;
type: "short_term" | "medium_term" | "long_term" | "ai_plan";
content: string;
activityPlan?: string;
dietPlan?: string;
status: "pending" | "approved" | "rejected" | "completed";
createdAt: Date;
approvedAt?: Date;
approvedBy?: string;
}
export interface FitnessGoal {
id: string;
userId: string;
fitnessProfileId?: string;
goalType: "weight_target" | "strength_milestone" | "endurance_target" | "flexibility_goal" | "habit_building" | "custom";
title: string;
description?: string;
targetValue?: number;
currentValue?: number;
unit?: string;
startDate: Date;
targetDate?: Date;
completedDate?: Date;
status: "active" | "completed" | "abandoned" | "paused";
progress: number;
priority: "low" | "medium" | "high";
notes?: string;
createdAt: Date;
updatedAt: Date;
}
// Database Interface - allows us to swap implementations // Database Interface - allows us to swap implementations
export interface IDatabase { export interface IDatabase {
+1 -14
View File
@@ -1,14 +1 @@
import { type ClassValue, clsx } from "clsx" export { cn, formatDate } from "@fitai/shared"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatDate(date: string | Date) {
return new Date(date).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}
@@ -9,7 +9,7 @@ import { theme } from "../../styles/theme";
interface Recommendation { interface Recommendation {
id: string; id: string;
userId: string; userId: string;
content: string; recommendationText: string;
activityPlan?: string; activityPlan?: string;
dietPlan?: string; dietPlan?: string;
status: string; status: string;
@@ -141,7 +141,7 @@ export default function RecommendationsScreen() {
</View> </View>
<Text style={styles.sectionTitle}>Daily Advice</Text> <Text style={styles.sectionTitle}>Daily Advice</Text>
<Text style={styles.content}>{item.content}</Text> <Text style={styles.content}>{item.recommendationText}</Text>
{item.activityPlan && ( {item.activityPlan && (
<> <>
+1 -1
View File
@@ -15,7 +15,7 @@
- Shared types should be utilized from `@fitai/shared` or `@fitai/database`. - Shared types should be utilized from `@fitai/shared` or `@fitai/database`.
- **Configuration**: - **Configuration**:
- `apps/mobile/src/config/api.ts` contains hardcoded URLs (e.g., ngrok) which is brittle for development and production. - `apps/mobile/src/config/api.ts` contains hardcoded URLs (e.g., ngrok) which is brittle for development and production.
- API routes in `apps/admin` contain `console.log` statements that should be removed or replaced with a proper logging solution. - API routes in `apps/admin` contain `console.log` statements that should be removed or replaced with a proper logging solution.https://5e424f097c8b.ngrok-free.app
- **Error Handling**: - **Error Handling**:
- Basic error handling exists in API routes, but could be standardized (e.g., custom error classes, consistent error response format). - Basic error handling exists in API routes, but could be standardized (e.g., custom error classes, consistent error response format).
+21
View File
@@ -8,12 +8,33 @@
"name": "@fitai/shared", "name": "@fitai/shared",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/tailwind-merge": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+2
View File
@@ -9,6 +9,8 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
+72 -14
View File
@@ -3,7 +3,10 @@ export interface User {
email: string email: string
firstName: string firstName: string
lastName: string lastName: string
role: 'admin' | 'trainer' | 'client' password?: string
phone?: string
role: 'superAdmin' | 'admin' | 'trainer' | 'client'
imageUrl?: string
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
} }
@@ -11,18 +14,83 @@ export interface User {
export interface Client { export interface Client {
id: string id: string
userId: string userId: string
user: User user?: User
membershipType: 'basic' | 'premium' | 'vip' membershipType: 'basic' | 'premium' | 'vip'
membershipStatus: 'active' | 'inactive' | 'suspended' membershipStatus: 'active' | 'inactive' | 'suspended' | 'expired'
joinDate: Date joinDate: Date
lastVisit?: Date lastVisit?: Date
emergencyContact: { emergencyContact?: {
name: string name: string
phone: string phone: string
relationship: string relationship: string
} }
} }
export interface FitnessProfile {
id: string
userId: string
height: string
weight: string
age: string
gender: "male" | "female" | "other"
activityLevel: "sedentary" | "lightly_active" | "moderately_active" | "very_active" | "extremely_active"
fitnessGoals: string[]
exerciseHabits: string
dietHabits: string
medicalConditions: string
allergies?: string
injuries?: string
createdAt: Date
updatedAt: Date
}
export interface Attendance {
id: string
userId: string
clientId?: string
client?: Client
checkInTime: Date
checkOutTime?: Date
type: 'gym' | 'class' | 'personal_training'
notes?: string
createdAt?: Date
}
export interface Recommendation {
id: string
userId: string
fitnessProfileId?: string
type: "short_term" | "medium_term" | "long_term" | "ai_plan"
recommendationText: string
activityPlan?: string
dietPlan?: string
status: "pending" | "approved" | "rejected" | "completed"
createdAt: Date
approvedAt?: Date
approvedBy?: string
}
export interface FitnessGoal {
id: string
userId: string
fitnessProfileId?: string
goalType: "weight_target" | "strength_milestone" | "endurance_target" | "flexibility_goal" | "habit_building" | "custom"
title: string
description?: string
targetValue?: number
currentValue?: number
unit?: string
startDate: Date
targetDate?: Date
completedDate?: Date
status: "active" | "completed" | "abandoned" | "paused"
progress: number
priority: "low" | "medium" | "high"
notes?: string
createdAt: Date
updatedAt: Date
}
export interface Payment { export interface Payment {
id: string id: string
clientId: string clientId: string
@@ -36,16 +104,6 @@ export interface Payment {
description: string description: string
} }
export interface Attendance {
id: string
clientId: string
client: Client
checkInTime: Date
checkOutTime?: Date
type: 'gym' | 'class' | 'personal_training'
notes?: string
}
export interface Notification { export interface Notification {
id: string id: string
userId: string userId: string
+11 -3
View File
@@ -1,9 +1,17 @@
export const formatDate = (date: Date): string => { import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export const formatDate = (date: Date | string): string => {
const d = new Date(date)
return new Intl.DateTimeFormat('en-US', { return new Intl.DateTimeFormat('en-US', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'long',
day: 'numeric', day: 'numeric',
}).format(date) }).format(d)
} }
export const formatCurrency = ( export const formatCurrency = (