Makefile, AppCache and numerous other changes
6
.gitignore
vendored
@@ -1,3 +1,9 @@
|
||||
bin/*
|
||||
build-state/*
|
||||
data/distribution/*
|
||||
!data/distribution/contents.lr
|
||||
|
||||
|
||||
# Created by https://www.gitignore.io/api/macos
|
||||
# Edit at https://www.gitignore.io/?templates=macos
|
||||
|
||||
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "src/packages/force-update"]
|
||||
path = src/packages/force-update
|
||||
url = https://github.com/relikd/lektor-force-update-plugin
|
||||
38
Makefile
Normal file
@@ -0,0 +1,38 @@
|
||||
PROJDIR := 'src'
|
||||
|
||||
help:
|
||||
@echo
|
||||
@echo 'make clean - Removes all temporary server-build files (not ./bin)'
|
||||
@echo 'make server - Start lektor server with live change updates'
|
||||
@echo 'make build - Build deployable website into ./bin'
|
||||
@echo
|
||||
@echo 'make find-links - Search for cross reference between recipes'
|
||||
@echo
|
||||
|
||||
# Project build & clean
|
||||
|
||||
clean:
|
||||
@cd '$(PROJDIR)' && \
|
||||
temp_path="$$(lektor project-info --output-path)" && \
|
||||
if [[ -d "$$temp_path" ]]; then \
|
||||
echo "rm -rf $$temp_path"; rm -rf "$$temp_path"; \
|
||||
fi
|
||||
|
||||
server:
|
||||
@cd '$(PROJDIR)' && \
|
||||
(rm content/recipes; ln -s ../../data/development/ content/recipes) && \
|
||||
lektor server
|
||||
|
||||
build:
|
||||
@cd '$(PROJDIR)' && \
|
||||
(rm content/recipes; ln -s ../../data/distribution/ content/recipes) && \
|
||||
lektor build --output-path ../bin --buildstate-path ../build-state -f ENABLE_APPCACHE
|
||||
|
||||
# Helper methods on all recipes
|
||||
|
||||
find-links:
|
||||
@echo
|
||||
@cd '$(PROJDIR)/content/recipes' && \
|
||||
find */*.lr -exec grep --color=auto -i ".\.\./[^ ]*" -o {} + \
|
||||
|| echo 'nothing found.'
|
||||
@echo
|
||||
44
README.md
@@ -1,51 +1,47 @@
|
||||
Lektor recipes
|
||||
==============
|
||||
|
||||
Generating a static site for recipes.
|
||||
Small, fast, multi-language, indexed.
|
||||
Static site generator for recipes; built upon [Lektor](https://github.com/lektor/lektor/).
|
||||
|
||||

|
||||
|
||||
Styling is optimized for desktop, mobile, and print output.
|
||||
At some point I may add search filters and offline archives for mobile devices.
|
||||
|
||||
This project is built upon [Lektor](https://github.com/lektor/lektor/).
|
||||
Features
|
||||
-------
|
||||
|
||||
- Responsive design (desktop, mobile, print)
|
||||
- Mobile application (AppCache & app manifest)
|
||||
- Offline cache (local storage [150 recipes ~ 3mb])
|
||||
- Multi-language (DE & EN, more can be added)
|
||||
- Blazing fast (due to cache and minimal data usage)
|
||||
- Indexed (group by time, ingredients, or tags)
|
||||
- ~~static search~~ (**not yet**, but coming soon…)
|
||||
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
1. [Download](https://www.getlektor.com/) Lektor and follow the instructions.
|
||||
1. Download [Lektor](https://www.getlektor.com/) and follow the instructions.
|
||||
|
||||
2. Clone this repository and change to the `src` directory.
|
||||
|
||||
3. Run `lektor server` to run a local server and preview the page. **Note:** Open http://127.0.0.1:5000/en/ instead of the default `/` path.\**
|
||||
3. Run `make server` to run a local server and preview the page.
|
||||
|
||||
|
||||
### Deploy
|
||||
|
||||
You need to add a deployment setting to the project file.
|
||||
Either apply something from the [official docs](https://www.getlektor.com/docs/deployment/),
|
||||
or run a custom rsync command:
|
||||
|
||||
```
|
||||
rsync -rclzv --delete --exclude=.* SRC DST
|
||||
```
|
||||
|
||||
\** You don't have to worry about the redirect.
|
||||
The `root/index.html` is copied to the destination.
|
||||
Instead, you could also delete `root/` and change the project file.
|
||||
Set `url_prefix` to `/` for one of the alternates.
|
||||
4. For distribution run `make build` and add an [official deploy](https://www.getlektor.com/docs/deployment/).
|
||||
|
||||
|
||||
### Modify
|
||||
|
||||
Thanks to Lektor you have a simple content management system (see screenshot below).
|
||||
Two things to note:
|
||||
A few things to note:
|
||||
|
||||
1. Measurements have to be added manually to settings. Don't forget to __pluralize__ (c, cup, cups, etc.)
|
||||
|
||||
2. You can __group ingredients__ if the line ends with a colon (`:`)
|
||||
2. You can add __ingredient groups__ if the line ends with a colon (`:`)
|
||||
|
||||
3. The preferred __image size__ is `800x600`. Please scale all images down to save bandwidth. Try to keep an aspect ratio of 4:3 for the first image, or it will be cropped on the recipe overview page! All other images will be shown unmodified in whatever aspect ratio is provided (individual recipe page)
|
||||
|
||||
4. __AppCache__ is disabled during development. However, you can pass `-f ENABLE_APPCACHE` to any `lektor` command to enable it. The makefile does this by default for the `build` target.
|
||||
|
||||
Also, see [Lektor docs](https://www.getlektor.com/docs/) and [jinja2 template](https://jinja.palletsprojects.com/en/2.10.x/templates/) documentation.
|
||||
|
||||
|
||||
19
data/development/brownies-raw/contents+de.lr
Normal file
@@ -0,0 +1,19 @@
|
||||
yield: 20×20cm Backform
|
||||
---
|
||||
ingredients:
|
||||
|
||||
2 Tassen Walnüsse
|
||||
1 Tasse Kakao
|
||||
1/4 TL Salz
|
||||
2 1/2 Tassen Datteln, weich, Medjool
|
||||
|
||||
Optional:
|
||||
1 Tasse Mandeln, grob gehackt
|
||||
---
|
||||
directions:
|
||||
|
||||
Walnüsse im Mixer fein hacken. Kakao und Salz hinzugeben und miteinander vermixen.
|
||||
|
||||
Datteln nach und nach einzeln hinzugeben, während der Mixer weiter läuft. Die Mixtur sollte bröckelig sein aber beim Zusammendrücken leicht zusammenhalten (wenn es nicht zusammenhält, noch mehr Datteln hinzugeben)
|
||||
|
||||
In einer Schüssel die Mandeln mit der Mixtur vermischen und in eine Kuchenform pressen. Bis zum Servieren im Kühlschrank, luftdicht verschlossen halten.
|
||||
19
data/development/brownies-raw/contents+en.lr
Normal file
@@ -0,0 +1,19 @@
|
||||
yield: 8×8 square pan
|
||||
---
|
||||
ingredients:
|
||||
|
||||
2 cups walnuts
|
||||
1 cup cocoa powder
|
||||
1/4 tsp salt
|
||||
2 1/2 cups dates, soft, medjool
|
||||
|
||||
Optional:
|
||||
1 cup almonds, roughly chopped
|
||||
---
|
||||
directions:
|
||||
|
||||
Blend walnuts on high until finely ground. Add the cocoa and salt. Pulse to combine.
|
||||
|
||||
Add the dates one at a time through the feed tube of the food processor while it is running. What you should end up with is a mix that appears rather like cake crumbs. But when pressed, will easily stick together (if the mixture does not hold together well, add more dates).
|
||||
|
||||
In a large bowl, combine the walnut-cocoa mix with the chopped almonds. Press into a lined cake pan or mold. Place in the fridge until ready to serve. Store in an airtight container.
|
||||
13
data/development/brownies-raw/contents.lr
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Brownies, Raw
|
||||
---
|
||||
tags: cake, glutenfree, raw, sweet
|
||||
---
|
||||
time: 15
|
||||
---
|
||||
difficulty: easy
|
||||
---
|
||||
rating: 4
|
||||
---
|
||||
source: https://mynewroots.org/site/2011/04/the-raw-brownie-2/
|
||||
---
|
||||
date: 2013-09-01
|
||||
BIN
data/development/brownies-raw/image.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
133
data/development/gluten-free-flour/contents+de.lr
Normal file
@@ -0,0 +1,133 @@
|
||||
name: Glutenfreies Mehl
|
||||
---
|
||||
yield: 9 Tassen
|
||||
---
|
||||
ingredients:
|
||||
|
||||
6 Tassen Mehl
|
||||
3 Tassen Stärke
|
||||
---
|
||||
directions:
|
||||
|
||||
1) Mindestens 1–2 Stärken (leichtes Mehl) und 1 mittleres Mehl mischen. Je nach Geschmack und Textur noch ein weiteres Mehl (mittel oder schwer) hinzufügen.
|
||||
|
||||
2) Mehl mit einem Löffel umfüllen und mit einem Messer glattstreichen.
|
||||
|
||||
3) In einen luftdichten Behälter geben und gut schütteln.
|
||||
|
||||
4) An einem dunklen, trockenen Ort oder im Kühlschrank aufbewahren.
|
||||
|
||||
|
||||
### Existierende Mischungen
|
||||
|
||||
#### [Cara's All-Purpose Mischung](https://forkandbeans.com/2013/02/13/the-best-gluten-free-flour-blend/)
|
||||
- 3 Tassen __Braunes Reismehl__, extrafein gemahlen
|
||||
- 3 Tassen __Sorghummehl__, extrafein gemahlen
|
||||
- 1.5 Tassen __Pfeilwurzmehl__
|
||||
- 1.5 Tassen __Kartoffelstärke__
|
||||
|
||||
Für die [Leichte Mischung](https://forkandbeans.com/2015/11/18/gluten-free-flour-blend-for-cakes/) (perfekt für Kuchen, Cupcakes, Muffins, etc.) – Sorghum ersetzen durch Weißes Reismehl.
|
||||
|
||||
|
||||
#### [Sarah's All-Purpose Mischung](https://sarahbakesgfree.com/2012/05/sarahs-gluten-free-flour-blend.html)
|
||||
- 4 Tassen __Braunes Reismehl__
|
||||
- 2 Tassen __Weißes Reismehl__
|
||||
- 2 Tassen __Kartoffelstärke__
|
||||
- 1/2 Tasse __Tapiokastärke__
|
||||
- 1/2 Tasse __Maisstärke__
|
||||
- 5 TL __Xanthan__
|
||||
|
||||
|
||||
### Mischung selbst zusammen stellen
|
||||
|
||||
#### Leichte Mehle:
|
||||
Das sind alle Stärken — ein Muss bei der Herstellung von glutenfreien Mehlen.
|
||||
|
||||
<dl>
|
||||
<dt>Pfeilwurzmehl</dt>
|
||||
<dd>
|
||||
Diese leicht verdauliche Stärke wird aus einer Kombination mehrerer Wurzeln gewonnen. Es kann als gesündere Alternative für Maisstärke, zur Verdickung von Soßen verwendet werden.
|
||||
</dd>
|
||||
<dt>Maisstärke</dt>
|
||||
<dd>
|
||||
Aus Mais gemahlen, ist diese Stärke ein hervorragendes Binde- und Verdickungsmittel. Maisstärke erzeugt bspw. bei Brot eine großartige Kruste.
|
||||
</dd>
|
||||
<dt>Kartoffelstärke</dt>
|
||||
<dd>
|
||||
Ausgezeichnete Stärke um Backwaren mit Feuchtigkeit zu versorgen. Bitte beachte, dass Kartoffelstärke und Kartoffelmehl zwei verschiedene Dinge sind – Etikett sorgfältig lesen.
|
||||
</dd>
|
||||
<dt>Tapiokastärke / Tapiokamehl</dt>
|
||||
<dd>
|
||||
Aus der Maniokwurzel extrahiert und gebleicht, wird Tapiokastärke typischerweise als Verdickungsmittel in Rezepten verwendet. Dies ist eine aromatisierte Stärke. Sie verleiht dem Backgut eine gewisse Leichtigkeit in der Textur.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
#### Mittlere Mehle:
|
||||
Obwohl diese Mehle an sich nahrhaft sind, sind sie bei der Verwendung in einer Rezeptur etwas leichter und stabiler. Sie können in Verbindung mit einer Stärke auch einzeln verwendet werden.
|
||||
|
||||
<dl>
|
||||
<dt>Favabohne / Ackerbohne</dt>
|
||||
<dd>
|
||||
Die Favabohne kommt typischerweise in einer Mischung aus Kichererbsenmehl vor. Sie lässt den Teig schön aufgehen, hat aber einen ausgeprägten Geschmack.
|
||||
</dd>
|
||||
<dt>Kichererbse</dt>
|
||||
<dd>
|
||||
Einer der besten glutenfreien Mehle, der einzige Haken ist ein sehr ausgeprägter Bohnengeschmack. Lässt die Backwaren außergewöhnlich gut aufgehen. Kann einzeln mit Stärke verwendet werden.
|
||||
</dd>
|
||||
<dt>Hirse</dt>
|
||||
<dd>
|
||||
Mit einem trockenen und leicht nussigen Geschmack ist Hirsemehl ein überwiegend stärkehaltiges Getreide. Der Proteingehalt gleicht dem von Vollkornmehl.
|
||||
</dd>
|
||||
<dt>Hafer</dt>
|
||||
<dd>
|
||||
Dieses Mehl, das direkt aus Hafer gemahlen wird, ist reich an Vitaminen und Ballaststoffen. Es ergibt einen schönen, gleichmäßigen Geschmack und lässt die Backwaren gut aufgehen. Kann einzeln oder in Kombination mit anderen Mehlen verwendet werden.
|
||||
</dd>
|
||||
<dt>Quinoa</dt>
|
||||
<dd>
|
||||
Trotz seines sehr ausgeprägten Geschmacks ist Quinoa ein weiteres großartiges Mehl. Es kann einzeln oder in Kombination mit anderen Mehlen verwendet werden. Es ist nahrhaft, enthält viele Mineralien und Vitamine, und produziert eine hervorragende Textur.
|
||||
</dd>
|
||||
<dt>Sorghum</dt>
|
||||
<dd>
|
||||
Sorghum neigt dazu, die Textur und Leichtigkeit von Weizenmehl nachzuahmen und verleiht den Backwaren eine gewisse Zartheit. Aus diesem Grund ist es eines meiner Lieblingsmehle.
|
||||
</dd>
|
||||
<dt>Weißer Reis</dt>
|
||||
<dd>
|
||||
Berühmt dafür, eine kiesige Textur zu erzeugen. Bei diesem Mehl ist es wichtig, einen sehr feinen Mahlgrad zu wählen. Es ist ein sehr verbreitetes Mehl, das aus gutem Grund in vielen Mischungen verwendet wird. Es ist leicht und liefert hervorragende Ergebnisse. <b>Hinweis:</b> Süßer Reis unterscheidet sich vom weißem Reis und sollte eher wie eine Stärke (und in kleineren Mengen) verwendet werden.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
#### Schwere Mehle:
|
||||
Das sind dichtere und nahrhaftere Mehle. Sie werden selten einzeln verwendet, sondern in Kombination mit mittleren Mehlen.
|
||||
|
||||
<dl>
|
||||
<dt>Mandel</dt>
|
||||
<dd>
|
||||
Mit einem schönen Kick an Proteinen ist Mandelmehl eine gute Möglichkeit, deinen Backwaren einen butterigen Geschmack zu verleihen. Es ist ein gutes Bindemittel und verleiht Feuchtigkeit (besonders wenn du keine Eier verwendest). Für Paleo-Diät sind Nuss- und Kokosmehle eine Korn-freie Alternative.
|
||||
</dd>
|
||||
<dt>Amaranth</dt>
|
||||
<dd>
|
||||
Aus den Samen der Amaranthpflanze gewonnen, ist dieses gemahlene Mehl dichter und nährstoffreicher als die meisten Mehle. Es kann in Backwaren die wenig aufgehen einzeln verwendet werden, oder in Kombination mit einer Mischung aus mittleren Mehlen.
|
||||
</dd>
|
||||
<dt>Brauner Reis</dt>
|
||||
<dd>
|
||||
Braunes Reismehl ist sehr vergleichbar mit Vollkornmehl. Es hat viele Nährstoffe und verleiht deinem Rezept eine gute Struktur. Idealerweise sollte es superfein gemahlen sein – sonst entsteht eine kiesige Textur. Es kann mit einer Stärke einzeln verwendet werden oder in Kombination mit anderen mittleren Mehl(en).
|
||||
</dd>
|
||||
<dt>Buchweizen</dt>
|
||||
<dd>
|
||||
Lass dich sich nicht von dem Namen täuschen, Buchweizen wird nicht aus Weizen gewonnen, sondern aus einer Frucht. Dieses Mehl kann deinem Backgut einen schönen braunen Farbton verleihen, ist voll von Nährstoffen und Dichte. Es muss in Kombination mit einer Stärke und einem mittlerem Mehl verwendet werden, insbesondere für stark aufgehende Rezepte.
|
||||
</dd>
|
||||
<dt>Kokosnuss</dt>
|
||||
<dd>
|
||||
Kokosnuss absorbiert sehr viel Flüssigkeit im Rezept. Deshalb nur in kleineren Mengen (ca. 1/4 Tasse) und in Kombination mit anderen Mehlen verwenden.
|
||||
</dd>
|
||||
<dt>Mais</dt>
|
||||
<dd>
|
||||
Ein herzhaftes, dichtes Mehl. Maismehl kann der Mehlmischung eine schöne Textur verleihen, ähnlich wie bei einem Maisbrot.
|
||||
</dd>
|
||||
<dt>Teff</dt>
|
||||
<dd>
|
||||
Teff ist ein einzigartiges, aromatisiertes Vollkorn Mehl, das in äthiopischen Lebensmitteln verwendet wird. Es ist leicht, erzeugt aber gleichzeitig eine dichte Textur, so dass es am besten in kleineren Mengen (wie Kokosmehl) und in Kombination mit anderen Mehlen verwendet wird.
|
||||
</dd>
|
||||
</dl>
|
||||
134
data/development/gluten-free-flour/contents+en.lr
Normal file
@@ -0,0 +1,134 @@
|
||||
name: Gluten-Free Flour
|
||||
---
|
||||
yield: 9 cups
|
||||
---
|
||||
ingredients:
|
||||
|
||||
6 cups flour
|
||||
3 cups starch
|
||||
---
|
||||
directions:
|
||||
|
||||
1) You'll need 1–2 starches (light flour) and at least 1 medium flour. Based on the prefered flavor and texture, you can mix in another flour (medium or heavy).
|
||||
|
||||
2) Spoon out flours with a spoon and level off with a knife.
|
||||
|
||||
3) Place into an air-tight container and shake well.
|
||||
|
||||
4) Keep stored in a dark, dry place or in the refrigerator.
|
||||
|
||||
|
||||
### Existing blends
|
||||
|
||||
#### [Cara's All-Purpose Blend](https://forkandbeans.com/2013/02/13/the-best-gluten-free-flour-blend/)
|
||||
- 3 cups brown rice flour, superfine ground
|
||||
- 3 cups sorghum flour, superfine ground
|
||||
- 1.5 cups arrowroot powder
|
||||
- 1.5 cups potato starch
|
||||
|
||||
|
||||
For the [Light Blend](https://forkandbeans.com/2015/11/18/gluten-free-flour-blend-for-cakes/) (perfect for cakes, cupcakes, muffins, etc.) – replace sorghum with white rice flour.
|
||||
|
||||
|
||||
#### [Sarah's All-Purpose Blend](https://sarahbakesgfree.com/2012/05/sarahs-gluten-free-flour-blend.html)
|
||||
- 4 cups brown rice flour
|
||||
- 2 cups white rice flour
|
||||
- 2 cups potato starch
|
||||
- 1/2 cup tapioca flour
|
||||
- 1/2 cup corn starch
|
||||
- 5 tsp xanthan gum
|
||||
|
||||
|
||||
### Create your own blend
|
||||
|
||||
#### Light based flours:
|
||||
These are all of the starches — a must when creating a blend of gluten free flours.
|
||||
|
||||
<dl>
|
||||
<dt>Arrowroot Powder</dt>
|
||||
<dd>
|
||||
This easy-to-digest starch is extracted from a combination of several plant rootstocks. It can be used as a healthier sub for cornstarch in thickening up sauces and gravies.
|
||||
</dd>
|
||||
<dt>Corn Starch</dt>
|
||||
<dd>
|
||||
Ground from corn, this starch makes for a great binder and thickening agent. Check for a great non GMO brand because cornstarch can add a great outer crust for your breads.
|
||||
</dd>
|
||||
<dt>Potato Starch</dt>
|
||||
<dd>
|
||||
This is my go-to starch for it's ability to add moisture into baked goods. Please note that potato starch and potato flour are two different things – read the label carefully.
|
||||
</dd>
|
||||
<dt>Tapioca Starch / Tapioca Flour</dt>
|
||||
<dd>
|
||||
Extracted and bleached from the cassava root, tapioca starch is typically used as a thickening agent in recipes. This is a flavored starch to use in a blend for its ability to bring a certain lightness in texture to the baked good.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
#### Medium based flours:
|
||||
Though nutritious in their own right, these flours are a bit lighter when used in a recipe and are more stable to be used alone paired with a starch.
|
||||
|
||||
<dl>
|
||||
<dt>Fava Bean</dt>
|
||||
<dd>
|
||||
Fava bean can typically be found with a mix of garbanzo bean flour. It yields a really nice rise but has a distinct flavor.
|
||||
</dd>
|
||||
<dt>Garbanzo Bean / Chickpea</dt>
|
||||
<dd>
|
||||
One of the best result-producing gluten-free flours, the only catch is its a very distinct bean flavor. The rise in your baked goods will be exceptional and can be used alone with a starch.
|
||||
</dd>
|
||||
<dt>Millet</dt>
|
||||
<dd>
|
||||
With a dry and slighty nutty flavor, millet flour is a predominantly starchy grain with a protein content that is similar to whole wheat flour.
|
||||
</dd>
|
||||
<dt>Oat</dt>
|
||||
<dd>
|
||||
Ground straight from oats, this flour is rich in vitamins and fiber. It yields a nice even flavor and a great rise to your baked goods. Can be used alone or in a combination of other flours.
|
||||
</dd>
|
||||
<dt>Quinoa</dt>
|
||||
<dd>
|
||||
Despite it's very distinct flavor, quinoa is another great medium-based flour that can work alone or in combination to other flours. It's nutritious, dense with minerals and vitamins, and produces a great texture.
|
||||
</dd>
|
||||
<dt>Sorghum</dt>
|
||||
<dd>
|
||||
Sorghum tends to mimic the texture and lightness of wheat flour and will give your baked goods a certain tenderness. For this reason, it's one of my go-to flours in my flour blend.
|
||||
</dd>
|
||||
<dt>White Rice</dt>
|
||||
<dd>
|
||||
Notorious for yielding a gritty texture, it's important to get the finest ground for this flour. It's a very common flour used in blends for good reason, it's light and gives great results. <b>Note:</b> Sweet Rice is different from White Rice and should be used more like a starch and in smaller amounts.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
#### Heavy based flours:
|
||||
These are the more dense and nutritious flours that are rarely used alone and will need to be used in tandem with another medium-based flour.
|
||||
|
||||
<dl>
|
||||
<dt>Almond</dt>
|
||||
<dd>
|
||||
With a nice kick of protein, nut meal is a great way to give your baked goods a buttery flavor. It works best if you add a little into your mix (especially if you do not use eggs) to yield a nice binding result and overall moisture into your recipe. If you are Paleo, nut and coconut flours are you to-go grain-free flours.
|
||||
</dd>
|
||||
<dt>Amaranth</dt>
|
||||
<dd>
|
||||
Derived from the seeds of the amaranth plant, this stone ground flour is denser and more nutritious than most flours. It can be used alone in low rise baked goods or in combination with a mix of medium flours.
|
||||
</dd>
|
||||
<dt>Brown Rice</dt>
|
||||
<dd>
|
||||
Brown rice flour is very comparable to whole wheat flour, with its dense nutrition and great structure it lends to your recipe. It's best if you use it in superfine ground form – this way you will avoid that gritty texture. It can be used alone with a starch or in combination with another medium flour(s).
|
||||
</dd>
|
||||
<dt>Buckwheat</dt>
|
||||
<dd>
|
||||
Don't be fooled by its name, buckwheat is not derived by wheat but rather a fruit. This flour can give your baked good a nice brown hue, full of nutrition and density. It needs to be used in combination with a starch and medium flour, especially for higher rising recipes.
|
||||
</dd>
|
||||
<dt>Coconut</dt>
|
||||
<dd>
|
||||
Coconut has a great way of absorbing the liquid in a recipe, which is why it needs to be used in smaller amounts (think 1/4 cup) and in combo with other flours.
|
||||
</dd>
|
||||
<dt>Corn</dt>
|
||||
<dd>
|
||||
A hearty, dense flour, corn flour can add a nice texture to your flour blend, similar to a corn bread toothsome feel.
|
||||
</dd>
|
||||
<dt>Teff</dt>
|
||||
<dd>
|
||||
A unique flavored whole grain, Teff is a very common flour used in Ethiopian food. It's light but creates a dense texture at the same time so it's best if used in smaller amounts (like coconut flour) and in combination with other flours.
|
||||
</dd>
|
||||
</dl>
|
||||
9
data/development/gluten-free-flour/contents.lr
Normal file
@@ -0,0 +1,9 @@
|
||||
tags: bread, glutenfree, ingredient
|
||||
---
|
||||
time: 5
|
||||
---
|
||||
difficulty: easy
|
||||
---
|
||||
source: https://forkandbeans.com/2013/12/30/guide-gluten-free-flours/
|
||||
---
|
||||
date: 2013-08-09
|
||||
BIN
data/development/gluten-free-flour/image.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
@@ -1,7 +1,5 @@
|
||||
name: Vanille Ausstech-Kekse
|
||||
---
|
||||
yield: 26-28 Kekse
|
||||
---
|
||||
ingredients:
|
||||
|
||||
1/2 Tasse Margarine, oder Kokos Öl
|
||||
@@ -9,7 +7,7 @@ ingredients:
|
||||
1/2 TL Vanille Extrakt
|
||||
1/4 TL Vanilleshote
|
||||
1 Prise Salz
|
||||
2 1/4 Tassen Mehl, glutenfrei
|
||||
2 1/4 Tassen Mehl, glutenfrei, @../gluten-free-flour/
|
||||
---
|
||||
directions:
|
||||
|
||||
24
data/development/vanilla-cut-out-cookies/contents+en.lr
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Vanilla cut-out cookies
|
||||
---
|
||||
ingredients:
|
||||
|
||||
1/2 cup non-dairy butter, or coconut oil
|
||||
1/2 cup maple syrup
|
||||
1/2 tsp vanilla extract
|
||||
1/4 tsp vanilla bean
|
||||
1 dash salt
|
||||
2 1/4 cups flour, gluten-free, @../gluten-free-flour/
|
||||
---
|
||||
directions:
|
||||
|
||||
1. Preheat oven to 350°F. Line 2 cookie sheets with parchment paper. Prepare a rolling area with two additional sheets of parchment paper for that, and have your cookie cutter(s) handy.
|
||||
|
||||
2. Place butter in a large mixing bowl and whip it with a mixer until it's creamy. Add sweetener, vanilla extract and bean, and salt and mix once again to combine. Add in flour and use a wooden spoon to mix. Then get in there with your hands and mix everything together by working the dough until you can shape it into a ball {note: as depending on the flour mix you use there may be a slight variance, know that the consistency of the dough should not be sticky but should press together when pinched — be sure to knead it really well first for some time — if it's a little sticky, add a little more flour (try 1-2 Tbsp); if it's a little dry add a little more sweetener (try 1 Tbsp)}. Shape the dough into 2 balls and then flatten each into a disk.
|
||||
|
||||
3. Roll out one of the dough balls between two sheets of parchment paper to ¼" thickness {or thinner or thicker depending on how you want your cookies to turn out}. Use a cookie cutter to cut out the cookies. Carefully transfer to a prepared cookie sheets, spacing them ½" apart {they won't spread as they bake}. Gather up any dough scraps and repeat until all dough is used up. Repeat the process with the second dough ball.
|
||||
|
||||
4. Bake in a pre-heated oven for approximately 11-13 minutes, until the edges just begin to become golden. Remove from oven and place on a cooling rack. {Note: cookies will harden a little within minutes of cooling, so don't overbake}. Allow the cookies to cool for 10 minutes and enjoy!
|
||||
|
||||
__Note:__
|
||||
You can make your own glutenfree flour blend by combining:
|
||||
1 cup brown rice flour, ¾ cup tapioca starch, ½ cup sweet rice flour, ½ tsp guar gum
|
||||
@@ -6,6 +6,8 @@ rating: 4
|
||||
---
|
||||
difficulty: easy
|
||||
---
|
||||
source: https://www.unconventionalbaker.com/recipes/gluten-free-vegan-vanilla-cut-out-cookies/
|
||||
yield: 26-28
|
||||
---
|
||||
source: https://unconventionalbaker.com/recipes/gluten-free-vegan-vanilla-cut-out-cookies/
|
||||
---
|
||||
date: 2019-05-15
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
1
data/distribution/contents.lr
Normal file
@@ -0,0 +1 @@
|
||||
_model: recipes
|
||||
196
data/export-yummy.py
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
'''
|
||||
Usage: python3 generate-alternates.py '…/YummySoup.library/Database.SQL'
|
||||
|
||||
You may have to adjust `mapTag`, `clearUTF()`, and `slugify()` below.
|
||||
Output is generated in the current folder under `yummysoup-exported`.
|
||||
'''
|
||||
|
||||
# check if input param is SQL database file
|
||||
try:
|
||||
inputPath = os.path.abspath(sys.argv[1])
|
||||
if not os.path.isfile(inputPath) or not inputPath.upper().endswith('SQL'):
|
||||
raise Exception()
|
||||
base = os.path.dirname(inputPath)
|
||||
print('connecting...')
|
||||
db = sqlite3.connect(inputPath)
|
||||
except Exception:
|
||||
print()
|
||||
print(f'usage: {os.path.basename(sys.argv[0])} "path/to/db.SQL"')
|
||||
print('(e.g., "…/YummySoup! Librarys.library/Library Database.SQL")')
|
||||
print()
|
||||
exit()
|
||||
|
||||
# create output export dir if necessary
|
||||
_out = os.path.abspath('./yummysoup-exported/')
|
||||
if not os.path.exists(_out):
|
||||
os.mkdir(_out)
|
||||
|
||||
# map old tags to new one. Should be all available tags in YummySoup!
|
||||
# right hand side must be lower case string or None
|
||||
mapTag = {
|
||||
'': None,
|
||||
'Weihnachten': 'xmas',
|
||||
'Wurst': None,
|
||||
'Dressing': 'dressing',
|
||||
'Soße': 'sauce',
|
||||
'Hauptspeise': 'main-dish',
|
||||
'Süßes': 'sweet',
|
||||
'Zutat': 'ingredient',
|
||||
'Raw': 'raw',
|
||||
'Aufstrich': 'spread',
|
||||
'Brot': 'bread',
|
||||
'Kuchen': 'cake',
|
||||
'Kekse': 'cookies',
|
||||
'trocken': None,
|
||||
'Salat': 'salad',
|
||||
'Drink': 'drinks',
|
||||
'Riegel': None,
|
||||
'Schokolade': 'chocolate',
|
||||
'Dip': 'dip',
|
||||
'fruchtig': None,
|
||||
'Glutenfrei': 'glutenfree'
|
||||
}
|
||||
|
||||
|
||||
def ttoint(txt):
|
||||
i, n = txt.split(' ') if txt else (0, 'M')
|
||||
return int(i) * [1, 60, 1440]['MST'.index(n[0])]
|
||||
|
||||
|
||||
# def matchTime(time):
|
||||
# if time in [0, 25, 135, 165, 300]:
|
||||
# return [None, 30, 150, 150, 360][[0, 25, 135, 165, 300].index(time)]
|
||||
# prev = 99999
|
||||
# val = time
|
||||
# for x in [5, 10, 15, 20, 30, 45, 60, 75, 90, 105,
|
||||
# 120, 150, 180, 240, 360, 480, 720, 1440]:
|
||||
# diff = abs(time - x)
|
||||
# if diff < prev:
|
||||
# prev = diff
|
||||
# val = x
|
||||
# elif diff == prev:
|
||||
# print(time)
|
||||
# return val
|
||||
|
||||
|
||||
def clearUTF(txt):
|
||||
return txt.replace('\\U00df', 'ß').replace('\\U00f1', 'ñ')\
|
||||
.replace('\\U00c4', 'Ä').replace('\\U00e4', 'ä')\
|
||||
.replace('\\U00d6', 'Ö').replace('\\U00f6', 'ö')\
|
||||
.replace('\\U00dc', 'Ü').replace('\\U00fc', 'ü')
|
||||
|
||||
|
||||
def slugify(txt):
|
||||
return txt.lower().replace(' ', '-').replace(':', '').replace('ß', 'ss')\
|
||||
.replace('(', '').replace(')', '').replace(',', '').replace('ê', 'e')\
|
||||
.replace('ä', 'ae').replace('ü', 'ue').replace('ö', 'oe').strip('-')
|
||||
|
||||
|
||||
def formatIngredient(info):
|
||||
try:
|
||||
if info['isG'] in ['YES', '1']:
|
||||
return '\n' + info['nam']
|
||||
except KeyError:
|
||||
pass
|
||||
txt = info['nam'].replace(',', ' ')
|
||||
if info['mea']:
|
||||
txt = '{} {}'.format(info['mea'], txt)
|
||||
if info['qua']:
|
||||
txt = '{} {}'.format(info['qua'], txt)
|
||||
if info['met']:
|
||||
txt = '{}, {}'.format(txt, info['met'])
|
||||
return txt
|
||||
|
||||
|
||||
def ingredientToStr(txt):
|
||||
res = ''
|
||||
for ing in clearUTF(txt).split('},'):
|
||||
ing = ing.strip('{()} \n')
|
||||
info = {'qua': '', 'mea': '', 'nam': '', 'met': ''}
|
||||
for prop in ing.split(';'):
|
||||
if not prop:
|
||||
continue
|
||||
k, v = [x.strip('\n "') for x in prop.split('=')]
|
||||
info[k[:3]] = v
|
||||
res += '\n' + formatIngredient(info)
|
||||
return res
|
||||
|
||||
|
||||
def directionsToStr(txt):
|
||||
return txt.replace('<font face="" size="">', '').replace(' ', '')\
|
||||
.replace('</font>', '').replace('<br>', '').replace('<b>', '__').\
|
||||
replace('</b>', '__').replace('<i>', '_').replace('</i>', '_')\
|
||||
.replace('℃', '°C').replace(' °C', '°C')\
|
||||
.replace('½', '1/2').replace('¼', '1/4').replace('⅛', '1/8')\
|
||||
.replace('⅓', '1/3').replace('⅔', '2/3').replace('¾', '3/4')
|
||||
|
||||
|
||||
def prnt(key, val, inline=True):
|
||||
return '' if not val else '{}:{}{}\n---\n'.format(
|
||||
key, ' ' if inline else '\n\n', str(val).strip())
|
||||
|
||||
|
||||
def export(slug, content, img):
|
||||
output = os.path.join(_out, slug)
|
||||
for i in range(10):
|
||||
folder = output
|
||||
if i > 0:
|
||||
folder += '-%d' % i
|
||||
if not os.path.isdir(folder):
|
||||
output = folder
|
||||
break
|
||||
os.mkdir(output)
|
||||
with open(os.path.join(output, 'contents.lr'), 'w') as f:
|
||||
f.write(txt.strip().rstrip('-'))
|
||||
|
||||
for i in range(1, 10):
|
||||
src = img % i
|
||||
dest = os.path.join(output, f'image{"" if i == 1 else i}.jpg')
|
||||
if not os.path.isfile(src):
|
||||
break
|
||||
with open(src, 'rb') as a, open(dest, 'wb') as b:
|
||||
b.write(a.read())
|
||||
|
||||
|
||||
print('exporting...')
|
||||
for row in db.cursor().execute('''SELECT * FROM ZRECIPES'''):
|
||||
difficulty, rating, date, img = row[4], row[7], row[9], row[10]
|
||||
duration, tags, name, yields = row[12:15], row[15], row[17], row[21]
|
||||
notes, directions, source, ingredients = row[23], row[25], row[26], row[27]
|
||||
|
||||
# preprocess
|
||||
date = datetime.fromtimestamp(date + 978307200).strftime('%Y-%m-%d')
|
||||
img = os.path.join(base, 'Images', img + '-Image%d.jpg')
|
||||
duration = sum([ttoint(x) for x in duration]) # matchTime()
|
||||
tags = ', '.join(sorted([mapTag[x] for x in tags.split(',') if mapTag[x]]))
|
||||
slug = slugify(name)
|
||||
if yields:
|
||||
y = yields.split(' ')
|
||||
if len(y) == 3 and y[1].endswith('form'):
|
||||
yields = '{} {}'.format(y[2], y[1])
|
||||
|
||||
txt = ''
|
||||
txt += prnt('name', name)
|
||||
txt += prnt('tags', tags)
|
||||
txt += prnt('time', duration)
|
||||
txt += prnt('difficulty', [None, 'easy', 'medium', 'hard'][difficulty])
|
||||
txt += prnt('rating', rating)
|
||||
txt += prnt('yield', yields)
|
||||
txt += prnt('ingredients', ingredientToStr(ingredients), False)
|
||||
desc = directionsToStr(directions)
|
||||
if notes:
|
||||
desc = '{}\n\n__Notes:__ {}'.format(desc.strip(), notes)
|
||||
txt += prnt('directions', desc, False)
|
||||
txt += prnt('source', source)
|
||||
txt += prnt('date', date)
|
||||
|
||||
export(slug, txt, img)
|
||||
|
||||
db.close()
|
||||
print('done.')
|
||||
59
data/generate-alternates.py
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
'''
|
||||
Usage: python3 generate-alternates.py development/*
|
||||
|
||||
Input is a recipe folder.
|
||||
Will take the `contents.lr` and extract `contents+de.lr` and `contents+en.lr`.
|
||||
The content will be identical but its easier to edit this way.
|
||||
No necessary redundant data fields.
|
||||
'''
|
||||
|
||||
|
||||
def prnt(key, val, inline=True):
|
||||
return '' if not val else '{}:{}{}\n---\n'.format(
|
||||
key, ' ' if inline else '\n\n', str(val).strip())
|
||||
|
||||
|
||||
def splitContent(path):
|
||||
mode = 1
|
||||
idx = 0
|
||||
with open(os.path.join(path, 'contents.lr'), 'r') as fin:
|
||||
tmp = ['', '']
|
||||
for line in fin:
|
||||
if mode == 1:
|
||||
tag = line.split(':')[0]
|
||||
if tag in ['name', 'yield', 'ingredients', 'directions']:
|
||||
idx = 1
|
||||
else:
|
||||
idx = 0
|
||||
tmp[idx] += line
|
||||
mode = 2
|
||||
else:
|
||||
tmp[idx] += line
|
||||
if line == '---\n':
|
||||
mode = 1
|
||||
tmp[1] = tmp[1][:-4]
|
||||
return tmp
|
||||
|
||||
|
||||
def writeSplit(path):
|
||||
de_file = os.path.join(path, 'contents+de.lr')
|
||||
if not os.path.isdir(path) or os.path.exists(de_file):
|
||||
return
|
||||
print(path)
|
||||
content = splitContent(path)
|
||||
if not content[1]:
|
||||
return
|
||||
with open(de_file, 'w') as f:
|
||||
f.write(content[1])
|
||||
with open(os.path.join(path, 'contents+en.lr'), 'w') as f:
|
||||
f.write(content[1])
|
||||
with open(os.path.join(path, 'contents.lr'), 'w') as f:
|
||||
f.write(content[0])
|
||||
|
||||
|
||||
for x in sys.argv[1:]:
|
||||
writeSplit(x)
|
||||
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
lektor
|
||||
14
src/assets/app.webmanifest
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "42 recipes",
|
||||
"display": "standalone",
|
||||
"background_color" : "#EAE9E7",
|
||||
"theme_color": "#DC3A59",
|
||||
"scope": ".",
|
||||
"icons": [{
|
||||
"src": "img/icon-180.png",
|
||||
"sizes": "180x180"
|
||||
},{
|
||||
"src": "img/icon-196.png",
|
||||
"sizes": "196x196"
|
||||
}]
|
||||
}
|
||||
BIN
src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
20
src/assets/img/favicon.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="favicon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve">
|
||||
<polygon fill="#FFFFFF" points="952.7,1090.5 233.7,1090.5 66.4,907.3 64.1,672.2 233.7,532 233.7,111.5 952.7,111.5 952.7,523
|
||||
1135.8,658.6 1131.3,925.4 "/>
|
||||
<path d="M609.4,1165.1h-19.1c-63.2-2.7-125.5-23.9-176.2-61.5c-91.5,23.5-193,9.9-271.2-44.5C60.1,1004.4,6.8,910,0,811.3v-43.7
|
||||
c7.7-121,88.1-232.7,200.1-278.8c-0.3-118.2,0-315.6-0.1-433.9h800c0,118.2,0,315.7,0,434c112.3,45.7,192.3,157.9,200,278.8v42.6
|
||||
c-5.1,61.5-25.7,122.2-62.9,171.8c-47.7,65.1-120.2,111.4-199.5,126.5c-50.3,10.9-102.5,6-152.2-5.4
|
||||
C735,1141.5,672.4,1162,609.4,1165.1z M656.9,1068.1c93.2-23.3,164-113,165-209c-2.1-24.3,16.4-48.5,41.1-50
|
||||
c23.2-2.6,45.6,15.8,48.2,38.7c0.3,62.4-17.4,124.9-51.5,177.1c97.8,7.8,195.5-52.8,232.5-143.5c29.1-67.5,23.9-148.8-14-211.9
|
||||
c-24.6-43-64.2-76.8-109.5-96.4c-19.2-8.1-38.5-16.2-57.8-24.2c-0.2-106,0-291.1-0.1-397c-207.7,0-415.3,0-623,0
|
||||
c-0.1,105.9,0.2,291-0.2,396.9c-28.3,12.1-57.7,22.2-84.2,37.9C147,620,106,678.2,93.2,742.5c-17,79.5,11,166.4,71.4,220.8
|
||||
c46.7,43.4,111.5,66.4,175.1,62c-28.9-43.7-46.5-95.1-50.6-147.4c-0.1-14.1-2.5-29.3,3.7-42.6c8.4-18.7,30.5-30.1,50.6-25.1
|
||||
c18.9,3.7,33.8,21.3,34.9,40.5c-2.3,68.4,31.1,136.3,85.1,177.9C517,1071.2,590.9,1085.8,656.9,1068.1z"/>
|
||||
<path d="M688.3,316.1L688.3,316.1c-24.6,0-44.7,20-44.7,44.7v222.6c0,24.6,20,44.7,44.7,44.7l0,0c24.6,0,44.7-20,44.7-44.7V360.6
|
||||
C732.9,336.1,713,316.1,688.3,316.1z"/>
|
||||
<path d="M512.3,316.1L512.3,316.1c-24.6,0-44.7,20-44.7,44.7v222.6c0,24.6,20,44.7,44.7,44.7l0,0c24.6,0,44.7-20,44.7-44.7V360.6
|
||||
C557,336.1,537,316.1,512.3,316.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/img/icon-180.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/img/icon-196.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/img/icon-32.png
Normal file
|
After Width: | Height: | Size: 485 B |
36
src/assets/img/icon-glutenfree.svg
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="glutenfree" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#66CCCC;}
|
||||
.st1{fill:#FFB903;stroke:#000000;stroke-width:16;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<circle id="back" cx="500" cy="500" r="500"/>
|
||||
<circle id="color" class="st0" cx="500" cy="500" r="400"/>
|
||||
<g id="grain">
|
||||
<path class="st1" d="M628.1,358.7c4.3-42.9,19.3-65.4,29.3-75c45.7-44.3,96.9-65.9,114.1-47.7c17.2,18.2-6.1,68.9-51.8,113.3
|
||||
c-8.3,8.1-29.8,25.2-74.3,26.5"/>
|
||||
<path class="st1" d="M558.5,422.5c-10.5-21.3-19-56.9-18.9-93.8c0.2-63.9,20.6-115.2,45.5-114.8c25,0.4,45,52.3,44.8,116.2
|
||||
c-0.1,15.5-1.3,30.4-3.5,43.9"/>
|
||||
|
||||
<ellipse transform="matrix(9.121996e-03 -1 1 9.121996e-03 248.7508 1087.4275)" class="st1" cx="673.1" cy="418.2" rx="45.1" ry="115.6"/>
|
||||
<path class="st1" d="M541.2,461.9c14.9-2.6,31.5-3.8,49-3.7c63.9,0.8,115.1,21.5,114.6,46.4c-0.6,24.9-52.8,44.5-116.6,43.8
|
||||
c-46.7-0.5-88.2-12.9-106.1-28.6"/>
|
||||
|
||||
<ellipse transform="matrix(1.272804e-03 -1 1 1.272804e-03 85.9093 914.6296)" class="st1" cx="500.9" cy="414.3" rx="115.6" ry="45.1"/>
|
||||
<path class="st1" d="M390.1,590.6c-10.5-21.3-19-56.9-18.9-93.8c0.2-63.9,20.6-115.2,45.5-114.8c25,0.4,45,52.3,44.8,116.2
|
||||
c-0.1,15.5-1.3,30.4-3.5,43.9"/>
|
||||
|
||||
<ellipse transform="matrix(9.121996e-03 -1 1 9.121996e-03 -86.1381 1085.9659)" class="st1" cx="504.9" cy="586.4" rx="45.1" ry="115.6"/>
|
||||
<path class="st1" d="M372.7,630c14.9-2.6,31.5-3.8,49-3.7c63.9,0.8,115.1,21.5,114.6,46.4c-0.6,24.9-52.8,44.5-116.6,43.8
|
||||
c-46.7-0.5-88.2-12.9-106.1-28.6"/>
|
||||
|
||||
<ellipse transform="matrix(1.272804e-03 -1 1 1.272804e-03 -250.5945 913.9741)" class="st1" cx="332.3" cy="582.4" rx="115.6" ry="45.1"/>
|
||||
</g>
|
||||
<path id="stick" d="M236.3,777.6l-10.7-10.7c-11.8-11.8-15.1-27.9-7.4-35.6l77.8-77.8l53.7,53.7L271.9,785
|
||||
C264.1,792.8,248.1,789.4,236.3,777.6z"/>
|
||||
<g id="cross">
|
||||
<polygon points="123.2,175.1 65.8,257 876.8,824.9 934.2,743 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
42
src/assets/img/icon-glutenfree_old.svg
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="glutenfree" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#66CCCC;}
|
||||
.st1{fill:#E8BB00;stroke:#000000;stroke-width:16;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<circle id="back" cx="500" cy="500" r="500"/>
|
||||
<circle id="color" class="st0" cx="500" cy="500" r="400"/>
|
||||
<g id="grain">
|
||||
<path class="st1" d="M491.7,256.5c-19.8-24.2-23.6-43.4-23.4-53.4c0.7-46.2,15.9-83.4,34-82.9s32.2,38.4,31.5,84.6
|
||||
c-0.1,8.4-2.4,28.2-24.5,51.7"/>
|
||||
<path class="st1" d="M488.7,324.8c-16.3-5.5-38.9-19.4-57.8-38.4c-32.6-32.8-48.5-69.6-35.5-82.2c13-12.6,49.9,3.8,82.5,36.6
|
||||
c7.9,8,14.9,16.2,20.7,24.3"/>
|
||||
|
||||
<ellipse transform="matrix(0.7135 -0.7006 0.7006 0.7135 -28.7219 457.6503)" class="st1" cx="545.3" cy="263.9" rx="83.8" ry="32.7"/>
|
||||
<path class="st1" d="M500.1,353.9c6.3-9,14.2-18.1,23.2-27c33.1-32.3,70-48,82.5-34.9s-4.2,49.9-37.3,82.2
|
||||
c-24.2,23.7-51.8,38.6-69.1,39.7"/>
|
||||
<ellipse transform="matrix(0.708 -0.7062 0.7062 0.708 -114.4428 423.593)" class="st1" cx="455" cy="350.2" rx="32.7" ry="83.8"/>
|
||||
<path class="st1" d="M488.6,497.3c-16.3-5.5-38.9-19.4-57.8-38.4c-32.6-32.8-48.5-69.6-35.5-82.2c13-12.6,49.9,3.8,82.5,36.6
|
||||
c7.9,8,14.9,16.2,20.7,24.3"/>
|
||||
|
||||
<ellipse transform="matrix(0.7135 -0.7006 0.7006 0.7135 -149.5211 507.0457)" class="st1" cx="545.3" cy="436.4" rx="83.8" ry="32.7"/>
|
||||
<path class="st1" d="M499.9,526.4c6.3-9,14.2-18.1,23.2-27c33.1-32.3,70-48,82.5-34.9s-4.2,49.9-37.3,82.2
|
||||
c-24.2,23.7-51.8,38.6-69.1,39.7"/>
|
||||
|
||||
<ellipse transform="matrix(0.708 -0.7062 0.7062 0.708 -236.4149 473.8377)" class="st1" cx="454.8" cy="522.8" rx="32.7" ry="83.8"/>
|
||||
<path class="st1" d="M487.9,670.7c-16.3-5.5-38.9-19.4-57.8-38.4c-32.6-32.8-48.5-69.6-35.5-82.2s49.9,3.8,82.5,36.6
|
||||
c7.9,8,14.9,16.2,20.7,24.3"/>
|
||||
|
||||
<ellipse transform="matrix(0.7135 -0.7006 0.7006 0.7135 -271.2175 556.2379)" class="st1" cx="544.6" cy="609.8" rx="83.8" ry="32.7"/>
|
||||
<path class="st1" d="M499.2,699.8c6.3-9,14.2-18.1,23.2-27c33.1-32.3,70-48,82.5-34.9s-4.2,49.9-37.3,82.2
|
||||
c-24.2,23.7-51.8,38.6-69.1,39.7"/>
|
||||
|
||||
<ellipse transform="matrix(0.708 -0.7062 0.7062 0.708 -358.9856 523.9573)" class="st1" cx="454.1" cy="696.1" rx="32.7" ry="83.8"/>
|
||||
</g>
|
||||
<path id="stick" d="M505,884h-10c-11,0-20-9-20-20V754h50v110C525,875,516,884,505,884z"/>
|
||||
<g id="cross">
|
||||
<polygon points="123.2,175.1 65.8,257 876.8,824.9 934.2,743 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
19
src/assets/img/icon-raw.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="raw" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#993300;}
|
||||
.st1{fill:#FFD3C0;}
|
||||
</style>
|
||||
<circle id="back" cx="500" cy="500" r="500"/>
|
||||
<circle id="color" class="st0" cx="500" cy="500" r="400"/>
|
||||
<g id="RAW">
|
||||
<polygon id="w" class="st1" points="787,350 771.2,500 742.7,500 747.4,455 677.9,455 682.7,500 654.1,500 638.4,350 578,350
|
||||
609.6,650 669.9,650 660.4,560 689,560 693.7,605 731.8,605 736.5,560 764.9,560 755.4,650 815.8,650 847.3,350 "/>
|
||||
<path id="a" class="st1" d="M566,560L566,560l-6.3-60l0,0l-9.5-90l0,0l-6.3-60l0,0l0,0h-60.3l0,0h-16.7l0,0h-60.3l0,0l0,0l-6.3,60
|
||||
l0,0l-9.5,90l0,0l-6.3,60l0,0l-9.5,90h60.3l9.5-90h60.8l9.5,90h60.3L566,560z M451.2,500l9.5-90H490l9.5,90H451.2z"/>
|
||||
<path id="r" class="st1" d="M169.9,350l-0.1,301.1L230,650v-90l0,0c62.5,0,60,0,60,45v45h60v-45c0-45,0-75-45-75c45,0,45-30,45-75
|
||||
c0-105,0-105-120-105C212.5,350,169.9,350,169.9,350z M230,500c0-16,0-75.3,0-90c60,0,60,0,60,45S290,500,230,500z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
15
src/assets/img/icon-vegan.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="vegan" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#41A748;}
|
||||
</style>
|
||||
<circle id="back" cx="500" cy="500" r="500"/>
|
||||
<circle id="color" class="st0" cx="500" cy="500" r="400"/>
|
||||
<path id="v" d="M433.3,676.4c-32.9-86.9-65.2-170.8-133.3-236.9c-38.8-37.6-104-94.5-161-101.6c81.9,69.1,150.2,154.8,198.3,250.8
|
||||
c45.8,91.5,81.9,210,81.9,313.2h97.5c-0.8-73.5,10.6-149.3,36.4-218.2c18.7-50,44.9-111.8,86.1-147.6c46-1.7,99.3-5.3,124.2-49.5
|
||||
c18.9-33.7,13.2-72.6,23.4-108.5c10-35.2,34.6-60.8,50.2-93c-21.6,21.9-53.1,44.4-82.1,55.5c-27.2,10.4-57.3,6.6-85.7,10.2
|
||||
c-97.9,12.4-119.4,96.8-91.4,181.2c16.9-79.3,45.6-123,130.3-130.3c-8.4,30.5-47.8,55-67.7,77.7c-31.8,36.3-60.1,66-86.9,115.9
|
||||
c-26.8,50-32.7,75.3-54.4,108.9C482.5,728.3,449.2,726.1,433.3,676.4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
8
src/assets/img/icon-yield.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="fork_knife" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
|
||||
<path id="path" d="M210.3,0l-26.8,366.6c-9.8,53.6,118.8,100.8,116.6,155.4l-6.6,411.4c-2.6,66.6,56.6,66.6,56.6,66.6
|
||||
s59.2,0,56.6-66.6L400.4,522c-2-54.6,115.6-100.8,116.6-155.4L490.4,0H457l-3.4,306.6l-70,13.4L367,0h-33.4l-16.6,320l-70-13.4
|
||||
L243.5,0H210.3z M817,0C655,0,627,166,627,500c0,58,56.8,66.6,84,66.6l-5.4,366.6c-6,66.4,60.6,66.6,60.6,66.6s50.6,0,50.6-66.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 730 B |
@@ -1,10 +0,0 @@
|
||||
(function(){// show at least 2 columns on mobile devices
|
||||
var viewport = document.head.querySelector("meta[name=viewport]");
|
||||
if (viewport && screen.width < 485) {
|
||||
document.head.removeChild(viewport);
|
||||
var x = document.createElement("meta");
|
||||
x.setAttribute("name", "viewport");
|
||||
x.setAttribute("content", "width=485");
|
||||
document.head.appendChild(x);
|
||||
}
|
||||
})();
|
||||
9
src/assets/static/lozad.min.js
vendored
@@ -1,9 +0,0 @@
|
||||
/*! lozad.js - v1.9.0 - 2019-02-09
|
||||
* https://github.com/ApoorvSaxena/lozad.js
|
||||
* Copyright (c) 2019 Apoorv Saxena; Licensed MIT */
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.lozad=e()}(this,function(){"use strict";var g=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var o in r)Object.prototype.hasOwnProperty.call(r,o)&&(t[o]=r[o])}return t},n="undefined"!=typeof document&&document.documentMode,l={rootMargin:"0px",threshold:0,load:function(t){if("picture"===t.nodeName.toLowerCase()){var e=document.createElement("img");n&&t.getAttribute("data-iesrc")&&(e.src=t.getAttribute("data-iesrc")),t.getAttribute("data-alt")&&(e.alt=t.getAttribute("data-alt")),t.appendChild(e)}if("video"===t.nodeName.toLowerCase()&&!t.getAttribute("data-src")&&t.children){for(var r=t.children,o=void 0,a=0;a<=r.length-1;a++)(o=r[a].getAttribute("data-src"))&&(r[a].src=o);t.load()}t.getAttribute("data-src")&&(t.src=t.getAttribute("data-src")),t.getAttribute("data-srcset")&&t.setAttribute("srcset",t.getAttribute("data-srcset")),t.getAttribute("data-background-image")&&(t.style.backgroundImage="url('"+t.getAttribute("data-background-image")+"')"),t.getAttribute("data-toggle-class")&&t.classList.toggle(t.getAttribute("data-toggle-class"))},loaded:function(){}};
|
||||
/**
|
||||
* Detect IE browser
|
||||
* @const {boolean}
|
||||
* @private
|
||||
*/function f(t){t.setAttribute("data-loaded",!0)}var b=function(t){return"true"===t.getAttribute("data-loaded")};return function(){var r,o,a=0<arguments.length&&void 0!==arguments[0]?arguments[0]:".lozad",t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},e=g({},l,t),n=e.root,i=e.rootMargin,d=e.threshold,c=e.load,u=e.loaded,s=void 0;return window.IntersectionObserver&&(s=new IntersectionObserver((r=c,o=u,function(t,e){t.forEach(function(t){(0<t.intersectionRatio||t.isIntersecting)&&(e.unobserve(t.target),b(t.target)||(r(t.target),f(t.target),o(t.target)))})}),{root:n,rootMargin:i,threshold:d})),{observe:function(){for(var t=function(t){var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:document;return t instanceof Element?[t]:t instanceof NodeList?t:e.querySelectorAll(t)}(a,n),e=0;e<t.length;e++)b(t[e])||(s?s.observe(t[e]):(c(t[e]),f(t[e]),u(t[e])))},triggerLoad:function(t){b(t)||(c(t),f(t),u(t))},observer:s}}});
|
||||
39
src/assets/static/script.js
Normal file
@@ -0,0 +1,39 @@
|
||||
(function(){// main entry
|
||||
handleAppCache();
|
||||
updateViewport();
|
||||
})();
|
||||
function updateViewport() {// show at least 2 columns on mobile devices
|
||||
var viewport = document.head.querySelector("meta[name=viewport]");
|
||||
if (viewport && screen.width < 485) {
|
||||
document.head.removeChild(viewport);
|
||||
var x = document.createElement("meta");
|
||||
x.setAttribute("name", "viewport");
|
||||
x.setAttribute("content", "width=485");
|
||||
document.head.appendChild(x);
|
||||
}
|
||||
}
|
||||
function handleAppCache() {// update cache status icon
|
||||
var cache = window.applicationCache;
|
||||
if (cache) {
|
||||
cache.addEventListener('updateready', update);
|
||||
cache.addEventListener('cached', ready); // initial
|
||||
cache.addEventListener('noupdate', ready); // consecutive
|
||||
cache.addEventListener('downloading', busy);
|
||||
cache.addEventListener('obsolete', failed);
|
||||
if(cache.status===cache.UPDATEREADY){update()}
|
||||
if(cache.status===cache.IDLE){window.onload=(event)=>{ready()};}
|
||||
function update(){ready(); cache.swapCache(); window.location.reload()}
|
||||
function busy(){document.getElementById('cache-status').style='background:darkorange'}
|
||||
function ready(){document.getElementById('cache-status').style='background:forestgreen'}
|
||||
function failed(){document.getElementById('cache-status').style='background:red'}
|
||||
}
|
||||
}
|
||||
/*! lozad.js - v1.9.0 - 2019-02-09
|
||||
* https://github.com/ApoorvSaxena/lozad.js
|
||||
* Copyright (c) 2019 Apoorv Saxena; Licensed MIT */
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.lozad=e()}(this,function(){"use strict";var g=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var o in r)Object.prototype.hasOwnProperty.call(r,o)&&(t[o]=r[o])}return t},n="undefined"!=typeof document&&document.documentMode,l={rootMargin:"0px",threshold:0,load:function(t){if("picture"===t.nodeName.toLowerCase()){var e=document.createElement("img");n&&t.getAttribute("data-iesrc")&&(e.src=t.getAttribute("data-iesrc")),t.getAttribute("data-alt")&&(e.alt=t.getAttribute("data-alt")),t.appendChild(e)}if("video"===t.nodeName.toLowerCase()&&!t.getAttribute("data-src")&&t.children){for(var r=t.children,o=void 0,a=0;a<=r.length-1;a++)(o=r[a].getAttribute("data-src"))&&(r[a].src=o);t.load()}t.getAttribute("data-src")&&(t.src=t.getAttribute("data-src")),t.getAttribute("data-srcset")&&t.setAttribute("srcset",t.getAttribute("data-srcset")),t.getAttribute("data-background-image")&&(t.style.backgroundImage="url('"+t.getAttribute("data-background-image")+"')"),t.getAttribute("data-toggle-class")&&t.classList.toggle(t.getAttribute("data-toggle-class"))},loaded:function(){}};
|
||||
/**
|
||||
* Detect IE browser
|
||||
* @const {boolean}
|
||||
* @private
|
||||
*/function f(t){t.setAttribute("data-loaded",!0)}var b=function(t){return"true"===t.getAttribute("data-loaded")};return function(){var r,o,a=0<arguments.length&&void 0!==arguments[0]?arguments[0]:".lozad",t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},e=g({},l,t),n=e.root,i=e.rootMargin,d=e.threshold,c=e.load,u=e.loaded,s=void 0;return window.IntersectionObserver&&(s=new IntersectionObserver((r=c,o=u,function(t,e){t.forEach(function(t){(0<t.intersectionRatio||t.isIntersecting)&&(e.unobserve(t.target),b(t.target)||(r(t.target),f(t.target),o(t.target)))})}),{root:n,rootMargin:i,threshold:d})),{observe:function(){for(var t=function(t){var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:document;return t instanceof Element?[t]:t instanceof NodeList?t:e.querySelectorAll(t)}(a,n),e=0;e<t.length;e++)b(t[e])||(s?s.observe(t[e]):(c(t[e]),f(t[e]),u(t[e])))},triggerLoad:function(t){b(t)||(c(t),f(t),u(t))},observer:s}}});
|
||||
@@ -12,11 +12,22 @@
|
||||
.dark-red { color: var(--cRed2) }
|
||||
.light-red { color: var(--cRed3) }
|
||||
.mrgTopMd { margin-top: 0.7em }
|
||||
.v-scroll { overflow-x: scroll; white-space: nowrap; -webkit-overflow-scrolling: touch }
|
||||
.v-scroll { overflow-y: hidden; white-space: nowrap; -webkit-overflow-scrolling: touch }
|
||||
|
||||
ul.no-bullets { padding: unset; margin: unset; list-style: none }
|
||||
ul.li-lg-space li { padding-bottom: 0.3em }
|
||||
|
||||
i.icon:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
width: 1em; height: 1em
|
||||
}
|
||||
i.icon.gf:before {background-image: url('../img/icon-glutenfree.svg')}
|
||||
i.icon.raw:before {background-image: url('../img/icon-raw.svg')}
|
||||
i.icon.veg:before {background-image: url('../img/icon-vegan.svg')}
|
||||
i.icon.yield:before {background-image: url('../img/icon-yield.svg.svg')}
|
||||
|
||||
/*
|
||||
* General
|
||||
*/
|
||||
@@ -24,11 +35,15 @@ a { color: var(--cRed2); text-decoration: none }
|
||||
a:hover { color: var(--cRed1) }
|
||||
body {
|
||||
font-family: 'Verdana', sans-serif;
|
||||
background-color: var(--cBg3);
|
||||
color: var(--cTxt);
|
||||
margin: unset;
|
||||
background: var(--cBg3); color: var(--cTxt);
|
||||
}
|
||||
header #logo { font-size: 42px; display: block; margin-bottom: 15px }
|
||||
#logo { font-size: 42px; display: block; margin-bottom: 15px }
|
||||
#cache-status {
|
||||
position: absolute; right: 10px; top: 10px;
|
||||
border-radius: 50%; width: 10px; height: 10px;
|
||||
}
|
||||
header { position: relative }
|
||||
header a { color: var(--cTxt) }
|
||||
header, h1 { text-align: center }
|
||||
header, footer, .page {
|
||||
@@ -43,33 +58,32 @@ nav ul li { display: inline-block; margin: 0.1em 0.5em }
|
||||
nav ul li a.active { text-decoration: overline }
|
||||
footer table { margin: -10px 0 }
|
||||
|
||||
@media(max-width: 485px) { body { font-size: 1.4em } }
|
||||
@media screen and (max-width: 485px) { body { font-size: 1.4em } }
|
||||
@media print {
|
||||
header, footer { display: none }
|
||||
body, .page { background-color: #FFF }
|
||||
body, .page { background: #FFF }
|
||||
}
|
||||
|
||||
/*
|
||||
* Components
|
||||
* Parts & Components
|
||||
*/
|
||||
.tags { display: flex; flex-wrap: wrap; justify-content: center }
|
||||
.tags > * {
|
||||
background-color: #FFF;
|
||||
background: #FFF;
|
||||
border: 1px solid var(--cRed1);
|
||||
border-radius: 0.3em;
|
||||
padding: 0.3em 0.5em;
|
||||
margin: 0.2em;
|
||||
}
|
||||
.tags a:hover, .tags .active, a:hover .recipe-tile {
|
||||
background-color: var(--cRed1);
|
||||
color: #FFF;
|
||||
.tags a:hover, .tags .active, a:hover .recipe-tile, .recipe-tile .hover .time {
|
||||
background: var(--cRed1); color: #FFF;
|
||||
}
|
||||
header .tags { max-width: 600px; margin: 0 auto }
|
||||
.cluster dt { margin-top: 0.7em; font-size: 1.6em }
|
||||
.cluster dd { margin-top: 0.4em }
|
||||
.cluster dd a { white-space: nowrap }
|
||||
|
||||
@media(max-width: 500px) {
|
||||
@media(max-width: 32em) {
|
||||
.cluster dd { margin-left: 0 }
|
||||
.cluster dd a { white-space: unset }
|
||||
}
|
||||
@@ -77,25 +91,31 @@ header .tags { max-width: 600px; margin: 0 auto }
|
||||
/*
|
||||
* Grid overview
|
||||
*/
|
||||
.pagination { text-align: center; margin-top: 1em; }
|
||||
.pagination { text-align: center; margin-top: 1em }
|
||||
.recipe-tile {
|
||||
background-color: var(--cBg2);
|
||||
color: var(--cTxt);
|
||||
background: var(--cBg2); color: var(--cTxt);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 6px;
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
.recipe-tile .img-placeholder {
|
||||
background-color: #777;
|
||||
color: var(--cBg2);
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
.recipe-tile .placeholder {
|
||||
font: bold 25px/150px 'Courier New', monospace;
|
||||
background: #777; color: var(--cBg2);
|
||||
}
|
||||
a:hover .recipe-tile img { mix-blend-mode: overlay }
|
||||
.recipe-tile p { height: 2.6em; margin: 0.3em 10px; overflow-y: auto }
|
||||
.recipe-tile p { height: 2.5em; margin: 0.3em 10px; overflow-y: auto }
|
||||
.recipe-tile img, .recipe-tile .overlay { display: block; width: 200px; height: 150px }
|
||||
.recipe-tile .overlay { position: absolute }
|
||||
.recipe-tile .icon-bar { position: absolute; bottom: 3px; right: 3px }
|
||||
.recipe-tile .icon-bar i.icon { margin-left: 2px; font-size: 28px }
|
||||
a:hover .recipe-tile .hover { display: block; background: #0006 }
|
||||
.recipe-tile .hover { display: none; height: 100% }
|
||||
.recipe-tile .hover .time {
|
||||
position: relative; top: -1.25em;
|
||||
font: bold 1.1em/1.3em monospace;
|
||||
}
|
||||
/* snap to column grid */
|
||||
.tile-grid { width: fit-content; max-width: 1060px; margin: 0 auto }
|
||||
.latest .tile-grid { max-width: 636px }
|
||||
/* max-width = prev + 2*30; width = x * (200 + 2*6); */
|
||||
@@ -107,9 +127,9 @@ a:hover .recipe-tile img { mix-blend-mode: overlay }
|
||||
@media print and (orientation: portrait) { .tile-grid { width: 636px } }
|
||||
@media print and (orientation: landscape) { .tile-grid { width: 1060px } }
|
||||
@media print {
|
||||
a:hover .recipe-tile img { mix-blend-mode: unset !important }
|
||||
.recipe-tile, .recipe-tile .img-placeholder, a:hover .recipe-tile {
|
||||
background-color: #FFF; color: #000 }
|
||||
.recipe-tile .overlay { display: none }
|
||||
.recipe-tile, a:hover .recipe-tile, .recipe-tile .placeholder {
|
||||
background: #FFF; color: #000 }
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -120,16 +140,19 @@ a:hover .recipe-tile img { mix-blend-mode: overlay }
|
||||
.recipe #source { margin-left: -1em; margin-bottom: -1.5em }
|
||||
.recipe #metrics { float: right; margin: 0 0 15px 25px; max-width: 180px }
|
||||
.recipe #metrics > * { text-indent: -20px; margin-left: 20px; padding-top: 0.3em }
|
||||
.recipe #ingredients { float: left; margin: 0 25px 15px 0; max-width: 300px }
|
||||
.recipe #ingredients { float: left; margin: 0 30px 15px 0; max-width: 300px }
|
||||
.recipe #ingredients a { line-height: 1em }
|
||||
.recipe #directions ul { list-style-type: circle }
|
||||
.recipe #directions dl dt { color: var(--cRed2); font-weight: bold }
|
||||
.recipe #directions dl dd { margin-bottom: 1em }
|
||||
/* Colored, 3-part, difficulty bar */
|
||||
.difficulty.easy > div:nth-child(1) { background-color: #3C3 }
|
||||
.difficulty.easy > div:nth-child(1) { background: #3C3 }
|
||||
.difficulty.medium > div:nth-child(1),
|
||||
.difficulty.medium > div:nth-child(2) { background-color: #FC3 }
|
||||
.difficulty.medium > div:nth-child(2) { background: #FC3 }
|
||||
.difficulty.hard > div:nth-child(1),
|
||||
.difficulty.hard > div:nth-child(2),
|
||||
.difficulty.hard > div:nth-child(3) { background-color: #F30 }
|
||||
.difficulty > * { vertical-align: middle; }
|
||||
.difficulty.hard > div:nth-child(3) { background: #F30 }
|
||||
.difficulty > * { vertical-align: middle }
|
||||
.difficulty > div:nth-child(1) { border-radius: 50% 0 0 50% }
|
||||
.difficulty > div:nth-child(3) { border-radius: 0 50% 50% 0 }
|
||||
.difficulty > div {
|
||||
@@ -138,16 +161,26 @@ a:hover .recipe-tile img { mix-blend-mode: overlay }
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.recipe #img-carousel img { height: auto; width: 100%; padding: 0 }
|
||||
.recipe #metrics { float: unset; max-width: fit-content; margin: 20px auto }
|
||||
@media screen and (max-width: 50em) {
|
||||
.recipe h1 { margin-bottom: 0 }
|
||||
.recipe #img-carousel { height: calc(75vw - 2*50px) }
|
||||
.recipe #img-carousel img { height: 100%; max-width: 100%; padding: 0 }
|
||||
.recipe #metrics { float: unset; max-width: max-content; margin: .5em auto 2em }
|
||||
.recipe #metrics > *:not(:first-child) { margin-right: -100vw; max-width: 50vw }
|
||||
}
|
||||
@media screen and (max-width: 40em), print and (orientation: portrait) {
|
||||
.recipe #ingredients { float: unset; max-width: 100% }
|
||||
}
|
||||
@media screen and (max-width: 32em) {
|
||||
.recipe #img-carousel { padding: 0 10px; height: calc(75vw - 2*10px) }
|
||||
}
|
||||
@media(max-width: 600px) { .recipe #ingredients { float: unset; max-width: 100% } }
|
||||
@media(max-width: 500px) { .recipe #img-carousel { padding: 0 10px } }
|
||||
|
||||
@media print { #source, #rating, .difficulty { display: none } }
|
||||
@media print and (orientation: landscape) { #img-carousel img { display: none } }
|
||||
@media print {
|
||||
h1 { margin-top:0 }
|
||||
#source, #rating, .difficulty, #img-carousel { display: none }
|
||||
}
|
||||
/*@media print and (orientation: landscape) { #img-carousel img { display: none } }
|
||||
@media print and (orientation: portrait) {
|
||||
#img-carousel img:not(:first-child) { display: none }
|
||||
.recipe #metrics { float: unset; padding-bottom: 1em }
|
||||
}
|
||||
}*/
|
||||
|
||||
2
src/configs/force-update.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
enabled = yes
|
||||
endswith = .appcache
|
||||
3
src/content/app.appcache/contents.lr
Normal file
@@ -0,0 +1,3 @@
|
||||
_template: cache.manifest
|
||||
---
|
||||
_model: none
|
||||
@@ -10,4 +10,4 @@ xdata:
|
||||
120
|
||||
180
|
||||
360
|
||||
9999
|
||||
1440
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Vanilla cut-out cookies
|
||||
---
|
||||
yield: 26-28 cookies
|
||||
---
|
||||
ingredients:
|
||||
|
||||
1/2 cup non-dairy butter, or coconut oil
|
||||
1/2 cup maple syrup
|
||||
1/2 tsp vanilla extract
|
||||
1/4 tsp vanilla bean
|
||||
dash salt
|
||||
2 1/4 cups gluten-free flour blend
|
||||
---
|
||||
directions:
|
||||
|
||||
1) Preheat oven to 350°F. Line 2 cookie sheets with parchment paper. Prepare a rolling area with two additional sheets of parchment paper for that, and have your cookie cutter(s) handy.
|
||||
|
||||
2) Place butter in a large mixing bowl and whip it with a mixer until it’s creamy. Add sweetener, vanilla extract and bean, and salt and mix once again to combine. Add in flour and use a wooden spoon to mix. Then get in there with your hands and mix everything together by working the dough until you can shape it into a ball {note: as depending on the flour mix you use there may be a slight variance, know that the consistency of the dough should not be sticky but should press together when pinched — be sure to knead it really well first for some time — if it’s a little sticky, add a little more flour (try 1-2 tbsp); if it’s a little dry add a little more sweetener (try 1 tbsp)}. Shape the dough into 2 balls and then flatten each into a disk.
|
||||
|
||||
3) Roll out one of the dough balls between two sheets of parchment paper to ¼” thickness {or thinner or thicker depending on how you want your cookies to turn out}. Use a cookie cutter to cut out the cookies. Carefully transfer to a prepared cookie sheets, spacing them ½” apart {they won’t spread as they bake}. Gather up any dough scraps and repeat until all dough is used up. Repeat the process with the second dough ball.
|
||||
|
||||
4) Bake in a pre-heated oven for approximately 11-13 minutes, until the edges just begin to become golden. Remove from oven and place on a cooling rack. {Note: cookies will harden a little within minutes of cooling, so don’t overbake}. Allow the cookies to cool for 10 minutes and enjoy!
|
||||
|
||||
__Note:__
|
||||
You can make your own glutenfree flour blend by combining:
|
||||
1 cup brown rice flour, ¾ cup tapioca starch, ½ cup sweet rice flour, ½ tsp guar gum
|
||||
@@ -1 +1 @@
|
||||
measures: EL, TL, kg, g, L, dl, cl, ml, cm, Msp, Prise, Tasse, Tassen, Dose, Dosen, kleine, Bund, Packung, Packungen, Scheibe, Scheiben, Schuss, Stängel, Tropfen, Tube
|
||||
measures: EL TL kg g L dl cl ml cm Msp Prise Tasse Tassen Dose Dosen kleine große Bund Packung Packungen Scheibe Scheiben Schuss Stängel Tropfen Tube
|
||||
@@ -1 +1 @@
|
||||
measures: kg, g, L, dl, cl, ml, oz, lb, pt, qt, cm, tsp, tbsp, c, cup, cups, pkg, pck, drop, drops, tube, dash, dashes, ounce, ounces, small, medium, large, box, can, pinch, tin, clove, cloves
|
||||
measures: kg g L dl cl ml oz lb pt qt cm inch tsp tbsp c cup cups pkg pkgs drop drops tube dash dashes ounce ounces small medium large box can tin pinch pinches clove cloves stick sticks bunch handful splash splashes stem stems slice slices
|
||||
@@ -5,5 +5,3 @@ _hidden: yes
|
||||
replace_frac: yes
|
||||
---
|
||||
replace_temp: yes
|
||||
---
|
||||
show_empty_tags: no
|
||||
|
||||
1
src/content/tags/cheese/contents+de.lr
Normal file
@@ -0,0 +1 @@
|
||||
name: Käse
|
||||
1
src/content/tags/cheese/contents.lr
Normal file
@@ -0,0 +1 @@
|
||||
name: Cheese
|
||||
@@ -1 +0,0 @@
|
||||
name: Schokolade
|
||||
@@ -1 +0,0 @@
|
||||
name: Chocolate
|
||||
@@ -1 +1 @@
|
||||
name: Kekse
|
||||
name: Keks
|
||||
@@ -1 +1 @@
|
||||
name: Cookies
|
||||
name: Cookie
|
||||
1
src/content/tags/crust/contents+de.lr
Normal file
@@ -0,0 +1 @@
|
||||
name: Kuchenboden
|
||||
1
src/content/tags/crust/contents.lr
Normal file
@@ -0,0 +1 @@
|
||||
name: Crust
|
||||
@@ -1 +0,0 @@
|
||||
name: Dressing
|
||||
@@ -1 +1 @@
|
||||
name: Drinks
|
||||
name: Drink
|
||||
@@ -1 +1 @@
|
||||
name: Glutenfree
|
||||
name: Gluten-free
|
||||
1
src/content/tags/muffins/contents.lr
Normal file
@@ -0,0 +1 @@
|
||||
name: Muffin
|
||||
@@ -1 +0,0 @@
|
||||
name: Soße
|
||||
@@ -1 +0,0 @@
|
||||
name: Sauce
|
||||
1
src/content/tags/savory/contents+de.lr
Normal file
@@ -0,0 +1 @@
|
||||
name: Herzhaft
|
||||
1
src/content/tags/savory/contents.lr
Normal file
@@ -0,0 +1 @@
|
||||
name: Savory
|
||||
@@ -1 +1 @@
|
||||
name: Süßes
|
||||
name: Süß
|
||||
@@ -1,11 +1,5 @@
|
||||
[duration]
|
||||
label = Zeit
|
||||
day = Tag
|
||||
days = Tage
|
||||
hour = Std
|
||||
hours = Std
|
||||
min = Min
|
||||
mins = Min
|
||||
|
||||
[yield]
|
||||
label = Menge
|
||||
@@ -17,6 +11,9 @@ easy = Einfach
|
||||
medium = Mittel
|
||||
hard = Schwer
|
||||
|
||||
[ingredients]
|
||||
recipeLink = ⤳Rezept
|
||||
|
||||
[title]
|
||||
latest = Zuletzt hinzugefügt
|
||||
all_recipes = Alle Rezepte
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
[duration]
|
||||
label = Time
|
||||
day = day
|
||||
days = days
|
||||
hour = hour
|
||||
hours = hours
|
||||
min = minutes
|
||||
mins = min
|
||||
|
||||
[yield]
|
||||
label = Yield
|
||||
@@ -17,6 +11,9 @@ easy = Easy
|
||||
medium = Medium
|
||||
hard = Hard
|
||||
|
||||
[ingredients]
|
||||
recipeLink = ⤳recipe
|
||||
|
||||
[title]
|
||||
latest = Latest recipes
|
||||
all_recipes = All recipes
|
||||
|
||||
@@ -21,9 +21,8 @@ size = large
|
||||
[fields.time]
|
||||
label = Time / Zeit
|
||||
width = 1/8
|
||||
type = select
|
||||
choices = 5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120, 150, 180, 240, 360, 480, 720, 1440
|
||||
choice_labels = 5m, 10m, 15m, 20m, 30m, 45m, 1h, 1h 15m, 1h 30m, 1h 45m, 2h, 2h 30m, 3h, 4h, 6h, 8h, 12h, 24h
|
||||
type = integer
|
||||
addon_label = min
|
||||
|
||||
[fields.difficulty]
|
||||
label = Difficulty
|
||||
|
||||
@@ -12,10 +12,10 @@ enabled = no
|
||||
|
||||
[fields.measures]
|
||||
label = Measures
|
||||
description = Comma separated list
|
||||
description = Space separated list
|
||||
width = 3/5
|
||||
type = text
|
||||
default = kg, g, L, dl, cl, ml, oz, lb, pt, qt, cm, tsp, tbsp, c, cup, cups, pkg, pck
|
||||
default = kg g L dl cl ml oz lb pt qt cm tsp tbsp c cup cups pkg pck
|
||||
|
||||
[fields.replace_frac]
|
||||
label = Replace 1/2 with ½, ⅔, et.c
|
||||
@@ -26,9 +26,3 @@ type = boolean
|
||||
label = Replace °C/°F with ℃/℉
|
||||
width = 1/5
|
||||
type = boolean
|
||||
|
||||
[fields.show_empty_tags]
|
||||
label = Show empty tags
|
||||
description = Even if no recipes exist in that category
|
||||
width = 1/4
|
||||
type = boolean
|
||||
|
||||
1
src/packages/force-update
Submodule
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from lektor.pluginsystem import Plugin
|
||||
from lektor.pluginsystem import Plugin, get_plugin
|
||||
from lektor.databags import Databags
|
||||
from markupsafe import Markup
|
||||
from datetime import datetime
|
||||
import unicodedata
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# -------
|
||||
# Sorting
|
||||
@@ -39,19 +39,16 @@ def noUmlaut(text):
|
||||
return str(text)
|
||||
|
||||
|
||||
def pluralize(n, single, multi):
|
||||
if n == 0:
|
||||
return ''
|
||||
return u'{} {}'.format(n, single if n == 1 else multi)
|
||||
|
||||
|
||||
def replaceFractions(txt):
|
||||
res = ''
|
||||
for x in txt.split():
|
||||
try:
|
||||
i = ['1/2', '1/3', '2/3', '1/4', '3/4', '1/8', '-'].index(x)
|
||||
res += [u'½', u'⅓', u'⅔', u'¼', u'¾', u'⅛', u' - '][i]
|
||||
i = ['1/2', '1/3', '2/3', '1/4', '3/4', '1/8'].index(x)
|
||||
res += [u'½', u'⅓', u'⅔', u'¼', u'¾', u'⅛'][i]
|
||||
except ValueError:
|
||||
if x in u'-–—':
|
||||
res += u' - '
|
||||
else:
|
||||
res += ' ' + x
|
||||
return res.lstrip()
|
||||
|
||||
@@ -77,24 +74,6 @@ def updateSet_if(dic, parent, parentkey, value):
|
||||
dic[key] = set()
|
||||
dic[key].add(value)
|
||||
|
||||
|
||||
def updateSet_addMultiple(dic, key, others):
|
||||
try:
|
||||
dic[key]
|
||||
except KeyError:
|
||||
dic[key] = set()
|
||||
dic[key].update(others)
|
||||
|
||||
|
||||
def findCluster(key, clusterList=[30, 60, 120]):
|
||||
key = int(key) if key else 0
|
||||
if key > 0:
|
||||
for cluster in clusterList:
|
||||
if key < cluster:
|
||||
key = cluster
|
||||
break
|
||||
return key
|
||||
|
||||
# --------------------
|
||||
# Ingredient splitting
|
||||
|
||||
@@ -105,13 +84,14 @@ def splitIngredientLine(line):
|
||||
indices = [0, len(line)]
|
||||
for i, char in enumerate(line):
|
||||
if char.isspace():
|
||||
if capture:
|
||||
capture = False
|
||||
indices[state] = i
|
||||
state += 1
|
||||
continue
|
||||
elif capture:
|
||||
continue
|
||||
elif state == 1 and char in '0123456789-.,':
|
||||
elif state == 1 and char in u'0123456789-–—.,':
|
||||
state -= 1
|
||||
elif state > 1:
|
||||
break
|
||||
@@ -127,6 +107,9 @@ def parseIngredientLine(line, measureList=[], rep_frac=False):
|
||||
measure = line[idx[0]:idx[1]].lstrip()
|
||||
if measure.lower() in measureList:
|
||||
name = line[idx[1]:].lstrip()
|
||||
# if name.startswith('of '):
|
||||
# measure += ' of'
|
||||
# name = name[3:]
|
||||
else:
|
||||
measure = ''
|
||||
name = line[idx[0]:].lstrip()
|
||||
@@ -136,19 +119,18 @@ def parseIngredientLine(line, measureList=[], rep_frac=False):
|
||||
name, note = [x.strip() for x in name_note]
|
||||
return {'value': val, 'measure': measure, 'name': name, 'note': note}
|
||||
|
||||
# --------------------
|
||||
# Other Helper methods
|
||||
|
||||
|
||||
def groupByMergeCluster(dic, arr=[30, 60, 120], reverse=False):
|
||||
arr = sorted([int(x) for x in arr])
|
||||
groups = dict()
|
||||
for key, recipes in dic:
|
||||
key = findCluster(key, arr)
|
||||
if key == 0 and not reverse:
|
||||
key = ''
|
||||
updateSet_addMultiple(groups, key, recipes)
|
||||
return sorted(groups.items(), reverse=bool(reverse))
|
||||
def replace_atref_urls(text, label=None):
|
||||
if '@' not in text:
|
||||
return text
|
||||
result = list()
|
||||
for x in text.split():
|
||||
if x[0] == '@':
|
||||
x = x[1:]
|
||||
result.append(u'<a href="{}">{}</a>'.format(x, label or x))
|
||||
else:
|
||||
result.append(x)
|
||||
return Markup(' '.join(result))
|
||||
|
||||
# ----------------
|
||||
# Main entry point
|
||||
@@ -157,68 +139,56 @@ def groupByMergeCluster(dic, arr=[30, 60, 120], reverse=False):
|
||||
class HelperPlugin(Plugin):
|
||||
name = u'Helper'
|
||||
description = u'Some helper methods, filters, and templates.'
|
||||
alt = None
|
||||
availableTags = set()
|
||||
buildTime = None
|
||||
settings = dict()
|
||||
translations = dict()
|
||||
|
||||
# -----------
|
||||
# Event hooks
|
||||
# -----------
|
||||
|
||||
def on_before_build_all(self, builder, **extra):
|
||||
# display only tags that contain at least one recipe
|
||||
def processCLI(self, extra_flags):
|
||||
useCache = bool(extra_flags.get('ENABLE_APPCACHE'))
|
||||
plugin = get_plugin('force-update', self.env)
|
||||
if plugin.enabled and not useCache:
|
||||
plugin.enabled = False
|
||||
print('AppCache: ' + ('ENABLED' if useCache else 'DISABLED'))
|
||||
self.env.jinja_env.globals['ENABLE_APPCACHE'] = useCache
|
||||
|
||||
def processSettings(self):
|
||||
bag = Databags(self.env)
|
||||
pad = self.env.new_pad()
|
||||
for r in pad.query('recipes'):
|
||||
self.availableTags.update(r['tags'])
|
||||
for alt in self.env.load_config().iter_alternatives():
|
||||
set = pad.get('settings', alt=alt)
|
||||
self.translations[alt] = bag.lookup('i18n+' + alt)
|
||||
self.settings[alt] = {
|
||||
'measures': set['measures'].lower().split(),
|
||||
'replFrac': set['replace_frac']
|
||||
}
|
||||
|
||||
def on_after_prune(self, builder, **extra):
|
||||
# redirect to /en/
|
||||
for file in ['index.html']:
|
||||
src_f = os.path.join(self.env.root_path, 'root', file)
|
||||
if os.path.exists(src_f):
|
||||
dst_f = os.path.join(builder.destination_path, file)
|
||||
with open(dst_f, 'wb') as df:
|
||||
with open(src_f, 'rb') as sf:
|
||||
shutil.copyfileobj(sf, df)
|
||||
def on_before_build_all(self, builder, **extra):
|
||||
build_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
print('Build time: ' + build_time)
|
||||
self.env.jinja_env.globals['DATE_NOW'] = build_time
|
||||
# update project settings once per build
|
||||
self.processCLI(getattr(builder, 'extra_flags'))
|
||||
self.processSettings()
|
||||
|
||||
def on_process_template_context(self, context, **extra):
|
||||
self.alt = context['alt']
|
||||
# def on_process_template_context(self, context, **extra):
|
||||
# pass
|
||||
|
||||
def on_setup_env(self, **extra):
|
||||
# self.env.load_config().iter_alternatives()
|
||||
# pad = self.env.new_pad()
|
||||
# pad.query('groupby', alt=alt)
|
||||
def localizeDic(alt, partA, partB=None):
|
||||
if alt not in self.translations:
|
||||
raise RuntimeError(
|
||||
'localize() expects first parameter to be an alternate')
|
||||
if partB is None:
|
||||
partA, partB = partA.split('.', 1)
|
||||
return self.translations[alt][partA][partB]
|
||||
|
||||
def localizeDic(key, subkey=None):
|
||||
bag = Databags(self.env).lookup('i18n+{}.{}'.format(self.alt, key))
|
||||
return bag[subkey] if subkey else bag
|
||||
|
||||
def to_duration(time, cluster=None):
|
||||
time = int(time) if time else 0
|
||||
if (time <= 0):
|
||||
return ''
|
||||
# Calls itself without cluster argument
|
||||
if cluster:
|
||||
cluster = [int(x) for x in cluster]
|
||||
idx = cluster.index(time)
|
||||
if idx == 0:
|
||||
return '<' + to_duration(time)
|
||||
timeA = to_duration(cluster[idx - 1])
|
||||
if idx + 1 >= len(cluster):
|
||||
return '>' + timeA
|
||||
else:
|
||||
return u'{} – {}'.format(timeA, to_duration(time))
|
||||
days = time // (60 * 24)
|
||||
time -= days * (60 * 24)
|
||||
L = localizeDic('duration')
|
||||
return ' '.join([
|
||||
pluralize(days, L['day'], L['days']),
|
||||
pluralize(time // 60, L['hour'], L['hours']),
|
||||
pluralize(time % 60, L['min'], L['mins'])]).strip()
|
||||
|
||||
def ingredientsForRecipe(recipe):
|
||||
set = self.env.new_pad().get('settings', alt=self.alt)
|
||||
meaList = [x.strip() for x in set['measures'].lower().split(',')]
|
||||
repFrac = set['replace_frac']
|
||||
def ingredientsForRecipe(recipe, alt='en'):
|
||||
meaList = self.settings[alt]['measures']
|
||||
repFrac = self.settings[alt]['replFrac']
|
||||
|
||||
for line in recipe['ingredients']:
|
||||
line = line.strip()
|
||||
@@ -229,23 +199,21 @@ class HelperPlugin(Plugin):
|
||||
else:
|
||||
yield parseIngredientLine(line, meaList, repFrac)
|
||||
|
||||
def groupByAttribute(recipeList, attribute):
|
||||
def groupByAttribute(recipeList, attribute, alt='en'):
|
||||
groups = dict()
|
||||
for recipe in recipeList:
|
||||
if attribute == 'ingredients':
|
||||
for ing in ingredientsForRecipe(recipe):
|
||||
for ing in ingredientsForRecipe(recipe, alt):
|
||||
updateSet_if(groups, ing, 'name', recipe)
|
||||
else:
|
||||
updateSet_if(groups, recipe, attribute, recipe)
|
||||
# groups[undefinedKey].update(groups.pop('_undefined'))
|
||||
return groups.items()
|
||||
|
||||
self.env.jinja_env.filters['duration'] = to_duration
|
||||
self.env.jinja_env.filters['rating'] = numFillWithText
|
||||
self.env.jinja_env.filters['replaceFractions'] = replaceFractions
|
||||
self.env.jinja_env.filters['enumIngredients'] = ingredientsForRecipe
|
||||
self.env.jinja_env.filters['replaceAtRefURLs'] = replace_atref_urls
|
||||
self.env.jinja_env.filters['groupByAttribute'] = groupByAttribute
|
||||
self.env.jinja_env.filters['groupSort'] = groupByDictSort
|
||||
self.env.jinja_env.filters['groupMergeCluster'] = groupByMergeCluster
|
||||
self.env.jinja_env.globals['localize'] = localizeDic
|
||||
self.env.jinja_env.globals['availableTags'] = self.availableTags
|
||||
|
||||
5
src/packages/time-duration/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
dist
|
||||
build
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.egg-info
|
||||
4
src/packages/time-duration/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Time Duration
|
||||
|
||||
This plugin converts integer numbers to a human readable duration.
|
||||
E.g. 90 -> 1 hour 30 minutes
|
||||
86
src/packages/time-duration/lektor_time_duration.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from lektor.pluginsystem import Plugin
|
||||
|
||||
durationLocale = {
|
||||
'de': {'day': 'Tag', 'hour': 'Std', 'min': 'Min',
|
||||
'days': 'Tage', 'hours': 'Std', 'mins': 'Min'},
|
||||
'en': {'day': 'day', 'hour': 'hour', 'min': 'min',
|
||||
'days': 'days', 'hours': 'hours', 'mins': 'min'}
|
||||
}
|
||||
|
||||
# -----------
|
||||
# Single Time
|
||||
|
||||
|
||||
def pluralize(n, single, multi):
|
||||
if n == 0:
|
||||
return ''
|
||||
return u'{} {}'.format(n, single if n == 1 else multi)
|
||||
|
||||
|
||||
def to_duration(time, alt='en'):
|
||||
time = int(time) if time else 0
|
||||
if (time <= 0):
|
||||
return ''
|
||||
days = time // (60 * 24)
|
||||
time -= days * (60 * 24)
|
||||
L = durationLocale[alt]
|
||||
return ' '.join([
|
||||
pluralize(days, L['day'], L['days']),
|
||||
pluralize(time // 60, L['hour'], L['hours']),
|
||||
pluralize(time % 60, L['min'], L['mins'])]).strip()
|
||||
|
||||
# ------------
|
||||
# Time Cluster
|
||||
|
||||
|
||||
def to_time_in_cluster(time, cluster, alt='en'):
|
||||
for idx, x in enumerate(cluster):
|
||||
x = int(x)
|
||||
if x == time:
|
||||
if idx == 0:
|
||||
timeB = to_duration(time, alt)
|
||||
return '<' + timeB
|
||||
else:
|
||||
timeA = to_duration(cluster[idx - 1], alt)
|
||||
timeB = to_duration(time - 1, alt)
|
||||
return u'{} – {}'.format(timeA, timeB)
|
||||
else:
|
||||
return '>' + to_duration(cluster[-1], alt)
|
||||
|
||||
|
||||
def find_in_cluster(key, clusterList=[30, 60, 120]):
|
||||
key = int(key) if key else 0
|
||||
if key > 0:
|
||||
for cluster in clusterList:
|
||||
if key < cluster:
|
||||
key = cluster
|
||||
break
|
||||
else:
|
||||
key = clusterList[-1] + 1
|
||||
return key
|
||||
|
||||
|
||||
def group_by_time_cluster(dic, arr=[30, 60, 120], reverse=False):
|
||||
arr = sorted([int(x) for x in arr])
|
||||
groups = dict()
|
||||
for key, recipes in dic:
|
||||
key = find_in_cluster(key, arr)
|
||||
if key == 0 and not reverse:
|
||||
key = ''
|
||||
try:
|
||||
groups[key]
|
||||
except KeyError:
|
||||
groups[key] = set()
|
||||
groups[key].update(recipes)
|
||||
return sorted(groups.items(), reverse=bool(reverse))
|
||||
|
||||
|
||||
class TimeDurationPlugin(Plugin):
|
||||
name = u'Time Duration'
|
||||
description = u'Convert int to duration. E.g., 90 -> "1hr 30min".'
|
||||
|
||||
def on_setup_env(self, **extra):
|
||||
self.env.jinja_env.filters['duration'] = to_duration
|
||||
self.env.jinja_env.filters['durationCluster'] = to_time_in_cluster
|
||||
self.env.jinja_env.filters['groupTimeCluster'] = group_by_time_cluster
|
||||
38
src/packages/time-duration/setup.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import ast
|
||||
import io
|
||||
import re
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with io.open('README.md', 'rt', encoding="utf8") as f:
|
||||
readme = f.read()
|
||||
|
||||
_description_re = re.compile(r'description\s+=\s+(?P<description>.*)')
|
||||
|
||||
with open('lektor_time_duration.py', 'rb') as f:
|
||||
description = str(ast.literal_eval(_description_re.search(
|
||||
f.read().decode('utf-8')).group(1)))
|
||||
|
||||
setup(
|
||||
author=u'relikd',
|
||||
author_email='oleg@relikd.de',
|
||||
description=description,
|
||||
keywords='Lektor plugin',
|
||||
license='MIT',
|
||||
long_description=readme,
|
||||
long_description_content_type='text/markdown',
|
||||
name='lektor-time-duration',
|
||||
packages=find_packages(),
|
||||
py_modules=['lektor_time_duration'],
|
||||
# url='[link to your repository]',
|
||||
version='0.1',
|
||||
classifiers=[
|
||||
'Framework :: Lektor',
|
||||
'Environment :: Plugins',
|
||||
],
|
||||
entry_points={
|
||||
'lektor.plugins': [
|
||||
'time-duration = lektor_time_duration:TimeDurationPlugin',
|
||||
]
|
||||
}
|
||||
)
|
||||
54
src/templates/cache.manifest
Normal file
@@ -0,0 +1,54 @@
|
||||
CACHE MANIFEST
|
||||
# Date build: {{ DATE_NOW }}
|
||||
|
||||
{%- macro _print_(items) -%}
|
||||
{%- for item in items -%}
|
||||
{{ item|replace('../', '', 1) }}
|
||||
{% endfor -%}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro _add_(list, item) -%}
|
||||
{{- list.append(item|url) or pass -}}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- set root = site.get('/', this.alt) -%}
|
||||
|
||||
{%- set assetList = [] -%}
|
||||
{%- for asset in root.pad.asset_root.children recursive -%}
|
||||
{%- if asset.__class__.__name__ != 'Directory' -%}
|
||||
{{- _add_(assetList, asset) -}}
|
||||
{%- endif -%}
|
||||
{{- loop(asset.children) -}}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- set cacheList = [] -%}
|
||||
{{- _add_(cacheList, root) -}}
|
||||
{%- for x in root.children if x != this recursive -%}
|
||||
{{- _add_(cacheList, x) -}}
|
||||
|
||||
{%- set pg = x.datamodel.pagination_config -%}
|
||||
{%- if pg.enabled -%}
|
||||
{%- for page in range(2, pg.count_pages(x) + 1) -%}
|
||||
{{- _add_(cacheList, pg.get_record_for_page(x, page)) -}}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
|
||||
{% set img = x.attachments.images|sort(attribute='record_label')|first -%}
|
||||
{%- if img -%}
|
||||
{{- _add_(cacheList, img.thumbnail(200, 150, 'crop')) -}}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if x.datamodel.has_own_children -%}
|
||||
{{- loop(x.children) -}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{#- Generate cache file index #}
|
||||
# static
|
||||
{{ _print_(assetList) -}}
|
||||
# index
|
||||
{{ _print_(cacheList) -}}
|
||||
|
||||
{#- All other requests are forwarded #}
|
||||
NETWORK:
|
||||
*
|
||||
@@ -8,10 +8,10 @@
|
||||
{%- set sortType = this.xdata + [''] -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- set all = site.query('/recipes', this.alt) | groupByAttribute(this.group_key) | groupSort(sortType, this.reverse_order) -%}
|
||||
{%- set all = site.query('/recipes', this.alt) | groupByAttribute(this.group_key, this.alt) | groupSort(sortType, this.reverse_order) -%}
|
||||
|
||||
{%- if this.group_key == 'time' -%}
|
||||
{%- set all = all | groupMergeCluster(this.xdata, this.reverse_order) -%}
|
||||
{%- set all = all | groupTimeCluster(this.xdata, this.reverse_order) -%}
|
||||
{%- endif -%}
|
||||
|
||||
<h1>{{ this.name }}</h1>
|
||||
@@ -22,9 +22,9 @@
|
||||
{%- elif not attrib -%}
|
||||
{{ this.null_fallback }}
|
||||
{%- elif this.group_key == 'time' -%}
|
||||
{{ attrib | duration(this.xdata) }}
|
||||
{{ attrib | durationCluster(this.xdata, this.alt) }}
|
||||
{%- elif this.group_key == 'difficulty' -%}
|
||||
{{ localize('difficulty', attrib) }}
|
||||
{{ localize(this.alt, 'difficulty', attrib) }}
|
||||
{%- else -%}
|
||||
{{ attrib }}
|
||||
{%- endif -%}</dt>
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.75">
|
||||
<script type="text/javascript" src="{{ '/static/col2.js'|url }}"></script>
|
||||
<script type="text/javascript" src="{{ '/static/lozad.min.js'|url }}"></script>
|
||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}">
|
||||
<title>{% block title %}Welcome{% endblock %} · recipe lekture</title>
|
||||
<body>
|
||||
{%- if ENABLE_APPCACHE %}
|
||||
<html manifest="{{ site.get('app.appcache', alt=this.alt)|url }}">
|
||||
{% endif -%}
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.75">
|
||||
<script type="text/javascript" src="{{ '/static/script.js'|url }}"></script>
|
||||
<link rel="stylesheet" href="{{ '/static/style.css'|url }}">
|
||||
<link rel="icon" sizes="32x32" href="{{ '/img/icon-32.png'|url }}">
|
||||
<link rel="icon" sizes="196x196" href="{{ '/img/icon-196.png'|url }}">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ '/img/icon-180.png'|url }}">
|
||||
<link rel="manifest" href="{{ '/app.webmanifest'|asseturl }}">
|
||||
<title>{% block title %}Welcome{% endblock %} · recipe lekture</title>
|
||||
</head>
|
||||
<body> {#- ontouchstart="" #}
|
||||
<header>
|
||||
<a id="logo" href="{{ site.get('/', alt=this.alt)|url }}">recipe lekture</a>
|
||||
{%- if ENABLE_APPCACHE %}
|
||||
<i id="cache-status" title="cache status"></i>
|
||||
{%- endif %}
|
||||
<nav>
|
||||
<ul>
|
||||
{%- set allRecipes = site.get('recipes', this.alt) %}
|
||||
<li><a {% if this == allRecipes %}class="active"{% endif %} href="{{ allRecipes|url }}">{{ localize('title.all_recipes') }}</a></li>
|
||||
<li><a {% if this == allRecipes %}class="active"{% endif %} href="{{ allRecipes|url }}">{{ localize(this.alt, 'title.all_recipes') }}</a></li>
|
||||
{%- for navpage in site.query('/groupby', this.alt) %}
|
||||
<li><a {% if this.is_child_of(navpage) %}class="active"{% endif %} href="{{ navpage|url }}">{{ navpage.name }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="tags small">
|
||||
{%- set allowEmptyTags = site.get('settings', alt=this.alt)['show_empty_tags'] -%}
|
||||
{%- for tag in site.query('/tags', this.alt) %}
|
||||
{%- if allowEmptyTags or tag._id in availableTags -%}
|
||||
<a {%
|
||||
if this.is_child_of(tag) or (this.tags and tag._id in this.tags) -%}
|
||||
class="active"
|
||||
{%- endif %} href="{{ tag|url }}">{{ tag.name }}</a>
|
||||
{%- endif -%}
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -5,13 +5,24 @@
|
||||
{%- set img = recipe.attachments.images|sort(attribute='record_label')|first -%}
|
||||
<a href="{{ recipe|url }}">{#--#}
|
||||
<div class="recipe-tile">
|
||||
<div class="img-placeholder">
|
||||
{%- if img -%}
|
||||
<img class="lozad" width="200" height="150" data-src="{{ img.thumbnail(200, 150)|url }}" />
|
||||
{%- else -%}
|
||||
No Image
|
||||
{%- endif -%}
|
||||
|
||||
{#- overlay on hover and always-visible icons #}
|
||||
<div class="overlay">
|
||||
<div class="hover"><div class="time">{{ recipe.time|duration(recipe.alt) }}</div></div>
|
||||
<div class="icon-bar">
|
||||
{%- if 'raw' in recipe.tags -%}<i class="icon raw"></i>{%- endif -%}
|
||||
{%- if 'glutenfree' in recipe.tags -%}<i class="icon gf"></i>{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#- show image or placeholder text #}
|
||||
{% if img -%}
|
||||
<img class="lozad" data-src="{{ img.thumbnail(200, 150, 'crop')|url }}">
|
||||
{%- else -%}
|
||||
<div class="placeholder">No Image</div>
|
||||
{%- endif -%}
|
||||
|
||||
{#- recipe title #}
|
||||
<p>{{ recipe.name }}</p>
|
||||
</div>{#--#}
|
||||
</a>
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
{% block title %}{{ this.name }}{% endblock %}
|
||||
{% block body %}
|
||||
<article class="recipe">
|
||||
<!-- date added: {{ this.date }} -->
|
||||
<section id="img-carousel" class="v-scroll center">
|
||||
{%- for img in this.attachments.images|sort(attribute='record_label') %}
|
||||
<img src="{{ img|url }}" height="400px">
|
||||
<img class="lozad" data-src="{{ img|url }}" height="400">
|
||||
{%- endfor %}
|
||||
</section>
|
||||
|
||||
{% if this.source -%}
|
||||
<div id="source" class="small center">
|
||||
{%- if this.source.host -%}
|
||||
<a href="{{ this.source }}">⤳ {{ this.source.host }}</a>
|
||||
{%- else -%}
|
||||
⤳ {{ this.source }}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1>{{ this.name }}</h1>
|
||||
@@ -20,19 +25,19 @@
|
||||
<div class="difficulty {{this.difficulty}}">
|
||||
<div></div><div></div><div></div>
|
||||
{%- if this.difficulty %}
|
||||
<span>{{ localize('difficulty', this.difficulty) }}</span>
|
||||
<span>{{ localize(this.alt, 'difficulty', this.difficulty) }}</span>
|
||||
{%- else %}
|
||||
<span class="small">{{ localize('difficulty._unset') }}</span>
|
||||
<span class="small">{{ localize(this.alt, 'difficulty._unset') }}</span>
|
||||
{%- endif %}
|
||||
</div>
|
||||
<div>{{ localize('duration.label') }}: {{ this.time|duration if this.time else '—' }}</div>
|
||||
<div>{{ localize('yield.label') }}: {{ this.yield if this.yield else '—' }}</div>
|
||||
<div>{{ localize(this.alt, 'duration.label') }}: {{ this.time|duration(this.alt) if this.time else '—' }}</div>
|
||||
<div>{{ localize(this.alt, 'yield.label') }}: {{ this.yield or '—' }}</div>
|
||||
</section>
|
||||
|
||||
<section id="ingredients">
|
||||
<h2>{{ localize('title.ingredients') }}:</h2>
|
||||
<h2>{{ localize(this.alt, 'title.ingredients') }}:</h2>
|
||||
<ul class="no-bullets li-lg-space">
|
||||
{%- for ing in this|enumIngredients %}
|
||||
{%- for ing in this|enumIngredients(this.alt) %}
|
||||
{%- if ing['group'] %}
|
||||
<li class="dark-red bold mrgTopMd">{{ ing['group'] }}</li>
|
||||
{%- else %}
|
||||
@@ -41,7 +46,7 @@
|
||||
{%- if ing['measure'] %}{{ ing['measure'] }} {% endif -%}
|
||||
<span class="light-red">{{ ing['name'] }}</span>
|
||||
{%- if ing['note'] -%}
|
||||
<span class="small italic">{{ ', ' ~ ing['note'] }}</span>
|
||||
<span class="small italic">{{ ', ' ~ ing['note'] | replaceAtRefURLs(label=localize(this.alt, 'ingredients.recipeLink')) }}</span>
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- endif %}
|
||||
@@ -50,9 +55,9 @@
|
||||
</section>
|
||||
|
||||
<section id="directions">
|
||||
<h2>{{ localize('title.directions') }}:</h2>
|
||||
<h2>{{ localize(this.alt, 'title.directions') }}:</h2>
|
||||
{% if site.get('settings', alt=this.alt)['replace_temp'] -%}
|
||||
{{ this.directions|string|replace('°C', '℃')|replace('°F', '℉')|markdown }}
|
||||
{{ this.directions.html|replace('°C', '℃')|replace('°F', '℉') }}
|
||||
{% else -%}
|
||||
{{ this.directions }}
|
||||
{% endif -%}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "macros/recipes.html" import render_recipe_list %}
|
||||
{% from "macros/pagination.html" import render_pagination_all %}
|
||||
{% block title %}{{ localize('title.recipes') }}{% endblock %}
|
||||
{% block title %}{{ localize(this.alt, 'title.recipes') }}{% endblock %}
|
||||
{% block body %}
|
||||
<h1>{{ localize('title.recipes') }}</h1>
|
||||
<h1>{{ localize(this.alt, 'title.recipes') }}</h1>
|
||||
{{ render_recipe_list(this.pagination.items) }}
|
||||
{{ render_pagination_all(this.pagination) }}
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "layout.html" %}
|
||||
{% from "macros/recipes.html" import render_recipe_list %}
|
||||
{% block body %}
|
||||
<h1>{{ localize('title.latest') }}</h1>
|
||||
<h1>{{ localize(this.alt, 'title.latest') }}</h1>
|
||||
<div class="latest">
|
||||
{{ render_recipe_list(site.query('recipes', this.alt) | sort(attribute='date', reverse=True), limit=6) }}
|
||||
</div>
|
||||
|
||||