mirror of
https://gitlab.com/Zer0interrupt/recipe-manager.git
synced 2026-11-01 23:24:11 -05:00
feat: migrate database to SQLite and implement secure recipe sharing, unique visitor tracking, and link revocation
This commit is contained in:
parent
b9016991eb
commit
0895a97588
7 changed files with 1815 additions and 356 deletions
440
package-lock.json
generated
440
package-lock.json
generated
|
|
@ -9,9 +9,11 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
|
"better-sqlite3": "^12.10.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-rate-limit": "^8.5.2",
|
||||||
"puppeteer": "^25.1.0",
|
"puppeteer": "^25.1.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
||||||
|
|
@ -167,6 +169,60 @@
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "12.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
|
||||||
|
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"prebuild-install": "^7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.5",
|
"version": "1.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||||
|
|
@ -219,6 +275,30 @@
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
|
@ -299,6 +379,12 @@
|
||||||
"url": "https://github.com/sponsors/fb55"
|
"url": "https://github.com/sponsors/fb55"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/chromium-bidi": {
|
"node_modules/chromium-bidi": {
|
||||||
"version": "16.0.1",
|
"version": "16.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
|
||||||
|
|
@ -471,6 +557,30 @@
|
||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
|
|
@ -508,6 +618,15 @@
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/devtools-protocol": {
|
"node_modules/devtools-protocol": {
|
||||||
"version": "0.0.1624250",
|
"version": "0.0.1624250",
|
||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz",
|
||||||
|
|
@ -617,6 +736,15 @@
|
||||||
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
|
@ -698,6 +826,15 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"license": "(MIT OR WTFPL)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.22.2",
|
"version": "4.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||||
|
|
@ -744,6 +881,30 @@
|
||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||||
|
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
|
|
@ -837,6 +998,12 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-constants": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
|
@ -912,6 +1079,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
|
@ -1089,6 +1262,26 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
|
@ -1106,6 +1299,21 @@
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|
@ -1288,6 +1496,18 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.5",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
|
|
@ -1300,6 +1520,15 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mitt": {
|
"node_modules/mitt": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
|
@ -1328,6 +1557,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/modern-tar": {
|
"node_modules/modern-tar": {
|
||||||
"version": "0.7.6",
|
"version": "0.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
|
||||||
|
|
@ -1343,6 +1578,12 @@
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
|
@ -1352,6 +1593,18 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.92.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz",
|
||||||
|
"integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nth-check": {
|
"node_modules/nth-check": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
|
@ -1479,6 +1732,33 @@
|
||||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||||
|
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^2.0.0",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|
@ -1501,6 +1781,16 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/puppeteer": {
|
"node_modules/puppeteer": {
|
||||||
"version": "25.1.0",
|
"version": "25.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.1.0.tgz",
|
||||||
|
|
@ -1841,6 +2131,35 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
|
@ -1892,6 +2211,18 @@
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||||
|
"integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||||
|
|
@ -2051,6 +2382,51 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-concat": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
|
@ -2060,6 +2436,15 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
|
@ -2086,6 +2471,43 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
@ -2095,6 +2517,18 @@
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
"version": "1.6.18",
|
"version": "1.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
|
@ -2141,6 +2575,12 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
|
"better-sqlite3": "^12.10.0",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-rate-limit": "^8.5.2",
|
||||||
"puppeteer": "^25.1.0",
|
"puppeteer": "^25.1.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
"puppeteer-extra-plugin-stealth": "^2.11.2"
|
||||||
|
|
|
||||||
115
public/app.js
115
public/app.js
|
|
@ -85,6 +85,9 @@ const elements = {
|
||||||
btnDetailEdit: document.getElementById('btn-detail-edit'),
|
btnDetailEdit: document.getElementById('btn-detail-edit'),
|
||||||
btnDetailDelete: document.getElementById('btn-detail-delete'),
|
btnDetailDelete: document.getElementById('btn-detail-delete'),
|
||||||
btnDetailClone: document.getElementById('btn-detail-clone'),
|
btnDetailClone: document.getElementById('btn-detail-clone'),
|
||||||
|
btnDetailShare: document.getElementById('btn-detail-share'),
|
||||||
|
btnDetailRevokeShare: document.getElementById('btn-detail-revoke-share'),
|
||||||
|
tagShareStats: document.getElementById('tag-share-stats'),
|
||||||
recipeDetailCreator: document.getElementById('recipe-detail-creator'),
|
recipeDetailCreator: document.getElementById('recipe-detail-creator'),
|
||||||
recipeDetailCreatorName: document.getElementById('recipe-detail-creator-name'),
|
recipeDetailCreatorName: document.getElementById('recipe-detail-creator-name'),
|
||||||
recipeDetailTitle: document.getElementById('recipe-detail-title'),
|
recipeDetailTitle: document.getElementById('recipe-detail-title'),
|
||||||
|
|
@ -175,6 +178,8 @@ function registerEvents() {
|
||||||
elements.btnDetailEdit.addEventListener('click', editActiveRecipe);
|
elements.btnDetailEdit.addEventListener('click', editActiveRecipe);
|
||||||
elements.btnDetailDelete.addEventListener('click', deleteActiveRecipe);
|
elements.btnDetailDelete.addEventListener('click', deleteActiveRecipe);
|
||||||
elements.btnDetailClone.addEventListener('click', cloneActiveRecipe);
|
elements.btnDetailClone.addEventListener('click', cloneActiveRecipe);
|
||||||
|
elements.btnDetailShare.addEventListener('click', shareActiveRecipe);
|
||||||
|
elements.btnDetailRevokeShare.addEventListener('click', revokeActiveRecipeShare);
|
||||||
|
|
||||||
// Auth Form Submissions
|
// Auth Form Submissions
|
||||||
elements.loginForm.addEventListener('submit', handleLogin);
|
elements.loginForm.addEventListener('submit', handleLogin);
|
||||||
|
|
@ -539,6 +544,100 @@ async function cloneActiveRecipe() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function shareActiveRecipe() {
|
||||||
|
if (!activeRecipe) return;
|
||||||
|
|
||||||
|
if (activeRecipe.isPublic === false) {
|
||||||
|
if (confirm(`"${activeRecipe.title}" is currently private.\n\nTo share it with people outside ForkFolio, you must make it public first. Would you like to make this recipe public now?`)) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/recipes/${activeRecipe.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...getAuthHeaders()
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ isPublic: true })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to update privacy.');
|
||||||
|
|
||||||
|
const updated = await response.json();
|
||||||
|
activeRecipe = updated;
|
||||||
|
fetchRecipes();
|
||||||
|
|
||||||
|
// Re-render detail badge visual state
|
||||||
|
showRecipeDetails(updated);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling privacy for share link:', err);
|
||||||
|
alert('Failed to make the recipe public.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If public and has a share token
|
||||||
|
if (activeRecipe.shareToken) {
|
||||||
|
const shareLink = `${window.location.origin}/share.html?token=${activeRecipe.shareToken}`;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareLink);
|
||||||
|
alert(`Public share link successfully copied to your clipboard!\n\n${shareLink}`);
|
||||||
|
} catch (clipErr) {
|
||||||
|
console.warn('Clipboard write failed. Displaying fallback alert:', clipErr);
|
||||||
|
prompt('Public Share Link:', shareLink);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Sharing token was not generated. Please try saving this recipe as public again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokeActiveRecipeShare() {
|
||||||
|
if (!activeRecipe) return;
|
||||||
|
if (!confirm(`Are you sure you want to revoke the shared link for "${activeRecipe.title}"?\n\nThis will make the recipe private, disable the public link immediately, and clear all visitor history.`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/recipes/${activeRecipe.id}/revoke-share`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const err = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || 'Failed to revoke shared link.');
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(`Successfully revoked the public share link for "${activeRecipe.title}".`);
|
||||||
|
|
||||||
|
// Refresh list and details view
|
||||||
|
const listUrl = activeTab === 'community-vault' ? '/api/recipes/public' : '/api/recipes';
|
||||||
|
const listRes = await fetch(listUrl, { headers: getAuthHeaders() });
|
||||||
|
if (listRes.ok) {
|
||||||
|
allRecipes = await listRes.json();
|
||||||
|
renderLibrary();
|
||||||
|
const updated = allRecipes.find(r => r.id === activeRecipe.id);
|
||||||
|
if (updated) {
|
||||||
|
showRecipeDetails(updated);
|
||||||
|
} else {
|
||||||
|
closeModal('detail-modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error revoking recipe share:', error);
|
||||||
|
alert(`Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// DRAFT / MANUAL CREATOR MODAL POPULATOR
|
// DRAFT / MANUAL CREATOR MODAL POPULATOR
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
|
|
@ -850,9 +949,17 @@ function showRecipeDetails(recipe) {
|
||||||
if (canEdit) {
|
if (canEdit) {
|
||||||
elements.btnDetailEdit.style.display = 'flex';
|
elements.btnDetailEdit.style.display = 'flex';
|
||||||
elements.btnDetailDelete.style.display = 'flex';
|
elements.btnDetailDelete.style.display = 'flex';
|
||||||
|
|
||||||
|
// Revoke share button toggle
|
||||||
|
if (recipe.isPublic && recipe.shareToken) {
|
||||||
|
elements.btnDetailRevokeShare.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
elements.btnDetailRevokeShare.style.display = 'none';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
elements.btnDetailEdit.style.display = 'none';
|
elements.btnDetailEdit.style.display = 'none';
|
||||||
elements.btnDetailDelete.style.display = 'none';
|
elements.btnDetailDelete.style.display = 'none';
|
||||||
|
elements.btnDetailRevokeShare.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOwner) {
|
if (isOwner) {
|
||||||
|
|
@ -892,6 +999,14 @@ function showRecipeDetails(recipe) {
|
||||||
} else {
|
} else {
|
||||||
elements.tagSource.style.display = 'none';
|
elements.tagSource.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public unique visitors count stats badge
|
||||||
|
if (recipe.isPublic && recipe.shareToken && recipe.visitorCount !== undefined) {
|
||||||
|
elements.tagShareStats.style.display = 'flex';
|
||||||
|
elements.tagShareStats.querySelector('.val').textContent = `${recipe.visitorCount} Unique ${recipe.visitorCount === 1 ? 'Visitor' : 'Visitors'}`;
|
||||||
|
} else {
|
||||||
|
elements.tagShareStats.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Populate Ingredients with scaler
|
// Populate Ingredients with scaler
|
||||||
renderScaledIngredients();
|
renderScaledIngredients();
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,8 @@
|
||||||
<button class="btn btn-accent" id="btn-detail-clone" style="display: none;" title="Clone to My Cookbook">
|
<button class="btn btn-accent" id="btn-detail-clone" style="display: none;" title="Clone to My Cookbook">
|
||||||
<i data-lucide="copy"></i> <span>Clone to My Cookbook</span>
|
<i data-lucide="copy"></i> <span>Clone to My Cookbook</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-icon-round" id="btn-detail-share" title="Generate Public Share Link"><i data-lucide="share-2"></i></button>
|
||||||
|
<button class="btn btn-icon-round btn-danger-hover" id="btn-detail-revoke-share" style="display: none;" title="Disable Public Share Link"><i data-lucide="link-2-off" style="color: var(--danger);"></i></button>
|
||||||
<button class="btn btn-icon-round" id="btn-detail-edit" title="Edit Recipe"><i data-lucide="edit"></i></button>
|
<button class="btn btn-icon-round" id="btn-detail-edit" title="Edit Recipe"><i data-lucide="edit"></i></button>
|
||||||
<button class="btn btn-icon-round btn-danger-hover" id="btn-detail-delete" title="Delete Recipe"><i data-lucide="trash-2"></i></button>
|
<button class="btn btn-icon-round btn-danger-hover" id="btn-detail-delete" title="Delete Recipe"><i data-lucide="trash-2"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -267,6 +269,9 @@
|
||||||
<a id="tag-source" href="#" target="_blank" rel="noopener noreferrer" style="display: none;">
|
<a id="tag-source" href="#" target="_blank" rel="noopener noreferrer" style="display: none;">
|
||||||
<i data-lucide="external-link"></i> <span class="val">Original Source</span>
|
<i data-lucide="external-link"></i> <span class="val">Original Source</span>
|
||||||
</a>
|
</a>
|
||||||
|
<span id="tag-share-stats" style="display: none; background: rgba(99, 102, 241, 0.12); border: 1px dashed rgba(99, 102, 241, 0.3); color: #6366f1;">
|
||||||
|
<i data-lucide="eye" style="color: #6366f1;"></i> <span class="val">0 Visitors</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="recipe-detail-desc" id="recipe-detail-description">A quick and delicious salmon dinner.</p>
|
<p class="recipe-detail-desc" id="recipe-detail-description">A quick and delicious salmon dinner.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -431,7 +436,6 @@
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<label for="admin-new-username">Username *</label>
|
<label for="admin-new-username">Username *</label>
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<i data-lucide="user" class="input-icon"></i>
|
|
||||||
<input type="text" id="admin-new-username" required placeholder="e.g. chef_julie" autocomplete="off">
|
<input type="text" id="admin-new-username" required placeholder="e.g. chef_julie" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<p class="form-help text-sm opacity-80" style="margin-top: 4px; font-size: 0.8rem; color: var(--text-muted);">Lowercase letters, numbers, underscores (3-20 characters).</p>
|
<p class="form-help text-sm opacity-80" style="margin-top: 4px; font-size: 0.8rem; color: var(--text-muted);">Lowercase letters, numbers, underscores (3-20 characters).</p>
|
||||||
|
|
@ -439,7 +443,6 @@
|
||||||
<div class="form-group mb-6">
|
<div class="form-group mb-6">
|
||||||
<label for="admin-new-password">Temporary Password *</label>
|
<label for="admin-new-password">Temporary Password *</label>
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
<i data-lucide="lock" class="input-icon"></i>
|
|
||||||
<input type="password" id="admin-new-password" required placeholder="Minimum 6 characters" autocomplete="off">
|
<input type="password" id="admin-new-password" required placeholder="Minimum 6 characters" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
229
public/share.html
Normal file
229
public/share.html
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ForkFolio – Shared Recipe Viewer</title>
|
||||||
|
<!-- Google Fonts Outfit & Inter -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<!-- Lucide Icons -->
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<!-- Custom Styling -->
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
/* Styling adjustments specifically for the share page public view */
|
||||||
|
.share-cta-banner {
|
||||||
|
background: linear-gradient(135deg, rgba(251, 146, 60, 0.15) 0%, rgba(244, 63, 94, 0.15) 100%);
|
||||||
|
border: 1px dashed var(--primary);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.share-cta-banner h3 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
.share-cta-banner p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: 500px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.share-container {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px 80px 20px;
|
||||||
|
}
|
||||||
|
.share-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 5%;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background-color: var(--bg-nav);
|
||||||
|
backdrop-filter: var(--glass-blur);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.recipe-not-found {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 100px 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.recipe-not-found i {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.recipe-not-found h2 {
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="dark-theme">
|
||||||
|
<!-- Top Navigation -->
|
||||||
|
<header class="share-header">
|
||||||
|
<div class="header-logo" style="cursor: pointer;" onclick="window.location.href='/'">
|
||||||
|
<div class="logo-icon"><i data-lucide="cooking-pot"></i></div>
|
||||||
|
<span class="logo-text">Fork<span>Folio</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
<a href="/" class="btn btn-primary" title="Go to Cookbook homepage">
|
||||||
|
<i data-lucide="log-in"></i> <span>Join ForkFolio</span>
|
||||||
|
</a>
|
||||||
|
<button class="theme-toggle" id="theme-toggle" title="Toggle Theme">
|
||||||
|
<i data-lucide="sun" class="sun-icon"></i>
|
||||||
|
<i data-lucide="moon" class="moon-icon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Recipe Not Found Fallback -->
|
||||||
|
<div class="recipe-not-found" id="recipe-not-found-block">
|
||||||
|
<i data-lucide="alert-triangle"></i>
|
||||||
|
<h2>Recipe Not Found</h2>
|
||||||
|
<p>This link may have expired, or the chef has set this recipe back to private.</p>
|
||||||
|
<a href="/" class="btn btn-outline" style="margin-top: 20px;">
|
||||||
|
<i data-lucide="home"></i> Back to Homepage
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Share Container -->
|
||||||
|
<main class="share-container" id="share-recipe-block" style="display: none;">
|
||||||
|
<!-- CTA Onboarding Banner -->
|
||||||
|
<section class="share-cta-banner">
|
||||||
|
<h3>Cooked this recipe with ForkFolio!</h3>
|
||||||
|
<p>ForkFolio lets you save recipes from any website instantly, organize your kitchen cookbook, scale portions, and cook step-by-step with an interactive assistant.</p>
|
||||||
|
<a href="/" class="btn btn-accent" style="background-color: var(--primary); box-shadow: 0 4px 14px var(--primary-glow);">
|
||||||
|
<i data-lucide="chef-hat"></i> <span>Build My Own Cookbook</span>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Unified Detail View Layout -->
|
||||||
|
<article class="modal-box no-padding" style="max-height: none; box-shadow: var(--shadow-md); margin-bottom: 40px;">
|
||||||
|
<div class="detail-hero-banner" id="recipe-detail-banner" style="height: 440px;">
|
||||||
|
<!-- Banner display cover -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-main-content">
|
||||||
|
<div class="detail-header-info">
|
||||||
|
<h2 id="recipe-detail-title">Recipe Title</h2>
|
||||||
|
<div class="recipe-creator-credit" style="margin-bottom: 12px;">
|
||||||
|
<span class="creator-badge" style="font-size: 0.85rem; padding: 4px 8px; background: rgba(245, 158, 11, 0.15); border: 1px dashed rgba(245, 158, 11, 0.3); border-radius: 6px; color: #f59e0b; display: inline-flex; align-items: center; gap: 4px;">
|
||||||
|
<i data-lucide="user" style="width: 12px; height: 12px;"></i> Shared by <span id="recipe-detail-creator-name">@chef</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="recipe-meta-tags">
|
||||||
|
<span id="tag-servings"><i data-lucide="users"></i> <span class="val">4 servings</span></span>
|
||||||
|
<span id="tag-prep"><i data-lucide="clock"></i> Prep: <span class="val">15 mins</span></span>
|
||||||
|
<span id="tag-cook"><i data-lucide="flame"></i> Cook: <span class="val">25 mins</span></span>
|
||||||
|
<a id="tag-source" href="#" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i data-lucide="external-link"></i> <span class="val">Original Source</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="recipe-detail-desc" id="recipe-detail-description">Description notes here.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-tabs-container">
|
||||||
|
<!-- Serving Adjuster & Cooking mode trigger -->
|
||||||
|
<div class="servings-scaler-panel">
|
||||||
|
<span class="scale-label">Scale Ingredients:</span>
|
||||||
|
<div class="scaler-controls">
|
||||||
|
<button class="scaler-btn" id="scaler-minus"><i data-lucide="minus"></i></button>
|
||||||
|
<span class="scaler-val" id="current-servings-val">1x</span>
|
||||||
|
<button class="scaler-btn" id="scaler-plus"><i data-lucide="plus"></i></button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-accent cooking-mode-trigger" id="btn-trigger-cookmode">
|
||||||
|
<i data-lucide="chef-hat"></i> <span>Start Cooking Mode</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-split-layout">
|
||||||
|
<!-- Left: Ingredients Checklist -->
|
||||||
|
<div class="detail-ingredients-column">
|
||||||
|
<h3>Ingredients</h3>
|
||||||
|
<p class="section-subtext">Check off items as you gather them</p>
|
||||||
|
<ul class="ingredients-check-list" id="recipe-detail-ingredients">
|
||||||
|
<!-- Checklist populated dynamically -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Instructions Checklist -->
|
||||||
|
<div class="detail-instructions-column">
|
||||||
|
<h3>Cooking Directions</h3>
|
||||||
|
<p class="section-subtext">Track your progress step-by-step</p>
|
||||||
|
<ol class="instructions-progress-list" id="recipe-detail-instructions">
|
||||||
|
<!-- Steps populated dynamically -->
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- FULLSCREEN INTERACTIVE COOKING MODE -->
|
||||||
|
<div class="modal-overlay cooking-mode-overlay" id="cooking-mode-modal">
|
||||||
|
<div class="cooking-container">
|
||||||
|
<div class="cooking-header">
|
||||||
|
<div class="cooking-brand">
|
||||||
|
<i data-lucide="chef-hat"></i> <span>Cooking Assistant</span>
|
||||||
|
</div>
|
||||||
|
<div class="cooking-title-indicator" id="cook-recipe-title">Recipe Title</div>
|
||||||
|
|
||||||
|
<!-- Live Stopwatch / Timer -->
|
||||||
|
<div class="cook-timer-panel">
|
||||||
|
<div class="timer-display" id="cook-timer-val">00:00</div>
|
||||||
|
<button class="timer-btn" id="cook-timer-play" title="Play/Pause stopwatch"><i data-lucide="play"></i></button>
|
||||||
|
<button class="timer-btn btn-outline" id="cook-timer-reset" title="Reset stopwatch"><i data-lucide="rotate-ccw"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="close-btn color-white" onclick="exitCookingMode()"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cooking-stepper-box">
|
||||||
|
<div class="step-progress-track">
|
||||||
|
<div class="step-progress-bar" id="cook-progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cooking-slide">
|
||||||
|
<span class="cooking-step-badge" id="cook-step-index">STEP 1 of 5</span>
|
||||||
|
<div class="cooking-step-text" id="cook-step-instruction">Step instructions...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cooking-controls">
|
||||||
|
<button class="btn btn-outline btn-lg color-white" id="cook-btn-prev">
|
||||||
|
<i data-lucide="chevron-left"></i> <span>Prev Step</span>
|
||||||
|
</button>
|
||||||
|
<div class="step-dots" id="cook-step-dots">
|
||||||
|
<!-- Step indicator dots -->
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-accent btn-lg" id="cook-btn-next">
|
||||||
|
<span>Next Step</span> <span class="btn-icon-wrapper"><i data-lucide="chevron-right"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="share.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
387
public/share.js
Normal file
387
public/share.js
Normal file
|
|
@ -0,0 +1,387 @@
|
||||||
|
// ForkFolio Shared Recipe Viewer Logic
|
||||||
|
let activeRecipe = null;
|
||||||
|
let currentMultiplier = 1;
|
||||||
|
let stopwatchInterval = null;
|
||||||
|
let stopwatchSeconds = 0;
|
||||||
|
let cookingActiveStep = 0;
|
||||||
|
|
||||||
|
// DOM Cache
|
||||||
|
const elements = {
|
||||||
|
themeToggle: document.getElementById('theme-toggle'),
|
||||||
|
shareRecipeBlock: document.getElementById('share-recipe-block'),
|
||||||
|
recipeNotFoundBlock: document.getElementById('recipe-not-found-block'),
|
||||||
|
|
||||||
|
// Recipe Details
|
||||||
|
recipeDetailBanner: document.getElementById('recipe-detail-banner'),
|
||||||
|
recipeDetailTitle: document.getElementById('recipe-detail-title'),
|
||||||
|
recipeDetailCreatorName: document.getElementById('recipe-detail-creator-name'),
|
||||||
|
tagServings: document.getElementById('tag-servings'),
|
||||||
|
tagPrep: document.getElementById('tag-prep'),
|
||||||
|
tagCook: document.getElementById('tag-cook'),
|
||||||
|
tagSource: document.getElementById('tag-source'),
|
||||||
|
recipeDetailDescription: document.getElementById('recipe-detail-description'),
|
||||||
|
recipeDetailIngredients: document.getElementById('recipe-detail-ingredients'),
|
||||||
|
recipeDetailInstructions: document.getElementById('recipe-detail-instructions'),
|
||||||
|
|
||||||
|
// Scaler
|
||||||
|
scalerMinus: document.getElementById('scaler-minus'),
|
||||||
|
scalerPlus: document.getElementById('scaler-plus'),
|
||||||
|
currentServingsVal: document.getElementById('current-servings-val'),
|
||||||
|
btnTriggerCookmode: document.getElementById('btn-trigger-cookmode'),
|
||||||
|
|
||||||
|
// Cooking Mode Modal
|
||||||
|
cookingModeModal: document.getElementById('cooking-mode-modal'),
|
||||||
|
cookRecipeTitle: document.getElementById('cook-recipe-title'),
|
||||||
|
cookTimerVal: document.getElementById('cook-timer-val'),
|
||||||
|
cookTimerPlay: document.getElementById('cook-timer-play'),
|
||||||
|
cookTimerReset: document.getElementById('cook-timer-reset'),
|
||||||
|
cookProgressBar: document.getElementById('cook-progress-bar'),
|
||||||
|
cookStepIndex: document.getElementById('cook-step-index'),
|
||||||
|
cookStepInstruction: document.getElementById('cook-step-instruction'),
|
||||||
|
cookBtnPrev: document.getElementById('cook-btn-prev'),
|
||||||
|
cookBtnNext: document.getElementById('cook-btn-next'),
|
||||||
|
cookStepDots: document.getElementById('cook-step-dots')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start Lifecycle
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initTheme();
|
||||||
|
registerEvents();
|
||||||
|
fetchSharedRecipe();
|
||||||
|
});
|
||||||
|
|
||||||
|
function initTheme() {
|
||||||
|
const savedTheme = localStorage.getItem('gourmet-theme') || 'dark';
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
document.body.classList.add('dark-theme');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dark-theme');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
document.body.classList.toggle('dark-theme');
|
||||||
|
const isDark = document.body.classList.contains('dark-theme');
|
||||||
|
localStorage.setItem('gourmet-theme', isDark ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerEvents() {
|
||||||
|
elements.themeToggle.addEventListener('click', toggleTheme);
|
||||||
|
|
||||||
|
// Portions Adjustments
|
||||||
|
elements.scalerMinus.addEventListener('click', () => adjustMultiplier(-0.5));
|
||||||
|
elements.scalerPlus.addEventListener('click', () => adjustMultiplier(0.5));
|
||||||
|
|
||||||
|
// Cooking steppers triggers
|
||||||
|
elements.btnTriggerCookmode.addEventListener('click', enterCookingMode);
|
||||||
|
elements.cookBtnPrev.addEventListener('click', prevCookingStep);
|
||||||
|
elements.cookBtnNext.addEventListener('click', nextCookingStep);
|
||||||
|
elements.cookTimerPlay.addEventListener('click', toggleTimer);
|
||||||
|
elements.cookTimerReset.addEventListener('click', resetTimer);
|
||||||
|
|
||||||
|
// Hotkeys for cooking stepper
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (elements.cookingModeModal.classList.contains('active')) {
|
||||||
|
if (e.key === 'ArrowRight' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
nextCookingStep();
|
||||||
|
} else if (e.key === 'ArrowLeft') {
|
||||||
|
e.preventDefault();
|
||||||
|
prevCookingStep();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
exitCookingMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch public shared recipe
|
||||||
|
async function fetchSharedRecipe() {
|
||||||
|
const token = new URLSearchParams(window.location.search).get('token');
|
||||||
|
if (!token) {
|
||||||
|
showErrorBlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/share/recipes/${token}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
activeRecipe = await response.json();
|
||||||
|
renderRecipePage(activeRecipe);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load shared recipe:', err);
|
||||||
|
showErrorBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErrorBlock() {
|
||||||
|
elements.recipeNotFoundBlock.style.display = 'flex';
|
||||||
|
elements.shareRecipeBlock.style.display = 'none';
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRecipePage(recipe) {
|
||||||
|
elements.recipeNotFoundBlock.style.display = 'none';
|
||||||
|
elements.shareRecipeBlock.style.display = 'block';
|
||||||
|
|
||||||
|
// Banner Image Cover
|
||||||
|
if (recipe.image) {
|
||||||
|
elements.recipeDetailBanner.style.backgroundImage = `url('${recipe.image}')`;
|
||||||
|
} else {
|
||||||
|
elements.recipeDetailBanner.style.backgroundImage = 'none';
|
||||||
|
elements.recipeDetailBanner.style.backgroundColor = 'var(--bg-app)';
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.recipeDetailTitle.textContent = recipe.title;
|
||||||
|
elements.recipeDetailCreatorName.textContent = `@${recipe.createdBy || 'gourmet_system'}`;
|
||||||
|
elements.recipeDetailDescription.textContent = recipe.description || 'No description available for this recipe.';
|
||||||
|
|
||||||
|
// Servings and timings tags
|
||||||
|
if (recipe.servings) {
|
||||||
|
elements.tagServings.style.display = 'flex';
|
||||||
|
elements.tagServings.querySelector('.val').textContent = `${recipe.servings} Servings`;
|
||||||
|
} else {
|
||||||
|
elements.tagServings.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipe.prepTime) {
|
||||||
|
elements.tagPrep.style.display = 'flex';
|
||||||
|
elements.tagPrep.querySelector('.val').textContent = recipe.prepTime;
|
||||||
|
} else {
|
||||||
|
elements.tagPrep.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipe.cookTime) {
|
||||||
|
elements.tagCook.style.display = 'flex';
|
||||||
|
elements.tagCook.querySelector('.val').textContent = recipe.cookTime;
|
||||||
|
} else {
|
||||||
|
elements.tagCook.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipe.sourceUrl) {
|
||||||
|
elements.tagSource.style.display = 'flex';
|
||||||
|
elements.tagSource.href = recipe.sourceUrl;
|
||||||
|
} else {
|
||||||
|
elements.tagSource.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render lists
|
||||||
|
renderScaledIngredients();
|
||||||
|
renderInstructions();
|
||||||
|
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ingredient Scaling Algorithms
|
||||||
|
function adjustMultiplier(change) {
|
||||||
|
const newMulti = currentMultiplier + change;
|
||||||
|
if (newMulti < 0.5 || newMulti > 10) return;
|
||||||
|
|
||||||
|
currentMultiplier = newMulti;
|
||||||
|
elements.currentServingsVal.textContent = `${currentMultiplier}x`;
|
||||||
|
renderScaledIngredients();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScaledIngredients() {
|
||||||
|
elements.recipeDetailIngredients.innerHTML = '';
|
||||||
|
if (!activeRecipe || !activeRecipe.ingredients) return;
|
||||||
|
|
||||||
|
activeRecipe.ingredients.forEach((ing) => {
|
||||||
|
const scaledText = scaleIngredientText(ing, currentMultiplier);
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'ingredient-checkbox-item';
|
||||||
|
li.innerHTML = `
|
||||||
|
<div class="checkbox-custom"><i data-lucide="check" style="width:12px;height:12px;"></i></div>
|
||||||
|
<span class="ingredient-text">${scaledText}</span>
|
||||||
|
`;
|
||||||
|
li.addEventListener('click', () => li.classList.toggle('checked'));
|
||||||
|
elements.recipeDetailIngredients.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scaleIngredientText(text, multiplier) {
|
||||||
|
if (multiplier === 1) return text;
|
||||||
|
|
||||||
|
const mixedRegex = /^(\d+)\s+(\d+\/\d+)\s*(.*)/;
|
||||||
|
const mixedMatch = text.match(mixedRegex);
|
||||||
|
if (mixedMatch) {
|
||||||
|
const whole = parseInt(mixedMatch[1], 10);
|
||||||
|
const fracParts = mixedMatch[2].split('/');
|
||||||
|
const fracVal = parseInt(fracParts[0], 10) / parseInt(fracParts[1], 10);
|
||||||
|
const total = (whole + fracVal) * multiplier;
|
||||||
|
return `${formatScaledNumber(total)} ${mixedMatch[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fracRegex = /^(\d+\/\d+)\s*(.*)/;
|
||||||
|
const fracMatch = text.match(fracRegex);
|
||||||
|
if (fracMatch) {
|
||||||
|
const parts = fracMatch[1].split('/');
|
||||||
|
const val = (parseInt(parts[0], 10) / parseInt(parts[1], 10)) * multiplier;
|
||||||
|
return `${formatScaledNumber(val)} ${fracMatch[2]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numRegex = /^(\d+(?:\.\d+)?)\s*(.*)/;
|
||||||
|
const numMatch = text.match(numRegex);
|
||||||
|
if (numMatch) {
|
||||||
|
const val = parseFloat(numMatch[1]) * multiplier;
|
||||||
|
return `${formatScaledNumber(val)} ${numMatch[2]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatScaledNumber(num) {
|
||||||
|
if (num % 1 === 0) return num.toString();
|
||||||
|
const rounded = Math.round(num * 100) / 100;
|
||||||
|
const decimal = rounded % 1;
|
||||||
|
const integer = Math.floor(rounded);
|
||||||
|
|
||||||
|
if (Math.abs(decimal - 0.25) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/4';
|
||||||
|
if (Math.abs(decimal - 0.5) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/2';
|
||||||
|
if (Math.abs(decimal - 0.75) < 0.05) return (integer > 0 ? integer + ' ' : '') + '3/4';
|
||||||
|
if (Math.abs(decimal - 0.33) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/3';
|
||||||
|
if (Math.abs(decimal - 0.66) < 0.05) return (integer > 0 ? integer + ' ' : '') + '2/3';
|
||||||
|
|
||||||
|
return rounded.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Directions Steps
|
||||||
|
function renderInstructions() {
|
||||||
|
elements.recipeDetailInstructions.innerHTML = '';
|
||||||
|
if (!activeRecipe || !activeRecipe.instructions) return;
|
||||||
|
|
||||||
|
activeRecipe.instructions.forEach((step, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
if (step.startsWith('###')) {
|
||||||
|
li.className = 'instruction-step-item section-header-step';
|
||||||
|
li.innerHTML = `<div class="step-text-block">${step.replace('###', '').trim()}</div>`;
|
||||||
|
} else {
|
||||||
|
li.className = 'instruction-step-item';
|
||||||
|
li.innerHTML = `
|
||||||
|
<div class="step-number-bullet">${index + 1}</div>
|
||||||
|
<div class="step-text-block">${step}</div>
|
||||||
|
`;
|
||||||
|
li.addEventListener('click', () => li.classList.toggle('checked'));
|
||||||
|
}
|
||||||
|
elements.recipeDetailInstructions.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standalone Cooking Assistant Mode
|
||||||
|
function enterCookingMode() {
|
||||||
|
if (!activeRecipe || !activeRecipe.instructions || activeRecipe.instructions.length === 0) return;
|
||||||
|
|
||||||
|
elements.cookRecipeTitle.textContent = activeRecipe.title;
|
||||||
|
cookingActiveStep = 0;
|
||||||
|
|
||||||
|
// Filter out sub-section headers (###)
|
||||||
|
const cleanInstructions = activeRecipe.instructions.filter(step => !step.startsWith('###'));
|
||||||
|
if (cleanInstructions.length === 0) return;
|
||||||
|
|
||||||
|
elements.cookingModeModal.classList.add('active');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
renderCookingStep(cleanInstructions);
|
||||||
|
resetTimer();
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitCookingMode() {
|
||||||
|
elements.cookingModeModal.classList.remove('active');
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCookingStep(instructions) {
|
||||||
|
const stepText = instructions[cookingActiveStep];
|
||||||
|
elements.cookStepIndex.textContent = `Step ${cookingActiveStep + 1} of ${instructions.length}`;
|
||||||
|
elements.cookStepInstruction.textContent = stepText;
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
const progressPercent = ((cookingActiveStep + 1) / instructions.length) * 100;
|
||||||
|
elements.cookProgressBar.style.width = `${progressPercent}%`;
|
||||||
|
|
||||||
|
// Navigation dots
|
||||||
|
elements.cookStepDots.innerHTML = '';
|
||||||
|
instructions.forEach((_, idx) => {
|
||||||
|
const dot = document.createElement('div');
|
||||||
|
dot.className = `dot ${idx === cookingActiveStep ? 'active' : ''}`;
|
||||||
|
dot.addEventListener('click', () => {
|
||||||
|
cookingActiveStep = idx;
|
||||||
|
renderCookingStep(instructions);
|
||||||
|
});
|
||||||
|
elements.cookStepDots.appendChild(dot);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable/disable navigation buttons
|
||||||
|
elements.cookBtnPrev.disabled = cookingActiveStep === 0;
|
||||||
|
const nextBtnIconWrapper = elements.cookBtnNext.querySelector('.btn-icon-wrapper');
|
||||||
|
if (cookingActiveStep === instructions.length - 1) {
|
||||||
|
elements.cookBtnNext.querySelector('span').textContent = 'Finish Cooking';
|
||||||
|
if (nextBtnIconWrapper) nextBtnIconWrapper.innerHTML = '<i data-lucide="check"></i>';
|
||||||
|
} else {
|
||||||
|
elements.cookBtnNext.querySelector('span').textContent = 'Next Step';
|
||||||
|
if (nextBtnIconWrapper) nextBtnIconWrapper.innerHTML = '<i data-lucide="chevron-right"></i>';
|
||||||
|
}
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevCookingStep() {
|
||||||
|
const cleanInstructions = activeRecipe.instructions.filter(step => !step.startsWith('###'));
|
||||||
|
if (cookingActiveStep > 0) {
|
||||||
|
cookingActiveStep--;
|
||||||
|
renderCookingStep(cleanInstructions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextCookingStep() {
|
||||||
|
const cleanInstructions = activeRecipe.instructions.filter(step => !step.startsWith('###'));
|
||||||
|
if (cookingActiveStep < cleanInstructions.length - 1) {
|
||||||
|
cookingActiveStep++;
|
||||||
|
renderCookingStep(cleanInstructions);
|
||||||
|
} else {
|
||||||
|
exitCookingMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer Stopwatch Controls
|
||||||
|
function startTimer() {
|
||||||
|
if (stopwatchInterval) return;
|
||||||
|
elements.cookTimerPlay.innerHTML = '<i data-lucide="pause"></i>';
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
stopwatchInterval = setInterval(() => {
|
||||||
|
stopwatchSeconds++;
|
||||||
|
const mins = Math.floor(stopwatchSeconds / 60).toString().padStart(2, '0');
|
||||||
|
const secs = (stopwatchSeconds % 60).toString().padStart(2, '0');
|
||||||
|
elements.cookTimerVal.textContent = `${mins}:${secs}`;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (stopwatchInterval) {
|
||||||
|
clearInterval(stopwatchInterval);
|
||||||
|
stopwatchInterval = null;
|
||||||
|
}
|
||||||
|
elements.cookTimerPlay.innerHTML = '<i data-lucide="play"></i>';
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTimer() {
|
||||||
|
if (stopwatchInterval) {
|
||||||
|
stopTimer();
|
||||||
|
} else {
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTimer() {
|
||||||
|
stopTimer();
|
||||||
|
stopwatchSeconds = 0;
|
||||||
|
elements.cookTimerVal.textContent = '00:00';
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue