Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5502
}
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
# js-project-recipe-library
# 🍽️ Recipe Finder — API-driven recipe list

![mockup](assets/img/mockup.recipe.png)

## 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/
281 changes: 281 additions & 0 deletions assets/css/style.css
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);
Comment on lines +2 to +7

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

css variables! yay


/* button */
--btn-height: 48px;
--btn-font: 18px;
--btn-font-weight: 150;

Choose a reason for hiding this comment

The 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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great use of semantic tokens! Easy to read

}

* {

Choose a reason for hiding this comment

The 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);

Choose a reason for hiding this comment

The 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; */

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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));

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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;

Choose a reason for hiding this comment

The 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{

Choose a reason for hiding this comment

The 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{

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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)); }
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/Margharita-740429.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/belgiska_vafflor.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/cheeseburgers.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/mac_and_cheese_med_panko.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/mockup.recipe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/moules_frites.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/pad_thai_.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/img/sushi-california-rolls.jpg.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added backup.js
Empty file.
49 changes: 49 additions & 0 deletions index.html
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">

Choose a reason for hiding this comment

The 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">

Choose a reason for hiding this comment

The 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">

Choose a reason for hiding this comment

The 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">

Choose a reason for hiding this comment

The 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>
Loading