Compare commits

...

8 Commits

Author SHA1 Message Date
cd02307a9d initial tests for cospend incl. image upload 2025-06-05 19:40:35 +02:00
dd71832b10 switch from "Unterwegs" to "Snack"
All checks were successful
CI / update (push) Successful in 49s
2025-03-31 17:57:40 +02:00
ce7a542408 fix copilot autocomplete svg messup
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 13:09:15 +01:00
86225d3237 fix docs autolink
All checks were successful
CI / update (push) Successful in 16s
2025-02-02 13:07:42 +01:00
18a5241c1e fix docs autolink
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 12:58:34 +01:00
17a5d6155d fix for svelte 4
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 12:55:33 +01:00
15bf4fd922 remove unused health.bocken.org and papers.bocken.org
All checks were successful
CI / update (push) Successful in 1m26s
2025-02-02 12:44:21 +01:00
aab1f7da9a added tips-and-tricks route
All checks were successful
CI / update (push) Successful in 19s
2024-10-28 17:00:43 +01:00
13 changed files with 478 additions and 43 deletions

37
src/lib/db/db.ts Normal file
View File

@@ -0,0 +1,37 @@
import mongoose from 'mongoose';
import { MONGO_URL } from '$env/static/private';
/*
0 - disconnected
1 - connected
2 - connecting
3 - disconnecting
4 - uninitialized
*/
const mongoConnection = {
isConnected: 0,
};
export const dbConnect = async () => {
if (mongoConnection.isConnected === 1) {
return;
}
if (mongoose.connections.length > 0) {
mongoConnection.isConnected = mongoose.connections[0].readyState;
if (mongoConnection.isConnected === 1) {
return;
}
await mongoose.disconnect();
}
await mongoose.connect(MONGO_URL ?? '');
mongoConnection.isConnected = 1;
};
export const dbDisconnect = async () => {
if (process.env.NODE_ENV === 'development') return;
if (mongoConnection.isConnected === 0) return;
await mongoose.disconnect();
mongoConnection.isConnected = 0;
};

25
src/lib/models/Payment.ts Normal file
View File

@@ -0,0 +1,25 @@
import mongoose from 'mongoose';
const PaymentSchema = new mongoose.Schema(
{
type: {type: String, required: true, enum: ['payment', 'reimbursement']},
name: {type: String, required: true},
category : {type: String, required: false,},
date: {type: Date, default: Date.now},
images: [ {
mediapath: {type: String, required: false},
}],
description: {type: String, required: false},
note: {type: String, required: false},
tags : [String],
original_amount: {type: Number, required: true},
total_amount: {type: Number, required: true},
personal_amounts: [{
user: {type:String, required: true},
amount: {type: Number, required: true, default:0}
}],
currency: {type: String, required: true, default: 'CHF'},
}, {timestamps: true}
);
export const Payment= mongoose.model("Payment", PaymentSchema);

View File

@@ -1,24 +1,7 @@
<script> <script lang="ts">
import "$lib/css/nordtheme.css"; import "$lib/css/nordtheme.css";
import LinksGrid from "$lib/components/LinksGrid.svelte"; import LinksGrid from "$lib/components/LinksGrid.svelte";
export let data; export let data;
import { page } from "$app/stores"
const redirect_to_docs = () => {
if (!data.session){
alert("Du musst dich einloggen, um diese Seite zu betreten.");
window.location.href = "/auth/signin";
}
else if (data.session.user.groups.includes("paperless_users")){
window.location.href = "https://docs.bocken.org";
}
else if (data.session.user.groups.includes("paperless_eltern_users")){
window.location.href = "https://dokumente.bocken.org";
}
else
alert("Du hast keine Berechtigung, diese Seite zu betreten.");
}
</script> </script>
<style> <style>
.hero{ .hero{
@@ -148,19 +131,24 @@ section h2{
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>
<h3>Transmission</h3> <h3>Transmission</h3>
</a> </a>
<!-- TODO: clean this up with proper aria roles etc --> <!-- instead of redirect_to_docs(), use a normal link with internal checks for data.session -->
<a on:click="{() => redirect_to_docs()}" > {#if !data.session}
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg> <a href="/auth/signin">
<h3>Dokumente</h3> <svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg>
</a> <h3>Dokumente</h3>
<a href=https://papers.bocken.org> </a>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M320 32c-8.1 0-16.1 1.4-23.7 4.1L15.8 137.4C6.3 140.9 0 149.9 0 160s6.3 19.1 15.8 22.6l57.9 20.9C57.3 229.3 48 259.8 48 291.9v28.1c0 28.4-10.8 57.7-22.3 80.8c-6.5 13-13.9 25.8-22.5 37.6C0 442.7-.9 448.3 .9 453.4s6 8.9 11.2 10.2l64 16c4.2 1.1 8.7 .3 12.4-2s6.3-6.1 7.1-10.4c8.6-42.8 4.3-81.2-2.1-108.7C90.3 344.3 86 329.8 80 316.5V291.9c0-30.2 10.2-58.7 27.9-81.5c12.9-15.5 29.6-28 49.2-35.7l157-61.7c8.2-3.2 17.5 .8 20.7 9s-.8 17.5-9 20.7l-157 61.7c-12.4 4.9-23.3 12.4-32.2 21.6l159.6 57.6c7.6 2.7 15.6 4.1 23.7 4.1s16.1-1.4 23.7-4.1L624.2 182.6c9.5-3.4 15.8-12.5 15.8-22.6s-6.3-19.1-15.8-22.6L343.7 36.1C336.1 33.4 328.1 32 320 32zM128 408c0 35.3 86 72 192 72s192-36.7 192-72L496.7 262.6 354.5 314c-11.1 4-22.8 6-34.5 6s-23.5-2-34.5-6L143.3 262.6 128 408z"/></svg> {:else if data.session.user.groups.includes("paperless_users")}
<h3>Papers</h3> <a href="https://docs.bocken.org">
</a> <svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg>
<a href=https://health.bocken.org> <h3>Dokumente</h3>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M96 64c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32l0 160 0 64 0 160c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-64-32 0c-17.7 0-32-14.3-32-32l0-64c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-64c0-17.7 14.3-32 32-32l32 0 0-64zm448 0l0 64 32 0c17.7 0 32 14.3 32 32l0 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 64c0 17.7-14.3 32-32 32l-32 0 0 64c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-160 0-64 0-160c0-17.7 14.3-32 32-32l32 0c17.7 0 32 14.3 32 32zM416 224l0 64-192 0 0-64 192 0z"/></svg> </a>
<h3>Gym</h3> {:else if data.session.user.groups.includes("paperless_eltern_users")}
</a> <a href="https://dokumente.bocken.org">
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m106 512h300c24.814 0 45-20.186 45-45v-317h-105c-24.814 0-45-20.186-45-45v-105h-195c-24.814 0-45 20.186-45 45v422c0 24.814 20.186 45 45 45zm60-301h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h180c8.291 0 15 6.709 15 15s-6.709 15-15 15h-180c-8.291 0-15-6.709-15-15s6.709-15 15-15zm0 60h120c8.291 0 15 6.709 15 15s-6.709 15-15 15h-120c-8.291 0-15-6.709-15-15s6.709-15 15-15z"/><path d="m346 120h96.211l-111.211-111.211v96.211c0 8.276 6.724 15 15 15z"/></svg>
<h3>Dokumente</h3>
</a>
{/if}
<a href=https://audio.bocken.org> <a href=https://audio.bocken.org>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48l0 128c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80l0-16 0-48 0-48C0 146.6 114.6 32 256 32s256 114.6 256 256l0 48 0 48 0 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48l0-128c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>
<h3>Hörbücher & Podcasts</h3> <h3>Hörbücher & Podcasts</h3>

View File

@@ -0,0 +1,96 @@
import type { RequestHandler } from '@sveltejs/kit';
import fs from 'fs';
import path from 'path';
import { mkdir } from 'fs/promises';
import { Payment } from '$lib/models/Payment'; // adjust path as needed
import { dbConnect, dbDisconnect } from '$lib/db/db';
const UPLOAD_DIR = '/var/lib/www/static/test';
const BASE_CURRENCY = 'CHF'; // Default currency
export const POST: RequestHandler = async ({ request, locals }) => {
const formData = await request.formData();
try {
const name = formData.get('name') as string;
const category = formData.get('category') as string;
const date= new Date(formData.get('date') as string);
const description = formData.get('description') as string;
const note = formData.get('note') as string;
const tags = JSON.parse(formData.get('tags') as string) as string[];
let currency = formData.get('currency') as string;
let original_amount = parseFloat(formData.get('original_amount') as string);
let total_amount = NaN;
// if currency is not BASE_CURRENCY, fetch current conversion rate using frankfurter API and date in YYYY-MM-DD format
if (!currency || currency === BASE_CURRENCY) {
currency = BASE_CURRENCY;
total_amount = parseFloat(formData.get('total_amount') as string);
} else {
const date_fmt = date.toISOString().split('T')[0]; // Convert date to YYYY-MM-DD format
// Fetch conversion rate logic here (not implemented in this example)
const res = await fetch(`https://api.frankfurter.app/${date_fmt}?from=${currency}&to=${BASE_CURRENCY}`)
const { result } = await res.json();
if (!result || !result[BASE_CURRENCY]) {
return new Response(JSON.stringify({ message: 'Currency conversion failed.' }), { status: 400 });
}
// Assuming you want to convert the total amount to BASE_CURRENCY
const conversionRate = parseFloat(result.rates[BASE_CURRENCY]);
alert(`Conversion rate from ${currency} to ${BASE_CURRENCY} on ${date_fmt}: ${conversionRate}`);
total_amount = original_amount * conversionRate;
}
const personal_amounts = JSON.parse(formData.get('personal_amounts') as string) as { user: string, amount: number }[];
if (!name || isNaN(total_amount)) {
return new Response(JSON.stringify({ message: 'Invalid required fields.' }), { status: 400 });
}
// await mkdir(UPLOAD_DIR, { recursive: true });
const images: { mediapath: string }[] = [];
const imageFiles = formData.getAll('images') as File[];
for (const file of imageFiles) {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const safeName = `${Date.now()}_${file.name.replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
const fullPath = path.join(UPLOAD_DIR, safeName);
//fs.writeFileSync(fullPath, buffer);
images.push({ mediapath: `/static/test/${safeName}` });
}
await dbConnect();
const payment = new Payment({
name,
category,
date,
description,
note,
tags,
total_amount,
original_amount,
currency,
personal_amounts,
images
});
// let auth = await locals.auth();
// // if(!auth){
// throw error(401, "Not logged in")
// }
try{
await Payment.create(payment);
} catch(e){
throw error(400, e)
}
await dbDisconnect();
return new Response(JSON.stringify({ message: 'Payment event created successfully.' }), { status: 201 });
} catch (err) {
console.error(err);
return new Response(JSON.stringify({ message: 'Error processing request.' }), { status: 500 });
}
};

View File

@@ -0,0 +1,7 @@
import type { PageServerLoad } from "./$types"
export const load : PageServerLoad = (async ({locals}) => {
return {
session: await locals.auth(),
}
});

View File

@@ -0,0 +1,18 @@
<script>
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
export let data
let username = ""
if(data.user){
username = data.user.username
}
</script>
<Header>
<ul class=site_header slot=links>
<li><a href="/glaube/gebete">Gebete</a></li>
<li><a href="/glaube/rosenkranz">Rosenkranz</a></li>
<li><a href="/glaube/predigten">Predigten</a></li>
</ul>
<UserHeader {username} slot=right_side></UserHeader>
<slot></slot>
</Header>

View File

@@ -0,0 +1,133 @@
<style>
h1{
text-align: center;
font-size: 3em;
}
p{
max-width: 600px;
margin: 0 auto;
font-size: 1.1em;
}
</style>
<h1>Settlement Plan</h1>
<script lang="ts">
import { onMount } from 'svelte';
let name = '';
let category = '';
let date = new Date().toISOString().split('T')[0]; // format as yyyy-mm-dd
let images: File[] = [];
let description = '';
let note = '';
let tags = '';
let original_amount = 0;
let currency = 'CHF';
let payment_method = '';
let personal_amounts = [
{ user: 'alexander', amount: 0 },
{ user: 'anna', amount: 0 }
];
const handleSubmit = async () => {
const formData = new FormData();
formData.append('name', name);
formData.append('category', category);
formData.append('dateCreated', date);
formData.append('description', description);
formData.append('note', note);
formData.append('tags', JSON.stringify(tags.split(',').map(tag => tag.trim())));
formData.append('total_amount', total_amount.toString());
formData.append('currency', currency);
formData.append('payment_method', payment_method);
formData.append('personal_amounts', JSON.stringify(personal_amounts));
images.forEach((file, index) => {
formData.append('images', file);
});
const res = await fetch('/api/cospend/add', {
method: 'POST',
body: formData
});
const result = await res.json();
alert(result.message);
};
</script>
<form on:submit|preventDefault={handleSubmit} class="flex flex-col gap-4 max-w-xl">
<label>
Name:
<input type="text" bind:value={name} required />
</label>
<label>
Category:
<input type="text" bind:value={category} />
</label>
<label>
Date Created:
<input type="date" bind:value={date} />
</label>
<label>
Images:
<input type="file" multiple accept="image/*" on:change={(e) => images = Array.from(e.target.files)} />
</label>
<label>
Description:
<textarea bind:value={description}></textarea>
</label>
<label>
Note:
<textarea bind:value={note}></textarea>
</label>
<label>
Tags (comma separated):
<input type="text" bind:value={tags} />
</label>
<label>
Total Amount:
<input type="number" bind:value={original_amount} step="0.01" required />
</label>
<fieldset>
<legend>Personal Amounts</legend>
{#each personal_amounts as entry, i}
<div class="flex gap-2 items-center">
<label>{entry.user}</label>
<input type="number" bind:value={personal_amounts[i].amount} step="0.01" required />
</div>
{/each}
</fieldset>
<label>
Currency:
<select bind:value={currency}>
<option value="CHF">CHF</option>
<option value="EUR">EUR</option>
<option value="USD">USD</option>
</select>
</label>
<label>
Payment Method:
<select bind:value={payment_method}>
<option value="">-- Select --</option>
<option value="cash">Cash</option>
<option value="bank_transfer">Bank Transfer</option>
<option value="credit_card">Credit Card</option>
<option value="twint">Twint</option>
</select>
</label>
<button type="submit">Save Payment</button>
</form>

View File

@@ -70,6 +70,6 @@
<path d="m377.943 50.035v-35.035c0-8.284-6.716-15-15-15h-76.926c-11.523 0-22.046 4.357-30.017 11.505-7.971-7.148-18.494-11.505-30.018-11.505h-76.926c-8.284 0-15 6.716-15 15v35.035z"/> <path d="m377.943 50.035v-35.035c0-8.284-6.716-15-15-15h-76.926c-11.523 0-22.046 4.357-30.017 11.505-7.971-7.148-18.494-11.505-30.018-11.505h-76.926c-8.284 0-15 6.716-15 15v35.035z"/>
</g> </g>
</svg> </svg>
<h3>Predigten<h3> <h3>Predigten</h3>
</a> </a>
</LinksGrid> </LinksGrid>

View File

@@ -463,19 +463,19 @@ Der unten abgebildete Rosenkranz zeigt die aktuellen Gehmeinisse des Tages nach
</g> </g>
<g id=lbead5> <g id=lbead5>
<circle class=lbead /> <circle class=lbead />
<circle class=hitbox onclick="" /> <circle class=hitbox onclick="{true}" />
</g> </g>
<g id=lbead4> <g id=lbead4>
<circle class=lbead /> <circle class=lbead />
<circle class=hitbox onclick="" /> <circle class=hitbox onclick="{true}" />
</g> </g>
<g id=lbead3> <g id=lbead3>
<circle class=lbead /> <circle class=lbead />
<circle class=hitbox onclick="" /> <circle class=hitbox onclick="{true}" />
</g> </g>
<g id=lbead6> <g id=lbead6>
<circle class=lbead /> <circle class=lbead />
<circle class=hitbox onclick="" /> <circle class=hitbox onclick="{true}" />
</g> </g>
</g> </g>
<g class=beforedecades> <g class=beforedecades>
@@ -490,11 +490,11 @@ Der unten abgebildete Rosenkranz zeigt die aktuellen Gehmeinisse des Tages nach
</g> </g>
<g id=lbead1> <g id=lbead1>
<circle class=lbead /> <circle class=lbead />
<circle class=hitbox onclick="" /> <circle class=hitbox onclick="{true}" />
</g> </g>
<g id=lbead2> <g id=lbead2>
<circle class=lbead /> <circle class=lbead />
<circle class=hitbox onclick="" /> <circle class=hitbox onclick="{true}" />
</g> </g>
</g> </g>
</g> </g>
@@ -511,7 +511,7 @@ Dieser Plan ist wie folgt:
<div class=table > <div class=table >
<table> <table>
<tbody> <tbody>
<thead> <tr>
<td>Mo</td> <td>Mo</td>
<td>Di</td> <td>Di</td>
<td>Mi</td> <td>Mi</td>
@@ -519,7 +519,7 @@ Dieser Plan ist wie folgt:
<td>Fr</td> <td>Fr</td>
<td>Sa</td> <td>Sa</td>
<td>So</td> <td>So</td>
</thead> </tr>
<tr> <tr>
<td>freudenreich</td> <td>freudenreich</td>
<td>schmerzhaft</td> <td>schmerzhaft</td>
@@ -546,7 +546,7 @@ Der Plan ohne lichtreiche Geheimnisse ist wie folgt:
<div class=table> <div class=table>
<table> <table>
<tbody> <tbody>
<thead> <tr>
<td>Mo</td> <td>Mo</td>
<td>Di</td> <td>Di</td>
<td>Mi</td> <td>Mi</td>
@@ -554,7 +554,7 @@ Der Plan ohne lichtreiche Geheimnisse ist wie folgt:
<td>Fr</td> <td>Fr</td>
<td>Sa</td> <td>Sa</td>
<td>So</td> <td>So</td>
</thead> </tr>
<tr> <tr>
<td>freudenreich</td> <td>freudenreich</td>
<td>schmerzhaft</td> <td>schmerzhaft</td>

View File

@@ -15,6 +15,7 @@ if(data.session){
<li><a href="/rezepte/category">Kategorie</a></li> <li><a href="/rezepte/category">Kategorie</a></li>
<li><a href="/rezepte/icon">Icon</a></li> <li><a href="/rezepte/icon">Icon</a></li>
<li><a href="/rezepte/tag">Stichwörter</a></li> <li><a href="/rezepte/tag">Stichwörter</a></li>
<li><a href="/rezepte/tips-and-tricks">Tipps</a></li>
</ul> </ul>
<UserHeader slot=right_side {user}></UserHeader> <UserHeader slot=right_side {user}></UserHeader>
<slot></slot> <slot></slot>

View File

@@ -6,7 +6,7 @@
import Search from '$lib/components/Search.svelte'; import Search from '$lib/components/Search.svelte';
export let data: PageData; export let data: PageData;
export let current_month = new Date().getMonth() + 1 export let current_month = new Date().getMonth() + 1
const categories = ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Unterwegs"] const categories = ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Snack"]
</script> </script>
<style> <style>
h1{ h1{

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import type { PageData } from './$types';
import AddButton from '$lib/components/AddButton.svelte';
import Converter from './Converter.svelte';
</script>
<style>
h1{
text-align: center;
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
.content{
max-width: 800px;
margin: 0 auto;
background-color: var(--nord0);
padding: 1rem;
margin-block: 1rem;
}
</style>
<svelte:head>
<title>Bocken Rezepte</title>
<meta name="description" content="Eine stetig wachsende Ansammlung an Rezepten aus der Bockenschen Küche." />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/ragu_aus_rindsrippen.webp" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="Pasta al Ragu mit Linguine" />
</svelte:head>
<h1>Tipps & Tricks</h1>
<div class=content>
<h2>Trockenhefe vs. Frischhefe</h2>
<Converter></Converter>
<p>
Frischhefe ist mit Trockenhefe ersetzbar, jedoch muss man ein paar Kleinigkeiten beachten:
</p>
<ol>
<li>Nur ein Drittel der Menge verwenden.</li>
<li>Falls ein kalter Teig zubereitet wird, die Trockenhefe umbedingt zuerst zur Gärprobe in warmer Flüssigkeit (je nach Rezept z.B. Milch oder Wasser) mit einem TL Zucker für ~10 Minuten <q>aufwachen</q> lassen.</li>
<li>Generell ist die Meinung das Trockenhefe etwas <q>energischer</q> ist am Anfang der Gärung und etwas langsamer am Ende der Gare. Dementsprechend eventuell die Stock- und Stückgare verkürzen bzw. verlängern.</li>
</ol>
</div>
<div class=content>
<h2>Fensterprobe</h2>
<p>
Die Fensterprobe ist eine Methode um den optimalen Knetzustand eines Teiges zu bestimmen.
Dazu wird ein kleines, ca. Walnussgrosses Stück Teig zwischen den Fingern auseinandergezogen. Ist der Teig elastisch und reißt nicht bis der Teig so dünn ist, dass man leicht licht durchsehen kann, so ist der Teig optimal verknetet.
</p>
<p>
Teig lässt sich leichter verkneten wenn er noch trockener ist. Daher lohnt es sich zunächst etwa 10% der Flüssigkeit zurückzuhalten und erst nach und nach zuzugeben nachdem der Teig bereits für einige Minuten geknetet wurde.
</p>
</div>
<AddButton></AddButton>

View File

@@ -0,0 +1,68 @@
<script>
class HefeConverter {
constructor(trockenhefe = 1) {
this._trockenhefe = trockenhefe;
this._frischhefe = this._trockenhefe * 3;
}
get trockenhefe() {
return Math.round(this._trockenhefe * 100) / 100 + "g";
}
set trockenhefe(value) {
this._trockenhefe = value.replace(/\D/g, '');
this._frischhefe = this._trockenhefe * 3;
}
get frischhefe() {
return this._frischhefe+"g";
}
set frischhefe(value) {
this._frischhefe = value.replace(/\D/g, '');
this._trockenhefe = this._frischhefe / 3;
}
}
const hefeConverter = new HefeConverter();
</script>
<style>
.converter_container {
width: fit-content;
display: flex;
flex-direction: row;
justify-content: center;
gap: 2rem;
background-color: var(--blue);
padding: 2rem;
margin-inline: auto;
align-items: center;
}
input {
width: 5rem;
height: 2rem;
font-size: 1rem;
text-align: center;
}
.flex_column {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 0.25rem;
}
</style>
<div class=converter_container>
<div class="flex_column">
<label for="trockenhefe">Trockenhefe</label>
<input type="text" bind:value={hefeConverter.trockenhefe} min="0" />
</div>
<div>
=
</div>
<div class="flex_column">
<label for="frischhefe">Frischhefe</label>
<input type="text" bind:value={hefeConverter.frischhefe} min="0"/>
</div>
</div>