-
Notifications
You must be signed in to change notification settings - Fork 60
Recipe library #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Recipe library #63
Changes from all commits
b12392e
d8c0c52
018933e
ba9d27f
cfb87db
08e204c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "liveServer.settings.port": 5502 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,37 @@ | ||
| # js-project-recipe-library | ||
| # 🍽️ Recipe Finder — API-driven recipe list | ||
|
|
||
|  | ||
|
|
||
| ## Live: <a href="https://quiet-chebakia-ed7a91.netlify.app/">Recipe library</a> | ||
|
|
||
| ### A responsive web app that fetches recipes from an external recipe API and allows the user to filter by cuisine, sort by cooking time, and randomly generate a recipe. Built with HTML, CSS, and Vanilla JavaScript (DOM + fetch). | ||
|
|
||
| 🎯 Purpose | ||
|
|
||
| - Practice fetch and async/await against an open API. | ||
| - Practice state management in the frontend (active filter, sort direction, last loaded recipes). | ||
| - Implement mobile-first and accessible UI logic (buttons, aria labels). | ||
|
|
||
| ✨ Features | ||
|
|
||
| - API retrieval of recipes (title, image, time, possibly cuisine). | ||
| - Filtering by cuisine (e.g. Italian, American, Asian, Belgian). | ||
| - Sorting by time (ascending/descending). | ||
| - "Not sure?" - shows a random recipe from the current list. | ||
| - Responsive layout with clear recipe cards. | ||
| - Error handling & loading state. | ||
|
|
||
| 🧩 Technical overview | ||
|
|
||
| - bJavaScript (ES6): fetch, async/await, event listeners, DOM update. | ||
| - State in memory: currentCuisine, currentSortDir, currentRecipes. | ||
| - Data structure: renders cards from the API response (title, image, readyInMinutes, cuisine). | ||
| - Accessibility: semantic buttons, aria on the filter section. | ||
|
|
||
| 🚀 Deployment (Netlify) | ||
|
|
||
| - Connect repo → Netlify | ||
| - Add env var: SPOONACULAR_API_KEY | ||
| - Build command: (empty if pure static page) | ||
| - Publish directory: project root (or dist if you are building) | ||
| - Deploy → Live: https://quiet-chebakia-ed7a91.netlify.app/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| :root { | ||
| --bg: #FAFBFF; | ||
| --h1: #0018A4; | ||
| --h2: black; | ||
| --h3: #6F6F6F; | ||
| --button1: rgb(204, 255, 226); | ||
| --button2: rgb(255, 236, 234); | ||
|
|
||
| /* button */ | ||
| --btn-height: 48px; | ||
| --btn-font: 18px; | ||
| --btn-font-weight: 150; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Font weight 150 feels a bit unconventional, it looks like usually fw is kept in 100, 200, 400, etc., but if it was a necessary style decision, it might work too |
||
| --btn-radius: 999px; | ||
| --btn-gap: 12px; | ||
| --block-gap: 16px; | ||
|
|
||
| /*Recipe card */ | ||
| --recipe-width: 300px; | ||
| --recipe-height: 621px; | ||
| --recipe-radius: 8px; | ||
| --recipe-shadow: rgba(0, 0, 0, 0.1); | ||
| --recipe-shadow-hover: rgba(0, 0, 0, 0.2); | ||
| --group-width: 1000px; | ||
| --group-height: 85px; | ||
|
|
||
| /* Layout */ | ||
| --container-max: 1200px; | ||
| --page-pad: 16px; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great use of semantic tokens! Easy to read |
||
| } | ||
|
|
||
| * { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also like to use * as a way to make it easier to style everything after, but since it might slow down the loading of the site, I would probably just double-check that this property is really a must one |
||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| body { | ||
| background-color: var(--bg); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really great that you are using variables, I think I might give it a try in the next project too:) |
||
| font-family: 'Arial', sans-serif; | ||
| margin: 0; | ||
| padding: 0; | ||
| } | ||
|
|
||
| h1 { | ||
| color: var(--h1); | ||
| font-size: 65px; | ||
| font-weight: bold; | ||
| /* margin: 50px 0; */ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove |
||
| margin: 40px 0 24px; | ||
| align-items: center; | ||
| justify-content: center; | ||
| text-align: center; | ||
| } | ||
| .recipe-header { | ||
| display: grid; | ||
| place-items: center; | ||
| } | ||
|
|
||
| /*buttonarea och filter 1*/ | ||
| .buttonarea, .filter{ | ||
| display: block; /* block på mobil */ | ||
| width: 100%; /* släpp var(--group-width) */ | ||
| /* height: auto; <-- implicit */ | ||
| box-sizing: border-box; | ||
|
Comment on lines
+58
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove the comments, or translate them to english if you need them |
||
| padding-left: max(clamp(12px, 6vw, 20px), env(safe-area-inset-left)); | ||
| padding-right: max(clamp(12px, 6vw, 20px), env(safe-area-inset-right)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great idea to use safe-area here!
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are you comfortable explaining what this styling does? if not, this might be a good place to add some comments that explains it. |
||
|
|
||
| } | ||
|
|
||
| .group{ | ||
| display: grid; | ||
| grid-template-columns: 1fr; /* 1 kolumn på mobil */ | ||
| grid-template-areas: | ||
| "head-left" | ||
| "head-right" | ||
| "buttons"; | ||
| gap: 16px; | ||
| margin: 0 auto; | ||
| max-width: var(--container-max); | ||
| } | ||
|
|
||
| .group h2{ | ||
| margin: 0; | ||
| } | ||
|
|
||
| .group h2:first-of-type{ | ||
| grid-area: head-left; | ||
| } | ||
| .group h2:nth-of-type(2){ | ||
| grid-area: head-right; text-align: left; | ||
| } | ||
|
|
||
| .btn, .btn-time, .btn-random{ | ||
| font-family: 'sans-serif'; | ||
| display: inline-flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| min-height: var(--btn-height); | ||
| height: var(--btn-height); | ||
| font-size: var(--btn-font); | ||
| padding: 0 18px; | ||
| border-radius: var(--btn-radius); | ||
| border: 2px solid transparent; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Smart to use transparent borders to avoid "jump" when button state is changing, great job:) |
||
| font-weight: var(--btn-font-weight); | ||
| white-space: nowrap; | ||
| margin: 0; | ||
| cursor: pointer; | ||
| /* gör att de bryter rad snyggt på mobil */ | ||
| flex: 0 1 150px; | ||
| } | ||
|
|
||
| .btn { | ||
| background: var(--button1); | ||
| color: var(--h1); | ||
| } | ||
| .btn-time { | ||
| background: var(--button2); | ||
| color: var(--h1); | ||
| } | ||
|
|
||
| .btn:hover, .btn-time:hover, .btn-random:hover{ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great use of states for smooth UX, both for hover and active states |
||
| border-color: var(--h1); | ||
| } | ||
| .btn:active, .btn-time:active, .btn-random:active{ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding active state overall is a great idea for better UX, however, here the change feels a little bit too subtle maybe? Although it is not critical, just a small thing |
||
| transform: translateY(1px); | ||
| border-color: var(--h1); | ||
| } | ||
|
|
||
| .button-row { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| gap: 10px; | ||
| justify-content: center; /*centrera rader i mobil */ | ||
| } | ||
|
|
||
| /* Fyll ut rad på mobil men behåll min-bredd */ | ||
| .button-row .btn, | ||
| .button-row .btn-time, | ||
| .button-row .btn-random { | ||
| flex: 0 1 150px; /* väx, krymp, min 140px */ | ||
| } | ||
|
|
||
| /*recipe and cards*/ | ||
| .recipe-grid, .recipe-card-placeholder{ | ||
| display: grid; | ||
| grid-template-columns: repeat(7, 1fr); | ||
| gap: 24px; | ||
| } | ||
|
|
||
| .recipe-card { | ||
| display: block; | ||
| border: 2px solid transparent; | ||
| border-radius: var(--recipe-radius); | ||
| width: 100%; | ||
| padding: 16px 16px 24px 16px; | ||
| overflow: hidden; | ||
| background: #fff; | ||
| box-shadow: 0 1px 4px var(--recipe-shadow); | ||
| transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; | ||
| } | ||
|
|
||
| .recipe-card:hover { | ||
| border-color: var(--h1); | ||
| transform: translateY(-2px); | ||
| box-shadow: 0 4px 12px var(--recipe-shadow-hover); | ||
| } | ||
|
Comment on lines
+157
to
+164
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. smooth subtle animation on the hover |
||
|
|
||
| .recipe-card img{ | ||
| width: 100%; | ||
| aspect-ratio: 16 / 10; | ||
| object-fit: cover; | ||
| display: block; | ||
| margin: 0; | ||
| border-radius: calc(var(--recipe-radius) - 2px); | ||
| } | ||
|
|
||
| .recipe-card h3{ | ||
| margin: 10px 12px 4px; | ||
| font-size: 16px; | ||
| font-weight: 700; | ||
| color: #151827; | ||
| } | ||
|
|
||
| .recipe-card p{ | ||
| margin: 0 12px 12px; | ||
| color: #6F6F6F; | ||
| font-size: 1em; | ||
| line-height: 1.5; | ||
| } | ||
|
|
||
| /* 1) Allra minsta mobiler (≤ 399px) */ | ||
| @media (max-width: 399px) { | ||
| /* Mindre sidomarginaler */ | ||
| .buttonarea, .filter { | ||
| padding-left: 12px; | ||
| padding-right: 12px; | ||
| } | ||
|
|
||
| /* Rubrik: mindre & tightare marginaler */ | ||
| h1 { | ||
| font-size: 34px; | ||
| margin: 24px 0 16px; | ||
| text-align: center; | ||
| } | ||
|
|
||
| /* Grid: 1 kolumn, lite mindre gap och full bredd */ | ||
| .recipe-grid, | ||
| .recipe-card-placeholder{ | ||
| grid-template-columns: 1fr; | ||
| gap: 16px; | ||
| width: 100%; | ||
| margin: 16px auto 24px; | ||
| padding: 0 12px; | ||
| } | ||
|
|
||
| /* Kort: mindre padding, inga hårda höjder (du har redan tagit bort height) */ | ||
| .recipe-card { | ||
| padding: 12px; | ||
| box-shadow: 0 1px 4px var(--recipe-shadow); | ||
| } | ||
| .recipe-card img{ | ||
| aspect-ratio: 16 / 10; | ||
| object-fit: cover; | ||
| } | ||
| .recipe-card h3{ | ||
| font-size: 15px; | ||
| margin: 8px 4px 4px; | ||
| } | ||
| .recipe-card p{ | ||
| margin: 0 4px 10px; | ||
| font-size: 1em; | ||
| line-height: 1.45; | ||
| } | ||
|
|
||
| /* Knapprader: en knapp per rad på minsta mobiler */ | ||
| .button-row { | ||
| gap: 8px; | ||
| justify-content: stretch; | ||
| } | ||
| .button-row .btn, | ||
| .button-row .btn-time, | ||
| .button-row .btn-random { | ||
| flex: 1 1 100%; | ||
| min-height: 44px; | ||
| font-size: 16px; | ||
| } | ||
| } | ||
|
|
||
| /* 2) Små/mellanmobiler (400–639px): två knappar i rad, 1 kolumn grid */ | ||
| @media (min-width: 400px) and (max-width: 639px) { | ||
| .recipe-grid, | ||
| .recipe-card-placeholder{ | ||
| grid-template-columns: 1fr; /* håll 1 kolumn här också */ | ||
| gap: 20px; | ||
| width: 100%; | ||
| padding: 0 16px; | ||
| } | ||
|
|
||
| .button-row .btn, | ||
| .button-row .btn-time, | ||
| .button-row .btn-random { | ||
| flex: 1 1 calc(50% - 8px); /* två knappar per rad */ | ||
| } | ||
| } | ||
|
|
||
| /* 3 small tablets */ | ||
| @media (min-width: 640px){ | ||
| .group{ | ||
| grid-template-columns: 1fr 1fr; | ||
| grid-template-areas: | ||
| "head-left head-right" | ||
| "buttons buttons"; | ||
| } | ||
| .recipe-grid{ grid-template-columns: repeat(2, minmax(0,1fr)); } | ||
| } | ||
|
|
||
| @media (min-width: 960px){ /* laptop */ | ||
| .recipe-grid{ grid-template-columns: repeat(3, minmax(0,1fr)); } | ||
| } | ||
|
|
||
| @media (min-width: 1280px){ /* desktop */ | ||
| .recipe-grid{ grid-template-columns: repeat(4, minmax(0,1fr)); } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Recipe Library</title> | ||
| <link rel="stylesheet" href="assets/css/style.css"> | ||
| </head> | ||
| <body> | ||
| <main class="recipe-library"> | ||
| <h1 class="recipe-header"> | ||
| Recipe Library | ||
| </h1> | ||
|
|
||
| <!-- Buttons för filter av --> | ||
| <section class="buttonarea"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great naming of sections, very easy to read! |
||
| <div class="filter" aria-label="Filter & Sort"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great accessibility moment with aria-label! |
||
| <div class="group"> | ||
| <div class="btn-row"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, intent is clear |
||
| <h2>Filter on kitchen</h2> | ||
| <button class="btn" data-cuisine="all">All</button> | ||
| <button class="btn" data-cuisine="italian">Italian</button> | ||
| <button class="btn" data-cuisine="american">American</button> | ||
| <button class="btn" data-cuisine="asia">Asian</button> | ||
| <button class="btn" data-cuisine="belgia">Belgian</button> | ||
| </div> | ||
| <!-- Sort time / random recipebuttons --> | ||
|
|
||
| <div class="btn-row"> | ||
| <h2>Sort on time</h2> | ||
| <button class="btn-time" data-sort="desc">Descending</button> | ||
| <button class="btn-time" data-sort="asc">Ascending</button> | ||
| <button class="btn-random">Not sure?</button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </section> | ||
|
|
||
|
|
||
| <!-- Recipe Cards --> | ||
| <section class="recipe-cards" id="recipe-cards"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overall very clean code, great job! |
||
| <div | ||
| class="recipe-grid"> | ||
| </div> | ||
| </section> | ||
| <script src="index.js"></script> | ||
| </main> | ||
| </body> | ||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
css variables! yay