Compare commits
4 Commits
d3f733638e
...
ffa00654d8
Author | SHA1 | Date | |
---|---|---|---|
ffa00654d8 | |||
99d531ba8c | |||
6426e333f9 | |||
68480757dc |
@@ -15,6 +15,15 @@ export default defineConfig({
|
|||||||
'@renderer': resolve('src/renderer/src')
|
'@renderer': resolve('src/renderer/src')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [react()]
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -19,7 +19,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^4.0.0"
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-router-dom": "^7.7.0",
|
||||||
|
"sass": "^1.89.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config": "^2.0.0",
|
"@electron-toolkit/eslint-config": "^2.0.0",
|
||||||
|
317
dashboard/pnpm-lock.yaml
generated
317
dashboard/pnpm-lock.yaml
generated
@@ -14,6 +14,15 @@ importers:
|
|||||||
'@electron-toolkit/utils':
|
'@electron-toolkit/utils':
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0(electron@35.7.2)
|
version: 4.0.0(electron@35.7.2)
|
||||||
|
react-icons:
|
||||||
|
specifier: ^5.5.0
|
||||||
|
version: 5.5.0(react@19.1.0)
|
||||||
|
react-router-dom:
|
||||||
|
specifier: ^7.7.0
|
||||||
|
version: 7.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
sass:
|
||||||
|
specifier: ^1.89.2
|
||||||
|
version: 1.89.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@electron-toolkit/eslint-config':
|
'@electron-toolkit/eslint-config':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
@@ -23,7 +32,7 @@ importers:
|
|||||||
version: 3.0.0(eslint@9.31.0)(prettier@3.6.2)
|
version: 3.0.0(eslint@9.31.0)(prettier@3.6.2)
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.3.4
|
specifier: ^4.3.4
|
||||||
version: 4.7.0(vite@6.3.5(@types/node@24.0.14))
|
version: 4.7.0(vite@6.3.5(@types/node@24.0.14)(sass@1.89.2))
|
||||||
electron:
|
electron:
|
||||||
specifier: ^35.1.5
|
specifier: ^35.1.5
|
||||||
version: 35.7.2
|
version: 35.7.2
|
||||||
@@ -32,7 +41,7 @@ importers:
|
|||||||
version: 25.1.8(electron-builder-squirrel-windows@25.1.8)
|
version: 25.1.8(electron-builder-squirrel-windows@25.1.8)
|
||||||
electron-vite:
|
electron-vite:
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(vite@6.3.5(@types/node@24.0.14))
|
version: 3.1.0(vite@6.3.5(@types/node@24.0.14)(sass@1.89.2))
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.24.0
|
specifier: ^9.24.0
|
||||||
version: 9.31.0
|
version: 9.31.0
|
||||||
@@ -56,7 +65,7 @@ importers:
|
|||||||
version: 19.1.0(react@19.1.0)
|
version: 19.1.0(react@19.1.0)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.2.6
|
specifier: ^6.2.6
|
||||||
version: 6.3.5(@types/node@24.0.14)
|
version: 6.3.5(@types/node@24.0.14)(sass@1.89.2)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -467,6 +476,88 @@ packages:
|
|||||||
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
|
||||||
deprecated: This functionality has been moved to @npmcli/fs
|
deprecated: This functionality has been moved to @npmcli/fs
|
||||||
|
|
||||||
|
'@parcel/watcher-android-arm64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-x64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||||
|
resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||||
|
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||||
|
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||||
|
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||||
|
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||||
|
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-arm64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-ia32@2.5.1':
|
||||||
|
resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-x64@2.5.1':
|
||||||
|
resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@parcel/watcher@2.5.1':
|
||||||
|
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -823,6 +914,10 @@ packages:
|
|||||||
brace-expansion@2.0.2:
|
brace-expansion@2.0.2:
|
||||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||||
|
|
||||||
|
braces@3.0.3:
|
||||||
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
browserslist@4.25.1:
|
browserslist@4.25.1:
|
||||||
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
|
resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==}
|
||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
@@ -883,6 +978,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
chokidar@4.0.3:
|
||||||
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
chownr@2.0.0:
|
chownr@2.0.0:
|
||||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -960,6 +1059,10 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
cookie@1.0.2:
|
||||||
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
core-util-is@1.0.2:
|
core-util-is@1.0.2:
|
||||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
||||||
|
|
||||||
@@ -1032,6 +1135,11 @@ packages:
|
|||||||
delegates@1.0.0:
|
delegates@1.0.0:
|
||||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||||
|
|
||||||
|
detect-libc@1.0.3:
|
||||||
|
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
detect-libc@2.0.4:
|
detect-libc@2.0.4:
|
||||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1293,6 +1401,10 @@ packages:
|
|||||||
filelist@1.0.4:
|
filelist@1.0.4:
|
||||||
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
|
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
|
||||||
|
|
||||||
|
fill-range@7.1.1:
|
||||||
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
find-up@5.0.0:
|
find-up@5.0.0:
|
||||||
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1506,6 +1618,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
immutable@5.1.3:
|
||||||
|
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1611,6 +1726,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
|
resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-number@7.0.0:
|
||||||
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
is-regex@1.2.1:
|
is-regex@1.2.1:
|
||||||
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1802,6 +1921,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
micromatch@4.0.8:
|
||||||
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
mime-db@1.52.0:
|
mime-db@1.52.0:
|
||||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -1908,6 +2031,9 @@ packages:
|
|||||||
node-addon-api@1.7.2:
|
node-addon-api@1.7.2:
|
||||||
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
|
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
node-api-version@0.2.1:
|
node-api-version@0.2.1:
|
||||||
resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==}
|
resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==}
|
||||||
|
|
||||||
@@ -2036,6 +2162,10 @@ packages:
|
|||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
picomatch@2.3.1:
|
||||||
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
picomatch@4.0.3:
|
picomatch@4.0.3:
|
||||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2103,6 +2233,11 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.1.0
|
react: ^19.1.0
|
||||||
|
|
||||||
|
react-icons@5.5.0:
|
||||||
|
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '*'
|
||||||
|
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
@@ -2110,6 +2245,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
react-router-dom@7.7.0:
|
||||||
|
resolution: {integrity: sha512-wwGS19VkNBkneVh9/YD0pK3IsjWxQUVMDD6drlG7eJpo1rXBtctBqDyBm/k+oKHRAm1x9XWT3JFC82QI9YOXXA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18'
|
||||||
|
react-dom: '>=18'
|
||||||
|
|
||||||
|
react-router@7.7.0:
|
||||||
|
resolution: {integrity: sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18'
|
||||||
|
react-dom: '>=18'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react@19.1.0:
|
react@19.1.0:
|
||||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2128,6 +2280,10 @@ packages:
|
|||||||
readdir-glob@1.1.3:
|
readdir-glob@1.1.3:
|
||||||
resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==}
|
resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==}
|
||||||
|
|
||||||
|
readdirp@4.1.2:
|
||||||
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2204,6 +2360,11 @@ packages:
|
|||||||
sanitize-filename@1.6.3:
|
sanitize-filename@1.6.3:
|
||||||
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
|
resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==}
|
||||||
|
|
||||||
|
sass@1.89.2:
|
||||||
|
resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
sax@1.4.1:
|
sax@1.4.1:
|
||||||
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
||||||
|
|
||||||
@@ -2229,6 +2390,9 @@ packages:
|
|||||||
set-blocking@2.0.0:
|
set-blocking@2.0.0:
|
||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
|
set-cookie-parser@2.7.1:
|
||||||
|
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2401,6 +2565,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
|
resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
|
|
||||||
|
to-regex-range@5.0.1:
|
||||||
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
truncate-utf8-bytes@1.0.2:
|
truncate-utf8-bytes@1.0.2:
|
||||||
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
||||||
|
|
||||||
@@ -3007,6 +3175,67 @@ snapshots:
|
|||||||
mkdirp: 1.0.4
|
mkdirp: 1.0.4
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
|
|
||||||
|
'@parcel/watcher-android-arm64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-darwin-x64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-arm64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-ia32@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher-win32-x64@2.5.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@parcel/watcher@2.5.1':
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 1.0.3
|
||||||
|
is-glob: 4.0.3
|
||||||
|
micromatch: 4.0.8
|
||||||
|
node-addon-api: 7.1.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@parcel/watcher-android-arm64': 2.5.1
|
||||||
|
'@parcel/watcher-darwin-arm64': 2.5.1
|
||||||
|
'@parcel/watcher-darwin-x64': 2.5.1
|
||||||
|
'@parcel/watcher-freebsd-x64': 2.5.1
|
||||||
|
'@parcel/watcher-linux-arm-glibc': 2.5.1
|
||||||
|
'@parcel/watcher-linux-arm-musl': 2.5.1
|
||||||
|
'@parcel/watcher-linux-arm64-glibc': 2.5.1
|
||||||
|
'@parcel/watcher-linux-arm64-musl': 2.5.1
|
||||||
|
'@parcel/watcher-linux-x64-glibc': 2.5.1
|
||||||
|
'@parcel/watcher-linux-x64-musl': 2.5.1
|
||||||
|
'@parcel/watcher-win32-arm64': 2.5.1
|
||||||
|
'@parcel/watcher-win32-ia32': 2.5.1
|
||||||
|
'@parcel/watcher-win32-x64': 2.5.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -3156,7 +3385,7 @@ snapshots:
|
|||||||
'@types/node': 22.16.4
|
'@types/node': 22.16.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@24.0.14))':
|
'@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@24.0.14)(sass@1.89.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.0
|
'@babel/core': 7.28.0
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0)
|
||||||
@@ -3164,7 +3393,7 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
vite: 6.3.5(@types/node@24.0.14)
|
vite: 6.3.5(@types/node@24.0.14)(sass@1.89.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -3408,6 +3637,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
|
braces@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
fill-range: 7.1.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
browserslist@4.25.1:
|
browserslist@4.25.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001727
|
caniuse-lite: 1.0.30001727
|
||||||
@@ -3515,6 +3749,10 @@ snapshots:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
chokidar@4.0.3:
|
||||||
|
dependencies:
|
||||||
|
readdirp: 4.1.2
|
||||||
|
|
||||||
chownr@2.0.0: {}
|
chownr@2.0.0: {}
|
||||||
|
|
||||||
chromium-pickle-js@0.2.0: {}
|
chromium-pickle-js@0.2.0: {}
|
||||||
@@ -3581,6 +3819,8 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
core-util-is@1.0.2:
|
core-util-is@1.0.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -3654,6 +3894,9 @@ snapshots:
|
|||||||
|
|
||||||
delegates@1.0.0: {}
|
delegates@1.0.0: {}
|
||||||
|
|
||||||
|
detect-libc@1.0.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
detect-libc@2.0.4: {}
|
detect-libc@2.0.4: {}
|
||||||
|
|
||||||
detect-node@2.1.0:
|
detect-node@2.1.0:
|
||||||
@@ -3755,7 +3998,7 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.187: {}
|
electron-to-chromium@1.5.187: {}
|
||||||
|
|
||||||
electron-vite@3.1.0(vite@6.3.5(@types/node@24.0.14)):
|
electron-vite@3.1.0(vite@6.3.5(@types/node@24.0.14)(sass@1.89.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.0
|
'@babel/core': 7.28.0
|
||||||
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0)
|
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0)
|
||||||
@@ -3763,7 +4006,7 @@ snapshots:
|
|||||||
esbuild: 0.25.6
|
esbuild: 0.25.6
|
||||||
magic-string: 0.30.17
|
magic-string: 0.30.17
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
vite: 6.3.5(@types/node@24.0.14)
|
vite: 6.3.5(@types/node@24.0.14)(sass@1.89.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -4078,6 +4321,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimatch: 5.1.6
|
minimatch: 5.1.6
|
||||||
|
|
||||||
|
fill-range@7.1.1:
|
||||||
|
dependencies:
|
||||||
|
to-regex-range: 5.0.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
find-up@5.0.0:
|
find-up@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
locate-path: 6.0.0
|
locate-path: 6.0.0
|
||||||
@@ -4348,6 +4596,8 @@ snapshots:
|
|||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
|
immutable@5.1.3: {}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
@@ -4453,6 +4703,9 @@ snapshots:
|
|||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
|
is-number@7.0.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
is-regex@1.2.1:
|
is-regex@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
@@ -4653,6 +4906,12 @@ snapshots:
|
|||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
|
micromatch@4.0.8:
|
||||||
|
dependencies:
|
||||||
|
braces: 3.0.3
|
||||||
|
picomatch: 2.3.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
mime-db@1.52.0: {}
|
mime-db@1.52.0: {}
|
||||||
|
|
||||||
mime-types@2.1.35:
|
mime-types@2.1.35:
|
||||||
@@ -4739,6 +4998,9 @@ snapshots:
|
|||||||
node-addon-api@1.7.2:
|
node-addon-api@1.7.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-addon-api@7.1.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-api-version@0.2.1:
|
node-api-version@0.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.2
|
semver: 7.7.2
|
||||||
@@ -4887,6 +5149,9 @@ snapshots:
|
|||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
|
picomatch@2.3.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
|
|
||||||
plist@3.1.0:
|
plist@3.1.0:
|
||||||
@@ -4942,10 +5207,28 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
scheduler: 0.26.0
|
scheduler: 0.26.0
|
||||||
|
|
||||||
|
react-icons@5.5.0(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-refresh@0.17.0: {}
|
react-refresh@0.17.0: {}
|
||||||
|
|
||||||
|
react-router-dom@7.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
react-router: 7.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
||||||
|
react-router@7.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
cookie: 1.0.2
|
||||||
|
react: 19.1.0
|
||||||
|
set-cookie-parser: 2.7.1
|
||||||
|
optionalDependencies:
|
||||||
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
|
|
||||||
react@19.1.0: {}
|
react@19.1.0: {}
|
||||||
|
|
||||||
read-binary-file-arch@1.0.6:
|
read-binary-file-arch@1.0.6:
|
||||||
@@ -4974,6 +5257,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimatch: 5.1.6
|
minimatch: 5.1.6
|
||||||
|
|
||||||
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@@ -5090,6 +5375,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
truncate-utf8-bytes: 1.0.2
|
truncate-utf8-bytes: 1.0.2
|
||||||
|
|
||||||
|
sass@1.89.2:
|
||||||
|
dependencies:
|
||||||
|
chokidar: 4.0.3
|
||||||
|
immutable: 5.1.3
|
||||||
|
source-map-js: 1.2.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@parcel/watcher': 2.5.1
|
||||||
|
|
||||||
sax@1.4.1: {}
|
sax@1.4.1: {}
|
||||||
|
|
||||||
scheduler@0.26.0: {}
|
scheduler@0.26.0: {}
|
||||||
@@ -5108,6 +5401,8 @@ snapshots:
|
|||||||
|
|
||||||
set-blocking@2.0.0: {}
|
set-blocking@2.0.0: {}
|
||||||
|
|
||||||
|
set-cookie-parser@2.7.1: {}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-data-property: 1.1.4
|
define-data-property: 1.1.4
|
||||||
@@ -5339,6 +5634,11 @@ snapshots:
|
|||||||
|
|
||||||
tmp@0.2.3: {}
|
tmp@0.2.3: {}
|
||||||
|
|
||||||
|
to-regex-range@5.0.1:
|
||||||
|
dependencies:
|
||||||
|
is-number: 7.0.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
truncate-utf8-bytes@1.0.2:
|
truncate-utf8-bytes@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
utf8-byte-length: 1.0.5
|
utf8-byte-length: 1.0.5
|
||||||
@@ -5429,7 +5729,7 @@ snapshots:
|
|||||||
extsprintf: 1.4.1
|
extsprintf: 1.4.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
vite@6.3.5(@types/node@24.0.14):
|
vite@6.3.5(@types/node@24.0.14)(sass@1.89.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.6
|
esbuild: 0.25.6
|
||||||
fdir: 6.4.6(picomatch@4.0.3)
|
fdir: 6.4.6(picomatch@4.0.3)
|
||||||
@@ -5440,6 +5740,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.0.14
|
'@types/node': 24.0.14
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
sass: 1.89.2
|
||||||
|
|
||||||
wcwidth@1.0.1:
|
wcwidth@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<title>OpenWall</title>
|
<title>OpenWall</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
content="default-src *; script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'; img-src * data: blob:; frame-src *; connect-src *; font-src *; media-src *; object-src *; child-src *;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@@ -1,9 +1,38 @@
|
|||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||||
|
import Header from './common/Header'
|
||||||
|
import Calendar from './pages/Calendar'
|
||||||
|
import Shopping from './pages/Shopping'
|
||||||
|
import Notes from './pages/Notes'
|
||||||
|
import Search from './pages/Search'
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const BACKGROUND_IMAGE_URL = 'https://cdn.pixabay.com/photo/2018/11/19/03/26/iceland-3824494_1280.jpg'
|
||||||
|
|
||||||
|
const APP_BACKGROUND_STYLE = {
|
||||||
|
backgroundImage: `url(${BACKGROUND_IMAGE_URL})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundAttachment: 'fixed',
|
||||||
|
minHeight: '100vh'
|
||||||
|
}
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Router>
|
||||||
<h1>Not implemented yet</h1>
|
<div className="app" style={APP_BACKGROUND_STYLE}>
|
||||||
</>
|
<Header />
|
||||||
|
<main className="app__main">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Calendar />} />
|
||||||
|
<Route path="/shopping" element={<Shopping />} />
|
||||||
|
<Route path="/notes" element={<Notes />} />
|
||||||
|
<Route path="/search" element={<Search />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App
|
119
dashboard/src/renderer/src/common/Header/Header.jsx
Normal file
119
dashboard/src/renderer/src/common/Header/Header.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
import { FiCalendar, FiShoppingBag, FiBookOpen, FiSearch } from 'react-icons/fi'
|
||||||
|
import './styles.sass'
|
||||||
|
|
||||||
|
// Static color configuration
|
||||||
|
const HEADER_COLORS = {
|
||||||
|
primary: 'rgba(76, 175, 160, 0.25)',
|
||||||
|
secondary: 'rgba(34, 139, 156, 0.35)',
|
||||||
|
tertiary: 'rgba(21, 94, 117, 0.4)',
|
||||||
|
accent: 'rgba(147, 197, 253, 0.3)'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon-specific colors for better differentiation
|
||||||
|
const ICON_COLORS = {
|
||||||
|
calendar: '#FF6B6B', // Red
|
||||||
|
shopping: '#4ECDC4', // Teal
|
||||||
|
notes: '#45B7D1', // Blue
|
||||||
|
search: '#FFA726' // Orange
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const [currentTime, setCurrentTime] = useState(new Date())
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCurrentTime(new Date())
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const formatTime = (date) => {
|
||||||
|
return date.toLocaleTimeString('de-DE', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCurrentMonth = (date) => {
|
||||||
|
return date.toLocaleDateString('de-DE', { month: 'long' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationItems = [
|
||||||
|
{ id: 'calendar', icon: FiCalendar, label: 'Kalender', path: '/' },
|
||||||
|
{ id: 'shopping', icon: FiShoppingBag, label: 'Einkaufen', path: '/shopping' },
|
||||||
|
{ id: 'notes', icon: FiBookOpen, label: 'Notizen', path: '/notes' },
|
||||||
|
{ id: 'search', icon: FiSearch, label: 'Suchen', path: '/search' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const getActiveIcon = () => {
|
||||||
|
const currentPath = location.pathname
|
||||||
|
const activeItem = navigationItems.find(item => item.path === currentPath)
|
||||||
|
return activeItem ? activeItem.id : 'calendar'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNavigation = (path) => {
|
||||||
|
navigate(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerStyle = {
|
||||||
|
background: `linear-gradient(135deg, ${HEADER_COLORS.primary}, ${HEADER_COLORS.secondary}, ${HEADER_COLORS.tertiary})`,
|
||||||
|
borderColor: HEADER_COLORS.accent
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonStyle = {
|
||||||
|
background: `linear-gradient(135deg, ${HEADER_COLORS.accent}, ${HEADER_COLORS.primary})`,
|
||||||
|
borderColor: HEADER_COLORS.accent,
|
||||||
|
color: '#ffffff',
|
||||||
|
boxShadow: `0 8px 32px ${HEADER_COLORS.tertiary}, inset 0 1px 0 ${HEADER_COLORS.accent}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeButtonStyle = {
|
||||||
|
background: `linear-gradient(135deg, ${HEADER_COLORS.primary}, ${HEADER_COLORS.secondary})`,
|
||||||
|
borderColor: HEADER_COLORS.primary,
|
||||||
|
color: '#ffffff',
|
||||||
|
boxShadow: `0 12px 40px ${HEADER_COLORS.primary}, inset 0 2px 0 ${HEADER_COLORS.accent}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="header" style={headerStyle}>
|
||||||
|
<nav className="header__nav">
|
||||||
|
<div className="header__left">
|
||||||
|
<span className="header__month">{getCurrentMonth(currentTime)}</span>
|
||||||
|
<span className="header__time">{formatTime(currentTime)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="header__right">
|
||||||
|
{navigationItems.map(({ id, icon: Icon, label, path }) => {
|
||||||
|
const isActive = getActiveIcon() === id
|
||||||
|
const iconColor = ICON_COLORS[id]
|
||||||
|
|
||||||
|
const buttonStyleWithIcon = {
|
||||||
|
...(isActive ? activeButtonStyle : buttonStyle),
|
||||||
|
'--icon-color': iconColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
className={`header__nav-item ${isActive ? 'header__nav-item--active' : ''}`}
|
||||||
|
onClick={() => handleNavigation(path)}
|
||||||
|
aria-label={label}
|
||||||
|
title={label}
|
||||||
|
style={buttonStyleWithIcon}
|
||||||
|
>
|
||||||
|
<Icon style={{ color: iconColor, filter: 'drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))' }} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header
|
1
dashboard/src/renderer/src/common/Header/index.js
Normal file
1
dashboard/src/renderer/src/common/Header/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Header'
|
161
dashboard/src/renderer/src/common/Header/styles.sass
Normal file
161
dashboard/src/renderer/src/common/Header/styles.sass
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
// Header Component - Dynamic Color Extraction from Background
|
||||||
|
.header
|
||||||
|
position: fixed
|
||||||
|
top: 0.5rem
|
||||||
|
left: 0.5rem
|
||||||
|
right: 0.5rem
|
||||||
|
z-index: 1000
|
||||||
|
padding: 1rem 2rem
|
||||||
|
backdrop-filter: blur(80px) saturate(180%)
|
||||||
|
border-radius: 24px
|
||||||
|
border: 1px solid
|
||||||
|
box-shadow: 0 20px 80px rgba(0, 0, 0, 0.15), 0 8px 32px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.4)
|
||||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
backdrop-filter: blur(100px) saturate(200%)
|
||||||
|
box-shadow: 0 24px 96px rgba(0, 0, 0, 0.25), 0 12px 48px rgba(0, 0, 0, 0.15), inset 0 2px 0 rgba(255, 255, 255, 0.5)
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
|
&__nav
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
max-width: 100%
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
&__left
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: flex-start
|
||||||
|
gap: 0.125rem
|
||||||
|
|
||||||
|
&__month
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: 700
|
||||||
|
color: rgba(255, 255, 255, 0.95)
|
||||||
|
letter-spacing: -0.02em
|
||||||
|
text-shadow: 0 2px 8px rgba(21, 94, 117, 0.6), 0 1px 2px rgba(0, 0, 0, 0.3)
|
||||||
|
line-height: 1.1
|
||||||
|
|
||||||
|
&__time
|
||||||
|
font-size: 2.25rem
|
||||||
|
font-weight: 800
|
||||||
|
color: #ffffff
|
||||||
|
font-variant-numeric: tabular-nums
|
||||||
|
text-shadow: 0 3px 12px rgba(21, 94, 117, 0.8), 0 1px 4px rgba(0, 0, 0, 0.4)
|
||||||
|
line-height: 0.9
|
||||||
|
letter-spacing: -0.05em
|
||||||
|
|
||||||
|
&__right
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
&__nav-item
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
width: 56px
|
||||||
|
height: 56px
|
||||||
|
border: none
|
||||||
|
border-radius: 18px
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
backdrop-filter: blur(40px)
|
||||||
|
border: 1px solid
|
||||||
|
font-size: 1.375rem
|
||||||
|
position: relative
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
// Allow custom icon colors
|
||||||
|
svg
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&::before
|
||||||
|
content: ''
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
opacity: 0
|
||||||
|
transition: opacity 0.3s ease
|
||||||
|
border-radius: 18px
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-3px) scale(1.08)
|
||||||
|
|
||||||
|
svg
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)) !important
|
||||||
|
|
||||||
|
&::before
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
&:active
|
||||||
|
transform: translateY(-1px) scale(1.03)
|
||||||
|
|
||||||
|
&--active
|
||||||
|
border: 1px solid
|
||||||
|
|
||||||
|
svg
|
||||||
|
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5)) !important
|
||||||
|
|
||||||
|
&::before
|
||||||
|
opacity: 1
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-3px) scale(1.08)
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px)
|
||||||
|
.header
|
||||||
|
top: 0.375rem
|
||||||
|
left: 0.375rem
|
||||||
|
right: 0.375rem
|
||||||
|
padding: 0.875rem 1.75rem
|
||||||
|
border-radius: 20px
|
||||||
|
|
||||||
|
&__left
|
||||||
|
gap: 0.0625rem
|
||||||
|
|
||||||
|
&__month
|
||||||
|
font-size: 1.25rem
|
||||||
|
|
||||||
|
&__time
|
||||||
|
font-size: 1.875rem
|
||||||
|
|
||||||
|
&__right
|
||||||
|
gap: 0.875rem
|
||||||
|
|
||||||
|
&__nav-item
|
||||||
|
width: 52px
|
||||||
|
height: 52px
|
||||||
|
border-radius: 16px
|
||||||
|
font-size: 1.25rem
|
||||||
|
|
||||||
|
@media (max-width: 480px)
|
||||||
|
.header
|
||||||
|
top: 0.25rem
|
||||||
|
left: 0.25rem
|
||||||
|
right: 0.25rem
|
||||||
|
padding: 0.75rem 1.5rem
|
||||||
|
border-radius: 18px
|
||||||
|
|
||||||
|
&__left
|
||||||
|
gap: 0.0625rem
|
||||||
|
|
||||||
|
&__month
|
||||||
|
font-size: 1.125rem
|
||||||
|
|
||||||
|
&__time
|
||||||
|
font-size: 1.625rem
|
||||||
|
|
||||||
|
&__right
|
||||||
|
gap: 0.75rem
|
||||||
|
|
||||||
|
&__nav-item
|
||||||
|
width: 48px
|
||||||
|
height: 48px
|
||||||
|
border-radius: 14px
|
||||||
|
font-size: 1.125rem
|
71
dashboard/src/renderer/src/index.sass
Normal file
71
dashboard/src/renderer/src/index.sass
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Global Styles - Light Mode with Waterfall Background
|
||||||
|
// Reset and base styles
|
||||||
|
*
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
html, body
|
||||||
|
height: 100vh
|
||||||
|
width: 100vw
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
body
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif
|
||||||
|
background: linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.3)), url('https://cdn.pixabay.com/photo/2024/07/16/23/33/waterfall-8900207_1280.png')
|
||||||
|
background-size: cover
|
||||||
|
background-position: center
|
||||||
|
background-attachment: fixed
|
||||||
|
color: #1e293b
|
||||||
|
font-weight: 400
|
||||||
|
letter-spacing: -0.01em
|
||||||
|
line-height: 1.5
|
||||||
|
-webkit-font-smoothing: antialiased
|
||||||
|
-moz-osx-font-smoothing: grayscale
|
||||||
|
|
||||||
|
#root
|
||||||
|
height: 100vh
|
||||||
|
width: 100vw
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
// App Layout Styles
|
||||||
|
.app
|
||||||
|
height: 100vh
|
||||||
|
width: 100vw
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
&__main
|
||||||
|
flex: 1
|
||||||
|
padding-top: 5rem
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
&__content
|
||||||
|
text-align: center
|
||||||
|
backdrop-filter: blur(40px) saturate(180%)
|
||||||
|
background: rgba(255, 255, 255, 0.25)
|
||||||
|
padding: 4rem
|
||||||
|
border-radius: 32px
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.1), 0 8px 32px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.4)
|
||||||
|
max-width: 600px
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 3rem
|
||||||
|
font-weight: 800
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
background: linear-gradient(135deg, #1e293b, #475569, #0f766e)
|
||||||
|
-webkit-background-clip: text
|
||||||
|
-webkit-text-fill-color: transparent
|
||||||
|
background-clip: text
|
||||||
|
line-height: 1.2
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 1.25rem
|
||||||
|
color: rgba(30, 41, 59, 0.8)
|
||||||
|
font-weight: 500
|
||||||
|
line-height: 1.6
|
@@ -1,5 +1,6 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.sass'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
|
9
dashboard/src/renderer/src/pages/Calendar.jsx
Normal file
9
dashboard/src/renderer/src/pages/Calendar.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const Calendar = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Kalender</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Calendar
|
203
dashboard/src/renderer/src/pages/Notes.jsx
Normal file
203
dashboard/src/renderer/src/pages/Notes.jsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { useRef, useEffect, useState } from 'react'
|
||||||
|
import { FiEdit3, FiTrash, FiChevronDown } from 'react-icons/fi'
|
||||||
|
import { TbEraser } from 'react-icons/tb'
|
||||||
|
import './Notes.sass'
|
||||||
|
|
||||||
|
const Notes = () => {
|
||||||
|
const canvasRef = useRef(null)
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false)
|
||||||
|
const [tool, setTool] = useState('pen') // 'pen' or 'eraser'
|
||||||
|
const [penColor, setPenColor] = useState('#2d3748')
|
||||||
|
const [showColorPicker, setShowColorPicker] = useState(false)
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
'#2d3748', // Dark gray
|
||||||
|
'#000000', // Black
|
||||||
|
'#e53e3e', // Red
|
||||||
|
'#3182ce', // Blue
|
||||||
|
'#38a169', // Green
|
||||||
|
'#d69e2e', // Yellow
|
||||||
|
'#805ad5', // Purple
|
||||||
|
'#dd6b20', // Orange
|
||||||
|
'#e91e63', // Pink
|
||||||
|
'#00acc1' // Cyan
|
||||||
|
]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
// Set canvas size
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
const container = canvas.parentElement
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
canvas.width = container.clientWidth
|
||||||
|
canvas.height = container.clientHeight
|
||||||
|
|
||||||
|
// Set drawing properties
|
||||||
|
ctx.lineCap = 'round'
|
||||||
|
ctx.lineJoin = 'round'
|
||||||
|
ctx.imageSmoothingEnabled = true
|
||||||
|
|
||||||
|
// Restore canvas content after resize
|
||||||
|
ctx.putImageData(imageData, 0, 0)
|
||||||
|
|
||||||
|
// Load saved canvas data
|
||||||
|
loadCanvasFromStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCanvas()
|
||||||
|
window.addEventListener('resize', resizeCanvas)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', resizeCanvas)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const saveCanvasToStorage = () => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
const dataURL = canvas.toDataURL()
|
||||||
|
localStorage.setItem('notes-canvas', dataURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCanvasFromStorage = () => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
const savedData = localStorage.getItem('notes-canvas')
|
||||||
|
|
||||||
|
if (savedData) {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
ctx.drawImage(img, 0, 0)
|
||||||
|
}
|
||||||
|
img.src = savedData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDrawing = (e) => {
|
||||||
|
setIsDrawing(true)
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
|
||||||
|
const x = e.clientX - rect.left
|
||||||
|
const y = e.clientY - rect.top
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const draw = (e) => {
|
||||||
|
if (!isDrawing) return
|
||||||
|
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
|
||||||
|
const x = e.clientX - rect.left
|
||||||
|
const y = e.clientY - rect.top
|
||||||
|
|
||||||
|
if (tool === 'pen') {
|
||||||
|
ctx.globalCompositeOperation = 'source-over'
|
||||||
|
ctx.strokeStyle = penColor
|
||||||
|
ctx.lineWidth = 3
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
ctx.stroke()
|
||||||
|
} else if (tool === 'eraser') {
|
||||||
|
ctx.globalCompositeOperation = 'destination-out'
|
||||||
|
ctx.lineWidth = 40 // Increased eraser size
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopDrawing = () => {
|
||||||
|
setIsDrawing(false)
|
||||||
|
// Save canvas state after drawing
|
||||||
|
saveCanvasToStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearCanvas = () => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
// Clear saved state
|
||||||
|
localStorage.removeItem('notes-canvas')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notes-page">
|
||||||
|
<div className="notes-container">
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className="notes-toolbar">
|
||||||
|
<div className="pen-tool-group">
|
||||||
|
<button
|
||||||
|
className={`tool-button ${tool === 'pen' ? 'active' : ''}`}
|
||||||
|
onClick={() => setTool('pen')}
|
||||||
|
title="Stift"
|
||||||
|
style={{ backgroundColor: tool === 'pen' ? penColor + '20' : undefined }}
|
||||||
|
>
|
||||||
|
<FiEdit3 style={{ color: tool === 'pen' ? penColor : undefined }} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="color-picker-button"
|
||||||
|
onClick={() => setShowColorPicker(!showColorPicker)}
|
||||||
|
title="Farbe wählen"
|
||||||
|
style={{ backgroundColor: penColor }}
|
||||||
|
>
|
||||||
|
<FiChevronDown />
|
||||||
|
</button>
|
||||||
|
{showColorPicker && (
|
||||||
|
<div className="color-picker-popover">
|
||||||
|
<div className="color-grid">
|
||||||
|
{colors.map((color) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
className={`color-option ${penColor === color ? 'active' : ''}`}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
onClick={() => {
|
||||||
|
setPenColor(color)
|
||||||
|
setShowColorPicker(false)
|
||||||
|
}}
|
||||||
|
title={`Farbe: ${color}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`tool-button ${tool === 'eraser' ? 'active' : ''}`}
|
||||||
|
onClick={() => setTool('eraser')}
|
||||||
|
title="Radiergummi"
|
||||||
|
>
|
||||||
|
<TbEraser />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="tool-button clear-button"
|
||||||
|
onClick={clearCanvas}
|
||||||
|
title="Alles löschen"
|
||||||
|
>
|
||||||
|
<FiTrash />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Canvas Container */}
|
||||||
|
<div className="canvas-container">
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="drawing-canvas"
|
||||||
|
onMouseDown={startDrawing}
|
||||||
|
onMouseMove={draw}
|
||||||
|
onMouseUp={stopDrawing}
|
||||||
|
onMouseLeave={stopDrawing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notes
|
249
dashboard/src/renderer/src/pages/Notes.sass
Normal file
249
dashboard/src/renderer/src/pages/Notes.sass
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
// Notes Page - Drawing Canvas with Glassmorphism (Matching Search Page Structure)
|
||||||
|
.notes-page
|
||||||
|
padding: 2rem 0.75rem 0.75rem 0.75rem
|
||||||
|
height: calc(100vh - 6rem)
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.notes-container
|
||||||
|
position: relative
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
backdrop-filter: blur(60px) saturate(200%)
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.2))
|
||||||
|
border-radius: 28px
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5)
|
||||||
|
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.1), 0 8px 32px rgba(0, 0, 0, 0.05), inset 0 2px 0 rgba(255, 255, 255, 0.6), inset 0 -1px 0 rgba(0, 0, 0, 0.1)
|
||||||
|
padding: 1.5rem
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
backdrop-filter: blur(80px) saturate(220%)
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.3))
|
||||||
|
box-shadow: 0 20px 80px rgba(0, 0, 0, 0.15), 0 10px 40px rgba(0, 0, 0, 0.08), inset 0 2px 0 rgba(255, 255, 255, 0.7)
|
||||||
|
transform: translateY(-2px)
|
||||||
|
|
||||||
|
.notes-toolbar
|
||||||
|
position: absolute
|
||||||
|
bottom: 1rem
|
||||||
|
left: 50%
|
||||||
|
transform: translateX(-50%)
|
||||||
|
z-index: 10
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.75rem
|
||||||
|
padding: 0.75rem 1rem
|
||||||
|
backdrop-filter: blur(40px) saturate(180%)
|
||||||
|
background: rgba(255, 255, 255, 0.2)
|
||||||
|
border-radius: 16px
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.4)
|
||||||
|
|
||||||
|
.pen-tool-group
|
||||||
|
position: relative
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.25rem
|
||||||
|
|
||||||
|
.color-picker-button
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
width: 24px
|
||||||
|
height: 44px
|
||||||
|
border: none
|
||||||
|
border-radius: 0 14px 14px 0
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
border-left: none
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
font-size: 0.875rem
|
||||||
|
color: white
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15)
|
||||||
|
|
||||||
|
.color-picker-popover
|
||||||
|
position: absolute
|
||||||
|
bottom: calc(100% + 0.5rem)
|
||||||
|
left: 0
|
||||||
|
backdrop-filter: blur(40px) saturate(180%)
|
||||||
|
background: rgba(255, 255, 255, 0.25)
|
||||||
|
border-radius: 16px
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.4)
|
||||||
|
padding: 0.75rem
|
||||||
|
z-index: 20
|
||||||
|
|
||||||
|
.color-grid
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(5, 1fr)
|
||||||
|
gap: 0.5rem
|
||||||
|
|
||||||
|
.color-option
|
||||||
|
width: 32px
|
||||||
|
height: 32px
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3)
|
||||||
|
border-radius: 12px
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.2s ease
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: scale(1.1)
|
||||||
|
border-color: rgba(255, 255, 255, 0.6)
|
||||||
|
|
||||||
|
&.active
|
||||||
|
transform: scale(1.15)
|
||||||
|
border-color: rgba(255, 255, 255, 0.8)
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2)
|
||||||
|
|
||||||
|
&::after
|
||||||
|
content: '✓'
|
||||||
|
position: absolute
|
||||||
|
top: 50%
|
||||||
|
left: 50%
|
||||||
|
transform: translate(-50%, -50%)
|
||||||
|
color: white
|
||||||
|
font-size: 0.875rem
|
||||||
|
font-weight: bold
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8)
|
||||||
|
|
||||||
|
.toolbar-section
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.75rem
|
||||||
|
|
||||||
|
.tool-button
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
width: 44px
|
||||||
|
height: 44px
|
||||||
|
border: none
|
||||||
|
border-radius: 14px
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
backdrop-filter: blur(20px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
color: rgba(50, 50, 50, 0.8)
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
font-size: 1.125rem
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.5)
|
||||||
|
color: rgba(50, 50, 50, 1)
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15)
|
||||||
|
|
||||||
|
&.active
|
||||||
|
background: rgba(255, 255, 255, 0.6)
|
||||||
|
color: rgba(50, 50, 50, 1)
|
||||||
|
border-color: rgba(255, 255, 255, 0.6)
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2)
|
||||||
|
|
||||||
|
&.clear-button
|
||||||
|
background: rgba(239, 68, 68, 0.3)
|
||||||
|
border-color: rgba(239, 68, 68, 0.4)
|
||||||
|
color: rgba(50, 50, 50, 0.8)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(239, 68, 68, 0.5)
|
||||||
|
color: rgba(50, 50, 50, 1)
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.2)
|
||||||
|
|
||||||
|
// Special styling for pen tool in group
|
||||||
|
.pen-tool-group &:first-child
|
||||||
|
border-radius: 14px 0 0 14px
|
||||||
|
border-right: none
|
||||||
|
|
||||||
|
.canvas-container
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
border-radius: 20px
|
||||||
|
background: rgba(255, 255, 255, 0.9)
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5)
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
.drawing-canvas
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
cursor: crosshair
|
||||||
|
display: block
|
||||||
|
background: #ffffff
|
||||||
|
border-radius: 20px
|
||||||
|
|
||||||
|
&.eraser-mode
|
||||||
|
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="8" fill="none" stroke="black" stroke-width="2"/></svg>') 10 10, auto
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px)
|
||||||
|
.notes-page
|
||||||
|
padding: 1.5rem 0.5rem 0.5rem 0.5rem
|
||||||
|
|
||||||
|
.notes-container
|
||||||
|
border-radius: 24px
|
||||||
|
padding: 1.25rem
|
||||||
|
|
||||||
|
.canvas-container
|
||||||
|
border-radius: 16px
|
||||||
|
|
||||||
|
.drawing-canvas
|
||||||
|
border-radius: 16px
|
||||||
|
|
||||||
|
.notes-toolbar
|
||||||
|
bottom: 0.75rem
|
||||||
|
padding: 0.625rem 0.875rem
|
||||||
|
gap: 0.625rem
|
||||||
|
|
||||||
|
.tool-button
|
||||||
|
width: 40px
|
||||||
|
height: 40px
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
.color-picker-button
|
||||||
|
width: 20px
|
||||||
|
height: 40px
|
||||||
|
|
||||||
|
.color-option
|
||||||
|
width: 28px
|
||||||
|
height: 28px
|
||||||
|
|
||||||
|
@media (max-width: 480px)
|
||||||
|
.notes-page
|
||||||
|
padding: 1rem 0.375rem 0.375rem 0.375rem
|
||||||
|
|
||||||
|
.notes-container
|
||||||
|
border-radius: 20px
|
||||||
|
padding: 1rem
|
||||||
|
|
||||||
|
.canvas-container
|
||||||
|
border-radius: 12px
|
||||||
|
|
||||||
|
.drawing-canvas
|
||||||
|
border-radius: 12px
|
||||||
|
|
||||||
|
.notes-toolbar
|
||||||
|
bottom: 0.5rem
|
||||||
|
padding: 0.5rem 0.75rem
|
||||||
|
gap: 0.5rem
|
||||||
|
border-radius: 14px
|
||||||
|
|
||||||
|
.tool-button
|
||||||
|
width: 36px
|
||||||
|
height: 36px
|
||||||
|
border-radius: 12px
|
||||||
|
font-size: 0.875rem
|
||||||
|
|
||||||
|
.color-picker-button
|
||||||
|
width: 18px
|
||||||
|
height: 36px
|
||||||
|
|
||||||
|
.color-option
|
||||||
|
width: 24px
|
||||||
|
height: 24px
|
||||||
|
|
||||||
|
.color-grid
|
||||||
|
grid-template-columns: repeat(4, 1fr)
|
20
dashboard/src/renderer/src/pages/Search.jsx
Normal file
20
dashboard/src/renderer/src/pages/Search.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import './Search.sass'
|
||||||
|
|
||||||
|
const Search = () => {
|
||||||
|
return (
|
||||||
|
<div className="search-page">
|
||||||
|
<div className="search-container">
|
||||||
|
<div className="google-embed-container">
|
||||||
|
<iframe
|
||||||
|
src="https://www.google.com/search?igu=1"
|
||||||
|
className="google-iframe"
|
||||||
|
title="Google Search"
|
||||||
|
allow="microphone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Search
|
58
dashboard/src/renderer/src/pages/Search.sass
Normal file
58
dashboard/src/renderer/src/pages/Search.sass
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// Search Page - Full Screen Glassmorphism Google Embed
|
||||||
|
.search-page
|
||||||
|
padding: 2rem 0.75rem 0.75rem 0.75rem
|
||||||
|
height: calc(100vh - 6rem)
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.search-container
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
.google-embed-container
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
backdrop-filter: blur(60px) saturate(200%)
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.2))
|
||||||
|
border-radius: 28px
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5)
|
||||||
|
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.1), 0 8px 32px rgba(0, 0, 0, 0.05), inset 0 2px 0 rgba(255, 255, 255, 0.6), inset 0 -1px 0 rgba(0, 0, 0, 0.1)
|
||||||
|
padding: 1.5rem
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
backdrop-filter: blur(80px) saturate(220%)
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.3))
|
||||||
|
box-shadow: 0 20px 80px rgba(0, 0, 0, 0.15), 0 10px 40px rgba(0, 0, 0, 0.08), inset 0 2px 0 rgba(255, 255, 255, 0.7)
|
||||||
|
transform: translateY(-2px)
|
||||||
|
|
||||||
|
.google-iframe
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
border: none
|
||||||
|
border-radius: 20px
|
||||||
|
background: rgba(255, 255, 255, 0.9)
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5)
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px)
|
||||||
|
.search-page
|
||||||
|
padding: 1.5rem 0.5rem 0.5rem 0.5rem
|
||||||
|
|
||||||
|
.google-embed-container
|
||||||
|
border-radius: 24px
|
||||||
|
padding: 1.25rem
|
||||||
|
|
||||||
|
.google-iframe
|
||||||
|
border-radius: 16px
|
||||||
|
|
||||||
|
@media (max-width: 480px)
|
||||||
|
.search-page
|
||||||
|
padding: 1rem 0.375rem 0.375rem 0.375rem
|
||||||
|
|
||||||
|
.google-embed-container
|
||||||
|
border-radius: 20px
|
||||||
|
padding: 1rem
|
||||||
|
|
||||||
|
.google-iframe
|
||||||
|
border-radius: 12px
|
379
dashboard/src/renderer/src/pages/Shopping.jsx
Normal file
379
dashboard/src/renderer/src/pages/Shopping.jsx
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { FaPlus, FaTrash, FaCheck, FaEdit, FaShoppingCart, FaClock } from 'react-icons/fa';
|
||||||
|
import shoppingService from '../services/ShoppingService';
|
||||||
|
import './Shopping.sass';
|
||||||
|
|
||||||
|
const Shopping = () => {
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [initialLoad, setInitialLoad] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [newItem, setNewItem] = useState({ name: '', amount: '1' });
|
||||||
|
const [editingItem, setEditingItem] = useState(null);
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
|
||||||
|
// Load shopping items on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadItems();
|
||||||
|
|
||||||
|
// Set up auto-refresh every 5 seconds
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
loadItems();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Cleanup interval on component unmount
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadItems = async () => {
|
||||||
|
try {
|
||||||
|
if (initialLoad) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
const data = await shoppingService.getItems();
|
||||||
|
setItems(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load shopping items');
|
||||||
|
console.error('Error loading items:', err);
|
||||||
|
} finally {
|
||||||
|
if (initialLoad) {
|
||||||
|
setLoading(false);
|
||||||
|
setInitialLoad(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddItem = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newItem.name.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdItem = await shoppingService.createItem({
|
||||||
|
name: newItem.name.trim(),
|
||||||
|
amount: newItem.amount.trim() || '1'
|
||||||
|
});
|
||||||
|
setItems([createdItem, ...items]);
|
||||||
|
setNewItem({ name: '', amount: '1' });
|
||||||
|
setShowAddForm(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to add item');
|
||||||
|
console.error('Error adding item:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleItem = async (id) => {
|
||||||
|
try {
|
||||||
|
const updatedItem = await shoppingService.toggleItem(id);
|
||||||
|
setItems(items.map(item =>
|
||||||
|
item.id === id ? updatedItem : item
|
||||||
|
));
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to update item');
|
||||||
|
console.error('Error toggling item:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteItem = async (id) => {
|
||||||
|
try {
|
||||||
|
await shoppingService.deleteItem(id);
|
||||||
|
setItems(items.filter(item => item.id !== id));
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to delete item');
|
||||||
|
console.error('Error deleting item:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateItem = async (id, updatedData) => {
|
||||||
|
try {
|
||||||
|
const updatedItem = await shoppingService.updateItem(id, updatedData);
|
||||||
|
setItems(items.map(item =>
|
||||||
|
item.id === id ? updatedItem : item
|
||||||
|
));
|
||||||
|
setEditingItem(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to update item');
|
||||||
|
console.error('Error updating item:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteChecked = async () => {
|
||||||
|
try {
|
||||||
|
await shoppingService.deleteCheckedItems();
|
||||||
|
setItems(items.filter(item => !item.checked));
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to delete checked items');
|
||||||
|
console.error('Error deleting checked items:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeUntilDeletion = (checkedAt) => {
|
||||||
|
if (!checkedAt) return null;
|
||||||
|
const twoHoursLater = new Date(new Date(checkedAt).getTime() + 2 * 60 * 60 * 1000);
|
||||||
|
const now = new Date();
|
||||||
|
const timeDiff = twoHoursLater - now;
|
||||||
|
|
||||||
|
if (timeDiff <= 0) return 'Wird gelöscht...';
|
||||||
|
|
||||||
|
const minutes = Math.floor(timeDiff / (1000 * 60));
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainingMinutes = minutes % 60;
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${remainingMinutes}m`;
|
||||||
|
}
|
||||||
|
return `${minutes}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uncheckedItems = items.filter(item => !item.checked);
|
||||||
|
const checkedItems = items.filter(item => item.checked);
|
||||||
|
|
||||||
|
if (loading && initialLoad) {
|
||||||
|
return (
|
||||||
|
<div className="shopping-container">
|
||||||
|
<div className="loading">
|
||||||
|
<FaShoppingCart className="loading-icon" />
|
||||||
|
<p>Einkaufsliste wird geladen...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="shopping-container">
|
||||||
|
<div className="shopping-header">
|
||||||
|
<h1>
|
||||||
|
<FaShoppingCart />
|
||||||
|
Einkaufsliste
|
||||||
|
</h1>
|
||||||
|
<div className="header-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setShowAddForm(!showAddForm)}
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
Artikel hinzufügen
|
||||||
|
</button>
|
||||||
|
{checkedItems.length > 0 && (
|
||||||
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={handleDeleteChecked}
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
Erledigte löschen ({checkedItems.length})
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="error-message">
|
||||||
|
{error}
|
||||||
|
<button onClick={() => setError(null)}>×</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAddForm && (
|
||||||
|
<form className="add-item-form" onSubmit={handleAddItem}>
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Artikelname..."
|
||||||
|
value={newItem.name}
|
||||||
|
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Menge"
|
||||||
|
value={newItem.amount}
|
||||||
|
onChange={(e) => setNewItem({ ...newItem, amount: e.target.value })}
|
||||||
|
/>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddForm(false);
|
||||||
|
setNewItem({ name: '', amount: '1' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="shopping-lists">
|
||||||
|
{/* Unchecked Items */}
|
||||||
|
<div className="shopping-section">
|
||||||
|
<h2>Zu kaufen ({uncheckedItems.length})</h2>
|
||||||
|
<div className="items-list">
|
||||||
|
{uncheckedItems.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<FaShoppingCart />
|
||||||
|
<p>Keine Artikel auf der Einkaufsliste</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
uncheckedItems.map(item => (
|
||||||
|
<ShoppingItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onToggle={handleToggleItem}
|
||||||
|
onDelete={handleDeleteItem}
|
||||||
|
onUpdate={handleUpdateItem}
|
||||||
|
editingItem={editingItem}
|
||||||
|
setEditingItem={setEditingItem}
|
||||||
|
formatDate={formatDate}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Checked Items */}
|
||||||
|
{checkedItems.length > 0 && (
|
||||||
|
<div className="shopping-section">
|
||||||
|
<h2>Erledigt ({checkedItems.length})</h2>
|
||||||
|
<div className="items-list">
|
||||||
|
{checkedItems.map(item => (
|
||||||
|
<ShoppingItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onToggle={handleToggleItem}
|
||||||
|
onDelete={handleDeleteItem}
|
||||||
|
onUpdate={handleUpdateItem}
|
||||||
|
editingItem={editingItem}
|
||||||
|
setEditingItem={setEditingItem}
|
||||||
|
formatDate={formatDate}
|
||||||
|
getTimeUntilDeletion={getTimeUntilDeletion}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shopping Item Component
|
||||||
|
const ShoppingItem = ({
|
||||||
|
item,
|
||||||
|
onToggle,
|
||||||
|
onDelete,
|
||||||
|
onUpdate,
|
||||||
|
editingItem,
|
||||||
|
setEditingItem,
|
||||||
|
formatDate,
|
||||||
|
getTimeUntilDeletion
|
||||||
|
}) => {
|
||||||
|
const [editData, setEditData] = useState({ name: item.name, amount: item.amount });
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
setEditingItem(item.id);
|
||||||
|
setEditData({ name: item.name, amount: item.amount });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onUpdate(item.id, editData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditingItem(null);
|
||||||
|
setEditData({ name: item.name, amount: item.amount });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEditing = editingItem === item.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`shopping-item ${item.checked ? 'checked' : ''}`}>
|
||||||
|
<button
|
||||||
|
className="check-btn"
|
||||||
|
onClick={() => onToggle(item.id)}
|
||||||
|
>
|
||||||
|
<FaCheck />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="item-content">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="edit-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.name}
|
||||||
|
onChange={(e) => setEditData({ ...editData, name: e.target.value })}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleSave();
|
||||||
|
if (e.key === 'Escape') handleCancel();
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.amount}
|
||||||
|
onChange={(e) => setEditData({ ...editData, amount: e.target.value })}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleSave();
|
||||||
|
if (e.key === 'Escape') handleCancel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="item-main">
|
||||||
|
<span className="item-name">{item.name}</span>
|
||||||
|
<span className="item-amount">{item.amount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="item-meta">
|
||||||
|
<span className="item-date">
|
||||||
|
{formatDate(item.date)}
|
||||||
|
</span>
|
||||||
|
{item.checked && item.checkedAt && getTimeUntilDeletion && (
|
||||||
|
<span className="deletion-timer">
|
||||||
|
<FaClock />
|
||||||
|
{getTimeUntilDeletion(item.checkedAt)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="item-actions">
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<button className="btn btn-primary btn-small" onClick={handleSave}>
|
||||||
|
<FaCheck />
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary btn-small" onClick={handleCancel}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button className="btn btn-secondary btn-small" onClick={handleEdit}>
|
||||||
|
<FaEdit />
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-danger btn-small" onClick={() => onDelete(item.id)}>
|
||||||
|
<FaTrash />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Shopping;
|
477
dashboard/src/renderer/src/pages/Shopping.sass
Normal file
477
dashboard/src/renderer/src/pages/Shopping.sass
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
// Shopping Page - Single Unified Glassmorphism Container
|
||||||
|
.shopping-container
|
||||||
|
position: absolute
|
||||||
|
top: 7rem
|
||||||
|
left: 0.5rem
|
||||||
|
right: 0.5rem
|
||||||
|
bottom: 0.5rem
|
||||||
|
background: rgba(255, 255, 255, 0.35)
|
||||||
|
border-radius: 24px
|
||||||
|
backdrop-filter: blur(40px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.1)
|
||||||
|
padding: 2rem
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
.loading
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
flex: 1
|
||||||
|
color: #1e293b
|
||||||
|
|
||||||
|
.loading-icon
|
||||||
|
font-size: 3rem
|
||||||
|
margin-bottom: 1rem
|
||||||
|
color: #4CAF50
|
||||||
|
animation: spin 2s linear infinite
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 1.1rem
|
||||||
|
margin: 0
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
.shopping-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 2rem
|
||||||
|
padding-bottom: 1.5rem
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
flex-wrap: wrap
|
||||||
|
gap: 1.5rem
|
||||||
|
flex-shrink: 0
|
||||||
|
|
||||||
|
h1
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.75rem
|
||||||
|
margin: 0
|
||||||
|
color: #1e293b
|
||||||
|
font-size: 2.25rem
|
||||||
|
font-weight: 700
|
||||||
|
|
||||||
|
svg
|
||||||
|
color: #4CAF50
|
||||||
|
font-size: 2rem
|
||||||
|
|
||||||
|
.header-actions
|
||||||
|
display: flex
|
||||||
|
gap: 0.75rem
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
.error-message
|
||||||
|
background: rgba(244, 67, 54, 0.1)
|
||||||
|
border: 1px solid rgba(244, 67, 54, 0.3)
|
||||||
|
color: #dc2626
|
||||||
|
padding: 1rem 1.5rem
|
||||||
|
border-radius: 16px
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
border-left: 4px solid #f44336
|
||||||
|
font-weight: 600
|
||||||
|
flex-shrink: 0
|
||||||
|
|
||||||
|
button
|
||||||
|
background: rgba(244, 67, 54, 0.1)
|
||||||
|
border: 1px solid rgba(244, 67, 54, 0.3)
|
||||||
|
font-size: 1.2rem
|
||||||
|
cursor: pointer
|
||||||
|
color: #dc2626
|
||||||
|
padding: 0.5rem
|
||||||
|
border-radius: 8px
|
||||||
|
transition: all 0.3s ease
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(244, 67, 54, 0.2)
|
||||||
|
transform: scale(1.1)
|
||||||
|
|
||||||
|
.add-item-form
|
||||||
|
margin-bottom: 2rem
|
||||||
|
flex-shrink: 0
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
display: flex
|
||||||
|
gap: 1rem
|
||||||
|
align-items: center
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
input
|
||||||
|
flex: 1
|
||||||
|
min-width: 200px
|
||||||
|
padding: 1rem 1.25rem
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5)
|
||||||
|
border-radius: 12px
|
||||||
|
background: rgba(255, 255, 255, 0.6)
|
||||||
|
backdrop-filter: blur(20px)
|
||||||
|
font-size: 1rem
|
||||||
|
color: #1e293b
|
||||||
|
font-weight: 500
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border-color: rgba(76, 175, 80, 0.7)
|
||||||
|
background: rgba(255, 255, 255, 0.8)
|
||||||
|
box-shadow: 0 8px 32px rgba(76, 175, 80, 0.2)
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
|
&::placeholder
|
||||||
|
color: rgba(30, 41, 59, 0.7)
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
.shopping-lists
|
||||||
|
flex: 1
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 2rem
|
||||||
|
overflow-y: auto
|
||||||
|
padding-right: 0.5rem
|
||||||
|
|
||||||
|
&::-webkit-scrollbar
|
||||||
|
width: 6px
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
border-radius: 3px
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
border-radius: 3px
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.5)
|
||||||
|
|
||||||
|
.shopping-section
|
||||||
|
flex-shrink: 0
|
||||||
|
|
||||||
|
h2
|
||||||
|
color: #1e293b
|
||||||
|
margin: 0 0 1.5rem 0
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: 700
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
padding-bottom: 1rem
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
|
|
||||||
|
.empty-state
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
padding: 3rem 2rem
|
||||||
|
color: #64748b
|
||||||
|
text-align: center
|
||||||
|
background: rgba(255, 255, 255, 0.2)
|
||||||
|
border-radius: 16px
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
margin-top: 1rem
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 3rem
|
||||||
|
margin-bottom: 1rem
|
||||||
|
opacity: 0.6
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 1.1rem
|
||||||
|
margin: 0
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
.items-list
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 1rem
|
||||||
|
margin-top: 1rem
|
||||||
|
|
||||||
|
.shopping-item
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 1.25rem
|
||||||
|
padding: 1.5rem
|
||||||
|
background: rgba(255, 255, 255, 0.4)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6)
|
||||||
|
border-radius: 16px
|
||||||
|
transition: all 0.3s ease
|
||||||
|
backdrop-filter: blur(20px)
|
||||||
|
min-height: 80px
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15)
|
||||||
|
background: rgba(255, 255, 255, 0.55)
|
||||||
|
border-color: rgba(255, 255, 255, 0.8)
|
||||||
|
|
||||||
|
&.checked
|
||||||
|
background: rgba(76, 175, 80, 0.15)
|
||||||
|
border-color: rgba(76, 175, 80, 0.4)
|
||||||
|
|
||||||
|
.item-name
|
||||||
|
text-decoration: line-through
|
||||||
|
color: #64748b
|
||||||
|
|
||||||
|
.check-btn
|
||||||
|
background: linear-gradient(135deg, #4CAF50, #388e3c)
|
||||||
|
border-color: #4CAF50
|
||||||
|
color: white
|
||||||
|
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4)
|
||||||
|
|
||||||
|
.check-btn
|
||||||
|
width: 48px
|
||||||
|
height: 48px
|
||||||
|
border-radius: 50%
|
||||||
|
border: 2px solid rgba(76, 175, 80, 0.5)
|
||||||
|
background: rgba(255, 255, 255, 0.6)
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s ease
|
||||||
|
flex-shrink: 0
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
border-color: #4CAF50
|
||||||
|
background: rgba(76, 175, 80, 0.2)
|
||||||
|
transform: scale(1.1)
|
||||||
|
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.3)
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 1.1rem
|
||||||
|
font-weight: 700
|
||||||
|
|
||||||
|
.item-content
|
||||||
|
flex: 1
|
||||||
|
min-width: 0
|
||||||
|
|
||||||
|
.item-main
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 0.75rem
|
||||||
|
|
||||||
|
.item-name
|
||||||
|
font-weight: 700
|
||||||
|
color: #1e293b
|
||||||
|
font-size: 1.15rem
|
||||||
|
flex: 1
|
||||||
|
min-width: 0
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
.item-amount
|
||||||
|
background: rgba(59, 130, 246, 0.2)
|
||||||
|
color: #1e40af
|
||||||
|
padding: 0.5rem 1rem
|
||||||
|
border-radius: 10px
|
||||||
|
font-size: 0.9rem
|
||||||
|
font-weight: 700
|
||||||
|
white-space: nowrap
|
||||||
|
margin-left: 1rem
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.4)
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
|
||||||
|
.item-meta
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
.item-date
|
||||||
|
color: #64748b
|
||||||
|
font-size: 0.9rem
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
.deletion-timer
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
color: #f59e0b
|
||||||
|
font-size: 0.9rem
|
||||||
|
font-weight: 700
|
||||||
|
background: rgba(245, 158, 11, 0.15)
|
||||||
|
padding: 0.375rem 0.75rem
|
||||||
|
border-radius: 8px
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.4)
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 0.8rem
|
||||||
|
|
||||||
|
.edit-form
|
||||||
|
display: flex
|
||||||
|
gap: 1rem
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
input
|
||||||
|
padding: 1rem
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.6)
|
||||||
|
border-radius: 10px
|
||||||
|
font-size: 1rem
|
||||||
|
background: rgba(255, 255, 255, 0.7)
|
||||||
|
backdrop-filter: blur(10px)
|
||||||
|
color: #1e293b
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border-color: rgba(76, 175, 80, 0.7)
|
||||||
|
background: rgba(255, 255, 255, 0.85)
|
||||||
|
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.2)
|
||||||
|
|
||||||
|
input:first-child
|
||||||
|
flex: 2
|
||||||
|
|
||||||
|
input:last-child
|
||||||
|
flex: 1
|
||||||
|
min-width: 100px
|
||||||
|
|
||||||
|
.item-actions
|
||||||
|
display: flex
|
||||||
|
gap: 0.75rem
|
||||||
|
flex-shrink: 0
|
||||||
|
|
||||||
|
.btn
|
||||||
|
padding: 0.875rem 1.75rem
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5)
|
||||||
|
border-radius: 12px
|
||||||
|
cursor: pointer
|
||||||
|
font-size: 0.95rem
|
||||||
|
font-weight: 700
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.75rem
|
||||||
|
transition: all 0.3s ease
|
||||||
|
white-space: nowrap
|
||||||
|
backdrop-filter: blur(20px)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-2px)
|
||||||
|
|
||||||
|
&.btn-primary
|
||||||
|
background: linear-gradient(135deg, rgba(76, 175, 80, 0.4), rgba(56, 142, 60, 0.4))
|
||||||
|
color: white
|
||||||
|
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.2)
|
||||||
|
border-color: rgba(76, 175, 80, 0.5)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: linear-gradient(135deg, rgba(76, 175, 80, 0.6), rgba(56, 142, 60, 0.6))
|
||||||
|
box-shadow: 0 12px 40px rgba(76, 175, 80, 0.3)
|
||||||
|
|
||||||
|
&.btn-secondary
|
||||||
|
background: linear-gradient(135deg, rgba(100, 116, 139, 0.4), rgba(71, 85, 105, 0.4))
|
||||||
|
color: white
|
||||||
|
border-color: rgba(100, 116, 139, 0.5)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: linear-gradient(135deg, rgba(100, 116, 139, 0.6), rgba(71, 85, 105, 0.6))
|
||||||
|
box-shadow: 0 12px 40px rgba(100, 116, 139, 0.3)
|
||||||
|
|
||||||
|
&.btn-danger
|
||||||
|
background: linear-gradient(135deg, rgba(244, 67, 54, 0.4), rgba(211, 47, 47, 0.4))
|
||||||
|
color: white
|
||||||
|
border-color: rgba(244, 67, 54, 0.5)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: linear-gradient(135deg, rgba(244, 67, 54, 0.6), rgba(211, 47, 47, 0.6))
|
||||||
|
box-shadow: 0 12px 40px rgba(244, 67, 54, 0.3)
|
||||||
|
|
||||||
|
&.btn-small
|
||||||
|
padding: 0.625rem 1rem
|
||||||
|
font-size: 0.85rem
|
||||||
|
min-width: 44px
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
svg
|
||||||
|
margin: 0
|
||||||
|
font-size: 0.9rem
|
||||||
|
|
||||||
|
@keyframes spin
|
||||||
|
from
|
||||||
|
transform: rotate(0deg)
|
||||||
|
to
|
||||||
|
transform: rotate(360deg)
|
||||||
|
|
||||||
|
// Enhanced Responsive Design
|
||||||
|
@media (max-width: 768px)
|
||||||
|
.shopping-container
|
||||||
|
top: 6.5rem
|
||||||
|
left: 0.375rem
|
||||||
|
right: 0.375rem
|
||||||
|
bottom: 0.375rem
|
||||||
|
padding: 1.5rem
|
||||||
|
|
||||||
|
.shopping-header
|
||||||
|
flex-direction: column
|
||||||
|
align-items: stretch
|
||||||
|
text-align: center
|
||||||
|
padding-bottom: 1rem
|
||||||
|
|
||||||
|
h1
|
||||||
|
justify-content: center
|
||||||
|
margin-bottom: 1rem
|
||||||
|
font-size: 2rem
|
||||||
|
|
||||||
|
.header-actions
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
.add-item-form
|
||||||
|
.form-group
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
input
|
||||||
|
min-width: auto
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.shopping-item
|
||||||
|
padding: 1.25rem
|
||||||
|
|
||||||
|
.item-content .item-main
|
||||||
|
flex-direction: column
|
||||||
|
align-items: flex-start
|
||||||
|
gap: 0.75rem
|
||||||
|
|
||||||
|
.item-amount
|
||||||
|
margin-left: 0
|
||||||
|
align-self: flex-start
|
||||||
|
|
||||||
|
.item-actions
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
@media (max-width: 480px)
|
||||||
|
.shopping-container
|
||||||
|
top: 6rem
|
||||||
|
left: 0.25rem
|
||||||
|
right: 0.25rem
|
||||||
|
bottom: 0.25rem
|
||||||
|
padding: 1rem
|
||||||
|
|
||||||
|
.shopping-item
|
||||||
|
flex-direction: column
|
||||||
|
align-items: stretch
|
||||||
|
gap: 1.25rem
|
||||||
|
padding: 1rem
|
||||||
|
|
||||||
|
.check-btn
|
||||||
|
align-self: flex-start
|
||||||
|
|
||||||
|
.item-actions
|
||||||
|
flex-direction: row
|
||||||
|
justify-content: flex-end
|
||||||
|
gap: 0.5rem
|
||||||
|
|
||||||
|
.btn
|
||||||
|
padding: 0.75rem 1rem
|
||||||
|
font-size: 0.85rem
|
||||||
|
|
||||||
|
&.btn-small
|
||||||
|
padding: 0.5rem 0.75rem
|
546
dashboard/src/renderer/src/pages/pages.sass
Normal file
546
dashboard/src/renderer/src/pages/pages.sass
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
// Import individual page styles
|
||||||
|
@import './Shopping.sass'
|
||||||
|
|
||||||
|
// Common page container
|
||||||
|
.page-container
|
||||||
|
padding: 2rem
|
||||||
|
min-height: calc(100vh - 6rem)
|
||||||
|
overflow-y: auto
|
||||||
|
|
||||||
|
// Calendar Styles
|
||||||
|
.calendar-container
|
||||||
|
max-width: 1200px
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
.calendar-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 2rem
|
||||||
|
flex-wrap: wrap
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 2rem
|
||||||
|
font-weight: 700
|
||||||
|
color: #1e293b
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.calendar-nav
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 1rem
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
padding: 0.75rem 1.5rem
|
||||||
|
border-radius: 16px
|
||||||
|
backdrop-filter: blur(20px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
|
||||||
|
h2
|
||||||
|
font-size: 1.25rem
|
||||||
|
font-weight: 600
|
||||||
|
color: #1e293b
|
||||||
|
margin: 0
|
||||||
|
min-width: 200px
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
.nav-btn, .add-event-btn
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.2))
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5)
|
||||||
|
border-radius: 12px
|
||||||
|
padding: 0.75rem 1rem
|
||||||
|
color: #475569
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s ease
|
||||||
|
backdrop-filter: blur(20px)
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.4))
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1)
|
||||||
|
|
||||||
|
.add-event-btn
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
font-size: 0.875rem
|
||||||
|
|
||||||
|
.calendar-grid
|
||||||
|
background: rgba(255, 255, 255, 0.25)
|
||||||
|
border-radius: 24px
|
||||||
|
padding: 2rem
|
||||||
|
margin-bottom: 2rem
|
||||||
|
backdrop-filter: blur(40px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.1)
|
||||||
|
|
||||||
|
.calendar-days-header
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(7, 1fr)
|
||||||
|
gap: 1rem
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
.day-header
|
||||||
|
text-align: center
|
||||||
|
font-weight: 600
|
||||||
|
color: #475569
|
||||||
|
padding: 0.75rem
|
||||||
|
|
||||||
|
.calendar-days
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(7, 1fr)
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
.calendar-day
|
||||||
|
aspect-ratio: 1
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
border-radius: 12px
|
||||||
|
cursor: pointer
|
||||||
|
font-weight: 500
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&.empty
|
||||||
|
cursor: default
|
||||||
|
|
||||||
|
&.active
|
||||||
|
background: rgba(255, 255, 255, 0.2)
|
||||||
|
color: #1e293b
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.4)
|
||||||
|
transform: scale(1.05)
|
||||||
|
|
||||||
|
&.today
|
||||||
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(37, 99, 235, 0.3))
|
||||||
|
color: white
|
||||||
|
font-weight: 700
|
||||||
|
|
||||||
|
&.selected
|
||||||
|
background: linear-gradient(135deg, rgba(168, 85, 247, 0.3), rgba(147, 51, 234, 0.3))
|
||||||
|
color: white
|
||||||
|
|
||||||
|
// Notes Styles
|
||||||
|
.notes-container
|
||||||
|
max-width: 1200px
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
.notes-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 2rem
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 2rem
|
||||||
|
font-weight: 700
|
||||||
|
color: #1e293b
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.add-note-btn
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
background: linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(124, 58, 237, 0.3))
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
border-radius: 12px
|
||||||
|
padding: 0.75rem 1.5rem
|
||||||
|
color: white
|
||||||
|
cursor: pointer
|
||||||
|
font-weight: 500
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: linear-gradient(135deg, rgba(139, 92, 246, 0.4), rgba(124, 58, 237, 0.4))
|
||||||
|
transform: translateY(-2px)
|
||||||
|
|
||||||
|
.note-editor
|
||||||
|
background: rgba(255, 255, 255, 0.25)
|
||||||
|
border-radius: 20px
|
||||||
|
padding: 2rem
|
||||||
|
margin-bottom: 2rem
|
||||||
|
backdrop-filter: blur(40px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
.editor-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 1rem
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
.title-input
|
||||||
|
flex: 1
|
||||||
|
padding: 0.75rem 1rem
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
border-radius: 12px
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
color: #1e293b
|
||||||
|
font-size: 1.125rem
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border-color: rgba(59, 130, 246, 0.5)
|
||||||
|
|
||||||
|
.color-picker
|
||||||
|
display: flex
|
||||||
|
gap: 0.5rem
|
||||||
|
|
||||||
|
.color-option
|
||||||
|
width: 32px
|
||||||
|
height: 32px
|
||||||
|
border-radius: 50%
|
||||||
|
border: 2px solid transparent
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&.selected
|
||||||
|
border-color: rgba(255, 255, 255, 0.8)
|
||||||
|
transform: scale(1.1)
|
||||||
|
|
||||||
|
.content-input
|
||||||
|
width: 100%
|
||||||
|
padding: 1rem
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
border-radius: 12px
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
color: #1e293b
|
||||||
|
font-size: 1rem
|
||||||
|
line-height: 1.6
|
||||||
|
resize: vertical
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border-color: rgba(59, 130, 246, 0.5)
|
||||||
|
|
||||||
|
.editor-actions
|
||||||
|
display: flex
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
.save-btn, .cancel-btn
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
padding: 0.75rem 1.5rem
|
||||||
|
border-radius: 12px
|
||||||
|
cursor: pointer
|
||||||
|
font-weight: 500
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
.save-btn
|
||||||
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.3), rgba(5, 150, 105, 0.3))
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
color: white
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.4), rgba(5, 150, 105, 0.4))
|
||||||
|
|
||||||
|
.cancel-btn
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
color: #475569
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.4)
|
||||||
|
|
||||||
|
.notes-grid
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))
|
||||||
|
gap: 1.5rem
|
||||||
|
|
||||||
|
.note-card
|
||||||
|
background: rgba(255, 255, 255, 0.25)
|
||||||
|
border-radius: 20px
|
||||||
|
padding: 1.5rem
|
||||||
|
backdrop-filter: blur(40px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
border-left: 4px solid #3b82f6
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-4px)
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15)
|
||||||
|
|
||||||
|
.note-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: flex-start
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size: 1.125rem
|
||||||
|
font-weight: 600
|
||||||
|
color: #1e293b
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.note-actions
|
||||||
|
display: flex
|
||||||
|
gap: 0.5rem
|
||||||
|
|
||||||
|
button
|
||||||
|
background: transparent
|
||||||
|
border: none
|
||||||
|
color: #64748b
|
||||||
|
cursor: pointer
|
||||||
|
padding: 0.25rem
|
||||||
|
border-radius: 6px
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(100, 116, 139, 0.1)
|
||||||
|
color: #475569
|
||||||
|
|
||||||
|
.note-content
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
p
|
||||||
|
color: #475569
|
||||||
|
line-height: 1.6
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
|
||||||
|
&:last-child
|
||||||
|
margin-bottom: 0
|
||||||
|
|
||||||
|
.note-footer
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.2)
|
||||||
|
padding-top: 1rem
|
||||||
|
|
||||||
|
.note-date
|
||||||
|
font-size: 0.875rem
|
||||||
|
color: #64748b
|
||||||
|
|
||||||
|
// Search Styles
|
||||||
|
.search-container
|
||||||
|
max-width: 800px
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
.search-header
|
||||||
|
margin-bottom: 2rem
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 2rem
|
||||||
|
font-weight: 700
|
||||||
|
color: #1e293b
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
|
||||||
|
.search-input-container
|
||||||
|
position: relative
|
||||||
|
background: rgba(255, 255, 255, 0.25)
|
||||||
|
border-radius: 20px
|
||||||
|
padding: 1.5rem
|
||||||
|
backdrop-filter: blur(40px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
.search-icon
|
||||||
|
position: absolute
|
||||||
|
left: 2.5rem
|
||||||
|
top: 50%
|
||||||
|
transform: translateY(-50%)
|
||||||
|
color: #64748b
|
||||||
|
font-size: 1.25rem
|
||||||
|
|
||||||
|
.search-input
|
||||||
|
width: 100%
|
||||||
|
padding: 1rem 1rem 1rem 3rem
|
||||||
|
border: none
|
||||||
|
background: transparent
|
||||||
|
color: #1e293b
|
||||||
|
font-size: 1.125rem
|
||||||
|
outline: none
|
||||||
|
|
||||||
|
&::placeholder
|
||||||
|
color: rgba(30, 41, 59, 0.6)
|
||||||
|
|
||||||
|
.search-suggestions
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: 1fr 1fr
|
||||||
|
gap: 2rem
|
||||||
|
|
||||||
|
.recent-searches, .quick-actions
|
||||||
|
background: rgba(255, 255, 255, 0.25)
|
||||||
|
border-radius: 20px
|
||||||
|
padding: 1.5rem
|
||||||
|
backdrop-filter: blur(40px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size: 1.125rem
|
||||||
|
font-weight: 600
|
||||||
|
color: #1e293b
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
.recent-list
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 0.75rem
|
||||||
|
|
||||||
|
.recent-item
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 0.75rem
|
||||||
|
padding: 0.75rem 1rem
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
border-radius: 12px
|
||||||
|
color: #475569
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.4)
|
||||||
|
transform: translateX(4px)
|
||||||
|
|
||||||
|
.actions-grid
|
||||||
|
display: grid
|
||||||
|
grid-template-columns: 1fr 1fr
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
.quick-action
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
gap: 0.5rem
|
||||||
|
padding: 1rem
|
||||||
|
background: rgba(255, 255, 255, 0.3)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4)
|
||||||
|
border-radius: 12px
|
||||||
|
color: #475569
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s ease
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.4)
|
||||||
|
transform: translateY(-2px)
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 1.5rem
|
||||||
|
|
||||||
|
span
|
||||||
|
font-size: 0.875rem
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
.search-results
|
||||||
|
.results-header
|
||||||
|
margin-bottom: 1.5rem
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size: 1.125rem
|
||||||
|
font-weight: 600
|
||||||
|
color: #1e293b
|
||||||
|
|
||||||
|
.results-list
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
.result-item
|
||||||
|
display: flex
|
||||||
|
align-items: flex-start
|
||||||
|
gap: 1rem
|
||||||
|
padding: 1.5rem
|
||||||
|
background: rgba(255, 255, 255, 0.25)
|
||||||
|
border-radius: 16px
|
||||||
|
backdrop-filter: blur(40px)
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
cursor: pointer
|
||||||
|
transition: all 0.3s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1)
|
||||||
|
|
||||||
|
.result-icon
|
||||||
|
width: 48px
|
||||||
|
height: 48px
|
||||||
|
border-radius: 12px
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
color: white
|
||||||
|
font-size: 1.25rem
|
||||||
|
|
||||||
|
.result-content
|
||||||
|
flex: 1
|
||||||
|
|
||||||
|
.result-header
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
align-items: center
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
|
||||||
|
h4
|
||||||
|
font-size: 1.125rem
|
||||||
|
font-weight: 600
|
||||||
|
color: #1e293b
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.result-type
|
||||||
|
font-size: 0.875rem
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
.result-description
|
||||||
|
color: #475569
|
||||||
|
line-height: 1.5
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
|
||||||
|
.result-date
|
||||||
|
font-size: 0.875rem
|
||||||
|
color: #64748b
|
||||||
|
|
||||||
|
.no-results
|
||||||
|
text-align: center
|
||||||
|
padding: 3rem
|
||||||
|
color: #64748b
|
||||||
|
|
||||||
|
svg
|
||||||
|
margin-bottom: 1rem
|
||||||
|
opacity: 0.5
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size: 1.25rem
|
||||||
|
font-weight: 600
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 768px)
|
||||||
|
.page-container
|
||||||
|
padding: 1.5rem
|
||||||
|
|
||||||
|
.calendar-header, .shopping-header, .notes-header
|
||||||
|
flex-direction: column
|
||||||
|
align-items: flex-start
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
.search-suggestions
|
||||||
|
grid-template-columns: 1fr
|
||||||
|
|
||||||
|
.actions-grid
|
||||||
|
grid-template-columns: 1fr
|
||||||
|
|
||||||
|
.notes-grid
|
||||||
|
grid-template-columns: 1fr
|
||||||
|
|
||||||
|
.editor-header
|
||||||
|
flex-direction: column
|
||||||
|
align-items: stretch
|
||||||
|
|
||||||
|
@media (max-width: 480px)
|
||||||
|
.page-container
|
||||||
|
padding: 1rem
|
||||||
|
|
||||||
|
.calendar-days
|
||||||
|
gap: 0.5rem
|
||||||
|
|
||||||
|
.input-group
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.result-item
|
||||||
|
padding: 1rem
|
51
dashboard/src/renderer/src/services/ShoppingService.js
Normal file
51
dashboard/src/renderer/src/services/ShoppingService.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import requestUtil from '../utils/RequestUtil';
|
||||||
|
|
||||||
|
class ShoppingService {
|
||||||
|
constructor() {
|
||||||
|
this.endpoint = '/api/shopping';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all shopping items
|
||||||
|
async getItems() {
|
||||||
|
return requestUtil.get(this.endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a specific shopping item
|
||||||
|
async getItem(id) {
|
||||||
|
return requestUtil.get(`${this.endpoint}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new shopping item
|
||||||
|
async createItem(item) {
|
||||||
|
return requestUtil.post(this.endpoint, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a shopping item
|
||||||
|
async updateItem(id, item) {
|
||||||
|
return requestUtil.put(`${this.endpoint}/${id}`, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle checked status of an item
|
||||||
|
async toggleItem(id) {
|
||||||
|
return requestUtil.patch(`${this.endpoint}/${id}/toggle`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a shopping item
|
||||||
|
async deleteItem(id) {
|
||||||
|
return requestUtil.delete(`${this.endpoint}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all checked items
|
||||||
|
async deleteCheckedItems() {
|
||||||
|
return requestUtil.delete(`${this.endpoint}/checked/all`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
async healthCheck() {
|
||||||
|
return requestUtil.get('/api/health');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export a singleton instance
|
||||||
|
const shoppingService = new ShoppingService();
|
||||||
|
export default shoppingService;
|
85
dashboard/src/renderer/src/utils/RequestUtil.js
Normal file
85
dashboard/src/renderer/src/utils/RequestUtil.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
class RequestUtil {
|
||||||
|
constructor() {
|
||||||
|
this.baseURL = this.getBaseURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseURL() {
|
||||||
|
// In production, use the static endpoint, otherwise use proxy
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
return 'https://static.endpoint.com';
|
||||||
|
}
|
||||||
|
return ''; // Uses proxy in development
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(endpoint, options = {}) {
|
||||||
|
const url = `${this.baseURL}${endpoint}`;
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...defaultOptions,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, config);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ error: 'Request failed' }));
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Request failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET request
|
||||||
|
async get(endpoint) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST request
|
||||||
|
async post(endpoint, data = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT request
|
||||||
|
async put(endpoint, data = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH request
|
||||||
|
async patch(endpoint, data = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE request
|
||||||
|
async delete(endpoint) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export a singleton instance
|
||||||
|
const requestUtil = new RequestUtil();
|
||||||
|
export default requestUtil;
|
24
mobile-shopping/.gitignore
vendored
Normal file
24
mobile-shopping/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
29
mobile-shopping/eslint.config.js
Normal file
29
mobile-shopping/eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs['recommended-latest'],
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
25
mobile-shopping/index.html
Normal file
25
mobile-shopping/index.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/pwa-192x192.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Shopping List Mobile</title>
|
||||||
|
|
||||||
|
<!-- PWA Meta Tags -->
|
||||||
|
<meta name="description" content="Mobile shopping list app with auto-sync">
|
||||||
|
<meta name="theme-color" content="#007AFF">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="ShoppingList">
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
|
|
||||||
|
<!-- Additional PWA Meta Tags -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="application-name" content="ShoppingList">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
30
mobile-shopping/package.json
Normal file
30
mobile-shopping/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "mobile-shopping",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-icons": "^5.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.30.1",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
"eslint": "^9.30.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"sass": "^1.89.2",
|
||||||
|
"vite": "^7.0.4",
|
||||||
|
"vite-plugin-pwa": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
4789
mobile-shopping/pnpm-lock.yaml
generated
Normal file
4789
mobile-shopping/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
mobile-shopping/public/apple-touch-icon.png
Normal file
BIN
mobile-shopping/public/apple-touch-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
mobile-shopping/public/pwa-192x192.png
Normal file
BIN
mobile-shopping/public/pwa-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
mobile-shopping/public/pwa-512x512.png
Normal file
BIN
mobile-shopping/public/pwa-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
421
mobile-shopping/src/App.jsx
Normal file
421
mobile-shopping/src/App.jsx
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { FaPlus, FaTrash, FaCheck, FaEdit, FaShoppingCart, FaClock } from 'react-icons/fa';
|
||||||
|
import shoppingService from './services/ShoppingService';
|
||||||
|
import './App.sass';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [initialLoad, setInitialLoad] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [newItem, setNewItem] = useState({ name: '', amount: '1' });
|
||||||
|
const [editingItem, setEditingItem] = useState(null);
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
const [deferredPrompt, setDeferredPrompt] = useState(null);
|
||||||
|
const [showInstallPrompt, setShowInstallPrompt] = useState(false);
|
||||||
|
|
||||||
|
// Load shopping items on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadItems();
|
||||||
|
|
||||||
|
// Set up auto-refresh every 5 seconds
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
loadItems();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// PWA install prompt handling
|
||||||
|
const handleBeforeInstallPrompt = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDeferredPrompt(e);
|
||||||
|
setShowInstallPrompt(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||||
|
|
||||||
|
// Cleanup interval and event listener on component unmount
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadItems = async () => {
|
||||||
|
try {
|
||||||
|
if (initialLoad) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
const data = await shoppingService.getItems();
|
||||||
|
setItems(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to load shopping items');
|
||||||
|
console.error('Error loading items:', err);
|
||||||
|
} finally {
|
||||||
|
if (initialLoad) {
|
||||||
|
setLoading(false);
|
||||||
|
setInitialLoad(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddItem = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newItem.name.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdItem = await shoppingService.createItem({
|
||||||
|
name: newItem.name.trim(),
|
||||||
|
amount: newItem.amount.trim() || '1'
|
||||||
|
});
|
||||||
|
setItems([createdItem, ...items]);
|
||||||
|
setNewItem({ name: '', amount: '1' });
|
||||||
|
setShowAddForm(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to add item');
|
||||||
|
console.error('Error adding item:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleItem = async (id) => {
|
||||||
|
try {
|
||||||
|
const updatedItem = await shoppingService.toggleItem(id);
|
||||||
|
setItems(items.map(item =>
|
||||||
|
item.id === id ? updatedItem : item
|
||||||
|
));
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to update item');
|
||||||
|
console.error('Error toggling item:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteItem = async (id) => {
|
||||||
|
try {
|
||||||
|
await shoppingService.deleteItem(id);
|
||||||
|
setItems(items.filter(item => item.id !== id));
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to delete item');
|
||||||
|
console.error('Error deleting item:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateItem = async (id, updatedData) => {
|
||||||
|
try {
|
||||||
|
const updatedItem = await shoppingService.updateItem(id, updatedData);
|
||||||
|
setItems(items.map(item =>
|
||||||
|
item.id === id ? updatedItem : item
|
||||||
|
));
|
||||||
|
setEditingItem(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to update item');
|
||||||
|
console.error('Error updating item:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteChecked = async () => {
|
||||||
|
try {
|
||||||
|
await shoppingService.deleteCheckedItems();
|
||||||
|
setItems(items.filter(item => !item.checked));
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to delete checked items');
|
||||||
|
console.error('Error deleting checked items:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstallApp = async () => {
|
||||||
|
if (deferredPrompt) {
|
||||||
|
deferredPrompt.prompt();
|
||||||
|
const { outcome } = await deferredPrompt.userChoice;
|
||||||
|
if (outcome === 'accepted') {
|
||||||
|
setDeferredPrompt(null);
|
||||||
|
setShowInstallPrompt(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeUntilDeletion = (checkedAt) => {
|
||||||
|
if (!checkedAt) return null;
|
||||||
|
const twoHoursLater = new Date(new Date(checkedAt).getTime() + 2 * 60 * 60 * 1000);
|
||||||
|
const now = new Date();
|
||||||
|
const timeDiff = twoHoursLater - now;
|
||||||
|
|
||||||
|
if (timeDiff <= 0) return 'Wird gelöscht...';
|
||||||
|
|
||||||
|
const minutes = Math.floor(timeDiff / (1000 * 60));
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainingMinutes = minutes % 60;
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${remainingMinutes}m`;
|
||||||
|
}
|
||||||
|
return `${minutes}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uncheckedItems = items.filter(item => !item.checked);
|
||||||
|
const checkedItems = items.filter(item => item.checked);
|
||||||
|
|
||||||
|
if (loading && initialLoad) {
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<div className="loading">
|
||||||
|
<FaShoppingCart className="loading-icon" />
|
||||||
|
<p>Einkaufsliste wird geladen...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<header className="header">
|
||||||
|
<div className="header-content">
|
||||||
|
<h1>
|
||||||
|
<FaShoppingCart />
|
||||||
|
Einkaufsliste
|
||||||
|
</h1>
|
||||||
|
<div className="header-actions">
|
||||||
|
{checkedItems.length > 0 && (
|
||||||
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={handleDeleteChecked}
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
{checkedItems.length}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="main">
|
||||||
|
{error && (
|
||||||
|
<div className="error-message">
|
||||||
|
{error}
|
||||||
|
<button onClick={() => setError(null)}>×</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showInstallPrompt && (
|
||||||
|
<div className="install-prompt">
|
||||||
|
<p>Diese App auf dem Homescreen installieren?</p>
|
||||||
|
<div className="install-actions">
|
||||||
|
<button className="btn btn-primary" onClick={handleInstallApp}>
|
||||||
|
Installieren
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={() => setShowInstallPrompt(false)}>
|
||||||
|
Später
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAddForm && (
|
||||||
|
<div className="add-form-overlay">
|
||||||
|
<form className="add-form" onSubmit={handleAddItem}>
|
||||||
|
<h3>Neuer Artikel</h3>
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Artikelname..."
|
||||||
|
value={newItem.name}
|
||||||
|
onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Menge"
|
||||||
|
value={newItem.amount}
|
||||||
|
onChange={(e) => setNewItem({ ...newItem, amount: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-actions">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => setShowAddForm(false)}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">
|
||||||
|
Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="shopping-content">
|
||||||
|
{/* Unchecked Items */}
|
||||||
|
<section className="shopping-section">
|
||||||
|
<h2>Zu kaufen ({uncheckedItems.length})</h2>
|
||||||
|
<div className="items-list">
|
||||||
|
{uncheckedItems.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<FaShoppingCart />
|
||||||
|
<p>Keine Artikel auf der Einkaufsliste</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
uncheckedItems.map(item => (
|
||||||
|
<ShoppingItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onToggle={handleToggleItem}
|
||||||
|
onDelete={handleDeleteItem}
|
||||||
|
onUpdate={handleUpdateItem}
|
||||||
|
editingItem={editingItem}
|
||||||
|
setEditingItem={setEditingItem}
|
||||||
|
formatDate={formatDate}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Checked Items */}
|
||||||
|
{checkedItems.length > 0 && (
|
||||||
|
<section className="shopping-section">
|
||||||
|
<h2>Erledigt ({checkedItems.length})</h2>
|
||||||
|
<div className="items-list">
|
||||||
|
{checkedItems.map(item => (
|
||||||
|
<ShoppingItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onToggle={handleToggleItem}
|
||||||
|
onDelete={handleDeleteItem}
|
||||||
|
onUpdate={handleUpdateItem}
|
||||||
|
editingItem={editingItem}
|
||||||
|
setEditingItem={setEditingItem}
|
||||||
|
formatDate={formatDate}
|
||||||
|
getTimeUntilDeletion={getTimeUntilDeletion}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div className="fab-container">
|
||||||
|
<button
|
||||||
|
className="fab"
|
||||||
|
onClick={() => setShowAddForm(true)}
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shopping Item Component
|
||||||
|
const ShoppingItem = ({
|
||||||
|
item,
|
||||||
|
onToggle,
|
||||||
|
onDelete,
|
||||||
|
onUpdate,
|
||||||
|
editingItem,
|
||||||
|
setEditingItem,
|
||||||
|
formatDate,
|
||||||
|
getTimeUntilDeletion
|
||||||
|
}) => {
|
||||||
|
const [editData, setEditData] = useState({ name: item.name, amount: item.amount });
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
setEditingItem(item.id);
|
||||||
|
setEditData({ name: item.name, amount: item.amount });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onUpdate(item.id, editData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditingItem(null);
|
||||||
|
setEditData({ name: item.name, amount: item.amount });
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEditing = editingItem === item.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`shopping-item ${item.checked ? 'checked' : ''}`}>
|
||||||
|
<button
|
||||||
|
className="check-btn"
|
||||||
|
onClick={() => onToggle(item.id)}
|
||||||
|
>
|
||||||
|
<FaCheck />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="item-content">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="edit-form">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.name}
|
||||||
|
onChange={(e) => setEditData({ ...editData, name: e.target.value })}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleSave();
|
||||||
|
if (e.key === 'Escape') handleCancel();
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.amount}
|
||||||
|
onChange={(e) => setEditData({ ...editData, amount: e.target.value })}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleSave();
|
||||||
|
if (e.key === 'Escape') handleCancel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="edit-actions">
|
||||||
|
<button className="btn btn-primary btn-small" onClick={handleSave}>
|
||||||
|
<FaCheck />
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-secondary btn-small" onClick={handleCancel}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="item-main">
|
||||||
|
<span className="item-name">{item.name}</span>
|
||||||
|
<span className="item-amount">{item.amount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="item-meta">
|
||||||
|
<span className="item-date">
|
||||||
|
{formatDate(item.date)}
|
||||||
|
</span>
|
||||||
|
{item.checked && item.checkedAt && getTimeUntilDeletion && (
|
||||||
|
<span className="deletion-timer">
|
||||||
|
<FaClock />
|
||||||
|
{getTimeUntilDeletion(item.checkedAt)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="item-actions">
|
||||||
|
{!isEditing && (
|
||||||
|
<>
|
||||||
|
<button className="btn btn-secondary btn-small" onClick={handleEdit}>
|
||||||
|
<FaEdit />
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-danger btn-small" onClick={() => onDelete(item.id)}>
|
||||||
|
<FaTrash />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
513
mobile-shopping/src/App.sass
Normal file
513
mobile-shopping/src/App.sass
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
// Mobile Shopping App Styles
|
||||||
|
// Clean, simple design without glassmorphism
|
||||||
|
|
||||||
|
:root
|
||||||
|
--primary-color: #007AFF
|
||||||
|
--secondary-color: #5856D6
|
||||||
|
--success-color: #34C759
|
||||||
|
--danger-color: #FF3B30
|
||||||
|
--warning-color: #FF9500
|
||||||
|
--background-color: #f8f9fa
|
||||||
|
--surface-color: #ffffff
|
||||||
|
--border-color: #e1e5e9
|
||||||
|
--text-primary: #1d1d1f
|
||||||
|
--text-secondary: #6e6e73
|
||||||
|
--text-muted: #8e8e93
|
||||||
|
--shadow-light: 0 2px 8px rgba(0, 0, 0, 0.1)
|
||||||
|
--shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.15)
|
||||||
|
--border-radius: 12px
|
||||||
|
--border-radius-small: 8px
|
||||||
|
--spacing-xs: 4px
|
||||||
|
--spacing-sm: 8px
|
||||||
|
--spacing-md: 16px
|
||||||
|
--spacing-lg: 24px
|
||||||
|
--spacing-xl: 32px
|
||||||
|
|
||||||
|
*
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
body
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif
|
||||||
|
background-color: var(--background-color)
|
||||||
|
color: var(--text-primary)
|
||||||
|
line-height: 1.5
|
||||||
|
-webkit-font-smoothing: antialiased
|
||||||
|
-moz-osx-font-smoothing: grayscale
|
||||||
|
|
||||||
|
.app
|
||||||
|
min-height: 100vh
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
max-width: 100vw
|
||||||
|
overflow-x: hidden
|
||||||
|
|
||||||
|
// Header
|
||||||
|
.header
|
||||||
|
background: var(--surface-color)
|
||||||
|
border-bottom: 1px solid var(--border-color)
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md)
|
||||||
|
position: sticky
|
||||||
|
top: 0
|
||||||
|
z-index: 100
|
||||||
|
box-shadow: var(--shadow-light)
|
||||||
|
|
||||||
|
.header-content
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: space-between
|
||||||
|
max-width: 600px
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: 600
|
||||||
|
color: var(--text-primary)
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: var(--spacing-sm)
|
||||||
|
|
||||||
|
svg
|
||||||
|
color: var(--primary-color)
|
||||||
|
font-size: 1.25rem
|
||||||
|
|
||||||
|
.header-actions
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: var(--spacing-sm)
|
||||||
|
|
||||||
|
// Main Content
|
||||||
|
.main
|
||||||
|
flex: 1
|
||||||
|
padding: var(--spacing-md)
|
||||||
|
max-width: 600px
|
||||||
|
margin: 0 auto
|
||||||
|
width: 100%
|
||||||
|
padding-bottom: 100px // Space for FAB
|
||||||
|
|
||||||
|
// Error Message
|
||||||
|
.error-message
|
||||||
|
background: var(--danger-color)
|
||||||
|
color: white
|
||||||
|
padding: var(--spacing-md)
|
||||||
|
border-radius: var(--border-radius-small)
|
||||||
|
margin-bottom: var(--spacing-md)
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: space-between
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
button
|
||||||
|
background: none
|
||||||
|
border: none
|
||||||
|
color: white
|
||||||
|
font-size: 1.25rem
|
||||||
|
cursor: pointer
|
||||||
|
padding: 0
|
||||||
|
width: 24px
|
||||||
|
height: 24px
|
||||||
|
border-radius: 50%
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
|
||||||
|
// Install Prompt
|
||||||
|
.install-prompt
|
||||||
|
background: var(--primary-color)
|
||||||
|
color: white
|
||||||
|
padding: var(--spacing-md)
|
||||||
|
border-radius: var(--border-radius-small)
|
||||||
|
margin-bottom: var(--spacing-md)
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
p
|
||||||
|
margin-bottom: var(--spacing-md)
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
.install-actions
|
||||||
|
display: flex
|
||||||
|
gap: var(--spacing-sm)
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
.btn
|
||||||
|
color: var(--primary-color)
|
||||||
|
background: white
|
||||||
|
border: 1px solid white
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: #f0f0f0
|
||||||
|
|
||||||
|
&.btn-secondary
|
||||||
|
background: transparent
|
||||||
|
color: white
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: rgba(255, 255, 255, 0.1)
|
||||||
|
|
||||||
|
// Loading State
|
||||||
|
.loading
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
height: 60vh
|
||||||
|
color: var(--text-secondary)
|
||||||
|
|
||||||
|
.loading-icon
|
||||||
|
font-size: 3rem
|
||||||
|
color: var(--primary-color)
|
||||||
|
margin-bottom: var(--spacing-md)
|
||||||
|
animation: pulse 2s infinite
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 1.1rem
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
@keyframes pulse
|
||||||
|
0%, 100%
|
||||||
|
opacity: 1
|
||||||
|
50%
|
||||||
|
opacity: 0.5
|
||||||
|
|
||||||
|
// Shopping Content
|
||||||
|
.shopping-content
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: var(--spacing-xl)
|
||||||
|
|
||||||
|
.shopping-section
|
||||||
|
h2
|
||||||
|
font-size: 1.25rem
|
||||||
|
font-weight: 600
|
||||||
|
color: var(--text-primary)
|
||||||
|
margin-bottom: var(--spacing-md)
|
||||||
|
padding: 0 var(--spacing-xs)
|
||||||
|
|
||||||
|
.items-list
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: var(--spacing-sm)
|
||||||
|
|
||||||
|
// Empty State
|
||||||
|
.empty-state
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
padding: var(--spacing-xl)
|
||||||
|
color: var(--text-muted)
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 3rem
|
||||||
|
margin-bottom: var(--spacing-md)
|
||||||
|
opacity: 0.5
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 1.1rem
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
// Shopping Item
|
||||||
|
.shopping-item
|
||||||
|
background: var(--surface-color)
|
||||||
|
border: 1px solid var(--border-color)
|
||||||
|
border-radius: var(--border-radius)
|
||||||
|
padding: var(--spacing-md)
|
||||||
|
display: flex
|
||||||
|
align-items: flex-start
|
||||||
|
gap: var(--spacing-md)
|
||||||
|
transition: all 0.2s ease
|
||||||
|
box-shadow: var(--shadow-light)
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
box-shadow: var(--shadow-medium)
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
|
&.checked
|
||||||
|
opacity: 0.7
|
||||||
|
background: #f8f9fa
|
||||||
|
|
||||||
|
.item-name
|
||||||
|
text-decoration: line-through
|
||||||
|
color: var(--text-muted)
|
||||||
|
|
||||||
|
.check-btn
|
||||||
|
background: var(--success-color)
|
||||||
|
color: white
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: #28a745
|
||||||
|
|
||||||
|
.check-btn
|
||||||
|
width: 32px
|
||||||
|
height: 32px
|
||||||
|
border-radius: 50%
|
||||||
|
border: 2px solid var(--border-color)
|
||||||
|
background: var(--surface-color)
|
||||||
|
color: transparent
|
||||||
|
cursor: pointer
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
transition: all 0.2s ease
|
||||||
|
flex-shrink: 0
|
||||||
|
margin-top: 2px
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
border-color: var(--success-color)
|
||||||
|
background: rgba(52, 199, 89, 0.1)
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 0.875rem
|
||||||
|
|
||||||
|
.item-content
|
||||||
|
flex: 1
|
||||||
|
min-width: 0
|
||||||
|
|
||||||
|
.item-main
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: space-between
|
||||||
|
margin-bottom: var(--spacing-xs)
|
||||||
|
|
||||||
|
.item-name
|
||||||
|
font-weight: 500
|
||||||
|
color: var(--text-primary)
|
||||||
|
font-size: 1rem
|
||||||
|
flex: 1
|
||||||
|
word-break: break-word
|
||||||
|
|
||||||
|
.item-amount
|
||||||
|
background: var(--primary-color)
|
||||||
|
color: white
|
||||||
|
padding: 2px 8px
|
||||||
|
border-radius: 12px
|
||||||
|
font-size: 0.75rem
|
||||||
|
font-weight: 600
|
||||||
|
margin-left: var(--spacing-sm)
|
||||||
|
flex-shrink: 0
|
||||||
|
|
||||||
|
.item-meta
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: var(--spacing-md)
|
||||||
|
font-size: 0.875rem
|
||||||
|
color: var(--text-secondary)
|
||||||
|
|
||||||
|
.deletion-timer
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: var(--spacing-xs)
|
||||||
|
color: var(--warning-color)
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 0.75rem
|
||||||
|
|
||||||
|
.edit-form
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: var(--spacing-sm)
|
||||||
|
|
||||||
|
input
|
||||||
|
width: 100%
|
||||||
|
padding: var(--spacing-sm)
|
||||||
|
border: 1px solid var(--border-color)
|
||||||
|
border-radius: var(--border-radius-small)
|
||||||
|
font-size: 1rem
|
||||||
|
background: var(--surface-color)
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border-color: var(--primary-color)
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1)
|
||||||
|
|
||||||
|
.edit-actions
|
||||||
|
display: flex
|
||||||
|
gap: var(--spacing-sm)
|
||||||
|
margin-top: var(--spacing-xs)
|
||||||
|
|
||||||
|
.item-actions
|
||||||
|
display: flex
|
||||||
|
gap: var(--spacing-xs)
|
||||||
|
flex-shrink: 0
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
.btn
|
||||||
|
border: none
|
||||||
|
border-radius: var(--border-radius-small)
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md)
|
||||||
|
font-size: 0.875rem
|
||||||
|
font-weight: 500
|
||||||
|
cursor: pointer
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
gap: var(--spacing-xs)
|
||||||
|
transition: all 0.2s ease
|
||||||
|
text-decoration: none
|
||||||
|
|
||||||
|
&:disabled
|
||||||
|
opacity: 0.5
|
||||||
|
cursor: not-allowed
|
||||||
|
|
||||||
|
&.btn-primary
|
||||||
|
background: var(--primary-color)
|
||||||
|
color: white
|
||||||
|
|
||||||
|
&:hover:not(:disabled)
|
||||||
|
background: #0056b3
|
||||||
|
transform: translateY(-1px)
|
||||||
|
|
||||||
|
&.btn-secondary
|
||||||
|
background: var(--border-color)
|
||||||
|
color: var(--text-primary)
|
||||||
|
|
||||||
|
&:hover:not(:disabled)
|
||||||
|
background: #d1d5db
|
||||||
|
|
||||||
|
&.btn-danger
|
||||||
|
background: var(--danger-color)
|
||||||
|
color: white
|
||||||
|
|
||||||
|
&:hover:not(:disabled)
|
||||||
|
background: #dc3545
|
||||||
|
|
||||||
|
&.btn-small
|
||||||
|
padding: var(--spacing-xs)
|
||||||
|
font-size: 0.75rem
|
||||||
|
width: 32px
|
||||||
|
height: 32px
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 0.875rem
|
||||||
|
|
||||||
|
svg
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
// Add Form Overlay
|
||||||
|
.add-form-overlay
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
background: rgba(0, 0, 0, 0.5)
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
z-index: 1000
|
||||||
|
padding: var(--spacing-md)
|
||||||
|
|
||||||
|
.add-form
|
||||||
|
background: var(--surface-color)
|
||||||
|
border-radius: var(--border-radius)
|
||||||
|
padding: var(--spacing-lg)
|
||||||
|
width: 100%
|
||||||
|
max-width: 400px
|
||||||
|
box-shadow: var(--shadow-medium)
|
||||||
|
|
||||||
|
h3
|
||||||
|
font-size: 1.25rem
|
||||||
|
font-weight: 600
|
||||||
|
margin-bottom: var(--spacing-md)
|
||||||
|
color: var(--text-primary)
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: var(--spacing-sm)
|
||||||
|
margin-bottom: var(--spacing-lg)
|
||||||
|
|
||||||
|
input
|
||||||
|
width: 100%
|
||||||
|
padding: var(--spacing-md)
|
||||||
|
border: 1px solid var(--border-color)
|
||||||
|
border-radius: var(--border-radius-small)
|
||||||
|
font-size: 1rem
|
||||||
|
background: var(--surface-color)
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border-color: var(--primary-color)
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1)
|
||||||
|
|
||||||
|
.form-actions
|
||||||
|
display: flex
|
||||||
|
gap: var(--spacing-sm)
|
||||||
|
justify-content: flex-end
|
||||||
|
|
||||||
|
// Floating Action Button
|
||||||
|
.fab-container
|
||||||
|
position: fixed
|
||||||
|
bottom: var(--spacing-lg)
|
||||||
|
right: var(--spacing-lg)
|
||||||
|
z-index: 100
|
||||||
|
|
||||||
|
.fab
|
||||||
|
width: 56px
|
||||||
|
height: 56px
|
||||||
|
border-radius: 50%
|
||||||
|
background: var(--primary-color)
|
||||||
|
color: white
|
||||||
|
border: none
|
||||||
|
cursor: pointer
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
justify-content: center
|
||||||
|
font-size: 1.25rem
|
||||||
|
box-shadow: var(--shadow-medium)
|
||||||
|
transition: all 0.2s ease
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: #0056b3
|
||||||
|
transform: translateY(-2px)
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2)
|
||||||
|
|
||||||
|
&:active
|
||||||
|
transform: translateY(0)
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 480px)
|
||||||
|
.main
|
||||||
|
padding: var(--spacing-sm)
|
||||||
|
|
||||||
|
.header
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm)
|
||||||
|
|
||||||
|
.header-content h1
|
||||||
|
font-size: 1.25rem
|
||||||
|
|
||||||
|
.shopping-item
|
||||||
|
padding: var(--spacing-sm)
|
||||||
|
|
||||||
|
.item-content .item-main
|
||||||
|
flex-direction: column
|
||||||
|
align-items: flex-start
|
||||||
|
gap: var(--spacing-xs)
|
||||||
|
|
||||||
|
.item-amount
|
||||||
|
margin-left: 0
|
||||||
|
|
||||||
|
.item-actions
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.add-form-overlay
|
||||||
|
padding: var(--spacing-sm)
|
||||||
|
|
||||||
|
.add-form
|
||||||
|
padding: var(--spacing-md)
|
||||||
|
|
||||||
|
.fab-container
|
||||||
|
bottom: var(--spacing-md)
|
||||||
|
right: var(--spacing-md)
|
||||||
|
|
||||||
|
.fab
|
||||||
|
width: 48px
|
||||||
|
height: 48px
|
||||||
|
font-size: 1.125rem
|
556
mobile-shopping/src/App.scss
Normal file
556
mobile-shopping/src/App.scss
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
// Mobile Shopping App Styles
|
||||||
|
// Clean, simple design without glassmorphism
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #007AFF;
|
||||||
|
--secondary-color: #5856D6;
|
||||||
|
--success-color: #34C759;
|
||||||
|
--danger-color: #FF3B30;
|
||||||
|
--warning-color: #FF9500;
|
||||||
|
--background-color: #f8f9fa;
|
||||||
|
--surface-color: #ffffff;
|
||||||
|
--border-color: #e1e5e9;
|
||||||
|
--text-primary: #1d1d1f;
|
||||||
|
--text-secondary: #6e6e73;
|
||||||
|
--text-muted: #8e8e93;
|
||||||
|
--shadow-light: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-medium: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
--border-radius: 12px;
|
||||||
|
--border-radius-small: 8px;
|
||||||
|
--spacing-xs: 4px;
|
||||||
|
--spacing-sm: 8px;
|
||||||
|
--spacing-md: 16px;
|
||||||
|
--spacing-lg: 24px;
|
||||||
|
--spacing-xl: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
.header {
|
||||||
|
background: var(--surface-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: var(--shadow-light);
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Content
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 100px; // Space for FAB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error Message
|
||||||
|
.error-message {
|
||||||
|
background: var(--danger-color);
|
||||||
|
color: white;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading State
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 60vh;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shopping Content
|
||||||
|
.shopping-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shopping-section {
|
||||||
|
h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
padding: 0 var(--spacing-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty State
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shopping Item
|
||||||
|
.shopping-item {
|
||||||
|
background: var(--surface-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: var(--shadow-light);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--shadow-medium);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
opacity: 0.7;
|
||||||
|
background: #f8f9fa;
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-btn {
|
||||||
|
background: var(--success-color);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
background: var(--surface-color);
|
||||||
|
color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--success-color);
|
||||||
|
background: rgba(52, 199, 89, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.item-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-amount {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: var(--spacing-sm);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
.deletion-timer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
color: var(--warning-color);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--surface-color);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #0056b3;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-secondary {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #d1d5db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-danger {
|
||||||
|
background: var(--danger-color);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-small {
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Form Overlay
|
||||||
|
.add-form-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
|
||||||
|
.add-form {
|
||||||
|
background: var(--surface-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: var(--shadow-medium);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--surface-color);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floating Action Button
|
||||||
|
.fab-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--spacing-lg);
|
||||||
|
right: var(--spacing-lg);
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
box-shadow: var(--shadow-medium);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive Design
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.main {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shopping-item {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
|
||||||
|
.item-content .item-main {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
|
||||||
|
.item-amount {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-form-overlay {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
|
||||||
|
.add-form {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-container {
|
||||||
|
bottom: var(--spacing-md);
|
||||||
|
right: var(--spacing-md);
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
mobile-shopping/src/main.jsx
Normal file
22
mobile-shopping/src/main.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
// Register service worker for PWA
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then((registration) => {
|
||||||
|
console.log('SW registered: ', registration);
|
||||||
|
})
|
||||||
|
.catch((registrationError) => {
|
||||||
|
console.log('SW registration failed: ', registrationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
51
mobile-shopping/src/services/ShoppingService.js
Normal file
51
mobile-shopping/src/services/ShoppingService.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import requestUtil from '../utils/RequestUtil';
|
||||||
|
|
||||||
|
class ShoppingService {
|
||||||
|
constructor() {
|
||||||
|
this.endpoint = '/api/shopping';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all shopping items
|
||||||
|
async getItems() {
|
||||||
|
return requestUtil.get(this.endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a specific shopping item
|
||||||
|
async getItem(id) {
|
||||||
|
return requestUtil.get(`${this.endpoint}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new shopping item
|
||||||
|
async createItem(item) {
|
||||||
|
return requestUtil.post(this.endpoint, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a shopping item
|
||||||
|
async updateItem(id, item) {
|
||||||
|
return requestUtil.put(`${this.endpoint}/${id}`, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle checked status of an item
|
||||||
|
async toggleItem(id) {
|
||||||
|
return requestUtil.patch(`${this.endpoint}/${id}/toggle`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a shopping item
|
||||||
|
async deleteItem(id) {
|
||||||
|
return requestUtil.delete(`${this.endpoint}/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all checked items
|
||||||
|
async deleteCheckedItems() {
|
||||||
|
return requestUtil.delete(`${this.endpoint}/checked/all`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
async healthCheck() {
|
||||||
|
return requestUtil.get('/api/health');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export a singleton instance
|
||||||
|
const shoppingService = new ShoppingService();
|
||||||
|
export default shoppingService;
|
85
mobile-shopping/src/utils/RequestUtil.js
Normal file
85
mobile-shopping/src/utils/RequestUtil.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
class RequestUtil {
|
||||||
|
constructor() {
|
||||||
|
this.baseURL = this.getBaseURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseURL() {
|
||||||
|
// In production, use the static endpoint, otherwise use proxy
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
return 'https://static.endpoint.com';
|
||||||
|
}
|
||||||
|
return ''; // Uses proxy in development
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(endpoint, options = {}) {
|
||||||
|
const url = `${this.baseURL}${endpoint}`;
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
...defaultOptions,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, config);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({ error: 'Request failed' }));
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Request failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET request
|
||||||
|
async get(endpoint) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'GET'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST request
|
||||||
|
async post(endpoint, data = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT request
|
||||||
|
async put(endpoint, data = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH request
|
||||||
|
async patch(endpoint, data = {}) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE request
|
||||||
|
async delete(endpoint) {
|
||||||
|
return this.request(endpoint, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export a singleton instance
|
||||||
|
const requestUtil = new RequestUtil();
|
||||||
|
export default requestUtil;
|
53
mobile-shopping/vite.config.js
Normal file
53
mobile-shopping/vite.config.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
|
||||||
|
},
|
||||||
|
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'masked-icon.svg'],
|
||||||
|
manifest: {
|
||||||
|
name: 'Shopping List Mobile',
|
||||||
|
short_name: 'ShoppingList',
|
||||||
|
description: 'Mobile shopping list app with auto-sync',
|
||||||
|
theme_color: '#007AFF',
|
||||||
|
background_color: '#f8f9fa',
|
||||||
|
display: 'standalone',
|
||||||
|
scope: '/',
|
||||||
|
start_url: '/',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: 'pwa-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'pwa-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'pwa-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'any maskable'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
65
server/README.md
Normal file
65
server/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Shopping List Server
|
||||||
|
|
||||||
|
A simple Express.js server with SQLite database for managing shopping list items.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- CRUD operations for shopping list items
|
||||||
|
- Automatic cleanup of checked items after 2 hours
|
||||||
|
- SQLite database with Sequelize ORM
|
||||||
|
- CORS enabled for frontend integration
|
||||||
|
- RESTful API endpoints
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
Each shopping item has:
|
||||||
|
- `id`: Unique identifier (auto-increment)
|
||||||
|
- `name`: Item name (required)
|
||||||
|
- `amount`: Quantity/amount (default: "1")
|
||||||
|
- `checked`: Boolean status (default: false)
|
||||||
|
- `checkedAt`: Timestamp when item was checked
|
||||||
|
- `date`: Creation date
|
||||||
|
- `createdAt`: Auto-generated creation timestamp
|
||||||
|
- `updatedAt`: Auto-generated update timestamp
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Shopping Items
|
||||||
|
- `GET /api/shopping` - Get all shopping items
|
||||||
|
- `GET /api/shopping/:id` - Get a specific shopping item
|
||||||
|
- `POST /api/shopping` - Create a new shopping item
|
||||||
|
- `PUT /api/shopping/:id` - Update a shopping item
|
||||||
|
- `PATCH /api/shopping/:id/toggle` - Toggle checked status
|
||||||
|
- `DELETE /api/shopping/:id` - Delete a shopping item
|
||||||
|
- `DELETE /api/shopping/checked/all` - Delete all checked items
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
- `GET /api/health` - Server health check
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start the development server:
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Or start the production server:
|
||||||
|
```bash
|
||||||
|
pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will run on `http://localhost:3001` by default.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
- `PORT`: Server port (default: 3001)
|
||||||
|
- `NODE_ENV`: Environment mode (development/production)
|
||||||
|
|
||||||
|
## Auto-cleanup
|
||||||
|
|
||||||
|
Checked items are automatically deleted after 2 hours. The cleanup runs every 30 minutes.
|
BIN
server/database.sqlite
Normal file
BIN
server/database.sqlite
Normal file
Binary file not shown.
@@ -0,0 +1,78 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const { sequelize, cleanupCheckedItems } = require('./models');
|
||||||
|
const shoppingRoutes = require('./routes/shopping');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api/shopping', shoppingRoutes);
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'OK',
|
||||||
|
message: 'Shopping List Server is running',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({ error: 'Something went wrong!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use('*name', (req, res) => {
|
||||||
|
res.status(404).json({ error: 'Route not found' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize database and start server
|
||||||
|
const startServer = async () => {
|
||||||
|
try {
|
||||||
|
// Test database connection
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Database connection established successfully.');
|
||||||
|
|
||||||
|
// Sync database (create tables if they don't exist)
|
||||||
|
await sequelize.sync();
|
||||||
|
console.log('Database synchronized successfully.');
|
||||||
|
|
||||||
|
// Run initial cleanup
|
||||||
|
await cleanupCheckedItems();
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Shopping List Server is running on port ${PORT}`);
|
||||||
|
console.log(`Health check available at: http://localhost:${PORT}/api/health`);
|
||||||
|
console.log(`Shopping API available at: http://localhost:${PORT}/api/shopping`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('\nShutting down gracefully...');
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
console.log('\nShutting down gracefully...');
|
||||||
|
await sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
startServer();
|
50
server/models/ShoppingItem.js
Normal file
50
server/models/ShoppingItem.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
|
||||||
|
module.exports = (sequelize) => {
|
||||||
|
const ShoppingItem = sequelize.define('ShoppingItem', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: '1',
|
||||||
|
},
|
||||||
|
checked: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
checkedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
timestamps: true,
|
||||||
|
hooks: {
|
||||||
|
beforeUpdate: (item, options) => {
|
||||||
|
// Set checkedAt timestamp when item is checked
|
||||||
|
if (item.checked && !item.previous('checked')) {
|
||||||
|
item.checkedAt = new Date();
|
||||||
|
} else if (!item.checked) {
|
||||||
|
item.checkedAt = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ShoppingItem;
|
||||||
|
};
|
42
server/models/index.js
Normal file
42
server/models/index.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const { Sequelize } = require('sequelize');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Initialize Sequelize with SQLite
|
||||||
|
const sequelize = new Sequelize({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
storage: path.join(__dirname, '../database.sqlite'),
|
||||||
|
logging: false, // Set to console.log to see SQL queries
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import models
|
||||||
|
const ShoppingItem = require('./ShoppingItem')(sequelize);
|
||||||
|
|
||||||
|
// Function to clean up checked items older than 2 hours
|
||||||
|
const cleanupCheckedItems = async () => {
|
||||||
|
try {
|
||||||
|
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
|
||||||
|
const deletedCount = await ShoppingItem.destroy({
|
||||||
|
where: {
|
||||||
|
checked: true,
|
||||||
|
checkedAt: {
|
||||||
|
[Sequelize.Op.lt]: twoHoursAgo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
console.log(`Cleaned up ${deletedCount} checked items older than 2 hours`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning up checked items:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run cleanup every 30 minutes
|
||||||
|
setInterval(cleanupCheckedItems, 30 * 60 * 1000);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sequelize,
|
||||||
|
ShoppingItem,
|
||||||
|
cleanupCheckedItems,
|
||||||
|
};
|
2727
server/package-lock.json
generated
Normal file
2727
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
server/package.json
Normal file
25
server/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js",
|
||||||
|
"dev": "nodemon index.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"packageManager": "pnpm@10.13.1",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"sequelize": "^6.37.7",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.10"
|
||||||
|
}
|
||||||
|
}
|
975
server/pnpm-lock.yaml
generated
Normal file
975
server/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,975 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
cors:
|
||||||
|
specifier: ^2.8.5
|
||||||
|
version: 2.8.5
|
||||||
|
dotenv:
|
||||||
|
specifier: ^17.2.0
|
||||||
|
version: 17.2.0
|
||||||
|
express:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0
|
||||||
|
sequelize:
|
||||||
|
specifier: ^6.37.7
|
||||||
|
version: 6.37.7
|
||||||
|
devDependencies:
|
||||||
|
nodemon:
|
||||||
|
specifier: ^3.1.10
|
||||||
|
version: 3.1.10
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@types/debug@4.1.12':
|
||||||
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0':
|
||||||
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
|
'@types/node@24.0.14':
|
||||||
|
resolution: {integrity: sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==}
|
||||||
|
|
||||||
|
'@types/validator@13.15.2':
|
||||||
|
resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==}
|
||||||
|
|
||||||
|
accepts@2.0.0:
|
||||||
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
anymatch@3.1.3:
|
||||||
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
balanced-match@1.0.2:
|
||||||
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
binary-extensions@2.3.0:
|
||||||
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
body-parser@2.2.0:
|
||||||
|
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
brace-expansion@1.1.12:
|
||||||
|
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||||
|
|
||||||
|
braces@3.0.3:
|
||||||
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
bytes@3.1.2:
|
||||||
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
call-bound@1.0.4:
|
||||||
|
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
chokidar@3.6.0:
|
||||||
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
|
engines: {node: '>= 8.10.0'}
|
||||||
|
|
||||||
|
concat-map@0.0.1:
|
||||||
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
content-disposition@1.0.0:
|
||||||
|
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
content-type@1.0.5:
|
||||||
|
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
cookie-signature@1.2.2:
|
||||||
|
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||||
|
engines: {node: '>=6.6.0'}
|
||||||
|
|
||||||
|
cookie@0.7.2:
|
||||||
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
cors@2.8.5:
|
||||||
|
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
debug@4.4.1:
|
||||||
|
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
depd@2.0.0:
|
||||||
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
dotenv@17.2.0:
|
||||||
|
resolution: {integrity: sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
dottie@2.0.6:
|
||||||
|
resolution: {integrity: sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==}
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
ee-first@1.1.1:
|
||||||
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
|
encodeurl@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
es-define-property@1.0.1:
|
||||||
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-errors@1.3.0:
|
||||||
|
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-object-atoms@1.1.1:
|
||||||
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
escape-html@1.0.3:
|
||||||
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
|
||||||
|
etag@1.8.1:
|
||||||
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
express@5.1.0:
|
||||||
|
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
fill-range@7.1.1:
|
||||||
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
finalhandler@2.1.0:
|
||||||
|
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
forwarded@0.2.0:
|
||||||
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
fresh@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
function-bind@1.1.2:
|
||||||
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
|
get-intrinsic@1.3.0:
|
||||||
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
get-proto@1.0.1:
|
||||||
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
glob-parent@5.1.2:
|
||||||
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
gopd@1.2.0:
|
||||||
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
has-flag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
has-symbols@1.1.0:
|
||||||
|
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hasown@2.0.2:
|
||||||
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
http-errors@2.0.0:
|
||||||
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
iconv-lite@0.6.3:
|
||||||
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ignore-by-default@1.0.1:
|
||||||
|
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||||
|
|
||||||
|
inflection@1.13.4:
|
||||||
|
resolution: {integrity: sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==}
|
||||||
|
engines: {'0': node >= 0.4.0}
|
||||||
|
|
||||||
|
inherits@2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
ipaddr.js@1.9.1:
|
||||||
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
is-binary-path@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
is-extglob@2.1.1:
|
||||||
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-glob@4.0.3:
|
||||||
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-number@7.0.0:
|
||||||
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
|
is-promise@4.0.0:
|
||||||
|
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||||
|
|
||||||
|
lodash@4.17.21:
|
||||||
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
|
math-intrinsics@1.1.0:
|
||||||
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
media-typer@1.1.0:
|
||||||
|
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
merge-descriptors@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
mime-db@1.54.0:
|
||||||
|
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime-types@3.0.1:
|
||||||
|
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
minimatch@3.1.2:
|
||||||
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
|
|
||||||
|
moment-timezone@0.5.48:
|
||||||
|
resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==}
|
||||||
|
|
||||||
|
moment@2.30.1:
|
||||||
|
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||||
|
|
||||||
|
ms@2.1.3:
|
||||||
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
negotiator@1.0.0:
|
||||||
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
nodemon@3.1.10:
|
||||||
|
resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
normalize-path@3.0.0:
|
||||||
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
object-assign@4.1.1:
|
||||||
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
object-inspect@1.13.4:
|
||||||
|
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
on-finished@2.4.1:
|
||||||
|
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
once@1.4.0:
|
||||||
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
|
parseurl@1.3.3:
|
||||||
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
path-to-regexp@8.2.0:
|
||||||
|
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
pg-connection-string@2.9.1:
|
||||||
|
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
|
||||||
|
|
||||||
|
picomatch@2.3.1:
|
||||||
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
proxy-addr@2.0.7:
|
||||||
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
pstree.remy@1.1.8:
|
||||||
|
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||||
|
|
||||||
|
qs@6.14.0:
|
||||||
|
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
|
range-parser@1.2.1:
|
||||||
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
raw-body@3.0.0:
|
||||||
|
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
readdirp@3.6.0:
|
||||||
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
|
retry-as-promised@7.1.1:
|
||||||
|
resolution: {integrity: sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==}
|
||||||
|
|
||||||
|
router@2.2.0:
|
||||||
|
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
safe-buffer@5.2.1:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
|
safer-buffer@2.1.2:
|
||||||
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
|
semver@7.7.2:
|
||||||
|
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
send@1.2.0:
|
||||||
|
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
sequelize-pool@7.1.0:
|
||||||
|
resolution: {integrity: sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
|
sequelize@6.37.7:
|
||||||
|
resolution: {integrity: sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
ibm_db: '*'
|
||||||
|
mariadb: '*'
|
||||||
|
mysql2: '*'
|
||||||
|
oracledb: '*'
|
||||||
|
pg: '*'
|
||||||
|
pg-hstore: '*'
|
||||||
|
snowflake-sdk: '*'
|
||||||
|
sqlite3: '*'
|
||||||
|
tedious: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ibm_db:
|
||||||
|
optional: true
|
||||||
|
mariadb:
|
||||||
|
optional: true
|
||||||
|
mysql2:
|
||||||
|
optional: true
|
||||||
|
oracledb:
|
||||||
|
optional: true
|
||||||
|
pg:
|
||||||
|
optional: true
|
||||||
|
pg-hstore:
|
||||||
|
optional: true
|
||||||
|
snowflake-sdk:
|
||||||
|
optional: true
|
||||||
|
sqlite3:
|
||||||
|
optional: true
|
||||||
|
tedious:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
serve-static@2.2.0:
|
||||||
|
resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
setprototypeof@1.2.0:
|
||||||
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
|
side-channel-list@1.0.0:
|
||||||
|
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
side-channel-map@1.0.1:
|
||||||
|
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
side-channel-weakmap@1.0.2:
|
||||||
|
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
side-channel@1.1.0:
|
||||||
|
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
simple-update-notifier@2.0.0:
|
||||||
|
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
statuses@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
statuses@2.0.2:
|
||||||
|
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
to-regex-range@5.0.1:
|
||||||
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
|
toidentifier@1.0.1:
|
||||||
|
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||||
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
|
toposort-class@1.0.1:
|
||||||
|
resolution: {integrity: sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==}
|
||||||
|
|
||||||
|
touch@3.1.1:
|
||||||
|
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
type-is@2.0.1:
|
||||||
|
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
undefsafe@2.0.5:
|
||||||
|
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||||
|
|
||||||
|
undici-types@7.8.0:
|
||||||
|
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||||
|
|
||||||
|
unpipe@1.0.0:
|
||||||
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
uuid@8.3.2:
|
||||||
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
validator@13.15.15:
|
||||||
|
resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
vary@1.1.2:
|
||||||
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
wkx@0.5.0:
|
||||||
|
resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==}
|
||||||
|
|
||||||
|
wrappy@1.0.2:
|
||||||
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@types/debug@4.1.12':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
|
|
||||||
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
|
'@types/node@24.0.14':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.8.0
|
||||||
|
|
||||||
|
'@types/validator@13.15.2': {}
|
||||||
|
|
||||||
|
accepts@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
mime-types: 3.0.1
|
||||||
|
negotiator: 1.0.0
|
||||||
|
|
||||||
|
anymatch@3.1.3:
|
||||||
|
dependencies:
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
|
body-parser@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
content-type: 1.0.5
|
||||||
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
|
http-errors: 2.0.0
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
on-finished: 2.4.1
|
||||||
|
qs: 6.14.0
|
||||||
|
raw-body: 3.0.0
|
||||||
|
type-is: 2.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
brace-expansion@1.1.12:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
concat-map: 0.0.1
|
||||||
|
|
||||||
|
braces@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
fill-range: 7.1.1
|
||||||
|
|
||||||
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
|
call-bind-apply-helpers@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
call-bound@1.0.4:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
|
||||||
|
chokidar@3.6.0:
|
||||||
|
dependencies:
|
||||||
|
anymatch: 3.1.3
|
||||||
|
braces: 3.0.3
|
||||||
|
glob-parent: 5.1.2
|
||||||
|
is-binary-path: 2.1.0
|
||||||
|
is-glob: 4.0.3
|
||||||
|
normalize-path: 3.0.0
|
||||||
|
readdirp: 3.6.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
|
content-disposition@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
content-type@1.0.5: {}
|
||||||
|
|
||||||
|
cookie-signature@1.2.2: {}
|
||||||
|
|
||||||
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
|
cors@2.8.5:
|
||||||
|
dependencies:
|
||||||
|
object-assign: 4.1.1
|
||||||
|
vary: 1.1.2
|
||||||
|
|
||||||
|
debug@4.4.1(supports-color@5.5.0):
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
optionalDependencies:
|
||||||
|
supports-color: 5.5.0
|
||||||
|
|
||||||
|
depd@2.0.0: {}
|
||||||
|
|
||||||
|
dotenv@17.2.0: {}
|
||||||
|
|
||||||
|
dottie@2.0.6: {}
|
||||||
|
|
||||||
|
dunder-proto@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-errors: 1.3.0
|
||||||
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
es-define-property@1.0.1: {}
|
||||||
|
|
||||||
|
es-errors@1.3.0: {}
|
||||||
|
|
||||||
|
es-object-atoms@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
escape-html@1.0.3: {}
|
||||||
|
|
||||||
|
etag@1.8.1: {}
|
||||||
|
|
||||||
|
express@5.1.0:
|
||||||
|
dependencies:
|
||||||
|
accepts: 2.0.0
|
||||||
|
body-parser: 2.2.0
|
||||||
|
content-disposition: 1.0.0
|
||||||
|
content-type: 1.0.5
|
||||||
|
cookie: 0.7.2
|
||||||
|
cookie-signature: 1.2.2
|
||||||
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
|
encodeurl: 2.0.0
|
||||||
|
escape-html: 1.0.3
|
||||||
|
etag: 1.8.1
|
||||||
|
finalhandler: 2.1.0
|
||||||
|
fresh: 2.0.0
|
||||||
|
http-errors: 2.0.0
|
||||||
|
merge-descriptors: 2.0.0
|
||||||
|
mime-types: 3.0.1
|
||||||
|
on-finished: 2.4.1
|
||||||
|
once: 1.4.0
|
||||||
|
parseurl: 1.3.3
|
||||||
|
proxy-addr: 2.0.7
|
||||||
|
qs: 6.14.0
|
||||||
|
range-parser: 1.2.1
|
||||||
|
router: 2.2.0
|
||||||
|
send: 1.2.0
|
||||||
|
serve-static: 2.2.0
|
||||||
|
statuses: 2.0.2
|
||||||
|
type-is: 2.0.1
|
||||||
|
vary: 1.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
fill-range@7.1.1:
|
||||||
|
dependencies:
|
||||||
|
to-regex-range: 5.0.1
|
||||||
|
|
||||||
|
finalhandler@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
|
encodeurl: 2.0.0
|
||||||
|
escape-html: 1.0.3
|
||||||
|
on-finished: 2.4.1
|
||||||
|
parseurl: 1.3.3
|
||||||
|
statuses: 2.0.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
|
get-intrinsic@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
call-bind-apply-helpers: 1.0.2
|
||||||
|
es-define-property: 1.0.1
|
||||||
|
es-errors: 1.3.0
|
||||||
|
es-object-atoms: 1.1.1
|
||||||
|
function-bind: 1.1.2
|
||||||
|
get-proto: 1.0.1
|
||||||
|
gopd: 1.2.0
|
||||||
|
has-symbols: 1.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
math-intrinsics: 1.1.0
|
||||||
|
|
||||||
|
get-proto@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
dunder-proto: 1.0.1
|
||||||
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
|
glob-parent@5.1.2:
|
||||||
|
dependencies:
|
||||||
|
is-glob: 4.0.3
|
||||||
|
|
||||||
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
|
has-flag@3.0.0: {}
|
||||||
|
|
||||||
|
has-symbols@1.1.0: {}
|
||||||
|
|
||||||
|
hasown@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
http-errors@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
depd: 2.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
setprototypeof: 1.2.0
|
||||||
|
statuses: 2.0.1
|
||||||
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
|
iconv-lite@0.6.3:
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
ignore-by-default@1.0.1: {}
|
||||||
|
|
||||||
|
inflection@1.13.4: {}
|
||||||
|
|
||||||
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
|
is-binary-path@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
binary-extensions: 2.3.0
|
||||||
|
|
||||||
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
|
is-glob@4.0.3:
|
||||||
|
dependencies:
|
||||||
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
|
is-promise@4.0.0: {}
|
||||||
|
|
||||||
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
|
media-typer@1.1.0: {}
|
||||||
|
|
||||||
|
merge-descriptors@2.0.0: {}
|
||||||
|
|
||||||
|
mime-db@1.54.0: {}
|
||||||
|
|
||||||
|
mime-types@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.54.0
|
||||||
|
|
||||||
|
minimatch@3.1.2:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 1.1.12
|
||||||
|
|
||||||
|
moment-timezone@0.5.48:
|
||||||
|
dependencies:
|
||||||
|
moment: 2.30.1
|
||||||
|
|
||||||
|
moment@2.30.1: {}
|
||||||
|
|
||||||
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
nodemon@3.1.10:
|
||||||
|
dependencies:
|
||||||
|
chokidar: 3.6.0
|
||||||
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
|
ignore-by-default: 1.0.1
|
||||||
|
minimatch: 3.1.2
|
||||||
|
pstree.remy: 1.1.8
|
||||||
|
semver: 7.7.2
|
||||||
|
simple-update-notifier: 2.0.0
|
||||||
|
supports-color: 5.5.0
|
||||||
|
touch: 3.1.1
|
||||||
|
undefsafe: 2.0.5
|
||||||
|
|
||||||
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
|
object-inspect@1.13.4: {}
|
||||||
|
|
||||||
|
on-finished@2.4.1:
|
||||||
|
dependencies:
|
||||||
|
ee-first: 1.1.1
|
||||||
|
|
||||||
|
once@1.4.0:
|
||||||
|
dependencies:
|
||||||
|
wrappy: 1.0.2
|
||||||
|
|
||||||
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
|
path-to-regexp@8.2.0: {}
|
||||||
|
|
||||||
|
pg-connection-string@2.9.1: {}
|
||||||
|
|
||||||
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
|
proxy-addr@2.0.7:
|
||||||
|
dependencies:
|
||||||
|
forwarded: 0.2.0
|
||||||
|
ipaddr.js: 1.9.1
|
||||||
|
|
||||||
|
pstree.remy@1.1.8: {}
|
||||||
|
|
||||||
|
qs@6.14.0:
|
||||||
|
dependencies:
|
||||||
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
range-parser@1.2.1: {}
|
||||||
|
|
||||||
|
raw-body@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
bytes: 3.1.2
|
||||||
|
http-errors: 2.0.0
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
unpipe: 1.0.0
|
||||||
|
|
||||||
|
readdirp@3.6.0:
|
||||||
|
dependencies:
|
||||||
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
retry-as-promised@7.1.1: {}
|
||||||
|
|
||||||
|
router@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
|
depd: 2.0.0
|
||||||
|
is-promise: 4.0.0
|
||||||
|
parseurl: 1.3.3
|
||||||
|
path-to-regexp: 8.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
|
semver@7.7.2: {}
|
||||||
|
|
||||||
|
send@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
|
encodeurl: 2.0.0
|
||||||
|
escape-html: 1.0.3
|
||||||
|
etag: 1.8.1
|
||||||
|
fresh: 2.0.0
|
||||||
|
http-errors: 2.0.0
|
||||||
|
mime-types: 3.0.1
|
||||||
|
ms: 2.1.3
|
||||||
|
on-finished: 2.4.1
|
||||||
|
range-parser: 1.2.1
|
||||||
|
statuses: 2.0.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
sequelize-pool@7.1.0: {}
|
||||||
|
|
||||||
|
sequelize@6.37.7:
|
||||||
|
dependencies:
|
||||||
|
'@types/debug': 4.1.12
|
||||||
|
'@types/validator': 13.15.2
|
||||||
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
|
dottie: 2.0.6
|
||||||
|
inflection: 1.13.4
|
||||||
|
lodash: 4.17.21
|
||||||
|
moment: 2.30.1
|
||||||
|
moment-timezone: 0.5.48
|
||||||
|
pg-connection-string: 2.9.1
|
||||||
|
retry-as-promised: 7.1.1
|
||||||
|
semver: 7.7.2
|
||||||
|
sequelize-pool: 7.1.0
|
||||||
|
toposort-class: 1.0.1
|
||||||
|
uuid: 8.3.2
|
||||||
|
validator: 13.15.15
|
||||||
|
wkx: 0.5.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
serve-static@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
encodeurl: 2.0.0
|
||||||
|
escape-html: 1.0.3
|
||||||
|
parseurl: 1.3.3
|
||||||
|
send: 1.2.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
|
side-channel-list@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
|
||||||
|
side-channel-map@1.0.1:
|
||||||
|
dependencies:
|
||||||
|
call-bound: 1.0.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
|
||||||
|
side-channel-weakmap@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
call-bound: 1.0.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
side-channel-map: 1.0.1
|
||||||
|
|
||||||
|
side-channel@1.1.0:
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
object-inspect: 1.13.4
|
||||||
|
side-channel-list: 1.0.0
|
||||||
|
side-channel-map: 1.0.1
|
||||||
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
|
simple-update-notifier@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
semver: 7.7.2
|
||||||
|
|
||||||
|
statuses@2.0.1: {}
|
||||||
|
|
||||||
|
statuses@2.0.2: {}
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 3.0.0
|
||||||
|
|
||||||
|
to-regex-range@5.0.1:
|
||||||
|
dependencies:
|
||||||
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
toidentifier@1.0.1: {}
|
||||||
|
|
||||||
|
toposort-class@1.0.1: {}
|
||||||
|
|
||||||
|
touch@3.1.1: {}
|
||||||
|
|
||||||
|
type-is@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
content-type: 1.0.5
|
||||||
|
media-typer: 1.1.0
|
||||||
|
mime-types: 3.0.1
|
||||||
|
|
||||||
|
undefsafe@2.0.5: {}
|
||||||
|
|
||||||
|
undici-types@7.8.0: {}
|
||||||
|
|
||||||
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
|
validator@13.15.15: {}
|
||||||
|
|
||||||
|
vary@1.1.2: {}
|
||||||
|
|
||||||
|
wkx@0.5.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.0.14
|
||||||
|
|
||||||
|
wrappy@1.0.2: {}
|
146
server/routes/shopping.js
Normal file
146
server/routes/shopping.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { ShoppingItem } = require('../models');
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get all shopping items
|
||||||
|
router.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const items = await ShoppingItem.findAll({
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
});
|
||||||
|
res.json(items);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching shopping items:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch shopping items' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get a specific shopping item
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const item = await ShoppingItem.findByPk(id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return res.status(404).json({ error: 'Shopping item not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(item);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching shopping item:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch shopping item' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a new shopping item
|
||||||
|
router.post('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, amount, date } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'Name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await ShoppingItem.create({
|
||||||
|
name,
|
||||||
|
amount: amount || '1',
|
||||||
|
date: date || new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json(item);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating shopping item:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to create shopping item' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update a shopping item
|
||||||
|
router.put('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { name, amount, checked, date } = req.body;
|
||||||
|
|
||||||
|
const item = await ShoppingItem.findByPk(id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return res.status(404).json({ error: 'Shopping item not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields if provided
|
||||||
|
if (name !== undefined) item.name = name;
|
||||||
|
if (amount !== undefined) item.amount = amount;
|
||||||
|
if (checked !== undefined) item.checked = checked;
|
||||||
|
if (date !== undefined) item.date = date;
|
||||||
|
|
||||||
|
await item.save();
|
||||||
|
|
||||||
|
res.json(item);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating shopping item:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update shopping item' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle checked status of a shopping item
|
||||||
|
router.patch('/:id/toggle', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const item = await ShoppingItem.findByPk(id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return res.status(404).json({ error: 'Shopping item not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
item.checked = !item.checked;
|
||||||
|
await item.save();
|
||||||
|
|
||||||
|
res.json(item);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling shopping item:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to toggle shopping item' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a shopping item
|
||||||
|
router.delete('/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const item = await ShoppingItem.findByPk(id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return res.status(404).json({ error: 'Shopping item not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await item.destroy();
|
||||||
|
|
||||||
|
res.json({ message: 'Shopping item deleted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting shopping item:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to delete shopping item' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete all checked items
|
||||||
|
router.delete('/checked/all', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const deletedCount = await ShoppingItem.destroy({
|
||||||
|
where: {
|
||||||
|
checked: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: `Deleted ${deletedCount} checked items`,
|
||||||
|
deletedCount
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting checked items:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to delete checked items' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
Reference in New Issue
Block a user