Does not work: uploading images

Adding/Editing/Deleting works
SeasonsSelect works
Nice recipe layout
This commit is contained in:
Alexander Bocken 2023-06-24 15:31:10 +02:00
parent 3d0d3f41e2
commit 9392ff6ada
Signed by: Alexander
GPG Key ID: 1D237BE83F9B05E8
25 changed files with 1150 additions and 209 deletions

View File

@ -19,7 +19,11 @@ background-color: var(--red);
display: grid;
justify-content: center;
align-content: center;
}
@media screen and (max-width: 500px) {
.container{
margin: 1rem;
}
}
:global(.icon_svg){
width: 2rem;

View File

@ -215,12 +215,19 @@ input::placeholder{
color:var(--nord0);
}
.card .description{
box-sizing:border-box;
border: 2px solid var(--nord5);
border-radius: 30px;
padding-inline: 1em;
padding-block: 0.5em;
margin-inline: 1em;
margin-top: 0;
color: var(--nord4);
width: calc(300px - 2em); /*??*/
}
.card .description:hover{
color: var(--nord0);
border: 2px solid var(--nord0);
}
.card .tags{
display: flex;
@ -354,7 +361,7 @@ input::placeholder{
<input class=category placeholder=Kategorie... bind:value={card_data.category}/>
<div>
<input class=name placeholder=Name... bind:value={card_data.name}/>
<input class=description placeholder=Kurzbeschreibung... bind:value={card_data.description}/>
<p contenteditable class=description placeholder=Kurzbeschreibung... bind:innerText={card_data.description}></p>
</div>
<div class=tags>
{#each card_data.tags as tag}

View File

@ -10,6 +10,7 @@ import "$lib/css/action_button.css"
import { do_on_key } from '$lib/components/do_on_key.js'
export let ingredients
export let portions
let new_ingredient = {
amount: "",
@ -107,7 +108,6 @@ export function show_keys(event){
</script>
<style>
input::placeholder{
all:unset;
}
@ -117,6 +117,7 @@ input{
input.heading{
all: unset;
box-sizing: border-box;
background-color: var(--nord0);
padding: 1rem;
padding-inline: 2rem;
@ -146,13 +147,14 @@ input.heading:hover{
.heading_wrapper button{
position: absolute;
bottom: -1.5rem;
right: -5rem;
right: -2rem;
}
.adder{
box-sizing: border-box;
margin-inline: auto;
position: relative;
margin-block: 3rem;
width: 400px;
width: 90%;
border-radius: 20px;
transition: 200ms;
}
@ -269,32 +271,11 @@ dialog h2{
drop-shadow(0 0 1em black)
;
}
ul{
width: fit-content;
margin-inline: auto;
}
li{
font-size: 1.2rem;
max-width: 1000px;
align-items: center;
}
.li_wrapper{
display: flex;
justify-content: space-between;
}
.mod_icons{
display: flex;
flex-direction: row;
margin-left: 2rem;
}
li:nth-child(2n){
background-color: var(--nord4);
}
li:nth-child(2n+1){
background-color: var(--nord6);
}
.button_subtle{
padding: 0em;
animation: unset;
@ -306,16 +287,64 @@ li:nth-child(2n+1){
scale: 1.2 1.2;
}
h3{
margin-inline: auto;
width: fit-content;
display: flex;
flex-direction: row;
max-width: 1000px;
justify-content: space-between;
}
.ingredients_grid{
box-sizing: border-box;
display: grid;
font-size: 1.1em;
grid-template-columns: 2fr 3fr 1fr;
grid-template-rows: auto;
grid-auto-flow: row;
align-items: center;
row-gap: 0.5em;
column-gap: 0.5em;
}
.ingredients_grid > *{
cursor: pointer;
}
.ingredients_grid>*:nth-child(3n+1){
min-width: 5ch;
}
.list_wrapper{
padding-inline: 2em;
padding-block: 1em;
}
.list_wrapper p[contenteditable]{
border: 2px solid grey;
border-radius: 1000px;
padding: 0.25em 1em;
background-color: white;
transition: 200ms;
}
.list_wrapper p[contenteditable]:hover,
.list_wrapper p[contenteditable]:focus-within{
scale: 1.05 1.05;
}
@media screen and (max-width: 500px){
dialog h2{
margin-top: 2rem;
}
dialog .heading_wrapper{
width: 80%;
}
.ingredients_grid .mod_icons{
margin-left: 0;
}
}
</style>
<div class=list_wrapper>
<h4>Portionen:</h4>
<p contenteditable type="text" bind:innerText={portions}></p>
<h2>Zutaten</h2>
{#each ingredients as list, list_index}
<h3>
<div>
@ -329,23 +358,20 @@ h3{
<button class="action_button button_subtle" on:click="{() => show_modal_edit_subheading_ingredient(list_index)}">
<Pen fill=var(--nord1)></Pen> </button>
<button class="action_button button_subtle" on:click="{() => remove_list(list_index)}">
<Cross fill=var(--nord1)></Cross>
</button>
<Cross fill=var(--nord1)></Cross></button>
</div>
</h3>
<ul>
<div class=ingredients_grid>
{#each list.list as ingredient, ingredient_index}
<li><div class=li_wrapper><div>{ingredient.amount} {ingredient.unit} {ingredient.name}</div>
<div on:click={() => show_modal_edit_ingredient(list_index, ingredient_index)} >{ingredient.amount} {ingredient.unit}</div>
<div on:click={() => show_modal_edit_ingredient(list_index, ingredient_index)} >{ingredient.name}</div>
<div class=mod_icons><button class="action_button button_subtle" on:click={() => show_modal_edit_ingredient(list_index, ingredient_index)}>
<Pen fill=var(--nord1) height=1em width=1em></Pen>
</button>
<button class="action_button button_subtle" on:click="{() => remove_ingredient(list_index, ingredient_index)}">
<Cross fill=var(--nord1) height=1em width=1em></Cross>
</button></div></div>
</li>
<Pen fill=var(--nord1) height=1em width=1em></Pen></button>
<button class="action_button button_subtle" on:click="{() => remove_ingredient(list_index, ingredient_index)}"><Cross fill=var(--nord1) height=1em width=1em></Cross></button></div>
{/each}
</ul>
</div>
{/each}
</div>
<div class="adder shadow">
<input class=category type="text" bind:value={new_ingredient.sublist} placeholder="Kategorie (optional)" on:keypress={(event) => do_on_key(event, 'Enter', false, add_new_ingredient)}>
@ -358,7 +384,6 @@ h3{
</button>
</div>
</div>
<dialog id=edit_ingredient_modal>
<h2>Zutat verändern</h2>
<div class=adder>

View File

@ -12,6 +12,7 @@ import { do_on_key } from '$lib/components/do_on_key.js'
const step_placeholder = "Kartoffeln schälen..."
export let instructions
export let add_info
let new_step = {
name: "",
@ -32,6 +33,12 @@ function get_sublist_index(sublist_name, list){
return -1
}
export function remove_list(list_index){
if(instructions[list_index].steps.length > 1){
const response = confirm("Bist du dir sicher, dass du diese Liste löschen möchtest? Alle Zubereitungsschritte der Liste werden hiermit auch gelöscht.");
if(!response){
return
}
}
instructions.splice(list_index, 1);
instructions = instructions //tells svelte to update dom
}
@ -117,9 +124,23 @@ input::placeholder{
all:unset;
}
li > div{
display:flex;
flex-direction: row;
justify-items: space-between;
align-items:center;
}
li > div > div:first-child{
flex-grow: 1;
cursor: pointer;
}
li > div > div:last-child{
display: flex;
flex-direction: row;
}
input.heading{
all: unset;
box-sizing: border-box;
background-color: var(--nord0);
padding: 1rem;
padding-inline: 2rem;
@ -139,7 +160,7 @@ input.heading:focus-visible
.heading_wrapper{
position: relative;
width: 300px;
width: min(300px, 95dvw);
margin-inline: auto;
transition: 200ms;
}
@ -152,15 +173,20 @@ input.heading:focus-visible
.heading_wrapper button{
position: absolute;
bottom: -1.5rem;
right: -5rem;
right: -1.5rem;
}
.adder{
margin-inline: auto;
position: relative;
margin-block: 3rem;
width: 50ch;
width: 90%;
border-radius: 20px;
transition: 200ms;
background-color: var(--blue);
padding: 1.5rem 2rem;
}
dialog .adder{
width: 400px;
}
.shadow{
box-shadow: 0 0 1em 0.2em rgba(0,0,0,0.3);
@ -202,17 +228,18 @@ input.heading:focus-visible
font-family: sans-serif;
width: 100%;
font-size: 1.2rem;
padding: 2rem;
padding-top: 2.5rem;
border-radius: 20px;
background-color: var(--blue);
color: #bbb;
transition: 200ms;
border: 2px solid var(--nord4);
border-radius: 30px;
padding: 0.5em 1em;
color: var(--nord4);
transition: 100ms;
}
.add_step p:hover,
.add_step p:focus-visible
{
color: white;
scale: 1.02 1.02;
}
dialog{
@ -239,6 +266,21 @@ dialog h2{
drop-shadow(0 0 1em black)
;
}
@media screen and (max-width: 500px){
dialog h2{
margin-top: 2rem;
}
dialog .adder{
width: 85%;
padding-inline: 0.5em;
}
dialog .adder .category{
width: 70%;
}
dialog .adder input::placeholder{
font-size: 1.2rem;
}
}
dialog[open]{
animation: show 200ms ease forwards;
}
@ -250,10 +292,122 @@ dialog[open]{
backdrop-filter: blur(10px);
}
}
ol li::marker{
font-weight: bold;
color: var(--blue);
font-size: 1.2rem;
}
.instructions{
flex-basis: 0;
flex-grow: 2;
background-color: var(--nord5);
padding-block: 1rem;
padding-inline: 2rem;
}
.instructions ol{
padding-left: 1em;
}
.instructions li{
margin-block: 0.5em;
font-size: 1.1rem;
}
.additional_info{
display: flex;
flex-wrap: wrap;
gap: 1em;
}
.additional_info > *{
flex-grow: 0;
overflow: hidden;
padding: 1em;
background-color: #FAFAFE;
box-shadow: 0.3em 0.3em 1em 0.2em rgba(0,0,0,0.3);
/*max-width: 30%*/
}
.additional_info > div > *:not(h4){
line-height: 2em;
}
h4{
line-height: 1em;
margin-block: 0;
}
.button_subtle{
padding: 0em;
animation: unset;
margin: 0.2em 0.1em;
background-color: transparent;
box-shadow: unset;
display:inline;
}
.button_subtle:hover{
scale: 1.2 1.2;
}
h3{
display:flex;
gap: 1rem;
}
.additional_info input{
all:unset;
display: inline;
width: 10ch;
border-radius: 1000px;
border: 2px solid grey;
padding: 0em 0.5em;
}
.additional_info input::placeholder{
color: grey;
}
.additional_info p[contenteditable]{
display: inline;
padding: 0.25em 1em;
border: 2px solid grey;
border-radius: 1000px;
}
.additional_info div:has(p[contenteditable]){
transition: 200ms;
display: inline;
}
.additional_info div:has(p[contenteditable]):hover,
.additional_info div:has(p[contenteditable]):focus-within
{
transform: scale(1.1, 1.1);
}
@media screen and (max-width: 500px){
dialog h2{
margin-top: 2rem;
}
dialog .heading_wrapper{
width: 80%;
}
}
</style>
<div class=instructions>
<div class=additional_info>
<div><h4>Vorbereitung:</h4>
<p contenteditable type="text" bind:innerText={add_info.preparation}></p>
</div>
<div><h4>Stockgare:</h4>
<p contenteditable type="text" bind:innerText={add_info.fermentation.bulk}></p>
</div>
<div><h4>Stückgare:</h4>
<p contenteditable type="text" bind:innerText={add_info.fermentation.final}></p>
</div>
<div><h4>Backen:</h4>
<div><p type="text" bind:innerText={add_info.baking.length} contenteditable placeholder="40 min..."></p></div> bei <div><p type="text" bind:innerText={add_info.baking.temperature} contenteditable placeholder=200...></p></div> °C <div><p type="text" bind:innerText={add_info.baking.mode} contenteditable placeholder="Ober-/Unterhitze..."></p></div></div>
<div><h4>Auf dem Teller:</h4>
<div><p contenteditable type="text" bind:innerText={add_info.total_time}></p></div>
</div>
</div>
<h2>Zubereitung</h2>
{#each instructions as list, list_index}
<h3>
{#if list.name}
@ -261,31 +415,34 @@ dialog[open]{
{:else}
Leer
{/if}
<button class=edit on:click="{() => show_modal_edit_subheading_step(list_index)}">
<Pen></Pen> </button>
<button class=remove on:click="{() => remove_list(list_index)}">
<Cross></Cross>
<div>
<button class="action_button button_subtle" on:click="{() => show_modal_edit_subheading_step(list_index)}">
<Pen fill=var(--nord1)></Pen> </button>
<button class="action_button button_subtle" on:click="{() => remove_list(list_index)}">
<Cross fill=var(--nord1)></Cross>
</button>
</div>
</h3>
<ol>
{#each list.steps as step, step_index}
<li>{step}
<button class=edit on:click={() => show_modal_edit_step(list_index, step_index)}>
<Pen></Pen>
<li><div><div on:click={() => show_modal_edit_step(list_index, step_index)}>{step}</div>
<div><button class="action_button button_subtle" on:click={() => show_modal_edit_step(list_index, step_index)}>
<Pen fill=var(--nord1)></Pen>
</button>
<button class=remove on:click="{() => remove_step(list_index, step_index)}">
<Cross></Cross>
<button class="action_button button_subtle" on:click="{() => remove_step(list_index, step_index)}">
<Cross fill=var(--nord1)></Cross>
</button>
</div></div>
</li>
{/each}
</ol>
{/each}
</div>
<div class='adder shadow'>
<input class=category type="text" bind:value={new_step.name} placeholder="Kategorie (optional)"on:keypress={(event) => do_on_key(event, 'Enter', false , add_new_step)} >
<div class=add_step>
<p id=step contenteditable on:focus='{clear_step}' on:blur={add_placeholder} bind:innerHTML={new_step.step} on:keypress={(event) => do_on_key(event, 'Enter', true , add_new_step)}></p>
<p id=step contenteditable on:focus='{clear_step}' on:blur={add_placeholder} bind:innerText={new_step.step} on:keypress={(event) => do_on_key(event, 'Enter', true , add_new_step)}></p>
<button on:click={() => add_new_step()} class=action_button>
<Plus fill=white style="height: 2rem; width: 2rem"></Plus>
</button>

View File

@ -0,0 +1,45 @@
<script>
export let data
</script>
<style>
*{
font-family: sans-serif;
}
.ingredients{
flex-basis: 0;
flex-grow: 1;
padding-block: 1rem;
padding-inline: 2rem;
}
.ingredients_grid{
display: grid;
font-size: 1.1rem;
grid-template-columns: 1fr 3fr;
grid-template-rows: auto;
grid-auto-flow: row;
row-gap: 0.5em;
column-gap: 0.5em;
}
h4{
margin-block: 0;
}
</style>
{#if data.ingredients}
<div class=ingredients>
{#if data.portions}
<h4>Portionen:</h4>
{data.portions}
{/if}
<h2>Zutaten</h2>
{#each data.ingredients as list}
{#if list.name}
<h3>{list.name}</h3>
{/if}
<div class=ingredients_grid>
{#each list.list as item}
<div class=amount>{item.amount} {item.unit}</div><div class=name>{@html item.name}</div>
{/each}
</div>
{/each}
</div>
{/if}

View File

@ -0,0 +1,90 @@
<script>
export let data
</script>
<style>
*{
font-family: sans-serif;
}
ol li::marker{
font-weight: bold;
color: var(--blue);
font-size: 1.2rem;
}
.instructions{
flex-basis: 0;
flex-grow: 2;
background-color: var(--nord5);
padding-block: 1rem;
padding-inline: 2rem;
}
.instructions ol{
padding-left: 1em;
}
.instructions li{
margin-block: 0.5em;
font-size: 1.1rem;
}
.additional_info{
display: flex;
flex-wrap: wrap;
gap: 1em;
}
.additional_info > *{
flex-grow: 0;
padding: 1em;
background-color: #FAFAFE;
box-shadow: 0.3em 0.3em 1em 0.2em rgba(0,0,0,0.3);
max-width: 30%
}
@media screen and (max-width: 500px){
.additional_info > *{
max-width: 60%;
}
}
h4{
margin-block: 0;
}
</style>
<div class=instructions>
<div class=additional_info>
{#if data.preparation}
<div><h4>Vorbereitung:</h4>{data.preparation}</div>
{/if}
{#if data.fermentation}
{#if data.fermentation.bulk}
<div><h4>Stockgare:</h4>{data.fermentation.bulk}</div>
{/if}
{#if data.fermentation.final}
<div><h4>Stückgare:</h4> {data.fermentation.final}</div>
{/if}
{/if}
{#if data.baking.temperature}
<div><h4>Backen:</h4> {data.baking.length} bei {data.baking.temperature} °C {data.baking.mode}</div>
{/if}
{#if data.total_time}
<div><h4>Auf dem Teller:</h4>{data.total_time}</div>
{/if}
</div>
{#if data.instructions}
<h2>Zubereitung</h2>
{#each data.instructions as list}
{#if list.name}
<h3>{list.name}</h3>
{/if}
<ol>
{#each list.steps as step}
<li>{@html step}</li>
{/each}
</ol>
{/each}
{/if}
</div>

View File

@ -5,7 +5,6 @@
onMount(() => {
const recipes = document.querySelectorAll(".search_me");
console.log("######", recipes)
const search = document.getElementById("search");
const clearSearch = document.getElementById("clear-search");

View File

@ -1,9 +1,16 @@
<script lang=ts>
import "$lib/components/nordtheme.css"
import { season } from '$lib/js/season_store.js'
import {onMount} from "svelte";
let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
export let season : Number[]
let season_local
season.subscribe((s) => {
season_local = s
});
export function set_season(){
let temp = []
@ -13,18 +20,18 @@ export function set_season(){
temp.push(i+1)
}
}
season = temp
season.update((s) => temp)
}
function write_season(season){
const el = document.getElementById("labels");
for(var i = 0; i < season.length; i++){
el.children[i].children[0].children[0].checked = true
el.children[season[i]-1].children[0].children[0].checked = true
}
}
onMount(() => {
write_season(season)
write_season(season_local)
});
</script>
@ -75,6 +82,3 @@ input[type=checkbox]::after
</div>
{/each}
</div>
<button on:click={() => console.log("season", season)}> PRINT SEASON FROM SEASON_SELECT</button>

View File

@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const season = writable([]);

View File

@ -16,16 +16,17 @@ const RecipeSchema = new mongoose.Schema(
description: {type: String, required: true},
tags : [String],
season : [Number],
baking: { temperature: String,
length: String,
mode: String,
baking: { temperature: {type:String, default: ""},
length: {type:String, default: ""},
mode: {type:String, default: ""},
},
preparation : String,
fermentation: {bulk: String,
final: String,
preparation : {type:String, default: ""},
fermentation: { bulk: {type:String, default: ""},
final: {type:String, default: ""},
},
portions : String,
total_time : String,
portions :{type:String, default: ""},
total_time : {type:String, default: ""},
ingredients : [ { name: {type: String, default: ""},
list: [{name: {type: String, default: ""},
unit: String,
@ -34,6 +35,7 @@ const RecipeSchema = new mongoose.Schema(
}],
instructions : [{name: {type: String, default: ""},
steps: [String]}],
preamble : String,
addendum : String,
},
);

View File

@ -4,10 +4,12 @@ import "$lib/components/nordtheme.css"
<style>
:global(*){
box-sizing: border-box;
font-family: sans-serif;
}
:global(body){
margin:0;
padding:0;
background-color: #fbf9f3;
}
li{
list-style-type:none;
@ -17,9 +19,10 @@ li{
li>a{
text-decoration: none;
font-family: sans-serif;
font-size: 1.5rem;
font-size: 1.2rem;
color: inherit
}
li:hover,
li:focus-within
{
@ -27,7 +30,6 @@ li:focus-within
color: var(--red);
transform: scale(1.1,1.1);
}
ul {
padding-block: 2rem;
display: flex;
@ -38,6 +40,16 @@ ul {
margin: 0;
margin-inline: auto;
}
@media screen and (max-width: 500px) {
ul{
flex-direction: column;
}
li:hover,
li:focus-within{
transform: unset;
}
}
nav{
background-color: var(--nord0);
}
@ -49,7 +61,7 @@ nav{
footer{
padding-block: 1rem;
text-align: center;
margin-top: auto;
margin-top: 3rem;
}
</style>
<div class=wrapper style="">

View File

@ -0,0 +1 @@
{"terminal": "nvimterm"}

View File

@ -16,7 +16,7 @@ export const POST: RequestHandler = async ({request}) => {
await dbConnect();
await Recipe.create(recipe_json);
await dbDisconnect();
return {status: 400} //TODO: cleanup error throwing
return {status: 200} //TODO: cleanup error throwing
}
else{
console.log("PASSWORD INCORRECT")

View File

@ -0,0 +1,23 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../utils/db';
import type {RecipeModelType} from '../../../types/types';
import { BEARER_TOKEN } from '$env/static/private'
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request}) => {
let message = await request.json()
const short_name = message.old_short_name
const bearer_token = message.headers.bearer
if(bearer_token === BEARER_TOKEN){
console.log("PASSWORD CORRECT")
await dbConnect();
await Recipe.findOneAndDelete({short_name: short_name});
await dbDisconnect();
return {status: 400} //TODO: cleanup error throwing
}
else{
console.log("PASSWORD INCORRECT")
return {status: 403}
}
};

View File

@ -3,6 +3,7 @@ import { Recipe } from '../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../utils/db';
import type {RecipeModelType} from '../../../types/types';
import { BEARER_TOKEN } from '$env/static/private'
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request}) => {
@ -10,16 +11,15 @@ export const POST: RequestHandler = async ({request}) => {
let message = await request.json()
const recipe_json = message.recipe
const bearer_token = message.headers.bearer
console.log("RECIPE:", recipe_json)
console.log("BEARER:", bearer_token)
if(bearer_token === BEARER_TOKEN){
await dbConnect();
await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json);
await dbDisconnect();
return {status: 400} //TODO: cleanup error throwing
const res = new Response(JSON.stringify({ message: "Updated Recipe successfully"}), { status: 200 })
return res
}
else{
console.log("PASSWORD INCORRECT")
return {status: 403}
console.log("INCORRECT PASSWORD")
throw error(403, "Password incorrect")
}
};

View File

@ -0,0 +1,13 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import type {BriefRecipeType} from '../../../../types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let icons = (await Recipe.distinct('icon').lean());
await dbDisconnect();
icons = JSON.parse(JSON.stringify(icons));
return json(icons);
};

View File

@ -0,0 +1,13 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = (await Recipe.find({tags: params.icon}, 'name short_name images tags category icon description season').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
};

View File

@ -16,10 +16,12 @@ const test_json = [
caption: "",
}],
description: "Alles was das Bauernherz erfreuen lässt in einer Mahlzeit.",
tags: ["Schweiz", "Käse", "Speck", "Nudeln", "Apfelmuß", "Kartoffeln"],
season: [6,7,8,9,10,11,12,1],
preamble: "Dieser Schweizer Klassiker ist wohl das beste Essen nach einem langen Tag von Skifahren. Die Beilage aus Apfelmus ist ein Muss.",
tags: ["Schweiz", "Käse", "Speck", "Nudeln", "Apfelmuß", "Kartoffeln", "Fleisch"],
season: [10,11,12,1],
portions: "4 Hauptspeisen",
total_time: "30 Minuten",
preparation: "10 min",
total_time: "30 min",
ingredients: [ {
name: "",
list: [

View File

@ -0,0 +1,77 @@
const obj =
{
short_name: "aelplermagronen",
name : "Älplermagronen",
category: "Hauptspeise",
icon: "🍂",
datecreated: 20230619,
datemodified: 20230619,
images: [{
mediapath: "aelplermagronen.webp",
alt: "Älplermagronen serviert mit Apfelmuß",
caption: "",
}],
description: "Alles was das Bauernherz erfreuen lässt in einer Mahlzeit.",
tags: ["Schweiz", "Käse", "Speck", "Nudeln", "Apfelmuß", "Kartoffeln"],
season: [6,7,8,9,10,11,12,1],
portions: "4 Hauptspeisen",
total_time: "30 Minuten",
ingredients: [ {
name: "",
list: [
{ name: "Speckwürfel",
unit: "g",
amount: "150"
},
{
name: "mittelgroße Zwiebeln",
unit: "",
amount: "3",
},
{
name: "Kartoffeln, festkochend",
unit: "g",
amount: "400",
},
{
name: "Milch",
unit: "L",
amount: "1-2",
},
{
name: "Maccaroni",
unit: "g",
amount: "400",
},
{
name: "Appenzeller",
unit: "g",
amount: "150",
},
{
name: "<a href=apfelmuss>Apfelmuß</a>",
unit: "",
amount: "",
},
]},
],
instructions: [
{name: "",
steps: [
"In einem großen Topf oder tiefer Pfanne Speckwürfel anbraten.",
"Zwiebel in Halbringe schneiden und im gleichen Topf schwitzen lassen.",
"Kartoffeln schälen und in ~1 cm<sup>3</sup> schneiden.",
"Wenn Ziwebeln genügend gekocht sind die Kartoffeln hinzufügen und Milch hinzufügen, sodass alles bedeckt ist. Ca. 10 Minuten kochen lassen.",
"Ca. 1 L Milch hinzugeben. Für den nächsten Schritt wollen wir die Maccaroni hinzufügen. Damit diese nicht zu breiig werden geben wir erst die Milch zu und lassen sie aufkochen.",
"Wenn die der Topf wieder kocht jetzt die Maccaroni hinzugeben.",
"Den Käse zerreiben oder in kleine Würfel schneiden.",
"Ein bis zwei Minuten bevor die Nudeln durchgekocht sind den Käse hinzugeben und schmelzen lassen.",
"Mit Salz und Muskat würzen.",
"Den Topf ein bisschen zu früh vom Herd nehmen und ein bisschen auskühlen lassen.",
"Mit <a href=apfelkompott>Apfelmuß oder Apfelkompott</a> servieren."
]
}
],
addendum: "<p>Man kann das Gericht noch dekanter machen indem man zu Teilen Rahm an Stelle von Milch verwendet. Zudem kann man das ganze auch noch in eine Auflaufform geben und im Ofen eine Kruste anbacken</p>",
}

View File

@ -0,0 +1,50 @@
const obj =
{
short_name: "<++>",
name : "<++>",
category: "<++>",
icon: "<++>",
datecreated: 20230619,
datemodified: 20230619,
images: [{
mediapath: "<++>.webp",
alt: "<++>",
caption: "<++>",
}],
description: "<++>",
tags: [<++>],
season: [<++>],
baking: {
temperature: "<++>",
length: "<++>",
mode: "<++>",
},
preparation: "<++>",
fermentation: {
bulk: "<++>",
final: "<++>"
},
portions: "<++>",
total_time: "<++>",
ingredients: [ {
name: "<++>",
list: [
{ name: "<++>",
unit: "<++>",
amount: <++>,
},
{
name: "<++>",
unit: "<++>",
amount: <++>,
},
]},
],
instructions: [
{name: "<++>",
steps: [
"<++>",
"<++>"
]
}
]}

View File

@ -8,7 +8,6 @@
export let data: PageData;
export let current_month = new Date().getMonth() + 1
</script>
<h1>Rezepte</h1>
<h2>In Saison</h2>
<section>

View File

@ -6,19 +6,42 @@
import "$lib/components/nordtheme.css"
import MultiImgWrapper from './MultiImgWrapper.svelte'
import EditButton from '$lib/components/EditButton.svelte';
import InstructionsPage from '$lib/components/InstructionsPage.svelte';
import IngredientsPage from '$lib/components/IngredientsPage.svelte';
export let data: PageData;
let hero_img_src = "/images/" + data.images[0].mediapath
export let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
function season_intervals() {
let interval_arr = []
let start = 0
for(var i = 0; i < data.season.length - 1; i++)
{
if(Math.abs(data.season[i] - data.season[i + 1])%11 > 1){
interval_arr.push([data.season[start], data.season[i]])
start=i+1
let start_i = 0
for(var i = 12; i > 0; i--){
if(data.season.includes(i)){
start_i = data.season.indexOf(i);
}
else{
break
}
}
interval_arr.push([data.season[start], data.season[data.season.length -1]])
var start = data.season[start_i]
var end_i
const len = data.season.length
for(var i = 0; i < len -1; i++){
if(data.season.includes((start + i) %12 + 1)){
end_i = (start_i + i + 1) % len
}
else{
interval_arr.push([start, data.season[end_i]])
start = data.season[(start + i + 1) % len]
}
}
if(interval_arr.length == 0){
interval_arr.push([start, data.season[end_i]])
}
return interval_arr
}
export let season_iv = season_intervals();
@ -29,20 +52,16 @@ font-family: sans-serif;
}
h1{
text-align: center;
padding: 0.5em 2em;
padding-block: 0.5em;
border-radius: 10000px;
margin:0;
font-size: 3rem;
}
.wrapper{
margin-inline: auto;
max-width: 700px;
padding-inline: 2rem;
}
.tags{
margin-block: 1rem;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 1em;
}
@ -64,12 +83,109 @@ h1{
background-color: var(--orange);
box-shadow: 0.1em 0.1em 0.2em 0.2em rgba(0,0,0,0.3);
}
.wrapper{
display: flex;
flex-direction: row;
max-width: 1000px;
justify-content: center;
margin-inline: auto;
}
@media screen and (max-width: 700px){
.wrapper{
flex-direction:column;
}
}
.title_container{
max-width: 1000px;
display: flex;
flex-direction: column;
margin-inline: auto;
}
.title{
position: relative;
width: min(800px, 80vw);
margin-inline: auto;
transform: translateY(-4rem);
background-color: var(--nord6);
padding: 1rem 2rem;
}
.title_container .img{
width: 100%;
height: 700px;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
.icon{
position: absolute;
top: -1em;
right: -0.75em;
text-decoration: unset;
background-color: #FAFAFE;
padding: 0.5em;
font-size: 1.5rem;
border-radius: 100000px;
transition: 100ms;
box-shadow: 0em 0em 1em 0.5em rgba(0,0,0,0.5);
}
.icon:hover,
.icon:focus-visible{
scale: 1.2 1.2;
animation: shake 0.5s ease forwards;
}
h4{
margin-block: 0;
}
.addendum{
max-width: 800px;
margin-inline: auto;
padding-inline: 2rem;
}
@media screen and (max-width: 800px){
.title{
width: 100%;
}
.icon{
right: 1rem;
}
}
@keyframes shake{
0%{
transform: rotate(0)
scale(1,1);
}
25%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle))
scale(1.2,1.2)
;
}
50%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(calc(-1* var(--angle)))
scale(1.2,1.2);
}
74%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle))
scale(1.2, 1.2);
}
100%{
transform: rotate(0)
scale(1.2,1.2);
}
}
</style>
<div class="wrapper">
<h1>{data.name}</h1>
<MultiImgWrapper wrap={data.images.length>1} class=double>
<!--<MultiImgWrapper wrap={data.images.length>1} class=double>
{#each data.images as img}
<img width=100% src="/images/{img.mediapath}" alt="{img.alt}">
<figure>
@ -78,76 +194,40 @@ h1{
{/if}
</figure>
{/each}
</MultiImgWrapper>
</MultiImgWrapper>-->
<div class=title_container>
<div class=img style="background-image: url({hero_img_src})"></div>
<div class=title>
<a class="icon" href='/rezepte/season/{data.season[0]}'>{data.icon}</a>
<h1>{data.name}</h1>
{#if data.preamble}
<p>{data.preamble}</p>
{/if}
<div class=tags>
Saison:
<h4>Saison:</h4>
{#each season_iv as season}
<a class=tag href="/rezepte/season/{season[0]}">{months[season[0] - 1]}-{months[season[1] - 1]}</a>
{/each}
</div>
<h4>Stichwörter:</h4>
<div class=tags>
Stichwörter:
{#each data.tags as tag}
<a href="/rezepte/tag/{tag}" class=tag>{tag}</a>
{/each}
</div>
</div>
</div>
{#if data.preparation}
<div>Vorbereitung: {data.preparation}</div>
{/if}
{#if data.fermentation}
{#if data.fermentation.bulk}
<div>Stockgare: {data.fermentation.bulk}</div>
{/if}
{#if data.fermentation.final}
<div>Stückgare: {data.fermentation.final}</div>
{/if}
{/if}
{#if data.baking}
<div>Backen: {data.baking.length} bei {data.baking.temperature} °C {data.baking.mode}</div>
{/if}
{#if data.total_time}
<div>Gesamtzeit: {data.total_time}</div>
{/if}
{#if data.ingredients}
<h2>Zutaten</h2>
{#each data.ingredients as list}
{#if list.name}
<h3>{list.name}</h3>
{/if}
<ul>
{#each list.list as item}
<li>{item.amount} {item.unit} {@html item.name}</li>
{/each}
</ul>
{/each}
{/if}
{#if data.instructions}
<h2>Zubereitung</h2>
{#each data.instructions as list}
{#if list.name}
<h3>{list.name}</h3>
{/if}
<ol>
{#each list.steps as step}
<li>{@html step}</li>
{/each}
</ol>
{/each}
{/if}
<div class=wrapper>
<IngredientsPage {data}></IngredientsPage>
<InstructionsPage {data}></InstructionsPage>
</div>
<div class=addendum>
{#if data.addendum}
{@html data.addendum}
{/if}

View File

@ -1,39 +1,92 @@
<script lang="ts">
import Check from '$lib/assets/icons/Check.svelte';
import SeasonSelect from '$lib/components/SeasonSelect.svelte';
import '$lib/css/action_button.css'
export let data: PageData;
let preamble = ""
let addendum = ""
import { season } from '$lib/js/season_store';
season.update(() => [])
let season_local
season.subscribe((s) => {
season_local = s
});
export let card_data ={
icon: "",
category: "",
name: "",
description: "",
tags: [],
}
let short_name
export let add_info ={
preparation: "",
fermentation: {
bulk: "",
final: "",
},
baking: {
length: "",
temperature: "",
mode: "",
},
total_time: "",
}
let images = []
export let portions = ""
let short_name = ""
let password
let datecreated = new Date()
let datemodified = datecreated
import type { PageData } from './$types';
import CardAdd from '$lib/components/CardAdd.svelte';
import SeasonSelect from '$lib/components/SeasonSelect.svelte';
export let season = []
import CreateIngredientList from '$lib/components/CreateIngredientList.svelte';
export let ingredients = []
import CreateStepList from '$lib/components/CreateStepList.svelte';
export let instructions = []
function get_season(){
let season = []
const el = document.getElementById("labels");
for(var i = 0; i < el.children.length; i++){
if(el.children[i].children[0].children[0].checked){
season.push(i+1)
}
}
return season
}
function write_season(season){
const el = document.getElementById("labels");
for(var i = 0; i < season.length; i++){
el.children[i].children[0].children[0].checked = true
}
}
async function doPost () {
console.log(add_info.total_time)
const res = await fetch('/api/add', {
method: 'POST',
body: JSON.stringify({
recipe: {
season: get_season(),
...card_data,
images: [{
mediapath: short_name + '.webp',
alt: "",
caption: ""
}],
...add_info,
images: {mediapath: short_name + '.webp', alt: "", caption: ""}, // TODO
season: season_local,
short_name,
datecreated,
datemodified,
instructions,
ingredients,
preamble,
addendum,
},
headers: {
'content-type': 'application/json',
@ -42,35 +95,146 @@
})
})
const json = await res.json()
result = JSON.stringify(json)
console.log(result)
}
</script>
<style>
input.temp{
all: unset;
input{
display: block;
border: unset;
margin: 1rem auto;
padding: 0.2em 1em;
padding: 0.5em 1em;
border-radius: 1000px;
background-color: var(--nord4);
font-size: 1.1rem;
transition: 100ms;
}
input:hover,
input:focus-visible
{
scale: 1.05 1.05;
}
.list_wrapper{
margin-inline: auto;
display: flex;
flex-direction: row;
max-width: 1000px;
gap: 2rem;
justify-content: center;
}
@media screen and (max-width: 700px){
.list_wrapper{
flex-direction: column;
}
}
input[type=password]{
box-sizing: border-box;
font-size: 1.5rem;
padding-block: 0.5em;
display: inline;
width: 100%;
}
.submit_wrapper{
position: relative;
margin-inline: auto;
width: max(300px, 50vw)
}
.submit_wrapper button{
position: absolute;
right:-1em;
bottom: -0.5em;
}
.submit_wrapper h2{
margin-bottom: 0;
}
h1{
text-align: center;
margin-bottom: 2rem;
}
.title_container{
max-width: 1000px;
display: flex;
flex-direction: column;
margin-inline: auto;
}
.title{
position: relative;
width: min(800px, 80vw);
margin-block: 2rem;
margin-inline: auto;
background-color: var(--nord6);
padding: 1rem 2rem;
}
.title p{
border: 2px solid var(--nord1);
border-radius: 10000px;
padding: 0.5em 1em;
font-size: 1.1rem;
transition: 200ms;
}
.title p:hover,
.title p:focus-within{
scale: 1.02 1.02;
}
.addendum{
font-size: 1.1rem;
max-width: 90%;
margin-inline: auto;
border: 2px solid var(--nord1);
border-radius: 45px;
padding: 1em 1em;
transition: 100ms;
}
.addendum:hover,
.addendum:focus-within
{
scale: 1.02 1.02;
}
.addendum_wrapper{
max-width: 1000px;
margin-inline: auto;
}
h3{
text-align: center;
}
</style>
<h1>Rezept hinzufügen</h1>
<h1>Rezept erstellen</h1>
<CardAdd {card_data}></CardAdd>
<input class=temp bind:value={short_name} placeholder="Kurzname"/>
<h3>Kurzname (für URL):</h3>
<input bind:value={short_name} placeholder="Kurzname"/>
<SeasonSelect {season}></SeasonSelect>
<div class=title_container>
<div class=title>
<h4>Eine etwas längere Beschreibung:</h4>
<p bind:innerText={preamble} contenteditable></p>
<div class=tags>
<h4>Saison:</h4>
<SeasonSelect></SeasonSelect>
</div>
</div>
</div>
<h2>Zutaten</h2>
<CreateIngredientList {ingredients}></CreateIngredientList>
<h2>Zubereitung</h2>
<CreateStepList {instructions} ></CreateStepList>
<input class=temp type="password" placeholder=Passwort bind:value={password}>
<button on:click={doPost}>ADD RECIPE</button>
<div class=list_wrapper>
<div>
<CreateIngredientList {ingredients} {portions}></CreateIngredientList>
</div>
<div>
<CreateStepList {instructions} {add_info}></CreateStepList>
</div>
</div>
<div class=addendum_wrapper>
<h3>Nachtrag:</h3>
<div class=addendum bind:innerText={addendum} contenteditable></div>
</div>
<div class=submit_wrapper>
<h2>Neues Rezept hinzufügen:</h2>
<input type="password" placeholder=Passwort bind:value={password}>
<button class=action_button on:click={doPost}><Check fill=white width=2rem height=2rem></Check></button>
</div>

View File

@ -1,5 +1,21 @@
<script lang="ts">
import Check from '$lib/assets/icons/Check.svelte';
import Cross from '$lib/assets/icons/Cross.svelte';
import SeasonSelect from '$lib/components/SeasonSelect.svelte';
import '$lib/css/action_button.css'
import { redirect } from '@sveltejs/kit';
export let data: PageData;
let preamble = data.recipe.preamble
let addendum = data.recipe.addendum
import { season } from '$lib/js/season_store';
season.update(() => data.recipe.season)
let season_local
season.subscribe((s) => {
season_local = s
});
let old_short_name = data.recipe.short_name
export let card_data ={
@ -9,8 +25,22 @@
description: data.recipe.description,
tags: data.recipe.tags,
}
export let add_info ={
preparation: data.recipe.preparation,
fermentation: {
bulk: data.recipe.fermentation.bulk,
final: data.recipe.fermentation.final,
},
baking: {
length: data.recipe.baking.length,
temperature: data.recipe.baking.temperature,
mode: data.recipe.baking.mode,
},
total_time: data.recipe.total_time,
}
let images = data.recipe.images
let season = data.recipe.season
export let portions = data.recipe.portions
let short_name = data.recipe.short_name
let password
@ -19,9 +49,6 @@
import type { PageData } from './$types';
import CardAdd from '$lib/components/CardAdd.svelte';
import MediaScroller from '$lib/components/MediaScroller.svelte';
import Card from '$lib/components/Card.svelte';
import Search from '$lib/components/Search.svelte';
import CreateIngredientList from '$lib/components/CreateIngredientList.svelte';
export let ingredients = data.recipe.ingredients
@ -29,6 +56,7 @@
import CreateStepList from '$lib/components/CreateStepList.svelte';
export let instructions = data.recipe.instructions
function get_season(){
let season = []
const el = document.getElementById("labels");
@ -46,19 +74,39 @@
}
}
async function doPost () {
async function doDelete(){
const response = confirm("Bist du dir sicher, dass du das Rezept löschen willst?")
if(!response){
return
}
const res = await fetch('/api/delete', {
method: 'POST',
body: JSON.stringify({
old_short_name,
headers: {
'content-type': 'application/json',
bearer: password,
}
})
})
}
async function doEdit() {
const res = await fetch('/api/edit', {
method: 'POST',
body: JSON.stringify({
recipe: {
...card_data,
...add_info,
images, // TODO
season: get_season(),
season: season_local,
short_name,
datecreated,
datemodified,
instructions,
ingredients,
addendum,
preamble
},
old_short_name,
headers: {
@ -67,36 +115,158 @@
}
})
})
const json = await res.json()
result = JSON.stringify(json)
console.log(result)
const item = await res.json();
}
</script>
<style>
input{
all: unset;
display: block;
border: unset;
margin: 1rem auto;
padding: 0.2em 1em;
padding: 0.5em 1em;
border-radius: 1000px;
background-color: var(--nord4);
font-size: 1.1rem;
transition: 100ms;
}
input:hover,
input:focus-visible
{
scale: 1.05 1.05;
}
.list_wrapper{
margin-inline: auto;
display: flex;
flex-direction: row;
max-width: 1000px;
gap: 2rem;
justify-content: center;
}
@media screen and (max-width: 700px){
.list_wrapper{
flex-direction: column;
}
}
input[type=password]{
box-sizing: border-box;
font-size: 1.5rem;
padding-block: 0.5em;
display: inline;
width: 100%;
}
.submit_wrapper{
position: relative;
margin-inline: auto;
width: max(300px, 50vw)
}
.submit_wrapper button{
position: absolute;
right:-1em;
bottom: -0.5em;
}
.submit_wrapper h2{
margin-bottom: 0;
}
h1{
text-align: center;
margin-bottom: 2rem;
}
.title_container{
max-width: 1000px;
display: flex;
flex-direction: column;
margin-inline: auto;
}
.title{
position: relative;
width: min(800px, 80vw);
margin-block: 2rem;
margin-inline: auto;
background-color: var(--nord6);
padding: 1rem 2rem;
}
.title p{
border: 2px solid var(--nord1);
border-radius: 10000px;
padding: 0.5em 1em;
font-size: 1.1rem;
transition: 200ms;
}
.title p:hover,
.title p:focus-within{
scale: 1.02 1.02;
}
.addendum{
font-size: 1.1rem;
max-width: 90%;
margin-inline: auto;
border: 2px solid var(--nord1);
border-radius: 45px;
padding: 1em 1em;
transition: 100ms;
}
.addendum:hover,
.addendum:focus-within
{
scale: 1.02 1.02;
}
.addendum_wrapper{
max-width: 1000px;
margin-inline: auto;
}
h3{
text-align: center;
}
.delete{
position: fixed;
right: 0;
bottom: 0;
margin: 2rem;
}
</style>
<h1>Rezept hinzufügen</h1>
<h1>Rezept editieren</h1>
<CardAdd {card_data}></CardAdd>
<button on:click={console.log(JSON.stringify(ingredients, null, 4))}>Printout Ingredients</button>
<button on:click={console.log(JSON.stringify(instructions, null, 4))}>Printout Instructions</button>
<button on:click={console.log(JSON.stringify(card_data, null, 4))}>Prinout Card Data</button>
<h3>Kurzname (für URL):</h3>
<input bind:value={short_name} placeholder="Kurzname"/>
<h2>Zutaten</h2>
<CreateIngredientList {ingredients}></CreateIngredientList>
<h2>Zubereitung</h2>
<CreateStepList {instructions} ></CreateStepList>
<div class=title_container>
<div class=title>
<h4>Eine etwas längere Beschreibung:</h4>
<p bind:innerText={preamble} contenteditable></p>
<div class=tags>
<h4>Saison:</h4>
<SeasonSelect></SeasonSelect>
</div>
</div>
</div>
<div class=list_wrapper>
<div>
<CreateIngredientList {ingredients} {portions}></CreateIngredientList>
</div>
<div>
<CreateStepList {instructions} {add_info}></CreateStepList>
</div>
</div>
<div class=addendum_wrapper>
<h3>Nachtrag:</h3>
<div class=addendum bind:innerText={addendum} contenteditable></div>
</div>
<div class=submit_wrapper>
<h2>Editiertes Rezept abspeichern:</h2>
<input type="password" placeholder=Passwort bind:value={password}>
<button on:click={doPost}>EDIT RECIPE</button>
<button class=action_button on:click={doEdit}><Check fill=white width=2rem height=2rem></Check></button>
</div>
<div class=submit_wrapper>
<h2>Rezept löschen:</h2>
<input type="password" placeholder=Passwort bind:value={password}>
<button class=action_button on:click={doDelete}><Cross fill=white width=2rem height=2rem></Cross></button>
</div>

View File

@ -38,6 +38,7 @@ export type RecipeModelType = {
name?: string;
steps: [string]
}]
preamble?: String
addendum?: string
};