Initial commit
101
.eleventy.js
Normal 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
|
|
@ -0,0 +1,3 @@
|
|||
.cache
|
||||
dist/
|
||||
node_modules
|
||||
26
README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||

|
||||
|
||||
# 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
|
After Width: | Height: | Size: 16 KiB |
4263
package-lock.json
generated
Normal file
29
package.json
Normal 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
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"text": "All recipes",
|
||||
"url": "/recipes/"
|
||||
},
|
||||
{
|
||||
"text": "About",
|
||||
"url": "/about/"
|
||||
}
|
||||
]
|
||||
9
src/_data/site.js
Normal 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"
|
||||
};
|
||||
2
src/_includes/components/footer.njk
Normal 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>
|
||||
29
src/_includes/components/navigation.njk
Normal 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>
|
||||
72
src/_includes/components/search.njk
Normal 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>
|
||||
16
src/_includes/components/taglist.njk
Normal 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>
|
||||
1
src/_includes/css/main.css
Normal file
1
src/_includes/css/main.css.map
Normal 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"}
|
||||
3
src/_includes/icons/close.svg
Normal 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 |
10
src/_includes/icons/logo.svg
Normal 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 |
1
src/_includes/icons/search.svg
Normal 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 |
1
src/_includes/icons/time.svg
Normal 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 |
38
src/_includes/layouts/base.njk
Normal 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>
|
||||
75
src/_includes/layouts/home.njk
Normal 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 %}
|
||||
72
src/_includes/layouts/recipe.njk
Normal 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 %}
|
||||
42
src/_includes/layouts/recipes-list.njk
Normal 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
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
layout: layouts/base.njk
|
||||
title: About
|
||||
---
|
||||
BIN
src/fonts/Vollkorn-Regular.woff
Normal file
BIN
src/fonts/Vollkorn-Regular.woff2
Normal file
BIN
src/fonts/Vollkorn-SemiBold.woff
Normal file
BIN
src/fonts/Vollkorn-SemiBold.woff2
Normal file
BIN
src/img/favicon-alt.ico
Normal file
|
After Width: | Height: | Size: 111 KiB |
7
src/index.md
Normal 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
|
|
@ -0,0 +1,2 @@
|
|||
import 'alpinejs';
|
||||
import './search';
|
||||
70
src/js/search.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
BIN
src/recipe-images/brownies.jpg
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
src/recipe-images/coconut-lentil-soup.jpg
Normal file
|
After Width: | Height: | Size: 573 KiB |
BIN
src/recipe-images/courgette-lemon-risotto.jpg
Normal file
|
After Width: | Height: | Size: 232 KiB |
4
src/recipes.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
layout: layouts/recipes-list.njk
|
||||
title: All recipes
|
||||
---
|
||||
29
src/recipes/brownies.md
Normal 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 30–35 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.
|
||||
34
src/recipes/coconut-lentil-soup.md
Normal 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, 6–8 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, 25–30 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.
|
||||
28
src/recipes/courgette-lemon-risotto.md
Normal 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
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"layout": "layouts/recipe",
|
||||
"servings": 0,
|
||||
"ingredients": []
|
||||
}
|
||||
83
src/scss/_global.scss
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
80
src/scss/components/_card.scss
Normal 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;
|
||||
}
|
||||
19
src/scss/components/_home.scss
Normal 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;
|
||||
}
|
||||
73
src/scss/components/_nav.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
28
src/scss/components/_recipe-tags.scss
Normal 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;
|
||||
}
|
||||
93
src/scss/components/_recipe.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
141
src/scss/components/_search.scss
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
---
|
||||