mirror of
https://github.com/taogaetz/chefbible.git
synced 2025-12-06 11:47:24 -05:00
added intelligent ingredient parsing and updated form look. changed native form submission to spa style json api
This commit is contained in:
parent
2a577cf6ab
commit
f5c4a5e008
226
package-lock.json
generated
226
package-lock.json
generated
@ -9,7 +9,8 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.14.0",
|
||||
"@prisma/extension-accelerate": "^2.0.2"
|
||||
"@prisma/extension-accelerate": "^2.0.2",
|
||||
"convert": "^5.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
@ -815,7 +816,6 @@
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
@ -829,7 +829,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@ -839,7 +838,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@ -1956,6 +1954,31 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@ -1990,6 +2013,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@ -2005,7 +2040,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@ -2160,6 +2194,15 @@
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/convert": {
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/convert/-/convert-5.12.0.tgz",
|
||||
"integrity": "sha512-gwi1eN0dZsVI//Z+VHPgosbV0u3vxCgPT5p9H3pK5RyGrHrKaQSUNKxoGTYFbkJWv+qYaGY4w394gDNNaXer/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"wireit": "0.14.12"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
@ -2650,7 +2693,6 @@
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@ -2667,7 +2709,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@ -2694,7 +2735,6 @@
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@ -2732,7 +2772,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@ -2783,7 +2822,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@ -2855,7 +2893,6 @@
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
@ -2912,11 +2949,22 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -2926,7 +2974,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@ -2939,7 +2986,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@ -3006,6 +3052,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||||
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -3340,7 +3392,6 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@ -3350,7 +3401,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@ -3364,7 +3414,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@ -3485,6 +3534,15 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
|
||||
@ -3928,6 +3986,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"retry": "^0.12.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -3959,7 +4028,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -4021,11 +4089,19 @@
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
@ -4076,7 +4152,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -4152,6 +4227,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
|
||||
@ -4345,7 +4426,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@ -4585,6 +4665,110 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wireit": {
|
||||
"version": "0.14.12",
|
||||
"resolved": "https://registry.npmjs.org/wireit/-/wireit-0.14.12.tgz",
|
||||
"integrity": "sha512-gNSd+nZmMo6cuICezYXRIayu6TSOeCSCDzjSF0q6g8FKDsRbdqrONrSZYzdk/uBISmRcv4vZtsno6GyGvdXwGA==",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"vscode-extension",
|
||||
"website"
|
||||
],
|
||||
"dependencies": {
|
||||
"brace-expansion": "^4.0.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"fast-glob": "^3.2.11",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"proper-lockfile": "^4.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"wireit": "bin/wireit.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wireit/node_modules/balanced-match": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz",
|
||||
"integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/wireit/node_modules/brace-expansion": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz",
|
||||
"integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/wireit/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/wireit/node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/wireit/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/wireit/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.14.0",
|
||||
"@prisma/extension-accelerate": "^2.0.2"
|
||||
"@prisma/extension-accelerate": "^2.0.2",
|
||||
"convert": "^5.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
const { recipe } = data;
|
||||
let scale = $state(1);
|
||||
|
||||
function formatQuantity(q: number | null | undefined): string {
|
||||
if (q == null) return '';
|
||||
const scaled = q * scale;
|
||||
return Number(scaled.toFixed(2)).toString();
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
goto('/');
|
||||
@ -20,7 +26,7 @@
|
||||
<header class="border-b border-base-300 bg-base-100/90 p-5 backdrop-blur-md">
|
||||
<button
|
||||
class="btn gap-2 text-base-content/70 btn-ghost hover:bg-base-200 hover:text-base-content"
|
||||
on:click={goBack}
|
||||
onclick={goBack}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
@ -104,6 +110,18 @@
|
||||
</svg>
|
||||
<span class="font-semibold">{recipe.station}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="font-semibold text-base-content/70">Scale</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.25"
|
||||
max="4"
|
||||
step="0.25"
|
||||
bind:value={scale}
|
||||
class="range range-primary"
|
||||
/>
|
||||
<span class="badge">{scale}×</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -122,7 +140,7 @@
|
||||
</span>
|
||||
{#if recipeIngredient.quantity || recipeIngredient.unit}
|
||||
<span class="font-medium text-base-content/70">
|
||||
{recipeIngredient.quantity}
|
||||
{formatQuantity(recipeIngredient.quantity)}
|
||||
{recipeIngredient.unit}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@ -1,60 +1,65 @@
|
||||
import type { Actions } from './$types';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import prisma from '$lib/server/prisma';
|
||||
// import type { Actions } from './$types';
|
||||
// import { fail } from '@sveltejs/kit';
|
||||
// import prisma from '$lib/server/prisma';
|
||||
|
||||
export const actions: Actions = {
|
||||
create: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
// export const actions: Actions = {
|
||||
// create: async ({ request }) => {
|
||||
// const formData = await request.formData();
|
||||
|
||||
const name = (formData.get('name') as string | null)?.trim() ?? '';
|
||||
const description = (formData.get('description') as string | null)?.trim() || null;
|
||||
const instructions = (formData.get('instructions') as string | null)?.trim() || null;
|
||||
const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string;
|
||||
const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string;
|
||||
const ingredientsRaw = (formData.get('ingredients') as string | null) || '';
|
||||
// const name = (formData.get('name') as string | null)?.trim() ?? '';
|
||||
// const description = (formData.get('description') as string | null)?.trim() || null;
|
||||
// const instructions = (formData.get('instructions') as string | null)?.trim() || null;
|
||||
// const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string;
|
||||
// const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string;
|
||||
// const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null;
|
||||
// let parsedIngredients: Array<{ name: string; quantity: number | null; unit: string | null; prep: string | null }> = [];
|
||||
|
||||
if (!name) {
|
||||
return fail(400, { message: 'Name is required', name, description, instructions, time, station, ingredients: ingredientsRaw });
|
||||
}
|
||||
// if (!name) {
|
||||
// return fail(400, { message: 'Name is required', name, description, instructions, time, station });
|
||||
// }
|
||||
|
||||
const recipe = await prisma.recipe.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
instructions,
|
||||
time,
|
||||
station
|
||||
}
|
||||
});
|
||||
// // Parse the ingredients JSON from client
|
||||
// if (parsedIngredientsRaw) {
|
||||
// try {
|
||||
// parsedIngredients = JSON.parse(parsedIngredientsRaw);
|
||||
// } catch (error) {
|
||||
// console.error('Failed to parse ingredients JSON:', error);
|
||||
// return fail(400, { message: 'Invalid ingredients format', name, description, instructions, time, station });
|
||||
// }
|
||||
// }
|
||||
|
||||
const ingredients = Array.from(
|
||||
new Set(
|
||||
ingredientsRaw
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0)
|
||||
)
|
||||
);
|
||||
// const recipe = await prisma.recipe.create({
|
||||
// data: {
|
||||
// name,
|
||||
// description,
|
||||
// instructions,
|
||||
// time,
|
||||
// station
|
||||
// }
|
||||
// });
|
||||
|
||||
if (ingredients.length > 0) {
|
||||
for (const ingredientName of ingredients) {
|
||||
const ingredient = await prisma.ingredient.upsert({
|
||||
where: { name: ingredientName },
|
||||
update: {},
|
||||
create: { name: ingredientName }
|
||||
});
|
||||
// if (parsedIngredients.length > 0) {
|
||||
// for (const parsed of parsedIngredients) {
|
||||
// if (!parsed.name) continue;
|
||||
// const ingredient = await prisma.ingredient.upsert({
|
||||
// where: { name: parsed.name },
|
||||
// update: {},
|
||||
// create: { name: parsed.name }
|
||||
// });
|
||||
// await prisma.recipeIngredient.create({
|
||||
// data: {
|
||||
// recipeId: recipe.id,
|
||||
// ingredientId: ingredient.id,
|
||||
// quantity: parsed.quantity,
|
||||
// unit: parsed.unit,
|
||||
// prep: parsed.prep
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
await prisma.recipeIngredient.create({
|
||||
data: {
|
||||
recipeId: recipe.id,
|
||||
ingredientId: ingredient.id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw redirect(303, `/recipe/${recipe.id}`);
|
||||
}
|
||||
};
|
||||
// return { location: `/recipe/${recipe.id}` };
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
@ -1,45 +1,339 @@
|
||||
<script lang="ts">
|
||||
let pending = $state(false);
|
||||
let lastError = $state<string | null>(null);
|
||||
let lastSuccess = $state<string | null>(null);
|
||||
let ingredientsText = $state('');
|
||||
let formSubmitted = $state(false);
|
||||
|
||||
// Parsing functions for live preview
|
||||
function parseMixedNumber(input: string): number | null {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return null;
|
||||
// e.g. 1 1/2, 3/4, 2.5
|
||||
const parts = trimmed.split(/\s+/);
|
||||
let total = 0;
|
||||
for (const part of parts) {
|
||||
if (/^\d+\/\d+$/.test(part)) {
|
||||
const [num, den] = part.split('/').map(Number);
|
||||
if (den === 0) return null;
|
||||
total += num / den;
|
||||
} else if (/^\d+(?:\.\d+)?$/.test(part)) {
|
||||
total += Number(part);
|
||||
} else {
|
||||
// unexpected token
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
const unitAliases: Record<string, { canonical: string; system: 'volume' | 'mass' | 'count' }> = {
|
||||
// volume
|
||||
tsp: { canonical: 'ml', system: 'volume' },
|
||||
'tsp.': { canonical: 'ml', system: 'volume' },
|
||||
teaspoon: { canonical: 'ml', system: 'volume' },
|
||||
teaspoons: { canonical: 'ml', system: 'volume' },
|
||||
tbsp: { canonical: 'ml', system: 'volume' },
|
||||
'tbsp.': { canonical: 'ml', system: 'volume' },
|
||||
tablespoon: { canonical: 'ml', system: 'volume' },
|
||||
tablespoons: { canonical: 'ml', system: 'volume' },
|
||||
cup: { canonical: 'ml', system: 'volume' },
|
||||
cups: { canonical: 'ml', system: 'volume' },
|
||||
ml: { canonical: 'ml', system: 'volume' },
|
||||
milliliter: { canonical: 'ml', system: 'volume' },
|
||||
milliliters: { canonical: 'ml', system: 'volume' },
|
||||
l: { canonical: 'ml', system: 'volume' },
|
||||
liter: { canonical: 'ml', system: 'volume' },
|
||||
liters: { canonical: 'ml', system: 'volume' },
|
||||
pt: { canonical: 'ml', system: 'volume' },
|
||||
qt: { canonical: 'ml', system: 'volume' },
|
||||
gal: { canonical: 'ml', system: 'volume' },
|
||||
floz: { canonical: 'ml', system: 'volume' },
|
||||
'fl-oz': { canonical: 'ml', system: 'volume' },
|
||||
'fl oz': { canonical: 'ml', system: 'volume' },
|
||||
// mass
|
||||
g: { canonical: 'g', system: 'mass' },
|
||||
gram: { canonical: 'g', system: 'mass' },
|
||||
grams: { canonical: 'g', system: 'mass' },
|
||||
kg: { canonical: 'g', system: 'mass' },
|
||||
kilogram: { canonical: 'g', system: 'mass' },
|
||||
kilograms: { canonical: 'g', system: 'mass' },
|
||||
oz: { canonical: 'g', system: 'mass' },
|
||||
ounce: { canonical: 'g', system: 'mass' },
|
||||
ounces: { canonical: 'g', system: 'mass' },
|
||||
lb: { canonical: 'g', system: 'mass' },
|
||||
lbs: { canonical: 'g', system: 'mass' },
|
||||
pound: { canonical: 'g', system: 'mass' },
|
||||
pounds: { canonical: 'g', system: 'mass' },
|
||||
// count / generic pieces stay as-is
|
||||
clove: { canonical: 'clove', system: 'count' },
|
||||
cloves: { canonical: 'clove', system: 'count' },
|
||||
piece: { canonical: 'pc', system: 'count' },
|
||||
pieces: { canonical: 'pc', system: 'count' },
|
||||
pc: { canonical: 'pc', system: 'count' }
|
||||
};
|
||||
|
||||
function parseIngredientLine(
|
||||
line: string
|
||||
): { name: string; quantity: number | null; unit: string | null; prep: string | null } | null {
|
||||
const raw = line.trim();
|
||||
if (!raw) return null;
|
||||
|
||||
// Split the line into parts
|
||||
const parts = raw.split(/\s+/);
|
||||
|
||||
// If only one part, it's just a name
|
||||
if (parts.length === 1) {
|
||||
return { name: parts[0], quantity: null, unit: null, prep: null };
|
||||
}
|
||||
|
||||
// Check if first part is a quantity
|
||||
const firstPart = parts[0];
|
||||
const quantity = parseMixedNumber(firstPart);
|
||||
|
||||
if (quantity !== null) {
|
||||
// We have a quantity, check if second part is a unit
|
||||
if (parts.length >= 3) {
|
||||
const secondPart = parts[1];
|
||||
// Check if second part looks like a unit (short, alphanumeric, and in our known units)
|
||||
if (secondPart.length <= 4 && /^[a-zA-Z]+$/.test(secondPart)) {
|
||||
const unitLower = secondPart.toLowerCase();
|
||||
// Only treat as unit if it's in our known unit aliases
|
||||
if (unitAliases[unitLower]) {
|
||||
const unit = unitLower;
|
||||
const name = parts.slice(2).join(' ');
|
||||
// Check if there's prep info after a comma
|
||||
const commaIndex = name.indexOf(',');
|
||||
let finalName = name;
|
||||
let prep = null;
|
||||
if (commaIndex !== -1) {
|
||||
finalName = name.substring(0, commaIndex);
|
||||
prep = name.substring(commaIndex + 1).trim();
|
||||
}
|
||||
|
||||
return {
|
||||
name: finalName.trim(),
|
||||
quantity,
|
||||
unit,
|
||||
prep
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We have a quantity but no clear unit, check if it's a no-space unit like "3g"
|
||||
if (firstPart.length > 1) {
|
||||
const match = firstPart.match(/^(\d+(?:[\s-]+\d+\/\d+|\/\d+|\.\d+)?)([a-zA-Z]+)$/);
|
||||
if (match) {
|
||||
const [, qtyStr, unit] = match;
|
||||
const qtyNum = parseMixedNumber(qtyStr);
|
||||
if (qtyNum !== null) {
|
||||
const unitLower = unit.toLowerCase();
|
||||
// Only treat as unit if it's in our known unit aliases
|
||||
if (unitAliases[unitLower]) {
|
||||
const name = parts.slice(1).join(' ');
|
||||
const commaIndex = name.indexOf(',');
|
||||
let finalName = name;
|
||||
let prep = null;
|
||||
if (commaIndex !== -1) {
|
||||
finalName = name.substring(0, commaIndex);
|
||||
prep = name.substring(commaIndex + 1).trim();
|
||||
}
|
||||
|
||||
return {
|
||||
name: finalName.trim(),
|
||||
quantity: qtyNum,
|
||||
unit: unitLower,
|
||||
prep
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quantity but no unit found
|
||||
const name = parts.slice(1).join(' ');
|
||||
const commaIndex = name.indexOf(',');
|
||||
let finalName = name;
|
||||
let prep = null;
|
||||
if (commaIndex !== -1) {
|
||||
finalName = name.substring(0, commaIndex);
|
||||
prep = name.substring(commaIndex + 1).trim();
|
||||
}
|
||||
|
||||
return {
|
||||
name: finalName.trim(),
|
||||
quantity,
|
||||
unit: null,
|
||||
prep
|
||||
};
|
||||
}
|
||||
|
||||
// No quantity found in first part, check if it's a no-space unit like "1g"
|
||||
if (firstPart.length > 1) {
|
||||
const match = firstPart.match(/^(\d+(?:[\s-]+\d+\/\d+|\/\d+|\.\d+)?)([a-zA-Z]+)$/);
|
||||
if (match) {
|
||||
const [, qtyStr, unit] = match;
|
||||
const qtyNum = parseMixedNumber(qtyStr);
|
||||
if (qtyNum !== null) {
|
||||
const unitLower = unit.toLowerCase();
|
||||
// Only treat as unit if it's in our known unit aliases
|
||||
if (unitAliases[unitLower]) {
|
||||
const name = parts.slice(1).join(' ');
|
||||
const commaIndex = name.indexOf(',');
|
||||
let finalName = name;
|
||||
let prep = null;
|
||||
if (commaIndex !== -1) {
|
||||
finalName = name.substring(0, commaIndex);
|
||||
prep = name.substring(commaIndex + 1).trim();
|
||||
}
|
||||
|
||||
return {
|
||||
name: finalName.trim(),
|
||||
quantity: qtyNum,
|
||||
unit: unitLower,
|
||||
prep
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No quantity found, treat as name only
|
||||
const name = parts.join(' ');
|
||||
const commaIndex = name.indexOf(',');
|
||||
let finalName = name;
|
||||
let prep = null;
|
||||
if (commaIndex !== -1) {
|
||||
finalName = name.substring(0, commaIndex);
|
||||
prep = name.substring(commaIndex + 1).trim();
|
||||
}
|
||||
|
||||
return {
|
||||
name: finalName.trim(),
|
||||
quantity: null,
|
||||
unit: null,
|
||||
prep
|
||||
};
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
// Clear all form inputs
|
||||
const form = document.querySelector('form');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
ingredientsText = '';
|
||||
lastError = null;
|
||||
lastSuccess = null;
|
||||
formSubmitted = false;
|
||||
}
|
||||
|
||||
async function onsubmit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
if (!form) return;
|
||||
|
||||
pending = true;
|
||||
lastError = null;
|
||||
lastSuccess = null;
|
||||
|
||||
try {
|
||||
// native submit; progressive enhancement
|
||||
// Parse ingredients on client side
|
||||
const ingredientsRaw = ingredientsText.trim();
|
||||
const parsedIngredients: Array<{
|
||||
name: string;
|
||||
quantity: number | null;
|
||||
unit: string | null;
|
||||
prep: string | null;
|
||||
}> = [];
|
||||
|
||||
if (ingredientsRaw) {
|
||||
const lines = ingredientsRaw.split('\n').filter((line) => line.trim());
|
||||
for (const line of lines) {
|
||||
const parsed = parseIngredientLine(line.trim());
|
||||
if (parsed) {
|
||||
parsedIngredients.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create FormData with parsed ingredients
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Remove the raw ingredients textarea
|
||||
formData.delete('ingredients');
|
||||
|
||||
// Add parsed ingredients as JSON
|
||||
formData.append('parsedIngredients', JSON.stringify(parsedIngredients));
|
||||
|
||||
// Submit to server
|
||||
const response = await fetch('?/create', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Show success message
|
||||
lastSuccess = 'Recipe created successfully!';
|
||||
formSubmitted = true;
|
||||
|
||||
// Clear the form after a short delay
|
||||
clearForm();
|
||||
|
||||
// Redirect to the new recipe immediately after showing success message
|
||||
const result = await response.json();
|
||||
console.log(result);
|
||||
if (result.location) {
|
||||
window.location.href = result.location;
|
||||
}
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
lastError = errorData.message || 'Failed to create recipe';
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = 'An error occurred while creating the recipe';
|
||||
console.error('Error creating recipe:', error);
|
||||
} finally {
|
||||
// allow server navigation to take over
|
||||
setTimeout(() => (pending = false), 400);
|
||||
pending = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto my-6 max-w-3xl px-5">
|
||||
<h1 class="mb-5 text-2xl font-extrabold">New Recipe</h1>
|
||||
<div class="mx-auto my-4 w-full max-w-2xl px-3 sm:my-6 sm:px-6 lg:max-w-4xl">
|
||||
<h1 class="mb-4 text-xl font-extrabold sm:mb-5 sm:text-2xl">New Recipe</h1>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/create"
|
||||
{onsubmit}
|
||||
class="card grid gap-4 border border-base-200 bg-base-100 p-4 shadow-xl"
|
||||
class="card relative grid gap-4 border border-base-200 bg-base-100 p-3 shadow-xl sm:p-6"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<label class="form-control">
|
||||
{#if pending}
|
||||
<div
|
||||
class="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-base-100/80 backdrop-blur-sm"
|
||||
>
|
||||
<div class="text-center">
|
||||
<span class="loading loading-lg loading-spinner text-primary"></span>
|
||||
<p class="mt-2 text-base-content/70">Creating your recipe...</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<label class="form-control sm:col-span-2 lg:col-span-1">
|
||||
<span class="label"><span class="label-text font-bold">Name</span></span>
|
||||
<input
|
||||
name="name"
|
||||
required
|
||||
autocomplete="off"
|
||||
inputmode="text"
|
||||
class="input-bordered input input-lg"
|
||||
class="input-bordered input w-full"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Time</span></span>
|
||||
<select name="time" class="select-bordered select select-lg">
|
||||
<select name="time" class="select-bordered select w-full">
|
||||
<option>Quick</option>
|
||||
<option selected>Medium</option>
|
||||
<option>Long</option>
|
||||
@ -48,7 +342,7 @@
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Station</span></span>
|
||||
<select name="station" class="select-bordered select select-lg">
|
||||
<select name="station" class="select-bordered select w-full">
|
||||
<option>Garde Manger</option>
|
||||
<option selected>Pans</option>
|
||||
<option>Grill</option>
|
||||
@ -59,42 +353,103 @@
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Ingredients (one per line)</span></span
|
||||
>
|
||||
|
||||
<textarea
|
||||
name="ingredients"
|
||||
rows="6"
|
||||
placeholder="Eggs
|
||||
Butter
|
||||
Salt"
|
||||
class="textarea-bordered textarea textarea-lg"
|
||||
placeholder="2 cups flour, sifted
|
||||
3g salt
|
||||
500ml water"
|
||||
class="textarea-bordered textarea w-full"
|
||||
bind:value={ingredientsText}
|
||||
></textarea>
|
||||
</label>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-sm text-base-content/60">
|
||||
<strong>Pattern:</strong> [quantity] [unit] ingredient name[, prep notes]<br />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if ingredientsText}
|
||||
<div class="mt-3 rounded-lg bg-base-200 p-3">
|
||||
<div class="mb-2 text-sm font-semibold text-base-content/70">Live Preview:</div>
|
||||
{#each ingredientsText.split('\n').filter((line) => line.trim()) as line, i}
|
||||
{#if line.trim()}
|
||||
{@const parsed = parseIngredientLine(line.trim())}
|
||||
{#if parsed}
|
||||
<div class="mb-2 rounded border-l-4 border-primary bg-base-100 p-2">
|
||||
<div class="mb-1 text-xs text-base-content/50">Line {i + 1}:</div>
|
||||
<div class="flex flex-wrap gap-2 text-sm">
|
||||
{#if parsed.quantity}
|
||||
<span class="badge badge-sm badge-primary">Qty: {parsed.quantity}</span>
|
||||
{/if}
|
||||
{#if parsed.unit}
|
||||
<span class="badge badge-sm badge-secondary">Unit: {parsed.unit}</span>
|
||||
{/if}
|
||||
<span class="badge badge-sm badge-accent">Name: {parsed.name}</span>
|
||||
{#if parsed.prep}
|
||||
<span class="badge badge-outline badge-sm">Prep: {parsed.prep}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
<div
|
||||
class="tooltip tooltip-top"
|
||||
data-tip="tsp, tbsp, cup(s), ml, l, g, kg, oz, lb, clove, piece, pc"
|
||||
>
|
||||
<strong class="cursor-help underline decoration-dotted">Valid units</strong>
|
||||
</div>
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Description</span></span>
|
||||
<textarea name="description" rows="3" class="textarea-bordered textarea textarea-lg"
|
||||
></textarea>
|
||||
<textarea name="description" rows="3" class="textarea-bordered textarea w-full"></textarea>
|
||||
</label>
|
||||
|
||||
<label class="form-control">
|
||||
<span class="label"><span class="label-text font-bold">Instructions</span></span>
|
||||
<textarea name="instructions" rows="6" class="textarea-bordered textarea textarea-lg"
|
||||
></textarea>
|
||||
<textarea name="instructions" rows="6" class="textarea-bordered textarea w-full"></textarea>
|
||||
</label>
|
||||
|
||||
<div class="mt-1 flex justify-end gap-3">
|
||||
<a class="btn" href="/">Cancel</a>
|
||||
<div class="mt-2 flex flex-col gap-3 sm:flex-row sm:justify-end">
|
||||
<a class="btn order-2 w-full sm:order-1 sm:w-auto" href="/">Cancel</a>
|
||||
<button
|
||||
class="btn font-extrabold btn-lg btn-primary"
|
||||
class="btn order-1 w-full font-extrabold btn-primary sm:order-2 sm:w-auto"
|
||||
type="submit"
|
||||
name="/create"
|
||||
disabled={pending}
|
||||
>
|
||||
{pending ? 'Saving…' : 'Save'}
|
||||
{#if pending}
|
||||
<span class="loading loading-sm loading-spinner"></span>
|
||||
Creating Recipe…
|
||||
{:else}
|
||||
Create Recipe
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{#if lastError}
|
||||
<p class="text-error">{lastError}</p>
|
||||
{/if}
|
||||
{#if lastSuccess}
|
||||
<div class="mt-3 alert alert-success">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{lastSuccess}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
87
src/routes/recipe/new/+server.ts
Normal file
87
src/routes/recipe/new/+server.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import prisma from '$lib/server/prisma';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const name = (formData.get('name') as string | null)?.trim() ?? '';
|
||||
const description = (formData.get('description') as string | null)?.trim() || null;
|
||||
const instructions = (formData.get('instructions') as string | null)?.trim() || null;
|
||||
const time = ((formData.get('time') as string | null)?.trim() || 'Medium') as string;
|
||||
const station = ((formData.get('station') as string | null)?.trim() || 'Pans') as string;
|
||||
const parsedIngredientsRaw = formData.get('parsedIngredients') as string | null;
|
||||
let parsedIngredients: Array<{ name: string; quantity: number | null; unit: string | null; prep: string | null }> = [];
|
||||
|
||||
if (!name) {
|
||||
return new Response(JSON.stringify({ type: 'error', message: 'Name is required' }), { status: 400 });
|
||||
}
|
||||
|
||||
if (parsedIngredientsRaw) {
|
||||
try {
|
||||
parsedIngredients = JSON.parse(parsedIngredientsRaw);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse ingredients JSON:', error);
|
||||
return new Response(JSON.stringify({ type: 'error', message: 'Invalid ingredients format' }), { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
// Create recipe
|
||||
const recipe = await prisma.recipe.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
instructions,
|
||||
time,
|
||||
station
|
||||
}
|
||||
});
|
||||
|
||||
if (parsedIngredients.length > 0) {
|
||||
// Upsert ingredients first
|
||||
const ingredientNames = parsedIngredients.map(i => i.name).filter(Boolean);
|
||||
const existingIngredients = await prisma.ingredient.findMany({
|
||||
where: { name: { in: ingredientNames } }
|
||||
});
|
||||
|
||||
const existingNames = new Set(existingIngredients.map(i => i.name));
|
||||
|
||||
const createIngredients = ingredientNames
|
||||
.filter(n => !existingNames.has(n))
|
||||
.map(n => ({ name: n }));
|
||||
|
||||
if (createIngredients.length) {
|
||||
await prisma.ingredient.createMany({
|
||||
data: createIngredients
|
||||
});
|
||||
}
|
||||
|
||||
// Get all ingredients with ids
|
||||
const allIngredients = await prisma.ingredient.findMany({
|
||||
where: { name: { in: ingredientNames } }
|
||||
});
|
||||
const ingredientMap = new Map(allIngredients.map(i => [i.name, i.id]));
|
||||
|
||||
// Bulk create recipeIngredient
|
||||
const recipeIngredientsData = parsedIngredients
|
||||
.filter(i => i.name)
|
||||
.map(i => ({
|
||||
recipeId: recipe.id,
|
||||
ingredientId: ingredientMap.get(i.name)!,
|
||||
quantity: i.quantity,
|
||||
unit: i.unit,
|
||||
prep: i.prep
|
||||
}));
|
||||
|
||||
if (recipeIngredientsData.length) {
|
||||
await prisma.recipeIngredient.createMany({
|
||||
data: recipeIngredientsData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
type: 'success',
|
||||
status: 200,
|
||||
location: `/recipe/${recipe.id}`
|
||||
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user