Oh, something went wrong.
+Kunde inte hämta recept just nu.
+diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..f673a71b7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5502 +} \ No newline at end of file diff --git a/README.md b/README.md index 58f1a8a66..54c9fd7c2 100644 --- a/README.md +++ b/README.md @@ -1 +1,37 @@ -# js-project-recipe-library +# 🍽️ Recipe Finder — API-driven recipe list + + + +## Live: Recipe library + +### 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/ \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 000000000..967a1958b --- /dev/null +++ b/assets/css/style.css @@ -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; + --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; +} + + * { + box-sizing: border-box; +} + +body { + background-color: var(--bg); + font-family: 'Arial', sans-serif; + margin: 0; + padding: 0; +} + +h1 { + color: var(--h1); + font-size: 65px; + font-weight: bold; + /* margin: 50px 0; */ + 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; + padding-left: max(clamp(12px, 6vw, 20px), env(safe-area-inset-left)); + padding-right: max(clamp(12px, 6vw, 20px), env(safe-area-inset-right)); + +} + +.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; + 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{ + border-color: var(--h1); +} +.btn:active, .btn-time:active, .btn-random:active{ + 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); +} + +.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)); } +} \ No newline at end of file diff --git a/assets/img/Burrataboard-med-persika-och-mynta-768x1024.jpg b/assets/img/Burrataboard-med-persika-och-mynta-768x1024.jpg new file mode 100644 index 000000000..6f57ce12d Binary files /dev/null and b/assets/img/Burrataboard-med-persika-och-mynta-768x1024.jpg differ diff --git a/assets/img/Margharita-740429.jpg b/assets/img/Margharita-740429.jpg new file mode 100644 index 000000000..0da21eec9 Binary files /dev/null and b/assets/img/Margharita-740429.jpg differ diff --git a/assets/img/belgiska_vafflor.jpg b/assets/img/belgiska_vafflor.jpg new file mode 100644 index 000000000..b1833d531 Binary files /dev/null and b/assets/img/belgiska_vafflor.jpg differ diff --git a/assets/img/cheeseburgers.jpg b/assets/img/cheeseburgers.jpg new file mode 100644 index 000000000..4b6e92606 Binary files /dev/null and b/assets/img/cheeseburgers.jpg differ diff --git a/assets/img/mac_and_cheese_med_panko.jpg b/assets/img/mac_and_cheese_med_panko.jpg new file mode 100644 index 000000000..98bd7b896 Binary files /dev/null and b/assets/img/mac_and_cheese_med_panko.jpg differ diff --git a/assets/img/mockup.recipe.png b/assets/img/mockup.recipe.png new file mode 100644 index 000000000..ab62c94da Binary files /dev/null and b/assets/img/mockup.recipe.png differ diff --git a/assets/img/moules_frites.jpg b/assets/img/moules_frites.jpg new file mode 100644 index 000000000..322ba59ab Binary files /dev/null and b/assets/img/moules_frites.jpg differ diff --git a/assets/img/pad_thai_.jpg b/assets/img/pad_thai_.jpg new file mode 100644 index 000000000..9d9619394 Binary files /dev/null and b/assets/img/pad_thai_.jpg differ diff --git a/assets/img/sushi-california-rolls.jpg.webp b/assets/img/sushi-california-rolls.jpg.webp new file mode 100644 index 000000000..3d3f78769 Binary files /dev/null and b/assets/img/sushi-california-rolls.jpg.webp differ diff --git a/backup.js b/backup.js new file mode 100644 index 000000000..e69de29bb diff --git a/index.html b/index.html new file mode 100644 index 000000000..6ffb0d507 --- /dev/null +++ b/index.html @@ -0,0 +1,49 @@ + + +
+ + +Kunde inte hämta recept just nu.
+Inga recept hittades.
`; + return; + } + grid.innerHTML = list.map(r => recipeCardHTML(r)).join(''); +} + +/* + SKAPAR ETT RECEPTKORT + Chooses “safe” fallback values ​​if something is missing (e.g. title/image/time). + summary is cleaned from HTML and shortened to 160 characters. + Image has onerror="this.style.display='none'" → if the image is broken,${esc(cuisines)}
+${minutes ? `Time | ${minutes} mins` : 'Time | —'}
+ ${summary ? `${esc(summary)}
` : ''} + Open recipe +