1
0
Fork 0
mirror of https://github.com/TheThomaas/my-online-cookbook.git synced 2026-01-09 19:41:38 +00:00

Initial commit

This commit is contained in:
Maël Brunet 2021-05-15 00:01:57 +02:00
parent 43bd89bf35
commit 9ec8f0b203
54 changed files with 5716 additions and 0 deletions

101
.eleventy.js Normal file
View file

@ -0,0 +1,101 @@
const Image = require('@11ty/eleventy-img');
const util = require('util');
const emojiRegex = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/ug
module.exports = config => {
config.addPassthroughCopy({ public: './' });
config.addPassthroughCopy('src/css');
config.addPassthroughCopy('src/img');
config.addPassthroughCopy('src/fonts');
config.addPassthroughCopy('src/admin');
/* Collections */
config.addCollection('recipes', collection => {
return [...collection.getFilteredByGlob('./src/recipes/*.md')];
});
config.addCollection('tagList', collection => {
const tagsSet = new Set();
collection.getAll().forEach(item => {
if (!item.data.tags) return;
item.data.tags
.filter(tag => !['recipes'].includes(tag))
.forEach(tag => tagsSet.add(tag));
});
return Array.from(tagsSet).sort((first, second) => {
const firstStartingLetter = first.replace(emojiRegex, '').trim()[0].toLowerCase();
const secondStartingLetter = second.replace(emojiRegex, '').trim()[0].toLowerCase();
if(firstStartingLetter < secondStartingLetter) { return -1; }
if(firstStartingLetter > secondStartingLetter) { return 1; }
return 0;
});
});
/* Filters */
config.addFilter('console', function(value) {
return util.inspect(value);
});
config.addFilter('dateToYear', function (date) {
return date.getFullYear();
});
config.addFilter('noEmoji', function(value) {
return value.replace(emojiRegex, '').trim();
});
config.addFilter('onlyEmoji', function(value) {
let match = value.match(emojiRegex);
// If the string doesn't contain any emoji, instead we output the first letter wrapped in some custom styles
if (!match) {
match = `<span class="c-card__tag-first-letter">${value.charAt(0)}</span>`
}
return Array.isArray(match) ? match.join('') : match;
});
config.addFilter('limit', (arr, limit) => arr.slice(0, limit));
config.addFilter('lowercase', function(value) {
return value.toLowerCase();
});
config.addFilter('asArray', function(value) {
return value ? [...value] : []
});
/* Shortcodes */
const imageShortcode = async (src, className, alt, sizes) => {
let metadata = await Image(`./src/recipe-images/${src}.jpg`, {
widths: [600, 1500, 3000],
formats: ['webp', 'jpeg'],
outputDir: './dist/recipe-images',
urlPath: '/recipe-images/'
});
let imageAttributes = {
class: className,
alt,
sizes,
loading: "lazy",
decoding: "async"
};
return Image.generateHTML(metadata, imageAttributes);
}
config.addNunjucksAsyncShortcode('recipeimage', imageShortcode);
return {
dir: {
input: 'src',
output: 'dist',
includes: '_includes',
data: '_data'
},
passthroughFileCopy: true
}
};

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.cache
dist/
node_modules

26
README.md Normal file
View file

@ -0,0 +1,26 @@
![My Online Cookbook logo](https://raw.githubusercontent.com/maeligg/my-online-cookbook/main/github-readme.svg)
# My Online Cookbook
My Online Cookbook is a starter kit to create your own website of recipes, using [Eleventy](https://11ty.io) and [Netlify CMS](https://www.netlifycms.org/). It is meant to be both highly accessible (including to non-developers), as well as fully customisable should you want to use it as a starting off point.
Demo (this is what you get out of the box) : [mycookbook.netlify.app](mycookbook.netlify.app)
## Features
### 📘 Optimised for recipes
Unlike other general-purpose templates and website builders, My Online Cookbook is optimised for writing, reading and easily finding back your recipes. Quickly visualise which ingredients you need, navigate between recipes in the same categories, and automatically adapt quantities based on the number of servings.
### 💪 Powerful search
The kit includes a powerful live search system offering a UX on-par with third-party services like [Algolia](https://www.algolia.com/), without needing any external dependency.
### 🧰 Lightweight & easily extendable
Easily customise the theme color and other site attributes using the global data files, or dive into the code and change anything easily. The CSS is authored using [Sass](https://sass-lang.com/) and following the [BEM](https://en.bem.info/) naming convention. JavaScript is added where needed using [Alpine](https://github.com/alpinejs/alpine) and following a component-based approach. Images are processed and optimised at build-time using the [Eleventy image plugin](https://www.11ty.dev/docs/plugins/image/). Apart from Alpine, there are no run-time dependencies, making the site both extremely lightweight and easy to pick up and modify.
## Run the site locally
1. `npm install` to install all dependencies
2. `npm run dev` to serve the site locally
3. `npm run build` to build the site for production

1
github-readme.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

4263
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "my-online-cookbook",
"version": "0.1.0",
"description": "A starter kit to make your own online recipe cookbook",
"main": "index.js",
"scripts": {
"dev:css": "sass src/scss/main.scss:src/_includes/css/main.css --watch --style=compressed",
"dev:11ty": "eleventy --serve",
"dev": "npm-run-all --parallel dev:css dev:11ty",
"prod:css": "sass src/scss/main.scss:src/_includes/css/main.css --style=compressed",
"prod:11ty": "eleventy",
"build": "npm-run-all prod:css prod:11ty"
},
"author": "Maël Brunet",
"license": "MIT",
"devDependencies": {
"@11ty/eleventy": "^0.12.1",
"@11ty/eleventy-img": "^0.8.3",
"npm-run-all": "^4.1.5",
"sass": "^1.32.12"
},
"dependencies": {
"alpinejs": "^2.8.2"
},
"repository": {
"type": "git",
"url": "git+https://github.com/maeligg/my-online-cookbook"
}
}

10
src/_data/nav.json Normal file
View file

@ -0,0 +1,10 @@
[
{
"text": "All recipes",
"url": "/recipes/"
},
{
"text": "About",
"url": "/about/"
}
]

9
src/_data/site.js Normal file
View file

@ -0,0 +1,9 @@
module.exports = {
name: "My Online Cookbook",
metaDescription: "The online cookbook of John Doe",
author: "John Doe",
buildTime: new Date(),
primaryColor: "#ffdb70",
secondaryColor: "#32816e",
searchLabel: "Find recipes by name or ingredients"
};

View file

@ -0,0 +1,2 @@
<p>Made with <a href="https://github.com/maeligg/my-online-cookbook">My Online Cookbook</a></p>
<p>© {{ site.author }} {{ site.buildTime | dateToYear }}</p>

View file

@ -0,0 +1,29 @@
<nav class="c-nav">
<ul class="c-nav__list">
<li>
<a href="/" class="c-nav__home">
<span class="c-nav__logo">{% include "icons/logo.svg" %}</span>
<span class="c-nav__nav-item c-nav__home-text {{ 'c-nav__nav-item--active' if page.url == '/' }}">Home</span>
</a>
</li>
{% for navItem in nav %}
<li>
<a href="{{ navItem.url }}" class="c-nav__nav-item {{ 'c-nav__nav-item--active' if page.url == navItem.url }}">{{ navItem.text }}</a>
</li>
{% endfor %}
</ul>
<div x-data="{ searchOpen: false }">
<button class="c-search__search-toggle" @click="
searchOpen = !searchOpen;
$nextTick(() => document.querySelector('#search').focus(
));
">
<span class="u-sr-only">Search</span>
<span x-show="!searchOpen">{% include "icons/search.svg" %}</span>
<span x-show="searchOpen" x-cloak>{% include "icons/close.svg" %}</span>
</button>
<div x-show="searchOpen" @click.away="searchOpen = false" x-cloak>
{% include "components/search.njk" %}
</div>
</div>
</nav>

View file

@ -0,0 +1,72 @@
<div class="c-search__search-wrapper {{ 'c-search__search-wrapper--home' if homeSearch }}" x-data="{ searchResults: [], searchInput: '', matches: [] }">
<label for="{{ 'search-home' if homeSearch else 'search' }}" class="c-search__label {{ 'c-search__label--home' if homeSearch }}">{{ site.searchLabel }} :</label>
<div class="c-search__input-wrapper {{ 'c-search__input-wrapper--home' if homeSearch }}">
<input type="text" id="{{ 'search-home' if homeSearch else 'search' }}" class="c-search__input {{ 'c-search__input--home' if homeSearch }}" x-model="searchInput" @focus="
if (!window.searchResults) {
fetch('/search.json').then(res => res.json()).then(res => {
window.searchResults = res;
searchResults = window.searchResults;
});
} else {
searchResults = window.searchResults;
}
if (sessionStorage.getItem('searchResults')) {
searchResults = JSON.parse(sessionStorage.getItem('searchResults'));
} else {
fetch('/search.json').then(res => res.json()).then(res => {
sessionStorage.setItem('searchResults', JSON.stringify(res));
searchResults = random(res);
});
}
" @input="
matches = [];
if (searchInput.length < 3) { return };
searchResults.forEach(recipe => {
const matchTitle = recipe.title.toLowerCase().includes(searchInput);
const matchIngredient = recipe.ingredients.find(ingredient => ingredient.toLowerCase().includes(searchInput));
if (!matchTitle && !matchIngredient) { return };
const match = {...recipe};
if (matchTitle) { match.matchTitle = matchTitle };
if (matchIngredient) { match.matchIngredient = matchIngredient };
matches.push(match)
});
">
{% if homeSearch %}{% include "icons/search.svg" %}{% endif %}
<button class="c-search__close-button {{ 'c-search__close-button--home' if homeSearch }}" x-show="searchInput" @click="
searchInput = '';
matches = [];
" x-cloak>
<span class="u-sr-only">Close</span>
{% include "icons/close.svg" %}
</button>
</div>
<template x-if="matches.length">
<ul class="c-search__search-results">
<template x-for="(match, index) in matches" :key="index">
<li>
<a x-bind:href="match.url" x-html="highlightText(match.title, searchInput)" class="c-search__search-result-link {{ 'c-search__search-result-link--home' if homeSearch }}"></a>
<template x-if="match.matchIngredient">
<p class="c-search__search-result-ingredients {{ 'c-search__search-result-ingredients--home' if homeSearch }}" >
Contains: <span x-html="highlightText(match.matchIngredient, searchInput)"></span>
</p>
</template>
</li>
</template>
</ul>
</template>
</div>
<script>
function highlightText (string, subString) {
const index = string.toLowerCase().indexOf(subString.toLowerCase());
if (index === -1) return string;
return string.substring(0, index) + '<span class="u-highlight">' + string.substring(index, index + subString.length) + '</span>' + string.substring(index + subString.length);
}
</script>

View file

@ -0,0 +1,16 @@
<nav>
<ul class="c-tags">
{% if page.fileSlug == "recipes" %}
<span class="c-tags__tag c-tag__tag--selected">All recipes</span>
{% else %}
<a class="c-tags__tag" href="/recipes">All recipes</a>
{% endif %}
{% for tagItem in collections.tagList %}
{% if selectedTag == tagItem %}
<span class="c-tags__tag c-tag__tag--selected">{{ tagItem }}</span>
{% else %}
<a class="c-tags__tag" href="/tags/{{ tagItem | noEmoji | slug }}">{{ tagItem }}</a>
{% endif %}
{% endfor %}
</ul>
</nav>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/_reset.scss","../../scss/_utility.scss","../../scss/_global.scss","../../scss/_mixins.scss","../../scss/_typography.scss","../../scss/_layout.scss","../../scss/components/_card.scss","../../scss/components/_home.scss","../../scss/components/_nav.scss","../../scss/components/_recipe.scss","../../scss/components/_recipe-tags.scss","../../scss/components/_search.scss"],"names":[],"mappings":"AACA,qBAGE,sBAIF,+DAcE,SAIF,KACE,iBACA,SACA,UACA,uBACA,6BAIF,oBACE,gBACA,UAIF,IACE,eACA,cAIF,6BAIE,aAIF,uCACE,EACE,oCACA,uCACA,qCACA,iCC5DJ,QACI,wBAGJ,QACI,2BAGJ,WACI,kBACA,cACA,SACA,UACA,WACA,gBAGJ,cACI,wBAGJ,aACI,cACI,4BAKR,gBACI,uCAGJ,aACI,sCCjCJ,MACI,0BACA,0BACA,0BACA,0BACA,0BACA,0BACA,0BACA,uBAEA,kDACA,oDAEA,sEACA,8EACA,gDAGJ,KACI,aACA,sBAGJ,KACI,aACA,sBAIJ,UACI,wBAGJ,QACI,eChBA,kCDeJ,QAIQ,gBAIR,GACI,kBACA,4CACA,mBAGJ,GACI,kBACA,8CACA,mBAGJ,GACI,gDACA,mBAGJ,EACI,6BACA,8BACA,0BAEA,gBACI,aACA,8BAGJ,gBACI,yCAIR,OACI,eAIA,YACI,aACA,4CEhFR,WACI,uBACA,kBACA,gBACA,kBACA,gKAMJ,WACI,uBACA,kBACA,gBACA,kBACA,oKAMJ,KACI,eACA,iLACA,gBAGJ,kBACI,6BACA,gBC9BJ,aACI,cACA,iBACA,kBACA,mBAGJ,oBACI,aACA,aACA,8BAGJ,UACI,gBCdJ,iBACI,aACA,2DACA,SAGJ,QACI,cACA,gBACA,aACA,sBACA,mBACA,oCACA,4BACA,qBACA,4BAEA,4BACI,aACA,4BACA,4BACA,qBAGJ,sBACI,6DAKR,eACI,WACA,aACA,uCACA,iBACA,4BAGJ,cACI,aACA,aACA,SACA,8BACA,mBACA,sCAGJ,0BACI,WACA,YACA,oBACA,uBACA,mBACA,iBACA,uCACA,yBACA,kBAGJ,YACI,aACA,mBAEA,gBACI,iBACA,2BAIR,uBACI,YACA,aACA,mBACA,kBAGJ,eACI,iBACA,SC7EA,gBACI,WACA,kBACA,YACA,WACA,aACA,sFACA,sCACA,8BAIR,sBACI,oCACA,iBACA,iCACA,0BCjBJ,eACI,kBACA,kBAGJ,OACI,kBACA,WACA,aACA,8BACA,mBACA,yBACA,gBACA,iBAGJ,aACI,aACA,mBACA,SAGJ,iBACI,mCAGJ,aACI,aACA,mBACA,4BACA,qBAEA,sCACI,qBAIR,kBACI,iBAEA,kCAHJ,kBAIQ,kBACA,cACA,SACA,UACA,WACA,iBAIR,iBACI,gBACA,4BACA,qBAEA,8CACI,qBACA,uCACA,yBACA,kBAIR,yBACI,uCACA,yBACA,kBClEJ,wBACI,WACA,iBACA,YACA,iBAGJ,iBACI,aACA,sCAIA,+BACI,mBAEA,uCACI,YACA,iBAKZ,2BACI,mBAEA,+BACI,iBACA,2BAIR,kCACI,aACA,sBACA,SNjBA,kCMcJ,kCAMQ,aACA,gCACA,mCACA,oBACI,8CAEJ,UAIR,oBACI,mBACA,aACA,eACA,SNlCA,kCM8BJ,oBAOQ,eACA,iBNtCJ,kCM2CJ,+BAEQ,uBAIR,0BACI,YACA,WACA,UACA,oBACA,uBACA,mBACA,uCACA,YACA,kBACA,yBAEA,gEACI,uCN9DJ,kCMkEJ,gCAEQ,wBAGJ,kCACI,mBC1FR,QACI,aACA,SACA,uBACA,eAGJ,aACI,oBACA,gBACA,4BACA,qBACA,8BACA,kBACA,eAEA,sCACI,qBACA,yBACA,uCAIR,sBACI,yBACA,uCACA,8BC1BJ,yBACI,iBACA,aACA,aACA,6BACA,YAEA,2BACI,aAIR,0BACI,mBAEA,8BACI,gBAIR,+DACI,kBACA,SACA,QACA,WACA,oCACA,aACA,uCACA,kBACA,4BACA,eRZA,kCQEJ,+DAaQ,iBAIR,iBACI,cAGJ,uBACI,kBACA,iBACA,iBAGJ,yBACI,kBAGJ,+BACI,kBACA,WACA,gBACA,iBACA,kBAEA,mCACI,kBACA,UACA,SAIR,iBACI,WACA,iBACA,uCACA,mBAGJ,uBACI,4BACA,WACA,4BACA,uCAGJ,wBACI,aACA,kBACA,WACA,QACA,UACA,YACA,6BAEA,4BACI,WAIR,8BACI,WACA,SAEA,kCACI,WAKJ,8BACI,gBAIR,0BACI,gBAGJ,oCACI,iBAGJ,qCACI,kBAGJ,2CACI,eAGJ,uBACI,cACA,kBACA,mBACA,iBACA,YACA,kBACA,uCACA,yBACA,iBACA,qBAEA,0DACI,qBACA","file":"main.css"}

View file

@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0156 11.0175L21.5819 2.4509C22.1394 1.89367 22.1394 0.992719 21.5819 0.435496C21.0247 -0.121728 20.1238 -0.121728 19.5665 0.435496L10.9999 9.00212L2.43352 0.435496C1.87603 -0.121728 0.975338 -0.121728 0.418114 0.435496C-0.139371 0.992719 -0.139371 1.89367 0.418114 2.4509L8.98449 11.0175L0.418114 19.5841C-0.139371 20.1414 -0.139371 21.0423 0.418114 21.5995C0.695812 21.8775 1.06094 22.0171 1.42582 22.0171C1.79069 22.0171 2.15556 21.8775 2.43352 21.5995L10.9999 13.0329L19.5665 21.5995C19.8445 21.8775 20.2094 22.0171 20.5742 22.0171C20.9391 22.0171 21.304 21.8775 21.5819 21.5995C22.1394 21.0423 22.1394 20.1414 21.5819 19.5841L13.0156 11.0175Z" fill="#3A3A3A"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

View file

@ -0,0 +1,10 @@
<svg width="42" height="56" viewBox="0 0 42 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.495 0H3.49643C1.5654 0 0 1.65236 0 3.69074V52.3093C0 54.3476 1.5654 56 3.49643 56H38.495C40.4307 56 42 54.3435 42 52.3002V3.69983C42 1.65652 40.4307 0 38.495 0Z" fill="#FED35B"/>
<path d="M37.7006 1.07715H5.9569C4.20546 1.07715 2.78564 2.66751 2.78564 4.62942V51.4238C2.78564 53.3857 4.20546 54.9761 5.9569 54.9761H37.7006C39.4563 54.9761 40.8796 53.3817 40.8796 51.415V4.63816C40.8796 2.67152 39.4563 1.07715 37.7006 1.07715Z" fill="#EDEDED"/>
<path d="M36.3103 0H3.298C1.47656 0 0 1.65392 0 3.69424V52.3588C0 54.3991 1.47656 56.053 3.298 56.053H36.3103C38.1363 56.053 39.6165 54.395 39.6165 52.3497V3.70333C39.6165 1.65809 38.1363 0 36.3103 0Z" fill="#FEDA6F"/>
<path d="M3.298 0C1.47656 0 0 1.65392 0 3.69424V52.3588C0 54.3991 1.47656 56.053 3.298 56.053H5.38166V0H3.298Z" fill="var(--color-primary, #FED35B)"/>
<path d="M30.0798 21.5742H30.0798C29.784 21.5742 29.5441 21.8429 29.5441 22.1743V28.6913H28.5915V22.1743C28.5915 21.8429 28.3517 21.5742 28.0558 21.5742H28.0473C27.7515 21.5742 27.5116 21.8429 27.5116 22.1743V28.6913H26.559V22.1743C26.559 21.8429 26.3192 21.5742 26.0233 21.5742H26.0148C25.7189 21.5742 25.4791 21.8429 25.4791 22.1743V28.6913H24.5265V22.1743C24.5265 21.8429 24.2867 21.5742 23.9908 21.5742H23.9908C23.6949 21.5742 23.4551 21.8429 23.4551 22.1743V28.6457C23.4551 29.979 24.063 31.2191 25.0665 31.9342L25.8352 32.4825L25.4098 49.1212C25.3799 50.1195 26.0462 51.0077 26.9363 51.0666C27.9026 51.1305 28.6947 50.2521 28.6628 49.1847L28.2355 32.4825L29.0041 31.9342C30.0077 31.2191 30.6155 29.979 30.6155 28.6457V22.1743C30.6155 21.8429 30.3757 21.5742 30.0798 21.5742Z" fill="#344D5B"/>
<path d="M20.9977 27.6256C20.9977 24.2835 19.2186 21.5742 17.024 21.5742C14.8294 21.5742 13.0503 24.2835 13.0503 27.6256C13.0503 30.3168 14.2047 32.5948 15.7999 33.3812L15.3975 49.1213C15.3676 50.1195 16.0339 51.0077 16.924 51.0666C17.8903 51.1305 18.6824 50.2522 18.6505 49.1848L18.2463 33.3822C19.8424 32.5967 20.9977 30.3178 20.9977 27.6256Z" fill="#344D5B"/>
<path d="M17.0245 31.9471C18.7908 31.9471 20.2226 29.7667 20.2226 27.0771C20.2226 24.3874 18.7908 22.207 17.0245 22.207C15.2583 22.207 13.8264 24.3874 13.8264 27.0771C13.8264 29.7667 15.2583 31.9471 17.0245 31.9471Z" fill="#293B44"/>
<path d="M12.6011 6.69287H31.0636V15.1154H12.6011V6.69287Z" fill="#EDEDED"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1 @@
<svg width="22" height="22" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path d="M21.731 20.436l-6.256-6.256a8.666 8.666 0 001.941-5.47C17.416 3.907 13.51 0 8.708 0 3.907.001 0 3.908 0 8.709c0 4.802 3.907 8.709 8.708 8.709 2.072 0 3.974-.73 5.47-1.942l6.257 6.256a.914.914 0 001.296 0 .916.916 0 000-1.296zM8.708 15.584A6.882 6.882 0 011.833 8.71a6.882 6.882 0 016.875-6.875 6.882 6.882 0 016.875 6.875 6.882 6.882 0 01-6.875 6.875z" fill="#3A3A3A"/></g><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h22v22H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 572 B

View file

@ -0,0 +1 @@
<svg width="15" height="15" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path d="M9.015 13.662a6.11 6.11 0 01-.375.082.604.604 0 00.22 1.186c.149-.028.3-.06.447-.097a.603.603 0 10-.292-1.17zM13.475 5.58a.6.6 0 00.762.383.603.603 0 00.383-.762 7.546 7.546 0 00-.157-.43.603.603 0 10-1.12.449c.047.118.091.24.132.36zM10.975 12.802c-.107.07-.216.138-.327.202a.603.603 0 10.603 1.044c.131-.076.262-.156.389-.24a.603.603 0 00-.665-1.006zM14.994 7.264a.603.603 0 00-1.205.048c.005.127.007.256.004.383a.603.603 0 101.205.027 7.501 7.501 0 00-.004-.458zM13.376 11.215a.603.603 0 00-.844.12 6.354 6.354 0 01-.24.3.603.603 0 00.918.782c.099-.115.195-.236.286-.358a.603.603 0 00-.12-.844zM14.262 9.036a.603.603 0 00-.755.395c-.038.121-.08.243-.126.362a.603.603 0 001.127.43c.054-.142.104-.287.15-.432a.603.603 0 00-.396-.755zM6.384 13.75a6.227 6.227 0 01-1.546-.492l-.017-.01a6.568 6.568 0 01-.343-.173l-.002-.001a6.3 6.3 0 01-2.12-9.143 6.266 6.266 0 011.546-1.539l.022-.015a6.303 6.303 0 017.064-.057l-.47.681c-.132.19-.051.328.178.307l2.047-.183c.23-.02.366-.22.305-.441l-.55-1.98c-.061-.222-.219-.249-.35-.06l-.472.683A7.447 7.447 0 005.648.29h-.004l-.022.006A7.436 7.436 0 001.5 3.06l-.025.031a7.074 7.074 0 00-.261.376c-.007.01-.011.02-.017.03A7.434 7.434 0 00.008 7.88v.016c.007.15.019.304.035.455 0 .01.003.02.004.029a7.442 7.442 0 002.163 4.492l.008.008.003.002c.295.29.615.56.96.803a7.434 7.434 0 002.99 1.251.603.603 0 10.213-1.187z"/><path d="M7.13 2.683a.488.488 0 00-.488.488v4.86l4.446 2.299a.485.485 0 00.657-.21.488.488 0 00-.21-.657L7.619 7.438V3.17a.488.488 0 00-.488-.488z"/></g><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h15v15H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ metaTitle or title }} - {{ site.name }}</title>
<meta name="description" content="{{ metaDescription or site.metaDescription }}">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 105 55%22><text y=%22.9em%22 font-size=%2275%22>🍽️</text></svg>">
<style>
:root {
--color-primary: {{ site.primaryColor }};
--color-secondary: {{ site.secondaryColor }};
}
{% include "css/main.css" %}
</style>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.2/dist/alpine.min.js" defer></script>
</head>
<body class="{{bodyClass}}">
<header class="l-header">
{% include "components/navigation.njk" %}
</header>
<main>
{% block content %}
<div class="l-container">
<h1>{{title}}</h1>
{{content | safe}}
</div>
{% endblock content %}
</main>
<footer class="l-footer">
{% include "components/footer.njk" %}
</footer>
</body>
</html>

View file

@ -0,0 +1,75 @@
---
bodyClass: c-home
---
{% extends 'layouts/base.njk' %}
{% block content %}
<section>
<div class="l-container">
<h1>{{title}} <span class="c-home__title-author">{{ site.author }}</span></h1>
</div>
</section>
<section>
<div class="l-container">
{% set homeSearch = true %}
{% include "components/search.njk" %}
<a x-data="getRandomRecipe()" x-init="initRandomRecipe()" x-bind:href="url" class="c-search__random-link">🔀 {{ randomRecipe }}</a>
</div>
</section>
{% if collections["Favourite ⭐"] %}}
<section class="u-bgc-grey-100">
<div class="l-container">
<h2>{{ favouriteRecipes }}</h2>
<div class="c-card__wrapper">
{% set favouriteRecipes = collections["Favourite ⭐"] | limit(4) %}
{% for recipe in favouriteRecipes %}
<a class="c-card" href="{{ recipe.url }}">
{% recipeimage recipe.fileSlug, "c-card__image", recipe.data.title, "(min-width: 1150px) 25vw, (min-width: 850px) 33vw, (min-width: 550px) 50vw, 100vw" %}
<div class="c-card__info">
<div>
{% for tag in recipe.data.tags %}
{{ tag | onlyEmoji | safe }}
{% endfor %}
</div>
{% if recipe.data.time %}
<div class="card__time">
{% include 'icons/time.svg' %}
{{ recipe.data.time }}
</div>
{% endif %}
</div>
<div class="c-card__title-wrapper">
<h3 class="c-card__title">{{ recipe.data.title }}</h3>
</div>
</a>
{% endfor %}
</div>
</div>
</section>
{% endif %}
<script>
function getRandomRecipe() {
return {
url: '',
initRandomRecipe() {
const random = (recipes) => recipes[Math.floor(Math.random() * recipes.length)];
if (sessionStorage.getItem('searchResults')) {
this.url = random(JSON.parse(sessionStorage.getItem('searchResults'))).url;
} else {
fetch('/search.json').then(res => res.json()).then(res => {
sessionStorage.setItem('searchResults', JSON.stringify(res));
this.url = random(res).url;
});
}
}
}
}
</script>
{% endblock %}

View file

@ -0,0 +1,72 @@
{% extends 'layouts/base.njk' %}
{% block content %}
{% recipeimage page.fileSlug, "c-recipe__header-image", title, "100vw" %}
<h1 class="c-recipe__title">{{ title }}</h1>
<section>
<div class="l-container">
<div class="c-recipe__recipe-content-wrapper">
<div class="c-recipe__tag-list">
{% for tag in tags %}
<a class="c-tags__tag" href="/tags/{{ tag | noEmoji | slug }}">{{ tag }}</a>
{% endfor %}
</div>
<div class="c-recipe__ingredients-wrapper" x-data="{currentServings: {{ servings }}, ingredients: '{{ ingredients }}'.split(',') }">
{% if time or servings %}
<div class="c-recipe__additional-info">
{% if time %}
<p>{% include "icons/time.svg" %}{{ time }}</p>
{% endif %}
{% if servings %}
<p>
<template x-if="currentServings > 1">
<button @click="currentServings -= 1" class="c-recipe__serving-button">
-
<span class="u-sr-only">Remove 1 serving</span>
</button>
</template>
<span x-text="currentServings"></span>
<button @click="currentServings += 1" class="c-recipe__serving-button">
+
<span class="u-sr-only">Add 1 serving</span>
</button>
<span> servings</span>
</p>
{% endif %}
</div>
{% endif %}
<h3>Ingredients</h3>
<template x-if="currentServings">
<ul class="c-recipe__ingredients-list">
<template x-for="(ingredient, index) in ingredients" :key="index">
<li x-text="adaptQuantity(ingredient, {{servings}}, currentServings)"></li>
</template>
</ul>
</template>
<template x-if="!currentServings">
<ul class="c-recipe__ingredients-list">
{% for ingredient in ingredients %}
<li>{{ ingredient }}</li>
{% endfor %}
</ul>
</template>
</div>
<div class="c-recipe__instructions-wrapper">
{% if sourceLabel and sourceURL %}
<p>Source : <a href="{{ sourceURL }}">{{ sourceLabel }}</a></p>
{% endif %}
{{ content | safe }}
</div>
</div>
</div>
</section>
<script>
function adaptQuantity (ingredient, originalServings, currentServings) {
return ingredient.replace(/(\d|\.|,)+/, match => Number(Math.round(parseFloat(match) * currentServings / originalServings + 'e' + 2) + 'e-' + 2));
}
</script>
{% endblock %}

View file

@ -0,0 +1,42 @@
{% extends 'layouts/base.njk' %}
{% block content %}
<main>
<section>
<div class="l-container">
<h1>{{ selectedTag }}{{ " " if selectedTag }}{{title}}</h1>
{% include 'components/taglist.njk' %}
</div>
</section>
<section class="u-bgc-grey-100">
<div class="l-container">
<div class="c-card__wrapper">
{% for recipe in collections.recipes %}
{% if not selectedTag or selectedTag in recipe.data.tags %} {# If we don't have a selectedTag, we are on the all recipes page #}
<a class="c-card" href="{{ recipe.url }}">
{% recipeimage recipe.fileSlug, "c-card__image", recipe.data.title, "(min-width: 1150px) 25vw, (min-width: 850px) 33vw, (min-width: 550px) 50vw, 100vw" %}
<div class="c-card__info">
<div>
{% for tag in recipe.data.tags %}
{{ tag | onlyEmoji | safe }}
{% endfor %}
</div>
{% if recipe.data.time %}
<div class="card__time">
{% include 'icons/time.svg' %}
{{ recipe.data.time }}
</div>
{% endif %}
</div>
<div class="c-card__title-wrapper">
<h3 class="c-card__title">{{ recipe.data.title }}</h3>
</div>
</a>
{% endif %}
{% endfor %}
</div>
</div>
</section>
</main>
{% endblock %}

4
src/about.md Normal file
View file

@ -0,0 +1,4 @@
---
layout: layouts/base.njk
title: About
---

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/img/favicon-alt.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

7
src/index.md Normal file
View file

@ -0,0 +1,7 @@
---
layout: layouts/home.njk
title: This cookbook belongs to
metaTitle: Home
favouriteRecipes: Some of my favourite recipes
randomRecipe: random recipe
---

2
src/js/main.js Normal file
View file

@ -0,0 +1,2 @@
import 'alpinejs';
import './search';

70
src/js/search.js Normal file
View file

@ -0,0 +1,70 @@
const searchHomeInput = document.querySelector('.js-search-home-input');
const searchHomeResultsWrapper = document.querySelector('.js-search-home-results');
let searchResults;
const clearChildNodes = (parent) => {
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
}
const highlightText = (text, match) => {
const index = text.toLowerCase().indexOf(match.toLowerCase());
if (index === -1) return text;
return text.substring(0, index) + '<span class="u-highlight">' + text.substring(index, index + match.length) + '</span>' + text.substring(index + match.length);
}
if (searchHomeInput) {
// Fetch all recipes data when the user is about to search
searchHomeInput.addEventListener('focus', () => {
if (!searchResults) {
fetch('/search.json').then(res => res.json()).then(res => {
searchResults = res;
});
};
});
searchHomeInput.addEventListener('input', (e) => {
const searchInput = e.target.value.toLowerCase();
const results = [];
clearChildNodes(searchHomeResultsWrapper);
// For lower letter counts, results are unlikely to be relevant. So we don't show anything yet
if (searchInput.length < 3) { return };
searchResults.forEach(recipe => {
// We search on both recipe titles and ingredients. This could easily be extended to include the recipe tags, body, etc
const matchTitle = recipe.title.toLowerCase().includes(searchInput);
const matchIngredients = recipe.ingredients.filter(ingredient => ingredient.toLowerCase().includes(searchInput));
if (!matchTitle && !matchIngredients.length) { return };
const match = {...recipe};
if (matchTitle) { match.matchTitle = matchTitle };
if (matchIngredients.length) { match.matchIngredients = matchIngredients };
results.push(match)
});
// Now we have the search results, we just need to display them on the page
results.forEach(result => {
const listItem = document.createElement('li');
listItem.classList.add('c-search-block__search-result-title');
const link = document.createElement('a');
link.innerHTML = highlightText(result.title, searchInput);
link.setAttribute('href', result.url);
listItem.appendChild(link);
if (result.matchIngredients) {
const paragraph = document.createElement('p');
paragraph.classList.add('c-search-block__search-result-ingredients');
paragraph.innerHTML = 'Contains : ' + highlightText(result.matchIngredients.join(', '), searchInput);
listItem.appendChild(paragraph);
}
searchHomeResultsWrapper.appendChild(listItem);
});
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

4
src/recipes.md Normal file
View file

@ -0,0 +1,4 @@
---
layout: layouts/recipes-list.njk
title: All recipes
---

29
src/recipes/brownies.md Normal file
View file

@ -0,0 +1,29 @@
---
title: Simple brownies
tags:
- Sweet 🍬
- Cake 🍰
- Sharable
- Favourite ⭐
time: 45 min
sourceLabel: BBC good food
sourceURL: https://www.bbc.co.uk/food/recipes/richchocolatebrownie_1933/
servings: 4
ingredients:
- 225g butter (preferably unsalted)
- 450g caster sugar
- 140g dark chocolate broken into pieces
- 5 free-range medium eggs
- 110g plain flour
- 55g cocoa powder
---
Heat the oven to 190C/170C Fan/Gas 5. Line a 20x30cm baking tin with baking paper.
Gently melt the butter and the sugar together in a large pan. Once melted, take off the heat and add the chocolate. Stir until melted.
Beat in the eggs, then stir in the flour and the cocoa powder.
Pour the brownie batter into the prepared tin and bake for 3035 minutes, or until the top of the brownie is just firm but there is still a gentle wobble in the middle.
Take out of the oven and leave to cool in the tin. Cut the brownies into 5cm squares when only just warm, or completely cool.

View file

@ -0,0 +1,34 @@
---
title: Coconut lentil soup
tags:
- Soup 🥣
- Vegan 🌱
- Favourite ⭐
sourceLabel: Bon Appétit
sourceURL: https://www.bonappetit.com/recipe/vegan-coconut-lentil-soup
servings: 4
ingredients:
- 1 large onion
- 6 garlic cloves
- 3 tablespoons of grated ginger
- 2 tablespoons virgin coconut oil
- 5 teaspoons curry powder
- 0.5 teaspoon cayenne pepper
- 400g can unsweetened coconut milk
- 150g split red lentils
- 8 tablespoons unsweetened shredded coconut
- 2 teaspoons kosher salt
- 300g spinach
- 1 can crushed tomatoes
- plain whole-milk or non-dairy yogurt (for serving; optional)
---
Peel 1 onion and chop. Smash 6 garlic cloves with the flat side of your knife. Peel, then finely chop. Peel the ginger with a small spoon, then finely chop.
Heat 2 Tbsp. oil in large Dutch oven over medium. Add onion and cook, stirring often, just until translucent, 68 minutes. Add garlic and ginger and cook, stirring often, until garlic is starting to turn golden, about 5 minutes. Add 5 tsp. curry powder and 0.5 tsp. cayenne and cook, stirring constantly, until spices are aromatic and starting to stick to bottom of pot, about 1 minute. Add the coconut milk and stir to loosen spices, then stir in the lentils, the shredded coconut, 2 tsp. salt, and 1 liter of water.
Bring to a boil over medium-high heat, then reduce heat to medium-low to keep soup at a gentle simmer. Cook, stirring occasionally, until lentils are broken down and soup is thickened, 2530 minutes.
Meanwhile, coarsely chop the spinach. Add spinach and the tomatoes to pot and stir to combine. Taste and season with more salt. Simmer just to let flavors meld, about 5 minutes. Taste and season again with more salt.
Ladle soup into bowls. Top with yogurt, if desired.

View file

@ -0,0 +1,28 @@
---
title: Courgette & lemon risotto
tags:
- Italian 🇮🇹
- Vegetarian 🌿
- Favourite ⭐
time: 50 min
sourceLabel: BBC good food
sourceURL: https://www.bbcgoodfood.com/recipes/courgette-lemon-risotto/
servings: 2
ingredients:
- 50g butter
- 1 onion finely chopped
- 1 large garlic clove crushed
- 180g risotto rice
- 1 vegetable stock cube
- zest and juice 1 lemon
- 2 lemon thyme sprigs
- 250g courgette diced
- 50g parmesan (or vegetarian alternative) grated
- 2 tbsp crème fraîche
---
Melt the butter in a deep frying pan. Add the onion and fry gently until softened for about 8 mins, then add the garlic and stir for 1 min. Stir in the rice to coat it in the buttery onions and garlic for 1-2 mins.
Dissolve the stock cube in 1 litre of boiling water, then add a ladle of the stock to the rice, along with the lemon juice and thyme. Bubble over a medium heat, stirring constantly. When almost all the liquid has been absorbed, add another ladle of stock and keep stirring. Tip in the courgette and keep adding the stock, stirring every now and then until the rice is just tender and creamy.
To serve, stir in some seasoning, the lemon zest, Parmesan and crème fraîche.

5
src/recipes/recipes.json Normal file
View file

@ -0,0 +1,5 @@
{
"layout": "layouts/recipe",
"servings": 0,
"ingredients": []
}

83
src/scss/_global.scss Normal file
View file

@ -0,0 +1,83 @@
:root {
--color-grey-100: #F9FAFB;
--color-grey-200: #F3F4F6;
--color-grey-300: #E5E7EB;
--color-grey-400: #D1D5DB;
--color-grey-500: #9CA3AF;
--color-grey-800: #4B5563;
--color-grey-900: #3A3A3A;
--color-white: #ffffff;
--box-shadow-light: 0 4px 4px 0 rgba(0, 0, 0, .1);
--box-shadow-heavy: 0 4px 8px 4px rgba(0, 0, 0, .1);
--shadow-md: 0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px 0 rgba(0,0,0,0.06);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05);
--shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.25);
}
body {
display: flex;
flex-direction: column;
}
main {
display: flex;
flex-direction: column;
}
// The x-cloak attribute is automatically removed from elements when Alpine initializes. They should not be visible before then to avoid flickering content
[x-cloak] {
display: none !important;
}
section {
padding: 30px 0;
@include mq(medium) {
padding: 60px 0;
}
}
h1 {
text-align: center;
font-size: clamp(3rem,calc(1rem + 3vw),4rem);
margin-bottom: 18px;
}
h2 {
text-align: center;
font-size: clamp(2rem,calc(1rem + 2vw),3.4rem);
margin-bottom: 42px;
}
h3 {
font-size: clamp(1.6rem,calc(1rem + 1vw),2.6rem);
margin-bottom: 18px;
}
a {
color: var(--color-secondary);
text-decoration: underline 2px;
text-underline-offset: 2px;
&:hover, &:focus {
outline: none;
text-decoration: underline 4px;
}
&:focus-visible {
outline: 2px solid var(--color-secondary);
}
}
button {
cursor: pointer;
}
input {
&:focus {
outline: none;
box-shadow: 0 0 0 2px var(--color-secondary);
}
}

16
src/scss/_layout.scss Normal file
View file

@ -0,0 +1,16 @@
.l-container {
margin: 0 auto;
max-width: 1140px;
padding-left: 18px;
padding-right: 18px;
}
.l-header, .l-footer {
padding: 12px;
display: flex;
justify-content: space-between;
}
.l-footer {
margin-top: auto;
}

27
src/scss/_mixins.scss Normal file
View file

@ -0,0 +1,27 @@
$breakpoints-map: (
small: "all and (min-width: 576px)",
medium: "all and (min-width: 768px)",
large: "all and (min-width: 1200px)"
);
// -------------------------------------
// Mixin
// -------------------------------------
@mixin mq($breakpoint-name) {
// sanitize variable
$breakpoint-name: unquote($breakpoint-name);
// check if passed name is in $breakpoints-map
@if map-has-key($breakpoints-map, $breakpoint-name) {
$breakpoint-query: map-get($breakpoints-map, $breakpoint-name);
// write media query
@media #{$breakpoint-query} {
@content;
}
// throw error if passed parameter is not a key in $breakpoints-map
} @else {
@error "#{$breakpoint-name} is not a key in $breakpoints-map";
}
}

63
src/scss/_reset.scss Normal file
View file

@ -0,0 +1,63 @@
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
ul,
ol,
li,
figure,
figcaption,
blockquote,
dl,
dd {
margin: 0;
}
/* Set core body defaults */
body {
min-height: 100vh;
margin: 0;
padding: 0;
scroll-behavior: smooth;
text-rendering: optimizeSpeed;
}
/* Remove list styles on ul, ol elements with a class attribute */
ol[class], ul[class] {
list-style: none;
padding: 0;
}
/* Make images easier to work with */
img {
max-width: 100%;
display: block;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}
/* Remove all animations and transitions for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

32
src/scss/_typography.scss Normal file
View file

@ -0,0 +1,32 @@
@font-face {
font-family: 'Vollkorn';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Vollkorn Regular'),
local('Vollkorn-Regular'),
url('/fonts/Vollkorn-Regular.woff2') format('woff2'),
url('/fonts/Vollkorn-Regular.woff') format('woff');
}
@font-face {
font-family: 'Vollkorn';
font-style: normal;
font-weight: 600;
font-display: swap;
src: local('Vollkorn SemiBold'),
local('Vollkorn-SemiBold'),
url('/fonts/Vollkorn-SemiBold.woff2') format('woff2'),
url('/fonts/Vollkorn-SemiBold.woff') format('woff');
}
body {
font-size: 1rem;
font-family: Sentinel SSm A, Sentinel SSm B, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
line-height: 1.5;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Vollkorn', serif;
font-weight: 600;
}

35
src/scss/_utility.scss Normal file
View file

@ -0,0 +1,35 @@
.u-hide {
display: none !important;
}
.u-show {
display: initial !important;
}
.u-sr-only {
position:absolute;
left:-10000px;
top:auto;
width:1px;
height:1px;
overflow:hidden;
}
.u-print-only {
display: none !important;
}
@media print {
.u-print-only {
display: initial !important;
}
}
.u-bgc-grey-100 {
background-color: var(--color-grey-100);
}
.u-highlight {
background-color: rgba(250, 243, 145, 0.5);
}

View file

@ -0,0 +1,80 @@
.c-card__wrapper {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(250px, 1fr));
gap: 30px;
}
.c-card {
margin: 0 auto;
max-width: 600px;
display: flex;
flex-direction: column;
border-radius: 20px;
background-color: var(--color-white);
box-shadow: var(--shadow-md);
text-decoration: none;
color: var(--color-grey-900);
&:hover, &:focus {
outline: none;
box-shadow: var(--shadow-lg);
color: var(--color-grey-900);
text-decoration: none;
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--color-secondary),
var(--shadow-lg);
}
}
.c-card__image {
width: 100%;
height: 160px;
background-color: var(--color-grey-500); // fallback while image is loading
object-fit: cover;
border-radius: 20px 20px 0 0;
}
.c-card__info {
padding: 12px;
display: flex;
gap: 18px;
justify-content: space-between;
align-items: center;
background-color: var(--color-primary);
}
.c-card__tag-first-letter {
width: 22px;
height: 22px;
display: inline-flex;
justify-content: center;
align-items: center;
font-weight: bold;
background-color: var(--color-grey-900);
color: var(--color-white);
border-radius: 50%;
}
.card__time {
display: flex;
align-items: center;
svg {
margin-right: 6px;
fill: var(--color-grey-900);
}
}
.c-card__title-wrapper {
flex-grow: 1;
display: flex;
align-items: center;
padding: 12px 18px;
}
.c-card__title {
font-size: 1.4rem;
margin: 0;
}

View file

@ -0,0 +1,19 @@
.c-home {
&::before {
content: "";
position: absolute;
z-index: -10;
width: 100%;
height: 400px;
background-image: linear-gradient(to bottom, var(--color-primary), var(--color-white));
background-color: hsla(0, 0%, 100%, .3);
background-blend-mode: overlay;
}
}
.c-home__title-author {
background-color: var(--color-white);
padding: 2px 12px;
text-decoration: underline dashed;
text-underline-offset: 6px;
}

View file

@ -0,0 +1,73 @@
.c-tags__label {
margin-bottom: 6px;
text-align: center;
}
.c-nav {
position: relative;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--text-sm);
font-weight: 600;
font-size: 1.2rem;
}
.c-nav__list {
display: flex;
align-items: center;
gap: 30px;
}
.c-nav__logo svg {
box-shadow: var(--box-shadow-light);
}
.c-nav__home {
display: flex;
align-items: center;
color: var(--color-grey-900);
text-decoration: none;
&:hover, &:focus {
text-decoration: none;
}
}
.c-nav__home-text {
margin-left: 18px;
@media all and (max-width: 576px) {
position:absolute;
left:-10000px;
top:auto;
width:1px;
height:1px;
overflow:hidden;
}
}
.c-nav__nav-item {
padding: 4px 8px;
color: var(--color-grey-900);
text-decoration: none;
&:hover, &:focus {
text-decoration: none;
background-color: var(--color-grey-900);
color: var(--color-white);
border-radius: 6px;
}
}
.c-nav__nav-item--active {
background-color: var(--color-grey-900);
color: var(--color-white);
border-radius: 6px;
// text-decoration: underline 2px currentColor;
&:hover, &:focus {
// text-decoration: underline 4px currentColor;
}
}

View file

@ -0,0 +1,28 @@
.c-tags {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.c-tags__tag {
display: inline-flex;
padding: 2px 6px;
color: var(--color-grey-900);
text-decoration: none;
border: 1px solid currentColor;
border-radius: 8px;
font-size: 1rem;
&:hover, &:focus {
text-decoration: none;
color: var(--color-white);
background-color: var(--color-grey-900);
}
}
.c-tag__tag--selected {
color: var(--color-white);
background-color: var(--color-grey-900);
border: 1px solid currentColor;
}

View file

@ -0,0 +1,93 @@
.c-recipe__header-image {
width: 100%;
object-fit: cover;
height: 40vh;
min-height: 300px;
}
.c-recipe__title {
padding: 20px;
background-color: var(--color-primary);
}
.c-recipe__ingredients-list {
li {
margin-bottom: 12px;
&::before {
content: "-";
margin-right: 6px;
}
}
}
.c-recipe__additional-info {
margin-bottom: 30px;
svg {
margin-right: 6px;
fill: var(--color-grey-900);
}
}
.c-recipe__recipe-content-wrapper {
display: flex;
flex-direction: column;
gap: 30px;
@include mq(medium) {
display: grid;
grid-template-columns: 400px 1fr;
grid-template-rows: min-content 1fr;
grid-template-areas:
"ingredients tags"
"ingredients instructions";
gap: 18px;
}
}
.c-recipe__tag-list {
margin-bottom: 12px;
display: flex;
flex-wrap: wrap;
gap: 12px;
@include mq(medium) {
grid-area: tags;
margin-bottom: 0;
}
}
.c-recipe__ingredients-wrapper {
@include mq(medium) {
grid-area: ingredients;
}
}
.c-recipe__serving-button {
height: 20px;
width: 20px;
padding: 0;
display: inline-flex;
justify-content: center;
align-items: center;
background-color: var(--color-grey-900);
border: none;
border-radius: 50%;
color: var(--color-white);
&:hover, &:focus {
background-color: var(--color-grey-800);
}
}
.c-recipe__instructions-wrapper {
@include mq(medium) {
grid-area: instructions;
}
p {
margin-bottom: 18px;
}
}

View file

@ -0,0 +1,141 @@
.c-search__search-toggle {
margin-left: 20px;
padding: 10px;
display: flex;
background-color: transparent;
border: none;
> * {
display: flex;
}
}
.c-search__search-wrapper {
font-weight: normal;
> * + * {
margin-top: 12px;
}
}
.c-search__search-wrapper:not(.c-search__search-wrapper--home) {
position: absolute;
top: 60px;
right: 0;
width: 100%;
background-color: var(--color-white);
padding: 24px;
border: 1px solid var(--color-grey-300);
border-radius: 6px;
box-shadow: var(--shadow-xl);
font-size: 1rem;
@include mq(medium) {
max-width: 500px;
}
}
.c-search__label {
display: block;
}
.c-search__label--home {
text-align: center;
font-weight: bold;
font-size: 1.2rem;
}
.c-search__input-wrapper {
position: relative;
}
.c-search__input-wrapper--home {
position: relative;
width: 100%;
max-width: 500px;
margin-left: auto;
margin-right: auto;
> svg {
position: absolute;
left: 16px;
top: 16px;
}
}
.c-search__input {
width: 100%;
padding: 4px 12px;
border: 2px solid var(--color-grey-900);
border-radius: 30px;
}
.c-search__input--home {
padding: 12px 18px 12px 46px;
width: 100%;
padding: 12px 18px 12px 46px;
border: 2px solid var(--color-grey-900);
}
.c-search__close-button {
display: flex;
position: absolute;
right: 12px;
top: 8px;
padding: 0;
border: none;
background-color: transparent;
svg {
width: 12px;
}
}
.c-search__close-button--home {
right: 16px;
top: 16px;
svg {
width: 17px;
}
}
.c-search__search-results {
> * + * {
margin-top: 10px;
}
}
.c-search__search-results {
margin-top: 30px;
}
.c-search__search-result-link--home {
font-size: 1.2rem;
}
.c-search__search-result-ingredients {
font-style: italic;
}
.c-search__search-result-ingredients--home {
font-size: 1rem;
}
.c-search__random-link {
display: block;
width: max-content;
margin: 10px auto 0;
padding: 6px 10px;
border: none;
border-radius: 4px;
background-color: var(--color-grey-900);
color: var(--color-white);
font-weight: bold;
text-decoration: none;
&:hover, &:focus {
text-decoration: none;
background-color: var(--color-grey-800);
}
}

14
src/scss/main.scss Normal file
View file

@ -0,0 +1,14 @@
@import "reset.scss";
@import "mixins.scss";
@import "utility.scss";
@import "global.scss";
@import "typography.scss";
@import "layout.scss";
@import "components/card.scss";
@import "components/home.scss";
@import "components/nav.scss";
@import "components/recipe.scss";
@import "components/recipe-tags.scss";
@import "components/search.scss";

13
src/search.njk Normal file
View file

@ -0,0 +1,13 @@
---
permalink: search.json
---
[
{% for recipe in collections.recipes %}
{
"title" : "{{ recipe.data.title }}",
"url" : "{{ recipe.url }}",
"ingredients" : [{% for ingredient in recipe.data.ingredients %}"{{ingredient}}"{% if not loop.last %},{% endif %}{% endfor %}]
}{% if not loop.last %},{% endif %}
{% endfor %}
]

11
src/tag.md Normal file
View file

@ -0,0 +1,11 @@
---
pagination:
data: collections
size: 1
alias: selectedTag
permalink: /tags/{{ selectedTag | noEmoji | slug }}/
layout: layouts/recipes-list.njk
title: recipes
eleventyComputed:
metaTitle: All {{ selectedTag | noEmoji | lowercase }} recipes
---