Compare commits

...

169 commits

Author SHA1 Message Date
83cc273e52
chore: update deps 2025-11-29 08:54:12 +01:00
4ab8d98e2c
refactor: remove unused SentryErrorButton component and adjust related files
- Deleted the SentryErrorButton component as it was not needed.
- Updated the cookbook page to remove the SentryErrorButton reference.
- Adjusted the search component to fix a self-closing tag issue.
- Ensured the toggleLike function call is correctly formatted in the view component.
- Added sendDefaultPii option to Sentry configuration for improved error tracking.
2025-05-26 15:53:09 +02:00
bfd83a367b
feat: add Sentry error tracking integration
- Introduced SentryErrorButton component to demonstrate error tracking.
- Updated nuxt.config.ts to include Sentry module and configuration.
- Added Sentry client and server configuration files for error reporting.
- Updated package.json to include @sentry/nuxt and updated dependencies.
- Integrated SentryErrorButton component into the cookbook page.
2025-05-26 15:38:15 +02:00
3e34609fcf
add cookbook 2025-04-13 01:08:22 +02:00
35af888724
add cookbook page 2025-04-13 00:06:19 +02:00
fc022e024d
fix meal validation 2025-04-09 09:14:54 +02:00
29db641392
update dependencies 2025-01-02 19:01:34 +01:00
b7aca38912
13 sharing recipes (#49)
* fix: update description image

* feat: share using navigator api
2024-12-19 19:18:22 +01:00
3a66b00c74
refactor: consolidate the routers 2024-12-19 13:43:09 +01:00
32be4bb0df
fix landing page 2024-12-17 21:31:05 +01:00
0d93ff37e4
improve footer responsiveness 2024-12-17 21:28:52 +01:00
7decbe7cbe
autofocus search field on search page 2024-12-17 21:26:39 +01:00
5bd9f2c382
add search bar to search page and improve searhc on input hcange 2024-12-17 21:12:46 +01:00
b19cb02763
use icon instead of svg 2024-12-16 22:10:42 +01:00
a1bc45941d
ref: make the navbar responsive on small screens 2024-12-16 21:42:38 +01:00
534a98b36f
feat: Add mobile search icon and responsive search bar in navbar 2024-12-16 21:38:08 +01:00
55d857e658
ref: use slots to prevent repeating menu 2024-12-16 21:33:55 +01:00
7c4f6419cd
add explicit image dimensions 2024-12-16 21:26:36 +01:00
34a1f62813
improve accessibility 2024-12-16 21:26:36 +01:00
4329d61c43
image optimization 2024-12-15 15:38:14 +01:00
c756a129a2
fix: recipe links 2024-12-15 14:44:29 +01:00
8e7316dd81
untrack aider 2024-12-15 01:19:27 +01:00
77ea021b59
remove old imlpementation 2024-12-15 01:18:09 +01:00
27c7ffacf6
add seo 2024-12-15 01:18:09 +01:00
ba78374f81
fix: refactor and fixes 2024-12-15 01:18:09 +01:00
fb84b4bfc6
feat: Add category details page with image header, description, and related recipes 2024-12-15 01:18:09 +01:00
2010270bcf
feat: Add Zod validation for categories API response 2024-12-15 01:18:09 +01:00
296b2048e9
refactor: Remove header, set responsive card heights, and add description ellipsis 2024-12-15 01:18:09 +01:00
ed3d1bc853
add link to categories in the navbar 2024-12-15 01:18:09 +01:00
d75d292b2a
feat: Enhance categories page with improved error handling and card UI 2024-12-15 01:18:09 +01:00
85481b5f31
style: Format categories page template with consistent line breaks 2024-12-15 01:18:09 +01:00
eb296fe761
style: Improve categories page layout with loading, error, and empty states 2024-12-15 01:18:09 +01:00
e042e06b19
feat: Add categories page with category card and data fetching 2024-12-15 01:18:09 +01:00
2872907a98
feat: Add categories endpoint to recipes router 2024-12-15 01:18:09 +01:00
6dffeb5766
improve search results displays 2024-12-15 01:18:09 +01:00
6f5f66d02e
use debounce search on search page 2024-12-15 01:18:09 +01:00
72824daf63
feat: add a search result page 2024-12-15 01:18:09 +01:00
2d9fdd07c2
feat: search feature with debounce 2024-12-15 01:18:09 +01:00
a5d328a133
improve the landing page 2024-12-15 01:18:09 +01:00
2dee5123a4
add robots.txt 2024-12-15 01:18:09 +01:00
e85f98a532
feat: activate transitions 2024-12-15 01:18:09 +01:00
4d1f1f7486
ref: kebab-case 2024-12-15 01:18:09 +01:00
76624f206b
move app components to own directory 2024-12-15 01:18:09 +01:00
9b2fed6716
feat: improve footer and add links to github and portfolio 2024-12-15 01:18:09 +01:00
045d118b3f
feat: Add skeleton loading state for recipe details page
feat: Add detailed skeleton loading state matching recipe layout
2024-12-15 01:18:09 +01:00
e5ede6f01a
feat: improve SEO on index page 2024-12-15 01:18:09 +01:00
fbf840d448
chore: lint 2024-12-15 01:18:09 +01:00
a75ee69b94
feat: Add random recipe button with route navigation and composable trigger 2024-12-15 01:18:09 +01:00
821a48dfe4
handle random recipe page 2024-12-15 01:18:09 +01:00
3c4d4db1be
add seo 2024-12-15 01:18:09 +01:00
df158d3ff4
improve the layout 2024-12-15 01:18:09 +01:00
a9f4df0071
use trpc for recipe router 2024-12-15 01:18:09 +01:00
767471f2bb
add headers and middleware 2024-12-15 01:18:09 +01:00
ae63d45fe6
install trpc 2024-12-15 01:18:09 +01:00
f19874e4c7
add error handling 2024-12-15 01:18:09 +01:00
7c3bfb0ea8
find meals per id 2024-12-15 01:18:08 +01:00
884095b0e0
refactor 2024-12-15 01:18:08 +01:00
1539a03084
make api call on the server add tests 2024-12-15 01:18:08 +01:00
277ede1ad3
fix images 2024-12-15 01:18:08 +01:00
b2680e7d22
Merge pull request #38 from rjNemo/reborn
Migrate to nuxt
2024-12-15 01:18:08 +01:00
11d0e4682b
share types and fix linter errors 2024-12-15 01:18:08 +01:00
a567a4c41a
use shared component for the recipe 2024-12-15 01:18:08 +01:00
98888fd814
fetch by id 2024-12-15 01:18:08 +01:00
6d7c856ef9
full recipe 2024-12-15 01:18:08 +01:00
e80b11ef17
ingredients list 2024-12-15 01:18:08 +01:00
a89c2fcd24
fix: client side navigation 2024-12-15 01:18:08 +01:00
a146bea1ba
design card component 2024-12-15 01:18:08 +01:00
b17abd0cd4
faetch from api 2024-12-15 01:18:08 +01:00
df4c28235c
nuxt image 2024-12-15 01:18:08 +01:00
22a19315ec
lint 2024-12-15 01:18:08 +01:00
a7554b9801
prettier and esling 2024-12-15 01:18:08 +01:00
e63805539b
random recipe page 2024-12-15 01:18:08 +01:00
d35253be03
basic layout 2024-12-15 01:18:08 +01:00
7708d7dfe0
use typography plugin 2024-12-15 01:18:08 +01:00
0e944d614f
installed tailwind 2024-12-15 01:18:08 +01:00
d9af585209
use bun
use bun
2024-12-15 01:18:08 +01:00
dependabot[bot]
7c7c3f0114 Bump ws from 7.5.6 to 7.5.10
Bumps [ws](https://github.com/websockets/ws) from 7.5.6 to 7.5.10.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.6...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 14:04:17 +02:00
5fbd50075c
Merge pull request #40 from rjNemo/dependabot/npm_and_yarn/braces-3.0.3
Bump braces from 3.0.2 to 3.0.3
2024-06-13 23:03:26 +02:00
dependabot[bot]
df60890a42
Bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-13 21:01:34 +00:00
dcb3aa48dc
Merge pull request #39 from rjNemo/dependabot/npm_and_yarn/ejs-3.1.10
Bump ejs from 3.1.8 to 3.1.10
2024-06-13 23:01:05 +02:00
dependabot[bot]
5165f11263
Bump ejs from 3.1.8 to 3.1.10
Bumps [ejs](https://github.com/mde/ejs) from 3.1.8 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.8...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 02:24:52 +00:00
dependabot[bot]
2d2357e925
Bump express from 4.18.2 to 4.19.2 (#37)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-28 20:59:39 +01:00
dependabot[bot]
d6616db8d2
Bump webpack-dev-middleware from 5.2.2 to 5.3.4 (#36)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.2.2 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.2.2...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-24 10:48:34 +01:00
dependabot[bot]
2211b05eac
Bump follow-redirects from 1.15.4 to 1.15.6 (#35)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-17 17:43:45 +01:00
dependabot[bot]
90441f97f2
Bump ip from 1.1.5 to 1.1.9 (#34)
Bumps [ip](https://github.com/indutny/node-ip) from 1.1.5 to 1.1.9.
- [Commits](https://github.com/indutny/node-ip/compare/v1.1.5...v1.1.9)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-21 23:11:59 +01:00
dependabot[bot]
3e33f956a7
Bump follow-redirects from 1.14.8 to 1.15.4 (#33)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.8 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.8...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-10 21:17:20 +01:00
dependabot[bot]
9e30d1e221
Bump @babel/traverse from 7.16.5 to 7.23.2 (#32)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.16.5 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-17 16:41:31 +02:00
dependabot[bot]
96108820a6
Bump word-wrap from 1.2.3 to 1.2.4 (#31)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-19 09:22:20 +02:00
dependabot[bot]
d4e0ced0ae
Bump semver from 6.3.0 to 6.3.1 (#30)
Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v6.3.1/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v6.3.1)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-11 10:22:10 +02:00
dependabot[bot]
e5b1158ed5
Bump tough-cookie from 4.0.0 to 4.1.3 (#29)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.0.0 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.0.0...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-09 21:29:00 +02:00
dependabot[bot]
e5cabb2b4e
Bump webpack from 5.65.0 to 5.76.1 (#28)
Bumps [webpack](https://github.com/webpack/webpack) from 5.65.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.65.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-14 20:03:32 +01:00
dependabot[bot]
9451c420c3
Bump json5 from 1.0.1 to 1.0.2 (#27)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-07 18:37:09 +01:00
dependabot[bot]
04ea2c46b8
Bump express from 4.17.1 to 4.18.2 (#26)
Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.18.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.18.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-08 12:17:57 +01:00
dependabot[bot]
a80aa5d677
Bump decode-uri-component from 0.2.0 to 0.2.2 (#25)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-08 09:07:02 +01:00
dependabot[bot]
05f198ca74
Bump loader-utils from 1.4.1 to 1.4.2 (#24)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-16 22:10:10 +01:00
dependabot[bot]
ac48bc0fe7
Bump loader-utils from 1.4.0 to 1.4.1 (#23)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-09 22:09:36 +01:00
dependabot[bot]
61515e72c7
Bump async from 2.6.3 to 2.6.4 (#22)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 16:03:29 +02:00
dependabot[bot]
3fa6801129
Bump ejs from 3.1.6 to 3.1.8 (#21)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.8.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.8)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 16:02:07 +02:00
dependabot[bot]
9d2af60aa0
Bump terser from 5.10.0 to 5.14.2 (#20)
Bumps [terser](https://github.com/terser/terser) from 5.10.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 16:00:13 +02:00
dependabot[bot]
cbc7eefe1b
Bump minimist from 1.2.5 to 1.2.6 (#19)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-31 23:57:42 +02:00
dependabot[bot]
9bcabb2584
Bump follow-redirects from 1.14.7 to 1.14.8 (#18)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-16 11:09:50 +01:00
dependabot[bot]
82c523857e
Bump nanoid from 3.1.30 to 3.2.0 (#17)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.30 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.30...3.2.0)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-25 17:53:34 +01:00
dependabot[bot]
1819754d7d
Bump follow-redirects from 1.14.6 to 1.14.7 (#16)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.6 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.6...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-16 09:12:50 -04:00
Ruidy
3773e79185 chore: fix cve 2021-12-14 13:50:45 -04:00
Ruidy
75fb6726f6 chore: update deps 2021-12-14 13:35:48 -04:00
Ruidy
ea9b38cf76 chore: clear dead code, update todo 2021-12-13 12:49:20 -04:00
Ruidy
0ce8b1cfc2 feat: remove login and profile 2021-12-13 12:45:29 -04:00
Ruidy
09b69aeccb feat: remove LogOutButton.tsx 2021-12-13 12:30:54 -04:00
Ruidy
c3df2ba713 feat: remove LogInButton.tsx 2021-12-13 12:30:10 -04:00
Ruidy
b873a0b93e downgrade yarn version 2021-10-02 19:59:31 +02:00
Ruidy
f0ff25ef5e upgrade yarn 2021-09-26 00:52:55 +02:00
Ruidy
fecfc95982 minor refactorings 2021-09-25 15:40:11 +02:00
Ruidy
2ac94f24f1 edit README 2021-09-25 12:15:03 +02:00
Ruidy
16757b2218
State (#10)
* configure context

* refactor

* get meal with context

* random button with context

* async actions

* refactor meal client
2021-09-16 18:45:56 +02:00
Ruidy
e8ac939fc9
state (#9)
* configure context

* refactor

* get meal with context

* random button with context

* async actions

* refactor meal client
2021-04-05 11:58:43 +02:00
Ruidy
7cde13f071
firebase (#8)
* use typescript

* fix types
2021-04-03 19:17:38 +02:00
Ruidy
cb101b22ec
strict typing (#7)
* strict=true

* typing and props
2021-03-31 16:39:07 +02:00
Ruidy
0d8cc9c9b3 icons 2021-03-29 12:46:40 +02:00
Ruidy
8409d09373
refactoring (#6)
* use tsx

* compile

* refactor Router

* refactor layout

* refactor home container

* refactor meal container

* refactor categories container

* refactor category container

* refactor search and profile container

* refactor

* move state over

* move state over

* css

* css
2021-03-29 12:37:53 +02:00
Ruidy
0e3b0a7cee
refactoring (#5)
* use tsx

* compile

* refactor Router

* refactor layout

* refactor home container

* refactor meal container

* refactor categories container

* refactor category container

* refactor search and profile container

* refactor
2021-03-28 17:05:46 +02:00
0b5a0a991c fix cve again 2021-03-24 10:33:14 +00:00
0531ee0d39 fix cve 2021-03-24 10:24:22 +00:00
Ruidy
500c6a1b73
Rj nemo/gitpod setup (#4)
* Fully automate dev setup with Gitpod

This commit implements a fully-automated development setup using Gitpod.io, an
online IDE for GitLab, GitHub, and Bitbucket that enables Dev-Environments-As-Code.
This makes it easy for anyone to get a ready-to-code workspace for any branch,
issue or pull request almost instantly with a single click.

* ignore file
2021-02-10 20:33:01 +01:00
dependabot[bot]
00e603df0a
Bump node-fetch from 2.6.0 to 2.6.1 in /functions (#3)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-12-12 22:51:55 +01:00
dependabot[bot]
c5942e8d47
Bump lodash from 4.17.15 to 4.17.19 in /functions (#2)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-14 09:21:11 +02:00
Ruidy
32daef87c3
Merge pull request #1 from rjNemo/dependabot/npm_and_yarn/functions/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4 in /functions
2020-06-23 19:37:25 +02:00
dependabot[bot]
963f2217e1
Bump websocket-extensions from 0.1.3 to 0.1.4 in /functions
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-08 05:04:31 +00:00
a1605dadc6 [bug]: fix error when unauthenticated user renders page 2020-04-28 15:19:57 +02:00
a07c91a332 chore: update dependencies 2020-04-28 12:09:02 +02:00
c4212eeb54 chore: update dependencies 2020-04-28 12:07:18 +02:00
b36b005f47 chore: update dependencies 2020-04-28 12:04:03 +02:00
81f92227c2 use env var for auth0 2020-04-28 11:27:40 +02:00
7fd605fd5d [bug]: save right value to favs db 2020-04-28 11:20:44 +02:00
e664753691 ProfilePage displays favourite recipes 2020-04-28 10:19:34 +02:00
93b76aecdb enable saving authenticated user favs to cloud db 2020-04-28 08:21:23 +02:00
99a7226d40 adds firebase functions; update ProfileUI; 2020-04-27 13:46:37 +02:00
29087ca71f get favs data from firebase to profile page 2020-04-15 17:22:48 +02:00
6132ed34ca refactor ContactForm 2020-04-15 14:53:01 +02:00
b5706c3a2e add PageLayout; updated pages 2020-04-15 10:32:49 +02:00
2a6f843d36 refactoring. Add MainLayout and MainRouter 2020-04-15 09:34:41 +02:00
47c9d5f4d9 added FireBase firestore 2020-04-04 13:09:51 +02:00
0967f5f2ad Documentation 2020-04-03 22:21:54 +02:00
5b2cc29195 nodemailer 2020-03-08 13:03:55 +01:00
90f9bda6f4 contact 2020-03-07 11:40:31 +01:00
f4833c4214 Fav Button 2020-02-15 09:17:51 +01:00
c20a7330c8 decoupling views and controllers 2020-02-10 14:09:58 +01:00
df86b94dd6 CSS 2020-02-08 21:50:28 +01:00
abd510457c navbar done 2020-02-06 11:15:07 +01:00
b9ad1db190 navbar skeleton ++ 2020-02-04 20:50:27 +01:00
0c1e426669 navbar skeleton 2020-02-04 15:10:44 +01:00
99e61fdd74 secrets 2020-02-01 16:24:33 +01:00
bce9407986 cleanup 2020-01-31 20:23:43 +01:00
4c8ffd52f3 secrets 2020-01-31 17:08:43 +01:00
f329f6733a preloader 2020-01-31 16:20:16 +01:00
a649006d87 auth0 authentication & sample profile page 2020-01-31 11:00:19 +01:00
12fdc42bd5 navigation error handling 2020-01-31 08:24:30 +01:00
fd3362a169 cleanup 2020-01-30 22:18:39 +01:00
df80472fd7 contact page 2020-01-30 13:23:23 +01:00
df6c0e01bb footer links 2020-01-30 11:24:31 +01:00
24b6c29d7f logo font, meal page new style 2020-01-29 22:13:31 +01:00
257c14fa90 daily brush 2020-01-29 21:01:04 +01:00
85f6f9efc1 daily brush 2020-01-29 17:30:29 +01:00
162857407d error Handling 2020-01-28 12:19:44 +01:00
b624122f13 cardEntry 2020-01-28 07:53:55 +01:00
b9067c85bd styling UI 2020-01-27 21:35:54 +01:00
c23636c249 styling 2020-01-27 21:00:17 +01:00
cb9f6e3ed8 Landing 2020-01-27 20:01:41 +01:00
0a9a00dbc3 SearchBar & Page 2020-01-27 16:11:35 +01:00
a1462ed9e0 SearchBar & Page 2020-01-27 15:48:18 +01:00
87 changed files with 5094 additions and 15383 deletions

2
.env.example Normal file
View file

@ -0,0 +1,2 @@
NUXT_API_URL=https://www.themealdb.com/api/json/v1/1/
NUXT_PUBLIC_SENTRY_DSN=

40
.gitignore vendored
View file

@ -1,23 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# dependencies # Node dependencies
/node_modules node_modules
/.pnp
.pnp.js
# testing # Logs
/coverage logs
*.log
# production # Misc
/build
# misc
.DS_Store .DS_Store
.env.local .fleet
.env.development.local .idea
.env.test.local
.env.production.local
npm-debug.log* # Local env files
yarn-debug.log* .env
yarn-error.log* .env.*
!.env.example
.aider*
# Sentry Config File
.env.sentry-build-plugin

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
src/

15
CHANGELOG.md Normal file
View file

@ -0,0 +1,15 @@
# CHANGELOG
## v.0.1
- WebApp
- Random meal suggestion
- List of meals by categories
- Search by name: you're looking for a recipe? Ours are easy to make and yummy!
## v.0.2
- Progressive Web App
- User Interface Enhancement
- Secured User Profiles
- Contact form

92
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,92 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at ruidy.nemausat@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

21
LICENSE.md Normal file
View file

@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2021 Ruidy Nemausat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,13 +1,34 @@
# Chef's Meal Planner # Chef's Meal Planner
![header image](https://socialify.git.ci/rjnemo/meal_planner/image?description=1&font=Raleway&language=1&logo=https%3A%2F%2Fmood2food.netlify.app%2Flogo192.png&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Dark)
![license](https://img.shields.io/github/license/rjNemo/meal_planner?style=for-the-badge)
![release tag](https://img.shields.io/github/v/release/rjNemo/meal_planner?style=for-the-badge)
Free meal planner for cooks short on ideas! (like me …) Free meal planner for cooks short on ideas! (like me …)
## Demo
[🚀 App live at this address!](https://mood2food.netlify.app/)
![Screenshot](docs/short_clip.gif)
### Screenshots
#### Home page
![Screenshot](docs/homepage.png)
#### Meal page
![Screenshot](docs/mealpage.png)
## Features ## Features
- Random meal suggestion - Random meal suggestion
- Search by name: you're look for a recipe? Ours are easy to make and Yummy! - Search by name: you look for a recipe? Ours are easy to make and Yummy!
- What's in the fridge ? Choose your main ingredient and get a meal suggestion - What's in the fridge ? Choose your main ingredient and get a meal suggestion
- Choose by category: - Choose by a category:
- Beef - Beef
- Breakfast - Breakfast
- Chicken - Chicken
@ -48,33 +69,59 @@ Free meal planner for cooks short on ideas! (like me …)
- Unknown - Unknown
- Vietnamese - Vietnamese
- Cocktail selection - Cocktail selection
- Create a profile and save your favourite meals - Create a profile and save your favourite meals
- Notation system: know what are the most loved meals - Notation system: know what are the most loved meals
- Share recipe with your friends and family
- Suggestions based on what your personal taste - Suggestions based on what your personal taste
- Recipes in Video - Recipes in Video ✓
- Get a full menu (Starter, Main, Dessert + Cocktail)
- Send a daily suggestion to newsletter
- History
- Language selection
- Nutritive value
- Add personal notes
## Supports ## Supports
- Web - Web
- Progressive Web App - Progressive Web App
- Mobile - Mobile
## Technical Stack ## Deployment
- `React` client on the front-end The application is hosted on [Netlify](https://netlify.com/) at the following
- [Materialize](https://materializecss.com) CSS librairy for styling address: [link](https://mood2food.netlify.app/).
- Public API: [TheMealDb](https://www.themealdb.com/api.php) and [TheCocktailDb](https://www.thecocktaildb.com/api.php)
- Hosting: anywhere
## Versions ## Built With
### Features in V.1 - [Nuxt](https://nuxt.com/) - The Intuitive Vue Framework
- [tRPC](https://trpc.io/) - End-to-end typesafe APIs made easy
- [Tailwindcss](https://tailwindcss.com) - Rapidly build modern websites without
ever leaving your HTML.
- [daisyUI](https://daisyui.com/) - The most popular component library for
Tailwind CSS
- [TheMealDb](https://www.themealdb.com/api.php) - An open, crowd-sourced database
of Recipes from around the world
- WebApp ## Contributing
- Random meal suggestion
- List of meals by categories
- Search by name: you're looking for a recipe? Ours are easy to make and yummy!
## TO DO Please read [CONTRIBUTING.md](https://github.com/rjNemo/meal_planner/contributors)
for details on our code of conduct, and the process for submitting pull requests
to us.
- put a preloader ## Versioning
We use [SemVer](http://semver.org/) for versioning. For the versions available, see
the [tags on this repository](https://github.com/rjNemo/meal_planner/tags).
## Authors
- **Ruidy Nemausat** - _Initial work_ - [GitHub](https://github.com/rjNemo)
See also the list of [contributors](https://github.com/rjNemo/meal_planner/contributors)
who participated in this project.
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md)
file for details

18
TODO.md Normal file
View file

@ -0,0 +1,18 @@
# TO DO
- [x] use bun package manager
- [x] use nuxt framework
- [x] rewrite the random page, the current landing page
- [x] rewrite the recipe page
- [x] deploy
- [x] nuxt image
- [x] prettier and eslint
- [x] transition and loading times
- [ ] pwa
- [x] seo, robots.txt
- [x] update the README
- [ ] create image provider
- [x] fetch recipe per id
- [ ] add mood section
- [ ] store recipes into my db (SQLite)
- [ ] process them using AI

27
app.vue Normal file
View file

@ -0,0 +1,27 @@
<template>
<div data-theme="cupcake" class="flex flex-col h-screen">
<app-navbar>
<template #menu>
<li><nuxt-link to="/categories">Categories</nuxt-link></li>
<li><nuxt-link to="/cookbook">Cookbook</nuxt-link></li>
</template>
</app-navbar>
<main class="flex-grow">
<nuxt-page />
</main>
<app-footer />
</div>
</template>
<style>
.page-enter-active,
.page-leave-active {
transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(1rem);
}
</style>

3
assets/css/main.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

3450
bun.lock Normal file

File diff suppressed because it is too large Load diff

32
components/app/footer.vue Normal file
View file

@ -0,0 +1,32 @@
<template>
<footer
class="footer bg-base-300 text-base-content items-center p-4 flex justify-between"
>
<aside class="items-center">
<p>
<span class="hidden sm:inline"
>Copyright &copy; {{ new Date().getFullYear() }}
</span>
Made with
</p>
</aside>
<nav class="grid-flow-col gap-4">
<nuxt-link
to="https://github.com/rjNemo/meal_planner"
:external="true"
target="_blank"
aria-label="navigate to the source code on GitHub"
>
<icon name="cib:github" class="w-6 h-6" />
</nuxt-link>
<nuxt-link
to="https://ruidy.nemausat.com"
:external="true"
target="_blank"
aria-label="navigate to my website"
>
<icon name="uil:globe" class="w-6 h-6" />
</nuxt-link>
</nav>
</footer>
</template>

60
components/app/navbar.vue Normal file
View file

@ -0,0 +1,60 @@
<script setup lang="ts">
const router = useRouter();
const route = useRoute();
const searchQuery = ref((route.query.q as string) || "");
const { execute } = useRecipeRandom();
const handleRandomClick = async () => {
if (route.path !== "/random") {
await router.push("/random");
}
await execute();
};
</script>
<template>
<nav class="navbar bg-base-300">
<div class="navbar-start">
<div class="dropdown">
<div
tabindex="0"
role="button"
class="btn btn-ghost lg:hidden"
arial-label="Menu button"
>
<icon name="uil:bars" class="w-6 h-6" />
<!-- TODO: add transition into cross on click -->
</div>
<ul
tabindex="0"
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-200 rounded-box w-52"
>
<slot name="menu" />
</ul>
</div>
<nuxt-link to="/" class="btn btn-ghost text-xl">
<nuxt-img src="/logo192.png" width="50" height="50" alt="logo" />
<span style="font-family: cursive"> Chefs </span>
</nuxt-link>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<slot name="menu" />
</ul>
</div>
<div class="navbar-end gap-2">
<!-- Search icon for mobile -->
<nuxt-link
to="/search"
class="btn btn-ghost sm:hidden"
aria-label="Search"
>
<icon name="uil:search" class="w-6 h-6" />
</nuxt-link>
<!-- Search bar for larger screens -->
<recipe-search v-model="searchQuery" class="hidden sm:flex" />
<button class="btn btn-primary" @click="handleRandomClick">Random</button>
</div>
</nav>
</template>

View file

@ -0,0 +1,40 @@
<script setup lang="ts">
defineProps<{
title: string;
pictureUrl: string;
videoUrl: string;
category: string;
origin: string;
}>();
</script>
<template>
<div class="card-body items-center text-center bg-base-200">
<h2 class="card-title">{{ title }}</h2>
<figure class="px-10 py-5">
<nuxt-img
:src="pictureUrl"
alt="`${title} picture`"
:placeholder="[300]"
width="300"
height="300"
format="webp"
/>
</figure>
<div class="card-actions space-between">
<nuxt-link
:to="videoUrl"
target="_blank"
aria-label="watch the recipe in video"
>
<icon name="cib:youtube" color="red" />
</nuxt-link>
<div class="badge badge-secondary">
<icon name="cil:apple" /> {{ category }}
</div>
<div class="badge badge-secondary">
<icon name="cil:location-pin" /> {{ origin }}
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,26 @@
<script setup lang="ts">
defineProps<{
ingredients: { name: string; quantity: string }[];
}>();
</script>
<template>
<div class="overflow-x-auto">
<table class="table table-s table-pin-rows table-pin-cols">
<thead>
<tr>
<th />
<td>Ingredient</td>
<td>Quantity</td>
</tr>
</thead>
<tbody>
<tr v-for="(ingredient, i) in ingredients" :key="i">
<th>{{ i + 1 }}</th>
<td>{{ ingredient.name }}</td>
<td>{{ ingredient.quantity }}</td>
</tr>
</tbody>
</table>
</div>
</template>

View file

@ -0,0 +1,57 @@
<template>
<label
class="input input-bordered input-primary flex items-center gap-2 container mx-auto px-4 lg:px-8 my-4"
>
<input
v-model="model"
type="text"
class="grow"
placeholder="Search recipes..."
:autofocus="autofocus"
@focus="isFocused = true"
@blur="isFocused = false"
>
<kbd
class="hidden md:inline-block kbd kbd-sm"
:class="{ 'opacity-50': !isFocused }"
></kbd
>
<kbd
class="hidden md:inline-block kbd kbd-sm"
:class="{ 'opacity-50': !isFocused }"
>K</kbd
>
</label>
</template>
<script setup lang="ts">
const model = defineModel<string>();
defineProps<{ autofocus?: boolean }>();
const isFocused = ref(false);
// Debounced navigation
const debouncedSearch = useDebounceFn((query: string) => {
navigateTo(`/search?q=${encodeURIComponent(query || "")}`);
}, 200);
// Watch for changes in model
watch(model, (newQuery) => {
debouncedSearch(newQuery || "");
});
onMounted(() => {
const handleKeydown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
const inputEl = document.querySelector("input");
inputEl?.focus();
}
};
window.addEventListener("keydown", handleKeydown);
onUnmounted(() => {
window.removeEventListener("keydown", handleKeydown);
});
});
</script>

View file

@ -0,0 +1,93 @@
<script setup lang="ts">
import type { Recipe } from "~/types/recipe";
import { useLocalStorage } from "@vueuse/core";
const { recipe } = defineProps<{ recipe: Recipe }>();
const cookbook = useLocalStorage<Recipe[]>("cookbook", []);
const likedRecipes = ref(new Set<string>());
onMounted(() => {
likedRecipes.value = new Set(cookbook.value.map((recipe) => recipe.id));
console.log("cook", likedRecipes.value);
});
const toggleLike = (recipeId: string) => {
if (likedRecipes.value.has(recipeId)) {
likedRecipes.value.delete(recipeId);
cookbook.value = cookbook.value.filter((recipe) => recipe.id !== recipeId);
} else {
likedRecipes.value.add(recipeId);
const recipeToAdd = recipe;
if (recipeToAdd) {
cookbook.value = [...cookbook.value, recipeToAdd];
}
}
};
const shareRecipe = async (recipe: Recipe) => {
const url =
useRequestURL().href.split("/").slice(0, -1).join("/") + "/" + recipe.id;
if (navigator.share) {
try {
await navigator.share({
title: recipe.title,
text: `Check out this recipe: ${recipe.title}`,
url,
});
} catch (error) {
console.error(error);
alert("Failed to share the recipe.");
}
} else {
alert("Sharing not supported on this device.");
}
};
</script>
<template>
<div class="container mx-auto px-4 lg:px-8">
<div class="flex flex-col lg:flex-row lg:justify-start gap-6 py-4">
<div class="w-full lg:w-[480px]">
<div class="card bg-base-100 shadow-xl mx-auto lg:mx-0">
<recipe-card
:title="recipe.title"
:picture-url="recipe.pictureUrl"
:video-url="recipe.videoUrl"
:category="recipe.category"
:origin="recipe.origin"
/>
</div>
</div>
<div class="w-full lg:w-[480px]">
<recipe-ingredients :ingredients="recipe.ingredients" />
</div>
</div>
<div class="flex flex-col items-center py-6">
<h2 class="text-2xl lg:text-3xl font-semibold mb-4">Instructions</h2>
<p class="prose prose-lg max-w-none w-full">
{{ recipe.instructions }}
</p>
<div class="flex gap-4 mt-4">
<button class="btn btn-accent" @click="shareRecipe(recipe)">
<icon name="uil:share-alt" class="mr-2 w-6 h-6" />
Share Recipe
</button>
<button
class="btn btn-ghost"
:class="{ 'text-red-500': likedRecipes.has(recipe.id) }"
@click="toggleLike(recipe.id)"
>
<icon
:name="likedRecipes.has(recipe.id) ? 'uil:heart' : 'uil:heart-alt'"
class="w-6 h-6"
/>
Like
</button>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,4 @@
export function useCategories() {
const { $client } = useNuxtApp();
return $client.listCategories.useQuery();
}

View file

@ -0,0 +1,4 @@
export function useCategoryRecipes(category: string) {
const { $client } = useNuxtApp();
return $client.recipesByCategory.useQuery(category);
}

View file

@ -0,0 +1,4 @@
export default function useRecipeById(id: number) {
const { $client } = useNuxtApp();
return $client.recipeGet.useQuery(id);
}

View file

@ -0,0 +1,4 @@
export default function useRecipeRandom() {
const { $client } = useNuxtApp();
return $client.recipeRandom.useQuery();
}

View file

@ -0,0 +1,4 @@
export default function useRecipeSearch(query: string) {
const { $client } = useNuxtApp();
return $client.recipeSearch.useQuery(query);
}

BIN
docs/homepage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
docs/mealpage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
docs/short_clip.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

45
error.vue Normal file
View file

@ -0,0 +1,45 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-base-200">
<div class="text-center max-w-md p-8">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<!-- Error Icon -->
<div class="text-error text-6xl mb-4">
<icon name="uil:exclamation-triangle" class="w-16 h-16" />
</div>
<!-- Error Details -->
<h1 class="text-4xl font-bold mb-4">
{{ error?.statusCode || "Error" }}
</h1>
<p class="text-xl mb-6">
{{ error?.statusMessage || "Something went wrong" }}
</p>
<!-- Action Buttons -->
<div class="flex justify-center gap-4">
<button class="btn btn-primary" @click="handleError">
Try Again
</button>
<button class="btn btn-ghost" @click="navigateToHome">
Go Home
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
const error = useError();
const route = useRoute();
const handleError = () => {
clearError({ redirect: route.redirectedFrom?.fullPath ?? "/" });
};
const navigateToHome = () => {
clearError({ redirect: "/" });
};
</script>

6
eslint.config.mjs Normal file
View file

@ -0,0 +1,6 @@
// @ts-check
import withNuxt from "./.nuxt/eslint.config.mjs";
export default withNuxt({
ignores: ["**/src/*"],
});

83
nuxt.config.ts Normal file
View file

@ -0,0 +1,83 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
modules: [
"@nuxt/eslint",
"@nuxt/image",
"@nuxtjs/robots",
"@sentry/nuxt/module",
"@vueuse/nuxt",
"nuxt-delay-hydration",
"nuxt-icon",
],
app: {
head: {
title: "Mood2Food",
htmlAttrs: { lang: "en" },
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
hid: "description",
name: "description",
content: "Meal Planner",
},
],
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
},
pageTransition: { name: "page", mode: "out-in" },
layoutTransition: { name: "slide", mode: "out-in" },
},
build: {
transpile: ["trpc-nuxt"],
},
css: ["~/assets/css/main.css"],
delayHydration: {
mode: "init",
// enables nuxt-delay-hydration in dev mode for testing
debug: process.env.NODE_ENV === "development",
},
image: {
domains: ["www.themealdb.com"],
},
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
runtimeConfig: {
// The private keys which are only available server-side
apiUrl: "",
// Keys within public are also exposed client-side
public: {
sentry: {
dsn: "",
},
},
},
ssr: true,
compatibilityDate: "2024-12-13",
sentry: {
sourceMapsUploadOptions: {
org: "ruidy",
project: "meal-planner",
},
autoInjectServerSentry: "top-level-import",
},
sourcemap: {
client: "hidden",
},
});

14398
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,43 @@
{ {
"name": "meal-planner", "name": "chefs",
"version": "0.1.0",
"private": true, "private": true,
"dependencies": { "type": "module",
"@material-ui/core": "^4.9.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-router-dom": "^5.1.2",
"react-scripts": "3.3.0"
},
"scripts": { "scripts": {
"start": "react-scripts start", "build": "nuxt build",
"build": "react-scripts build", "dev": "nuxt dev --port=3009",
"test": "react-scripts test", "generate": "nuxt generate",
"eject": "react-scripts eject" "preview": "nuxt preview",
"postinstall": "nuxt prepare",
"format": "bun prettier . --write",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}, },
"eslintConfig": { "dependencies": {
"extends": "react-app" "@nuxt/eslint": "^0.7.6",
"@nuxt/image": "^1.11.0",
"@nuxtjs/robots": "5.0.1",
"@sentry/nuxt": "^9.47.1",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"@vueuse/nuxt": "12.0.0",
"nuxt": "^3.20.1",
"nuxt-icon": "^0.6.10",
"trpc-nuxt": "^0.10.22",
"vue": "^3.5.25",
"vue-router": "^4.6.3",
"zod": "^3.25.76"
}, },
"browserslist": { "devDependencies": {
"production": [ "@tailwindcss/typography": "^0.5.19",
">0.2%", "autoprefixer": "^10.4.22",
"not dead", "daisyui": "^4.12.24",
"not op_mini all" "nuxt-delay-hydration": "^1.3.8",
], "postcss": "^8.5.6",
"development": [ "prettier": "3.4.2",
"last 1 chrome version", "prettier-plugin-tailwindcss": "^0.6.14",
"last 1 firefox version", "tailwindcss": "^3.4.18"
"last 1 safari version" },
] "overrides": {
"@vercel/nft": "^0.27.4"
} }
} }

52
pages/[id].vue Normal file
View file

@ -0,0 +1,52 @@
<script setup lang="ts">
const { params } = useRoute();
const routeParam = params.id;
const id = typeof routeParam === "string" ? routeParam : routeParam[0];
const {
data: recipe,
status,
error,
} = id === "random" ? await useRecipeRandom() : await useRecipeById(Number(id));
if (error.value) {
if (error.value.message === "Recipe not found") {
throw createError({
statusCode: 404,
statusMessage: "Recipe not found",
});
}
throw createError({
statusCode: 400,
statusMessage: "Invalid recipe id",
message: error.value.message,
});
}
const url = useRequestURL();
useSeoMeta({
title: `${recipe.value!.title} | Mood2Food`,
description: "The perfect meal that fits your mood",
ogTitle: `${recipe.value!.title} | Mood2Food`,
ogDescription: "The perfect meal that fits your mood",
ogImage: recipe.value!.pictureUrl,
ogUrl: url.href,
twitterTitle: `${recipe.value!.title} | Mood2Food`,
twitterDescription: "The perfect meal that fits your mood",
twitterImage: recipe.value!.pictureUrl,
twitterCard: "summary",
});
</script>
<template>
<div v-if="status !== 'success'" class="container mx-auto px-4 lg:px-8">
<span
class="loading loading-bars loading-lg flex justify-center items-center min-h-screen mx-auto"
/>
</div>
<section v-else>
<recipe-view :recipe="recipe!" />
</section>
</template>

View file

@ -0,0 +1,88 @@
<script setup lang="ts">
const route = useRoute();
const categoryName = route.params.name as string;
const { data: recipes, status } = await useCategoryRecipes(categoryName);
if (!recipes.value) {
throw createError({
statusCode: 404,
statusMessage: "Category not found",
});
}
const { data: categories } = await useCategories();
const category = categories.value?.find((c) => c.name === categoryName);
const url = useRequestURL();
useSeoMeta({
title: `${categoryName} | Mood2Food`,
description: "The perfect meal that fits your mood",
ogTitle: `${categoryName} | Mood2Food`,
ogDescription: "The perfect meal that fits your mood",
ogImage: category!.picture,
ogUrl: url.href,
twitterTitle: `${categoryName} | Mood2Food`,
twitterDescription: "The perfect meal that fits your mood",
twitterImage: category!.picture,
twitterCard: "summary",
});
</script>
<template>
<div>
<div
class="hero h-[40vh] bg-cover bg-center relative"
:style="`background-image: url(${category!.picture})`"
>
<div class="hero-overlay bg-opacity-60" />
<div class="hero-content text-center text-neutral-content">
<h1 class="text-5xl font-bold">{{ category?.name || categoryName }}</h1>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<div class="prose max-w-none mb-12">
<p>{{ category!.description }}</p>
</div>
<div v-if="status === 'pending'" class="flex justify-center my-8">
<span class="loading loading-spinner loading-lg text-primary" />
</div>
<div
v-else-if="recipes?.length"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
>
<div
v-for="recipe in recipes"
:key="recipe.id"
class="card bg-base-100 shadow-xl"
>
<figure>
<nuxt-img
:src="recipe.pictureUrl"
:alt="recipe.title"
:placeholder="[300]"
height="300"
width="300"
format="webp"
/>
</figure>
<div class="card-body">
<h2 class="card-title">{{ recipe.title }}</h2>
<div class="card-actions justify-end">
<nuxt-link :to="`/${recipe.id}`" class="btn btn-primary">
View Recipe
</nuxt-link>
</div>
</div>
</div>
</div>
<div v-else class="alert alert-info">
<span>No recipes found in this category.</span>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,73 @@
<script setup lang="ts">
const { data: categories, status, error } = useCategories();
if (error.value) {
throw createError({
statusCode: 500,
message: error.value.message,
});
}
const url = useRequestURL();
useSeoMeta({
title: `Recipe categories | Mood2Food`,
description: "The perfect meal that fits your mood",
ogTitle: `Recipe categories | Mood2Food`,
ogDescription: "The perfect meal that fits your mood",
ogImage: "/logo192.png",
ogUrl: url.href,
twitterTitle: `Recipe categories | Mood2Food`,
twitterDescription: "The perfect meal that fits your mood",
twitterImage: "/logo192.png",
twitterCard: "summary",
});
</script>
<template>
<div class="container mx-auto px-4">
<div v-if="status === 'pending'" class="flex justify-center my-8">
<span class="loading loading-spinner loading-lg text-primary" />
</div>
<div
v-else-if="categories!.length > 0"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 my-8"
>
<div
v-for="category in categories"
:key="category.name"
class="card bg-base-100 shadow-xl h-[28rem] sm:h-[32rem] md:h-[36rem] lg:h-[32rem]"
>
<figure>
<nuxt-img
:src="category.picture"
:alt="category.name"
:placeholder="[160, 100]"
height="100"
width="160"
format="webp"
loading="lazy"
/>
</figure>
<div class="card-body">
<h2 class="card-title">{{ category.name }}</h2>
<p class="line-clamp-6 text-sm">
{{ category.description }}
</p>
<div class="card-actions justify-end">
<nuxt-link
:to="`/categories/${category.name}`"
class="btn btn-primary"
>
View Recipes
</nuxt-link>
</div>
</div>
</div>
</div>
<div v-else role="alert" class="alert alert-info my-8 items-center flex">
<icon name="uil:info-circle" class="w-8 h-8" />
<span>No categories available</span>
</div>
</div>
</template>

31
pages/cookbook.vue Normal file
View file

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { Recipe } from "~/types/recipe";
import { useStorage } from "@vueuse/core";
const cookbook = useStorage<Recipe[]>("cookbook", [], localStorage);
</script>
<template>
<main>
<div
v-if="cookbook.length === 0"
class="flex justify-center items-center min-h-screen"
>
<div class="alert alert-info">
<span>No recipes found in this category.</span>
</div>
</div>
<ul>
<li v-for="recipe in cookbook" :key="recipe.id">
<nuxt-link :to="`/${recipe.id}`">
<recipe-card
:title="recipe.title"
:picture-url="recipe.pictureUrl"
:video-url="recipe.videoUrl"
:category="recipe.category"
:origin="recipe.origin"
/>
</nuxt-link>
</li>
</ul>
</main>
</template>

38
pages/index.vue Normal file
View file

@ -0,0 +1,38 @@
<script setup lang="ts">
const url = useRequestURL();
useSeoMeta({
title: `Mood2Food`,
description: "The perfect meal that fits your mood",
ogTitle: `Mood2Food`,
ogDescription: "The perfect meal that fits your mood",
ogImage: "/logo192.png",
ogUrl: url.href,
twitterTitle: `Mood2Food`,
twitterDescription: "The perfect meal that fits your mood",
twitterImage: "/logo192.png",
twitterCard: "summary",
});
</script>
<template>
<div class="hero min-h-full bg-base-200">
<div class="hero-content flex-col lg:flex-row-reverse h-full">
<nuxt-img
src="/chef.svg"
alt="Chef holding a knife"
class="max-w-sm object-contain rounded-lg"
:placeholder="[400, 300]"
format="webp"
height="300"
width="400"
/>
<div class="flex flex-col justify-center">
<h1 class="text-5xl font-bold prose">Eat Something New</h1>
<p class="py-6 prose">Generate a random recipe.</p>
<nuxt-link to="/random" class="btn btn-primary">
Random Recipe Now
</nuxt-link>
</div>
</div>
</div>
</template>

90
pages/search.vue Normal file
View file

@ -0,0 +1,90 @@
<script setup lang="ts">
import type { Recipe } from "~/types/recipe";
const route = useRoute();
const searchQuery = computed(() => route.query.q as string);
const searchResults = ref<Recipe[]>([]);
const loading = ref(false);
if (searchQuery.value) {
loading.value = true;
const { data, error } = await useRecipeSearch(searchQuery.value);
if (error.value) {
throw createError({
statusCode: 500,
message: error.value.message,
});
}
searchResults.value = data.value!;
loading.value = false;
}
watch(searchQuery, async (newQuery) => {
loading.value = true;
const { data, error } = await useRecipeSearch(newQuery.trim());
if (error.value) {
throw createError({
statusCode: 500,
message: error.value.message,
});
}
searchResults.value = data.value!;
loading.value = false;
});
</script>
<template>
<div class="container mx-auto px-4">
<recipe-search
class="md:hidden mb-6"
:initial-query="searchQuery"
:autofocus="true"
/>
<div v-if="loading" class="flex justify-center my-8">
<span class="loading loading-spinner loading-lg text-primary" />
</div>
<div
v-if="searchResults.length > 0"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 my-8"
>
<div
v-for="recipe in searchResults"
:key="recipe.id"
class="card bg-base-100 shadow-xl"
>
<figure>
<nuxt-img
:src="recipe.pictureUrl"
:alt="recipe.title"
format="webp"
loading="lazy"
:placeholder="[350]"
height="350"
width="350"
/>
</figure>
<div class="card-body">
<h2 class="card-title">{{ recipe.title }}</h2>
<p>{{ recipe.category }} {{ recipe.origin }}</p>
<div class="card-actions justify-end">
<nuxt-link :to="`/${recipe.id}`" class="btn btn-primary">
View Recipe
</nuxt-link>
</div>
</div>
</div>
</div>
<div
v-else-if="searchQuery"
role="alert"
class="alert alert-info my-8 items-center flex"
>
<icon name="uil:info-circle" class="w-8 h-8" />
<span>No recipes found for "{{ searchQuery }}"</span>
</div>
</div>
</template>

30
plugins/client.ts Normal file
View file

@ -0,0 +1,30 @@
import { createTRPCNuxtClient, httpBatchLink } from "trpc-nuxt/client";
import type { AppRouter } from "~/server/trpc/routers";
export default defineNuxtPlugin(() => {
const headers = useRequestHeaders();
/**
* createTRPCNuxtClient adds a `useQuery` composable
* built on top of `useAsyncData`.
*/
const client = createTRPCNuxtClient<AppRouter>({
links: [
httpBatchLink({
url: "/api/trpc",
headers() {
// add custom headers here
return {
Authorization: "Bearer token",
...headers,
};
},
}),
],
});
return {
provide: {
client,
},
};
});

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

1
public/chef.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Chef's Meal Planner" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<title>Chef's Meal Planner</title>
</head>
<body>
<script>
document.addEventListener("DOMContentLoaded", function() {
var elems = document.querySelectorAll(".sidenav");
var instances = M.Sidenav.init(elems, options);
});
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,10 +1,10 @@
{ {
"short_name": "Meal Planner", "short_name": "Chef's",
"name": "Chef's Meal Planner", "name": "Chef's | Meal Planner",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 48x48 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
@ -16,10 +16,15 @@
"src": "logo512.png", "src": "logo512.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
},
{
"src": "apple-touch-icon.png",
"type": "image/png",
"sizes": "180x180"
} }
], ],
"start_url": ".", "start_url": "/",
"display": "standalone", "display": "standalone",
"theme_color": "#000000", "theme_color": "#ff6d00",
"background_color": "#ffffff" "background_color": "#ffffff"
} }

View file

@ -1,2 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

24
sentry.client.config.ts Normal file
View file

@ -0,0 +1,24 @@
import * as Sentry from "@sentry/nuxt";
Sentry.init({
dsn: useRuntimeConfig().public.sentry.dsn,
// We recommend adjusting this value in production, or using tracesSampler for finer control
tracesSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// If the entire session is not sampled, use the below sample rate to sample
// sessions when an error occurs.
replaysOnErrorSampleRate: 1.0,
// If you don't want to use Session Replay, just remove the line below:
integrations: [
Sentry.replayIntegration(),
Sentry.consoleLoggingIntegration(),
],
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
sendDefaultPii: true,
_experiments: {
enableLogs: true,
},
});

9
sentry.server.config.ts Normal file
View file

@ -0,0 +1,9 @@
import * as Sentry from "@sentry/nuxt";
Sentry.init({
dsn: useRuntimeConfig().public.sentry.dsn,
// We recommend adjusting this value in production, or using tracesSampler for finer control
tracesSampleRate: 1.0,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View file

@ -0,0 +1,8 @@
import { createNuxtApiHandler } from "trpc-nuxt";
import { appRouter } from "~/server/trpc/routers";
import { createContext } from "~/server/trpc/context";
export default createNuxtApiHandler({
router: appRouter,
createContext,
});

21
server/trpc/context.ts Normal file
View file

@ -0,0 +1,21 @@
import type { inferAsyncReturnType } from "@trpc/server";
/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export async function createContext(event: H3Event) {
const authorization = getRequestHeader(event, "authorization");
async function getUserFromHeader() {
if (authorization) {
return { isAdmin: true };
}
return null;
}
const user = await getUserFromHeader();
return {
user,
};
}
export type Context = inferAsyncReturnType<typeof createContext>;

View file

@ -0,0 +1,8 @@
import type { inferRouterOutputs } from "@trpc/server";
import { mergeRouters } from "../trpc";
import { recipeRouter } from "./recipes";
export const appRouter = mergeRouters(recipeRouter);
export type AppRouter = typeof appRouter;
export type RouterOutput = inferRouterOutputs<AppRouter>;

View file

@ -0,0 +1,105 @@
import { z } from "zod";
import {
categoriesResponseSchema,
categoryRecipesResponseSchema,
type CategoriesResponse,
} from "~/types/category";
import type { Meal } from "~/types/recipe";
import { parseRecipeData } from "~/utils/recipes";
import { publicProcedure, router } from "../trpc";
const { apiUrl } = useRuntimeConfig();
export const recipeRouter = router({
recipesByCategory: publicProcedure
.input(z.string())
.query(async ({ input }) => {
const data = await $fetch<{ meals: Meal[] }>(
new URL(`filter.php?c=${input}`, apiUrl).href,
);
const result = categoryRecipesResponseSchema.safeParse(data);
if (!result.success) {
throw createError({
statusCode: 404,
statusMessage: "Recipes for category not found",
});
}
return result.data.recipes;
}),
recipeGet: publicProcedure
.input(
z.coerce
.number({
required_error: "recipe id is required",
invalid_type_error: "recipe id must be a number",
})
.positive("recipe id must be positive"),
)
.query(async ({ input }) => {
const data = await $fetch<{ meals: Meal[] }>(
new URL(`lookup.php?i=${input}`, apiUrl).href,
);
if (!data?.meals) {
throw createError({
statusCode: 404,
statusMessage: "Recipe not found",
});
}
const recipes = parseRecipeData(data);
return recipes[0];
}),
recipeRandom: publicProcedure.query(async () => {
const data = await $fetch<{ meals: Meal[] }>(
new URL("random.php", apiUrl).toString(),
);
if (!data?.meals) {
throw createError({
statusCode: 500,
});
}
const recipes = parseRecipeData(data);
return recipes[0];
}),
recipeSearch: publicProcedure
.input(
z.string({
required_error: "search query is required",
}),
)
.query(async ({ input }) => {
const data = await $fetch<{ meals: Meal[] }>(
new URL(`search.php?s=${input}`, apiUrl).href,
);
if (!data?.meals) {
return [];
}
const recipes = parseRecipeData(data);
return recipes;
}),
listCategories: publicProcedure.query(async () => {
const response = await $fetch<CategoriesResponse>(
new URL("categories.php", apiUrl).href,
);
const result = categoriesResponseSchema.safeParse(response);
if (!result.success) {
throw createError({
statusCode: 500,
statusMessage: "Invalid API response format",
data: result.error,
});
}
return result.data.categories.sort((a, b) => a.name.localeCompare(b.name));
}),
});

26
server/trpc/trpc.ts Normal file
View file

@ -0,0 +1,26 @@
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "~/server/trpc/context";
// import { authMiddleware } from "~/server/trpc/middleware";
const t = initTRPC.context<Context>().create();
/**
* Unprotected procedure
**/
export const publicProcedure = t.procedure;
export const router = t.router;
export const middleware = t.middleware;
export const mergeRouters = t.mergeRouters;
export const authMiddleware = middleware(({ next, ctx }) => {
if (!ctx.user?.isAdmin) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
user: ctx.user,
},
});
});
export const privateProcedure = t.procedure.use(authMiddleware);

View file

@ -1,183 +0,0 @@
import React, { useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import HomePage from "./pages/Home";
import MealPage from "./pages/Meal";
import SearchPage from "./pages/Search";
import CategoryListPage from "./pages/CategoryList";
import CategoryPage from "./pages/Category";
import NotFound from "./pages/NotFound";
import Navbar from "./components/Navbar";
import SearchBar from "./components/SearchBar";
import Footer from "./components/Footer";
import "./index.css";
const App = () => {
// State Hooks
const [searchString, setSearchString] = useState("");
const [categories, setCategories] = useState({ categories: [] });
// const [isLoading, setIsLoading] = useState(true); For Preloader
// Default meal object. TODO: Find a better alternative …
const mealDef = {
meals: [
{
idMeal: "52837",
strMeal: "Pilchard puttanesca",
strDrinkAlternate: null,
strCategory: "Pasta",
strArea: "Italian",
strInstructions:
"Cook the pasta following pack instructions.\r\n\r\nHeat the oil in a non-stick frying pan and cook the onion, garlic and chilli for 3-4 mins to soften. Stir in the tomato pur\u00e9e and cook for 1 min, then add the pilchards with their sauce. Cook, breaking up the fish with a wooden spoon, then add the olives and continue to cook for a few more mins.\r\n\r\nDrain the pasta and add to the pan with 2-3 tbsp of the cooking water. Toss everything together well, then divide between plates and serve, scattered with Parmesan.",
strMealThumb:
"https://www.themealdb.com/images/media/meals/vvtvtr1511180578.jpg",
strTags: null,
strYoutube: "https://www.youtube.com/watch?v=wqZzLAPmr9k",
strIngredient1: "Spaghetti",
strIngredient2: "Olive Oil",
strIngredient3: "Onion",
strIngredient4: "Garlic",
strIngredient5: "Red Chilli",
strIngredient6: "Tomato Puree",
strIngredient7: "Pilchards",
strIngredient8: "Black Olives",
strIngredient9: "Parmesan",
strIngredient10: "",
strIngredient11: "",
strIngredient12: "",
strIngredient13: "",
strIngredient14: "",
strIngredient15: "",
strIngredient16: "",
strIngredient17: "",
strIngredient18: "",
strIngredient19: "",
strIngredient20: "",
strMeasure1: "300g",
strMeasure2: "1 tbls",
strMeasure3: "1 finely chopped ",
strMeasure4: "2 cloves minced",
strMeasure5: "1",
strMeasure6: "1 tbls",
strMeasure7: "425g",
strMeasure8: "70g",
strMeasure9: "Shaved",
strMeasure10: "",
strMeasure11: "",
strMeasure12: "",
strMeasure13: "",
strMeasure14: "",
strMeasure15: "",
strMeasure16: "",
strMeasure17: "",
strMeasure18: "",
strMeasure19: "",
strMeasure20: "",
strSource: "https://www.bbcgoodfood.com/recipes/pilchard-puttanesca",
dateModified: null
}
]
};
const [meal, setMeal] = useState(mealDef);
// Fetch API functions
const createURI = (keyword, option) => {
const ROOT = "https://www.themealdb.com/api/json/v1/1/";
if (option === null) {
return `${ROOT}${keyword}.php`;
} else if (option === "filter") {
return `${ROOT}${option}.php?c=${keyword}`;
} else if (option === "lookup") {
return `${ROOT}${option}.php?i=${keyword}`;
}
};
const getFromAPI = (keyword, set, option = null) => {
const URI = createURI(keyword, option);
fetch(URI)
.then(response => response.json())
.then(data => set(data));
};
// Fetch wrappers for each use
const getRandomMeal = () => {
// setIsLoading(true);
getFromAPI("random", setMeal);
// setIsLoading(false);
};
const getMeal = id => {
getFromAPI(id, setMeal, "lookup");
};
const getCategories = () => {
getFromAPI("categories", setCategories);
};
const handleChange = ev => {
const { value } = ev.target;
setSearchString(value);
};
const buttonUrl = "/random";
return (
<Router>
<Navbar handleClick={getRandomMeal} buttonUrl={buttonUrl} />
<div className="container">
<SearchBar searchString={searchString} handleChange={handleChange} />
</div>
<Switch>
<Route
exact
path="/"
render={props => (
<HomePage
{...props}
handleClick={getRandomMeal}
buttonUrl={buttonUrl}
/>
)}
/>
<Route
exact
path={buttonUrl}
render={props => (
<MealPage
{...props}
meal={meal}
getMeal={getRandomMeal}
// isLoading={isLoading}
/>
)}
/>
<Route
exact
path="/categories"
render={props => (
<CategoryListPage
{...props}
categories={categories}
getCategories={getCategories}
/>
)}
/>
<Route path="/categories/:strCategory/">
<CategoryPage
getFromAPI={getFromAPI}
getMeal={getMeal}
setMeal={setMeal}
meal={meal}
/>
</Route>
<Route path="/:idMeal">
<MealPage meal={meal} getMeal={getMeal} />
</Route>
<Route exact path="/search" component={SearchPage} />
{/* We'll have to input searchResults somewhere */}
<Route
render={props => <NotFound {...props} handleClick={getRandomMeal} />}
/>
</Switch>
<Footer />
</Router>
);
};
export default App;

View file

@ -1,9 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View file

@ -1,25 +0,0 @@
import React from "react";
import { Link, useRouteMatch } from "react-router-dom";
const CategoryEntry = props => {
const {
strCategory,
strCategoryThumb,
strCategoryDescription
} = props.category;
const { url } = useRouteMatch();
return (
<div className="row">
<Link to={`${url}/${strCategory}`}>
<li key={props.i}>
<img src={strCategoryThumb} alt={strCategory} />
<h3>{strCategory}</h3> {strCategoryDescription}
</li>
</Link>
</div>
);
};
export default CategoryEntry;

View file

@ -1,14 +0,0 @@
import React from "react";
const CopyrightText = () => {
return (
<span>
© 2020 - Chef's - Made with{" "}
<span role="img" aria-label="heart">
</span>
</span>
);
};
export default CopyrightText;

View file

@ -1,18 +0,0 @@
import React from "react";
import CopyrightText from "./CopyrightText";
import GitHubLink from "./GitHubLink";
const Footer = () => {
return (
<footer className="page-footer">
<div className="footer-copyright">
<div className="container">
<CopyrightText />
<GitHubLink />
</div>
</div>
</footer>
);
};
export default Footer;

View file

@ -1,16 +0,0 @@
import React from "react";
const GitHubLink = () => {
return (
<a
className="grey-text text-lighten-4 right"
href="https://github.com/rjNemo/meal_planner"
target="blank"
rel="noopener"
>
GitHub
</a>
);
};
export default GitHubLink;

View file

@ -1,18 +0,0 @@
import React from "react";
const IngredientList = props => {
const { ingredients } = props;
return (
<div className="ingredientList">
<h3>Ingredients</h3>
<ul>
{ingredients.map((ing, i) => (
<li key={i}>
<b>{ing[0]}:</b> {ing[1]}
</li>
))}
</ul>
</div>
);
};
export default IngredientList;

View file

@ -1,14 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
const Logo = () => {
return (
<Link to="/" className="brand-logo">
<span role="img" aria-label="cookie">
👩🍳 Chef's
</span>
</Link>
);
};
export default Logo;

View file

@ -1,56 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
const MealPresentation = props => {
const {
mealName,
imgAddress,
videoAddress,
mealCategory,
mealArea
} = props.meal;
return (
<div className="row">
<div className="col s12">
<div className="card blue-grey darken-1">
<div className="card-content white-text">
<span className="card-title">{mealName}</span>
<img className="responsive-img" src={imgAddress} alt={mealName} />
<ul>
<li>
<a href={videoAddress} target="blank">
See in video
</a>
</li>
{/* <video width="" height="" controls autoplay>
<source src={videoAddress} type="video/mp4" />
Your browser does not support the video tag.
</video> */}
{/* <iframe
title="video"
width="560"
height="315"
src="https://www.youtube.com/embed/wqZzLAPmr9k"
frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe> */}
<li>
<b>Category: </b> {mealCategory} (
<Link to={`/categories/${mealCategory}`}>
See every {mealCategory} recipes
</Link>
)
</li>
<li>
<b>Origin:</b> {mealArea}
</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default MealPresentation;

View file

@ -1,73 +0,0 @@
import React from "react";
import Logo from "./Logo";
import RandomButton from "./RandomButton";
import { Link } from "react-router-dom";
const Navbar = props => {
return (
<div className="row">
<nav>
<div className="nav-wrapper">
<div className="container">
<Logo />
<ul id="nav-mobile" className="right hide-on-med-and-down">
<li>
<Link to="/categories">Categories</Link>
</li>
<li>
<RandomButton
handleClick={props.handleClick}
url={props.buttonUrl}
/>
</li>
</ul>
</div>
</div>
</nav>
{/* <ul id="slide-out" class="sidenav">
<li>
<div class="user-view">
<div class="background">
<img src="images/office.jpg" />
</div>
<a href="#user">
<img class="circle" src="images/yuna.jpg" />
</a>
<a href="#name">
<span class="white-text name">John Doe</span>
</a>
<a href="#email">
<span class="white-text email">jdandturk@gmail.com</span>
</a>
</div>
</li>
<li>
<a href="#!">
<i class="material-icons">cloud</i>First Link With Icon
</a>
</li>
<li>
<a href="#!">Second Link</a>
</li>
<li>
<div class="divider"></div>
</li>
<li>
<a class="subheader">Subheader</a>
</li>
<li>
<a class="waves-effect" href="#!">
Third Link With Waves
</a>
</li>
</ul>
<a href="#" data-target="slide-out" class="sidenav-trigger">
<i class="material-icons">menu</i>
</a> */}
</div>
);
};
export default Navbar;

View file

@ -1,21 +0,0 @@
import React from "react";
const PreLoader = () => {
return (
<div className="preloader-wrapper active">
<div className="spinner-layer spinner-red-only">
<div className="circle-clipper left">
<div className="circle"></div>
</div>
<div className="gap-patch">
<div className="circle"></div>
</div>
<div className="circle-clipper right">
<div className="circle"></div>
</div>
</div>
</div>
);
};
export default PreLoader;

View file

@ -1,17 +0,0 @@
import React from "react";
import { Link } from "react-router-dom";
const RandomButton = props => {
return (
<Link to={props.url}>
<button
className="waves-effect waves-light btn-small"
onClick={props.handleClick}
>
Random Recipe
</button>
</Link>
);
};
export default RandomButton;

View file

@ -1,11 +0,0 @@
import React from "react";
const Recipe = props => {
return (
<div className="recipe">
<h3>Instructions</h3>
<div dangerouslySetInnerHTML={{ __html: props.recipe }} />
</div>
);
};
export default Recipe;

View file

@ -1,15 +0,0 @@
import React from "react";
const SearchBar = props => {
return (
<input
type="text"
name="search"
value={props.searchString}
placeholder="Search a recipe"
onChange={props.handleChange}
//{onSubmit={props.handleSubmit}
/>
);
};
export default SearchBar;

View file

@ -1,16 +0,0 @@
import React from "react";
const SearchResultList = () => {
return (
<div>
<ul>
<li>Recipe #1</li>
<li>Recipe #2</li>
<li>Recipe #</li>
<li>Recipe #N</li>
</ul>
</div>
);
};
export default SearchResultList;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

View file

@ -1,28 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
min-height: 100vh;
flex-direction: column;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
main {
flex: 1 0 auto;
}
div {
white-space: pre-wrap;
}
.background {
background-image: url(./images/parallax1.jpg);
}

View file

@ -1,9 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(<App />, document.getElementById("root"));
serviceWorker.unregister();

View file

@ -1,44 +0,0 @@
import React, { useEffect, useState } from "react";
import { useParams, Link, useRouteMatch } from "react-router-dom";
const CategoryPage = props => {
const [meals, setMeals] = useState({ meals: [] });
const { getFromAPI } = props;
const { strCategory } = useParams();
const getMeals = () => {
getFromAPI(strCategory, setMeals, "filter");
};
useEffect(() => {
getMeals();
}, []);
const { url } = useRouteMatch();
// const {
// strCategory,
// strCategoryThumb,
// strCategoryDescription
// } = props.category;
return (
<div className="container">
<h1>Chef's {strCategory} Recipes</h1>
{/* <img src={strCategoryThumb} alt={strCategory} />
<p>{strCategoryDescription}</p> */}
<ul>
{meals.meals.map((meal, i) => (
<li key={i}>
<Link to={`/${meal.idMeal}`}>
{/* <Link to="/"> */}
<img src={meal.strMealThumb} alt={meal.strMeal} />
<h3>{meal.strMeal}</h3>
</Link>
</li>
))}
</ul>
</div>
);
};
export default CategoryPage;

View file

@ -1,26 +0,0 @@
import React, { useEffect } from "react";
import CategoryEntry from "../components/CategoryEntry";
const CategoryListPage = props => {
const categories = props.categories.categories;
const { getCategories } = props;
useEffect(() => {
getCategories();
}, []);
return (
<div className="section">
<div className="container">
<h1>The Chef's Meal Categories</h1>
<ul>
{categories.map((category, i) => (
<CategoryEntry i={i} category={category} />
))}
</ul>
</div>
</div>
);
};
export default CategoryListPage;

View file

@ -1,15 +0,0 @@
import React from "react";
import RandomButton from "../components/RandomButton";
const HomePage = props => {
return (
<div className="section background">
<div className="container center-align">
<h1>The Chef's Meal Suggestions</h1>
<RandomButton handleClick={props.handleClick} url={props.buttonUrl} />
</div>
</div>
);
};
export default HomePage;

View file

@ -1,61 +0,0 @@
import React, { useEffect } from "react";
import MealPresentation from "../components/MealPresentation";
import IngredientList from "../components/IngredientList";
import Recipe from "../components/Recipe";
import { useParams } from "react-router-dom";
// import PreLoader from "../components/PreLoader";
const MealPage = props => {
const meal = props.meal.meals[0];
const { getMeal } = props;
const { idMeal } = useParams();
useEffect(() => {
idMeal === null ? getMeal() : getMeal(idMeal);
}, []);
const {
strMeal,
strMealThumb,
strYoutube,
strCategory,
strArea,
strInstructions
} = meal;
const item = {
mealName: strMeal,
imgAddress: strMealThumb,
videoAddress: strYoutube,
mealCategory: strCategory,
mealArea: strArea
};
let ingredientList = [];
var i;
for (i = 1; i <= 20; i++) {
var strIng = `strIngredient${i}`;
var strMes = `strMeasure${i}`;
if (meal[strIng] !== "" && meal[strIng] !== null) {
ingredientList.push([meal[strIng], meal[strMes]]);
}
}
// const page =
// return isLoading ? <PreLoader /> : page;
return (
<div className="container">
<div className="row">
<div className="col s6">
<MealPresentation meal={item} />
</div>
<div className="col s6">
<IngredientList ingredients={ingredientList} />
<Recipe recipe={strInstructions} />
</div>
</div>
</div>
);
};
export default MealPage;

View file

@ -1,21 +0,0 @@
import React from "react";
import RandomButton from "../components/RandomButton";
const NotFoundPage = props => {
return (
<div className="section">
<div className="container center-align">
<h1>Wrong Way!</h1>
<img
src="https://images.otstatic.com/prod/26153735/2/large.jpg"
alt="404 not found"
/>
<div className="row">
<RandomButton handleClick={props.handleClick} />
</div>
</div>
</div>
);
};
export default NotFoundPage;

View file

@ -1,18 +0,0 @@
import React, { Component } from "react";
import SearchResultList from "../components/SearchResultList";
export default class SearchPage extends Component {
constructor(props) {
super(props);
this.initState = {};
this.state = this.initState;
}
render() {
return (
<div>
<h1>Search Results</h1>
<SearchResultList />
</div>
);
}
}

View file

@ -1,137 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View file

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

19
tailwind.config.js Normal file
View file

@ -0,0 +1,19 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
],
theme: {
extend: {},
},
plugins: [require("@tailwindcss/typography"), require("daisyui")],
daisyui: {
themes: ["cupcake"],
logs: false,
},
};

4
tsconfig.json Normal file
View file

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

42
types/category.ts Normal file
View file

@ -0,0 +1,42 @@
import { z } from "zod";
const categorySchema = z
.object({
idCategory: z.string(),
strCategory: z.string(),
strCategoryThumb: z.string().url(),
strCategoryDescription: z.string(),
})
.transform((c) => ({
identity: c.idCategory,
name: c.strCategory,
picture: c.strCategoryThumb,
description: c.strCategoryDescription,
}));
export const categoriesResponseSchema = z.object({
categories: z.array(categorySchema),
});
export type Category = z.infer<typeof categorySchema>;
export type CategoriesResponse = z.infer<typeof categoriesResponseSchema>;
export const categoryRecipeSchema = z
.object({
strMeal: z.string(),
strMealThumb: z.string().url(),
idMeal: z.string(),
})
.transform((meal) => ({
title: meal.strMeal,
pictureUrl: meal.strMealThumb,
id: meal.idMeal,
}));
export const categoryRecipesResponseSchema = z
.object({
meals: z.array(categoryRecipeSchema),
})
.transform((data) => ({
recipes: data.meals,
}));

73
types/recipe.ts Normal file
View file

@ -0,0 +1,73 @@
import { z } from "zod";
export type Recipe = {
id: string;
title: string;
pictureUrl: string;
videoUrl: string;
category: string;
origin: string;
ingredients: { name: string; quantity: string }[];
instructions: string;
};
const mealSchema = z.object({
idMeal: z.string(),
strMeal: z.string(),
strCategory: z.string(),
strArea: z.string(),
strInstructions: z.string(),
strMealThumb: z.string().url(),
strTags: z.string().nullable(),
strYoutube: z.string(),
strIngredient1: z.string().nullish(),
strIngredient2: z.string().nullish(),
strIngredient3: z.string().nullish(),
strIngredient4: z.string().nullish(),
strIngredient5: z.string().nullish(),
strIngredient6: z.string().nullish(),
strIngredient7: z.string().nullish(),
strIngredient8: z.string().nullish(),
strIngredient9: z.string().nullish(),
strIngredient10: z.string().nullish(),
strIngredient11: z.string().nullish(),
strIngredient12: z.string().nullish(),
strIngredient13: z.string().nullish(),
strIngredient14: z.string().nullish(),
strIngredient15: z.string().nullish(),
strIngredient16: z.string().nullish(),
strIngredient17: z.string().nullish(),
strIngredient18: z.string().nullish(),
strIngredient19: z.string().nullish(),
strIngredient20: z.string().nullish(),
strMeasure1: z.string().nullish(),
strMeasure2: z.string().nullish(),
strMeasure3: z.string().nullish(),
strMeasure4: z.string().nullish(),
strMeasure5: z.string().nullish(),
strMeasure6: z.string().nullish(),
strMeasure7: z.string().nullish(),
strMeasure8: z.string().nullish(),
strMeasure9: z.string().nullish(),
strMeasure10: z.string().nullish(),
strMeasure11: z.string().nullish(),
strMeasure12: z.string().nullish(),
strMeasure13: z.string().nullish(),
strMeasure14: z.string().nullish(),
strMeasure15: z.string().nullish(),
strMeasure16: z.string().nullish(),
strMeasure17: z.string().nullish(),
strMeasure18: z.string().nullish(),
strMeasure19: z.string().nullish(),
strMeasure20: z.string().nullish(),
strSource: z.string().nullish(),
strImageSource: z.string().nullable(),
});
export const apiResponseSchema = z.object({
meals: z.array(mealSchema),
});
export type Meal = z.infer<typeof mealSchema>;
export type ApiResponse = z.infer<typeof apiResponseSchema>;

95
utils/recipes.test.ts Normal file
View file

@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import type { Recipe } from "~/types/recipe";
import { parseRecipeData } from "~/utils/recipes";
const sampleApiResponse = {
meals: [
{
idMeal: "52915",
strMeal: "French Omelette",
strDrinkAlternate: null,
strCategory: "Miscellaneous",
strArea: "French",
strInstructions:
"Get everything ready. Warm a 20cm (measured across the top) non-stick frying pan on a medium heat. Crack the eggs into a bowl and beat them with a fork so they break up and mix, but not as completely as you would for scrambled egg. With the heat on medium-hot, drop one knob of butter into the pan. It should bubble and sizzle, but not brown. Season the eggs with the Parmesan and a little salt and pepper, and pour into the pan.\r\nLet the eggs bubble slightly for a couple of seconds, then take a wooden fork or spatula and gently draw the mixture in from the sides of the pan a few times, so it gathers in folds in the centre. Leave for a few seconds, then stir again to lightly combine uncooked egg with cooked. Leave briefly again, and when partly cooked, stir a bit faster, stopping while theres some barely cooked egg left. With the pan flat on the heat, shake it back and forth a few times to settle the mixture. It should slide easily in the pan and look soft and moist on top. A quick burst of heat will brown the underside.\r\nGrip the handle underneath. Tilt the pan down away from you and let the omelette fall to the edge. Fold the side nearest to you over by a third with your fork, and keep it rolling over, so the omelette tips onto a plate or fold it in half, if thats easier. For a neat finish, cover the omelette with a piece of kitchen paper and plump it up a bit with your fingers. Rub the other knob of butter over to glaze. Serve immediately.",
strMealThumb:
"https://www.themealdb.com/images/media/meals/yvpuuy1511797244.jpg",
strTags: "Egg",
strYoutube: "https://www.youtube.com/watch?v=qXPhVYpQLPA",
strIngredient1: "Eggs",
strIngredient2: "Butter",
strIngredient3: "Parmesan",
strIngredient4: "Tarragon",
strIngredient5: "Parsley",
strIngredient6: "Chives",
strIngredient7: "Gruyère",
strIngredient8: "",
strIngredient9: "",
strIngredient10: "",
strIngredient11: "",
strIngredient12: "",
strIngredient13: "",
strIngredient14: "",
strIngredient15: "",
strIngredient16: "",
strIngredient17: "",
strIngredient18: "",
strIngredient19: "",
strIngredient20: "",
strMeasure1: "3",
strMeasure2: "2 knobs",
strMeasure3: "1 tsp",
strMeasure4: "3 chopped",
strMeasure5: "1 tbs chopped",
strMeasure6: "1 tbs chopped",
strMeasure7: "4 tbs",
strMeasure8: "",
strMeasure9: "",
strMeasure10: "",
strMeasure11: "",
strMeasure12: "",
strMeasure13: "",
strMeasure14: "",
strMeasure15: "",
strMeasure16: "",
strMeasure17: "",
strMeasure18: "",
strMeasure19: "",
strMeasure20: "",
strSource:
"https://www.bbcgoodfood.com/recipes/1669/ultimate-french-omelette",
strImageSource: null,
strCreativeCommonsConfirmed: null,
dateModified: null,
},
],
};
describe("parseRecipeData", () => {
it("should parse the API response into the Recipe type", () => {
const expectedResult: Recipe[] = [
{
title: "French Omelette",
pictureUrl:
"https://www.themealdb.com/images/media/meals/yvpuuy1511797244.jpg",
videoUrl: "https://www.youtube.com/watch?v=qXPhVYpQLPA",
category: "Miscellaneous",
origin: "French",
ingredients: [
{ name: "Eggs", quantity: "3" },
{ name: "Butter", quantity: "2 knobs" },
{ name: "Parmesan", quantity: "1 tsp" },
{ name: "Tarragon", quantity: "3 chopped" },
{ name: "Parsley", quantity: "1 tbs chopped" },
{ name: "Chives", quantity: "1 tbs chopped" },
{ name: "Gruyère", quantity: "4 tbs" },
],
instructions:
"Get everything ready. Warm a 20cm (measured across the top) non-stick frying pan on a medium heat. Crack the eggs into a bowl and beat them with a fork so they break up and mix, but not as completely as you would for scrambled egg. With the heat on medium-hot, drop one knob of butter into the pan. It should bubble and sizzle, but not brown. Season the eggs with the Parmesan and a little salt and pepper, and pour into the pan.\r\nLet the eggs bubble slightly for a couple of seconds, then take a wooden fork or spatula and gently draw the mixture in from the sides of the pan a few times, so it gathers in folds in the centre. Leave for a few seconds, then stir again to lightly combine uncooked egg with cooked. Leave briefly again, and when partly cooked, stir a bit faster, stopping while theres some barely cooked egg left. With the pan flat on the heat, shake it back and forth a few times to settle the mixture. It should slide easily in the pan and look soft and moist on top. A quick burst of heat will brown the underside.\r\nGrip the handle underneath. Tilt the pan down away from you and let the omelette fall to the edge. Fold the side nearest to you over by a third with your fork, and keep it rolling over, so the omelette tips onto a plate or fold it in half, if thats easier. For a neat finish, cover the omelette with a piece of kitchen paper and plump it up a bit with your fingers. Rub the other knob of butter over to glaze. Serve immediately.",
},
];
const result = parseRecipeData(sampleApiResponse);
expect(result).toEqual(expectedResult);
});
});

30
utils/recipes.ts Normal file
View file

@ -0,0 +1,30 @@
import type { ApiResponse, Meal, Recipe } from "~/types/recipe";
import { apiResponseSchema } from "~/types/recipe";
export function parseRecipeData(data: ApiResponse): Recipe[] {
return apiResponseSchema.parse(data).meals.map((meal: Meal) => {
const ingredients: { name: string; quantity: string }[] = [];
for (let i = 1; i <= 20; i++) {
const ingredientName = meal[`strIngredient${i}`];
const ingredientQuantity = meal[`strMeasure${i}`];
if (ingredientName?.trim() && ingredientQuantity?.trim()) {
ingredients.push({
name: ingredientName.trim(),
quantity: ingredientQuantity.trim(),
});
}
}
return {
id: meal.idMeal,
title: meal.strMeal,
pictureUrl: meal.strMealThumb,
videoUrl: meal.strYoutube,
category: meal.strCategory,
origin: meal.strArea,
ingredients,
instructions: meal.strInstructions,
};
});
}