Compare commits
13 Commits
367ea7a17e
...
svelte5
Author | SHA1 | Date | |
---|---|---|---|
7f06717615
|
|||
4f34ff5329
|
|||
69e719780c
|
|||
e2a76d4080
|
|||
b847b8f1c8
|
|||
b861f9aeec
|
|||
dd71832b10
|
|||
ce7a542408
|
|||
86225d3237
|
|||
18a5241c1e
|
|||
17a5d6155d
|
|||
15bf4fd922
|
|||
aab1f7da9a
|
88
README_DEV_AUTH.md
Normal file
88
README_DEV_AUTH.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Development Authentication Bypass
|
||||
|
||||
This document explains how to safely disable authentication during development.
|
||||
|
||||
## 🔐 Security Overview
|
||||
|
||||
The authentication bypass is designed with multiple layers of security:
|
||||
|
||||
1. **Development Mode Only**: Only works when `vite dev` is running
|
||||
2. **Explicit Opt-in**: Requires setting `DEV_DISABLE_AUTH=true`
|
||||
3. **Production Protection**: Build fails if enabled in production mode
|
||||
4. **Environment Isolation**: Uses local environment files (gitignored)
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### 1. Create Local Environment File
|
||||
|
||||
Create `.env.local` (this file is gitignored):
|
||||
|
||||
```bash
|
||||
# Copy from example
|
||||
cp .env.local.example .env.local
|
||||
```
|
||||
|
||||
### 2. Enable Development Bypass
|
||||
|
||||
Edit `.env.local` and set:
|
||||
|
||||
```env
|
||||
DEV_DISABLE_AUTH=true
|
||||
```
|
||||
|
||||
### 3. Start Development Server
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
You'll see a warning in the console:
|
||||
```
|
||||
🚨 AUTH DISABLED: Development mode with DEV_DISABLE_AUTH=true
|
||||
```
|
||||
|
||||
### 4. Access Protected Routes
|
||||
|
||||
Protected routes (`/rezepte/edit/*`, `/rezepte/add`) will now be accessible without authentication.
|
||||
|
||||
## 🛡️ Security Guarantees
|
||||
|
||||
### Production Safety
|
||||
- **Build-time Check**: Production builds fail if `DEV_DISABLE_AUTH=true`
|
||||
- **Runtime Check**: Double verification using `dev` flag from `$app/environment`
|
||||
- **No Environment Leakage**: Uses `process.env` (server-only) not client environment
|
||||
|
||||
### Development Isolation
|
||||
- **Gitignored Files**: `.env.local` is never committed
|
||||
- **Example Template**: `.env.local.example` shows safe defaults
|
||||
- **Clear Warnings**: Console warns when auth is disabled
|
||||
|
||||
## 🧪 Testing the Security
|
||||
|
||||
### Test Production Build Safety
|
||||
```bash
|
||||
# This should FAIL with security error
|
||||
DEV_DISABLE_AUTH=true pnpm run build
|
||||
```
|
||||
|
||||
### Test Normal Production Build
|
||||
```bash
|
||||
# This should succeed
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## 🔄 Re-enabling Authentication
|
||||
|
||||
Set in `.env.local`:
|
||||
```env
|
||||
DEV_DISABLE_AUTH=false
|
||||
```
|
||||
|
||||
Or simply delete/rename the `.env.local` file.
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
- **Never** commit `.env.local` to git
|
||||
- **Never** set `DEV_DISABLE_AUTH=true` in production environment
|
||||
- The bypass provides a mock session with `rezepte_users` group access
|
||||
- All other authentication flows (signin pages, etc.) remain unchanged
|
3318
package-lock_old.json
Normal file
3318
package-lock_old.json
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -10,22 +10,24 @@
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.0",
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"svelte": "^4.0.0",
|
||||
"svelte-check": "^3.4.6",
|
||||
"svelte-preprocess-import-assets": "^1.0.1",
|
||||
"@auth/core": "^0.40.0",
|
||||
"@sveltejs/adapter-auto": "^6.1.0",
|
||||
"@sveltejs/kit": "^2.37.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.1.3",
|
||||
"@types/node": "^22.12.0",
|
||||
"svelte": "^5.38.6",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^5.0.0"
|
||||
"vite": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/sveltekit": "^0.14.0",
|
||||
"@sveltejs/adapter-node": "^2.0.0",
|
||||
"@auth/sveltekit": "^1.10.0",
|
||||
"@sveltejs/adapter-node": "^5.0.0",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"mongoose": "^7.4.0",
|
||||
"sharp": "^0.32.3"
|
||||
"mongoose": "^8.0.0",
|
||||
"sharp": "^0.33.0"
|
||||
}
|
||||
}
|
||||
|
3211
pnpm-lock.yaml
generated
3211
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
18
src/app.d.ts
vendored
18
src/app.d.ts
vendored
@@ -1,12 +1,28 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
import type { Session } from "@auth/sveltekit";
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
interface Locals {
|
||||
auth(): Promise<Session | null>;
|
||||
}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@auth/sveltekit" {
|
||||
interface Session {
|
||||
user?: {
|
||||
name?: string | null;
|
||||
email?: string | null;
|
||||
image?: string | null;
|
||||
nickname?: string;
|
||||
groups?: string[];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
@@ -20,8 +20,8 @@ export const { handle, signIn, signOut } = SvelteKitAuth({
|
||||
return token;
|
||||
},
|
||||
session: async ({session, token}) => {
|
||||
session.user.nickname = token.nickname;
|
||||
session.user.groups = token.groups;
|
||||
session.user.nickname = token.nickname as string;
|
||||
session.user.groups = token.groups as string[];
|
||||
return session;
|
||||
},
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import * as auth from "./auth"
|
||||
async function authorization({ event, resolve }) {
|
||||
// Protect any routes under /authenticated
|
||||
if (event.url.pathname.startsWith('/rezepte/edit') || event.url.pathname.startsWith('/rezepte/add')) {
|
||||
const session = await event.locals.getSession();
|
||||
const session = await event.locals.auth();
|
||||
if (!session) {
|
||||
redirect(303, '/auth/signin');
|
||||
}
|
||||
|
1
src/lib/components/.jukit/.jukit_info.json
Normal file
1
src/lib/components/.jukit/.jukit_info.json
Normal file
@@ -0,0 +1 @@
|
||||
{"terminal": "nvimterm"}
|
@@ -52,6 +52,24 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
|
||||
}
|
||||
.icon{
|
||||
font-family: "Noto Color Emoji", emoji, sans-serif;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
right: -25px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--nord0);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5em;
|
||||
box-shadow: 0em 0em 1em 0.1em rgba(0, 0, 0, 0.6);
|
||||
transition: 100ms;
|
||||
z-index: 10;
|
||||
}
|
||||
#image{
|
||||
width: 300px;
|
||||
@@ -135,6 +153,7 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
|
||||
margin-bottom: 0.5em;
|
||||
transition: 100ms;
|
||||
box-shadow: 0em 0em 0.2em 0.05em rgba(0, 0, 0, 0.3);
|
||||
border: none;
|
||||
}
|
||||
.tag:hover,
|
||||
.tag:focus-visible
|
||||
@@ -159,7 +178,8 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
|
||||
padding-inline: 1em;
|
||||
border-radius: 1000px;
|
||||
transition: 100ms;
|
||||
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card_title .category:hover,
|
||||
.card_title .category:focus-within
|
||||
@@ -172,6 +192,18 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
|
||||
scale: 0.9 0.9;
|
||||
}
|
||||
|
||||
.icon:hover,
|
||||
.icon:focus-visible
|
||||
{
|
||||
transform: scale(1.1, 1.1);
|
||||
background-color: var(--nord3);
|
||||
box-shadow: 0.2em 0.2em 1em 0.1em rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.icon:focus {
|
||||
transform: scale(0.9, 0.9);
|
||||
}
|
||||
|
||||
.card:hover .icon,
|
||||
.card:focus-visible .icon
|
||||
{
|
||||
@@ -193,17 +225,17 @@ const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
|
||||
</div>
|
||||
</div>
|
||||
{#if icon_override || recipe.season.includes(current_month)}
|
||||
<a class=icon href="/rezepte/icon/{recipe.icon}">{recipe.icon}</a>
|
||||
<button class=icon on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/icon/${recipe.icon}`}}>{recipe.icon}</button>
|
||||
{/if}
|
||||
<div class="card_title">
|
||||
<a class=category href="/rezepte/category/{recipe.category}" >{recipe.category}</a>
|
||||
<button class=category on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/category/${recipe.category}`}}>{recipe.category}</button>
|
||||
<div>
|
||||
<div class=name>{@html recipe.name}</div>
|
||||
<div class=description>{@html recipe.description}</div>
|
||||
</div>
|
||||
<div class=tags>
|
||||
{#each recipe.tags as tag}
|
||||
<a class=tag href="/rezepte/tag/{tag}">{tag}</a>
|
||||
<button class=tag on:click={(e) => {e.stopPropagation(); window.location.href = `/rezepte/tag/${tag}`}}>{tag}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
34
src/lib/components/HefeSwapper.svelte
Normal file
34
src/lib/components/HefeSwapper.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
// get ingredients_store from IngredientsPage.svelte
|
||||
import ingredients_store from './IngredientsPage.svelte';
|
||||
let ingredients = [];
|
||||
ingredients_store.subscribe(value => {
|
||||
ingredients = value;
|
||||
});
|
||||
function toggleHefe(){
|
||||
if(data.ingredients[i].list[j].name == "Frischhefe"){
|
||||
data.ingredients[i].list[j].name = "Trockenhefe"
|
||||
data.ingredients[i].list[j].amount = item.amount / 3
|
||||
}
|
||||
else{
|
||||
item.name = "Frischhefe"
|
||||
item.amount = item.amount * 3
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
button{
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg{
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
fill: var(--blue);
|
||||
}
|
||||
</style>
|
||||
<button onclick={toggleHefe}>
|
||||
{item.amount} {item.unit} {item.name}
|
||||
<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="M105.1 202.6c7.7-21.8 20.2-42.3 37.8-59.8c62.5-62.5 163.8-62.5 226.3 0L386.3 160 352 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l111.5 0c0 0 0 0 0 0l.4 0c17.7 0 32-14.3 32-32l0-112c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 35.2L414.4 97.6c-87.5-87.5-229.3-87.5-316.8 0C73.2 122 55.6 150.7 44.8 181.4c-5.9 16.7 2.9 34.9 19.5 40.8s34.9-2.9 40.8-19.5zM39 289.3c-5 1.5-9.8 4.2-13.7 8.2c-4 4-6.7 8.8-8.1 14c-.3 1.2-.6 2.5-.8 3.8c-.3 1.7-.4 3.4-.4 5.1L16 432c0 17.7 14.3 32 32 32s32-14.3 32-32l0-35.1 17.6 17.5c0 0 0 0 0 0c87.5 87.4 229.3 87.4 316.7 0c24.4-24.4 42.1-53.1 52.9-83.8c5.9-16.7-2.9-34.9-19.5-40.8s-34.9 2.9-40.8 19.5c-7.7 21.8-20.2 42.3-37.8 59.8c-62.5 62.5-163.8 62.5-226.3 0l-.1-.1L125.6 352l34.4 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L48.4 288c-1.6 0-3.2 .1-4.8 .3s-3.1 .5-4.6 1z"/></svg>
|
||||
</button>
|
44
src/lib/db/db.ts
Normal file
44
src/lib/db/db.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/recipes';
|
||||
|
||||
if (!MONGODB_URI) {
|
||||
throw new Error('Please define the MONGODB_URI environment variable inside .env.local');
|
||||
}
|
||||
|
||||
/**
|
||||
* Global is used here to maintain a cached connection across hot reloads
|
||||
* in development. This prevents connections growing exponentially
|
||||
* during API Route usage.
|
||||
*/
|
||||
let cached = (global as any).mongoose;
|
||||
|
||||
if (!cached) {
|
||||
cached = (global as any).mongoose = { conn: null, promise: null };
|
||||
}
|
||||
|
||||
export async function dbConnect() {
|
||||
if (cached.conn) {
|
||||
return cached.conn;
|
||||
}
|
||||
|
||||
if (!cached.promise) {
|
||||
const opts = {
|
||||
bufferCommands: false,
|
||||
};
|
||||
|
||||
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
|
||||
return mongoose;
|
||||
});
|
||||
}
|
||||
cached.conn = await cached.promise;
|
||||
return cached.conn;
|
||||
}
|
||||
|
||||
export async function dbDisconnect() {
|
||||
if (cached.conn) {
|
||||
await cached.conn.disconnect();
|
||||
cached.conn = null;
|
||||
cached.promise = null;
|
||||
}
|
||||
}
|
14
src/lib/models/Payment.ts
Normal file
14
src/lib/models/Payment.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const paymentSchema = new mongoose.Schema({
|
||||
paid_by: { type: String, required: true },
|
||||
total_amount: { type: Number, required: true },
|
||||
for_self: { type: Number, default: 0 },
|
||||
for_other: { type: Number, default: 0 },
|
||||
currency: { type: String, default: 'CHF' },
|
||||
description: String,
|
||||
date: { type: Date, default: Date.now },
|
||||
receipt_image: String
|
||||
});
|
||||
|
||||
export const Payment = mongoose.models.Payment || mongoose.model('Payment', paymentSchema);
|
@@ -1,6 +1,6 @@
|
||||
import type { PageServerLoad } from "./$types"
|
||||
import type { LayoutServerLoad } from "./$types"
|
||||
|
||||
export const load : PageServerLoad = (async ({locals}) => {
|
||||
export const load : LayoutServerLoad = (async ({locals}) => {
|
||||
return {
|
||||
session: await locals.auth(),
|
||||
}
|
||||
|
@@ -1,24 +1,7 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "$lib/css/nordtheme.css";
|
||||
import LinksGrid from "$lib/components/LinksGrid.svelte";
|
||||
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>
|
||||
<style>
|
||||
.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>
|
||||
<h3>Transmission</h3>
|
||||
</a>
|
||||
<!-- TODO: clean this up with proper aria roles etc -->
|
||||
<a on:click="{() => redirect_to_docs()}" >
|
||||
<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>
|
||||
<a href=https://papers.bocken.org>
|
||||
<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>
|
||||
<h3>Papers</h3>
|
||||
</a>
|
||||
<a href=https://health.bocken.org>
|
||||
<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>
|
||||
<h3>Gym</h3>
|
||||
</a>
|
||||
<!-- instead of redirect_to_docs(), use a normal link with internal checks for data.session -->
|
||||
{#if !data.session}
|
||||
<a href="/auth/signin">
|
||||
<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>
|
||||
{:else if data.session.user.groups.includes("paperless_users")}
|
||||
<a href="https://docs.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>
|
||||
{:else if data.session.user.groups.includes("paperless_eltern_users")}
|
||||
<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>
|
||||
<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>
|
||||
|
3
src/routes/(main)/login/+page.server.ts
Normal file
3
src/routes/(main)/login/+page.server.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { signIn } from "../../../auth"
|
||||
import type { Actions } from "./$types"
|
||||
export const actions: Actions = { default: signIn }
|
3
src/routes/(main)/logout/+page.server.ts
Normal file
3
src/routes/(main)/logout/+page.server.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { signIn } from "../../../auth"
|
||||
import type { Actions } from "./$types"
|
||||
export const actions: Actions = { default: signIn }
|
119
src/routes/api/cospend/add/+server.ts
Normal file
119
src/routes/api/cospend/add/+server.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
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';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
const UPLOAD_DIR = './static/cospend';
|
||||
const BASE_CURRENCY = 'CHF'; // Default currency
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
let auth = await locals.auth();
|
||||
if(!auth){
|
||||
throw error(401, "Not logged in")
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
try {
|
||||
const name = formData.get('name') as string;
|
||||
const category = formData.get('category') as string;
|
||||
const transaction_date= new Date(formData.get('transaction_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[];
|
||||
const paid_by = formData.get('paid_by') as string
|
||||
const type = formData.get('type') as string
|
||||
|
||||
let currency = formData.get('currency') as string;
|
||||
let original_amount = parseFloat(formData.get('original_amount') as string);
|
||||
let total_amount = NaN;
|
||||
|
||||
let for_self = parseFloat(formData.get('for_self') as string);
|
||||
let for_other = parseFloat(formData.get('for_other') as string);
|
||||
let conversion_rate = 1.0; // Default conversion rate
|
||||
|
||||
// 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 = original_amount;
|
||||
} else {
|
||||
console.log(transaction_date);
|
||||
const date_fmt = transaction_date.toISOString().split('T')[0]; // Convert date to YYYY-MM-DD format
|
||||
// Fetch conversion rate logic here (not implemented in this example)
|
||||
console.log(`Fetching conversion rate for ${currency} to ${BASE_CURRENCY} on ${date_fmt}`);
|
||||
const res = await fetch(`https://api.frankfurter.app/${date_fmt}?from=${currency}&to=${BASE_CURRENCY}`)
|
||||
console.log(res);
|
||||
const result = await res.json();
|
||||
console.log(result);
|
||||
if (!result || !result.rates[BASE_CURRENCY]) {
|
||||
return new Response(JSON.stringify({ message: 'Currency conversion failed.' }), { status: 400 });
|
||||
}
|
||||
// Assuming you want to convert the total amount to BASE_CURRENCY
|
||||
conversion_rate = parseFloat(result.rates[BASE_CURRENCY]);
|
||||
console.log(`Conversion rate from ${currency} to ${BASE_CURRENCY} on ${date_fmt}: ${conversion_rate}`);
|
||||
total_amount = original_amount * conversion_rate;
|
||||
for_self = for_self * conversion_rate;
|
||||
for_other = for_other * conversion_rate;
|
||||
}
|
||||
|
||||
//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({
|
||||
type,
|
||||
name,
|
||||
category,
|
||||
transaction_date,
|
||||
images,
|
||||
description,
|
||||
note,
|
||||
tags,
|
||||
original_amount,
|
||||
total_amount,
|
||||
paid_by,
|
||||
for_self,
|
||||
for_other,
|
||||
conversion_rate,
|
||||
currency,
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
try{
|
||||
await Payment.create(payment);
|
||||
} catch(e){
|
||||
|
||||
return new Response(JSON.stringify({ message: `Error creating payment event. ${e}` }), { status: 500 });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
};
|
38
src/routes/api/cospend/balance/+server.ts
Normal file
38
src/routes/api/cospend/balance/+server.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
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 GET: RequestHandler = async ({ request, locals }) => {
|
||||
await dbConnect();
|
||||
|
||||
const result = await Payment.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: "$paid_by",
|
||||
totalPaid: { $sum: "$total_amount" },
|
||||
totalForSelf: { $sum: { $ifNull: ["$for_self", 0] } },
|
||||
totalForOther: { $sum: { $ifNull: ["$for_other", 0] } }
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
paid_by: "$_id",
|
||||
netTotal: {
|
||||
$multiply: [
|
||||
{ $add: [
|
||||
{ $subtract: ["$totalPaid", "$totalForSelf"] },
|
||||
"$totalForOther"
|
||||
] },
|
||||
0.5]
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
await dbDisconnect();
|
||||
return json(result);
|
||||
};
|
20
src/routes/api/cospend/page/[pageno]/+server.ts
Normal file
20
src/routes/api/cospend/page/[pageno]/+server.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import { Payment } from '$lib/models/Payment';
|
||||
import { dbConnect, dbDisconnect } from '$lib/db/db';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({params}) => {
|
||||
await dbConnect();
|
||||
const number_payments = 10;
|
||||
const number_skip = params.pageno ? (parseInt(params.pageno) - 1 ) * number_payments : 0;
|
||||
let payments = await Payment.find()
|
||||
.sort({ transaction_date: -1 })
|
||||
.skip(number_skip)
|
||||
.limit(number_payments);
|
||||
await dbDisconnect();
|
||||
|
||||
if(payments == null){
|
||||
throw error(404, "No more payments found");
|
||||
}
|
||||
return json(payments);
|
||||
};
|
18
src/routes/cospend/+page.ts
Normal file
18
src/routes/cospend/+page.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
|
||||
export async function load({ fetch, params}) {
|
||||
let balance_res = await fetch(`/api/cospend/balance`);
|
||||
if (!balance_res.ok) {
|
||||
throw error(balance_res.status, `Failed to fetch balance`);
|
||||
}
|
||||
let balance = await balance_res.json();
|
||||
const items_res = await fetch(`/api/cospend/page/1`);
|
||||
if (!items_res.ok) {
|
||||
throw error(items_res.status, `Failed to fetch items`);
|
||||
}
|
||||
let items = await items_res.json();
|
||||
return {
|
||||
balance,
|
||||
items
|
||||
};
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import type { PageServerLoad } from "./$types"
|
||||
import type { LayoutServerLoad } from "./$types"
|
||||
|
||||
export const load : PageServerLoad = (async ({locals}) => {
|
||||
export const load : LayoutServerLoad = (async ({locals}) => {
|
||||
return {
|
||||
session: await locals.auth(),
|
||||
}
|
||||
|
@@ -2,10 +2,6 @@
|
||||
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>
|
||||
@@ -13,6 +9,6 @@ if(data.user){
|
||||
<li><a href="/glaube/rosenkranz">Rosenkranz</a></li>
|
||||
<li><a href="/glaube/predigten">Predigten</a></li>
|
||||
</ul>
|
||||
<UserHeader {username} slot=right_side></UserHeader>
|
||||
<UserHeader user={data.session?.user} slot=right_side></UserHeader>
|
||||
<slot></slot>
|
||||
</Header>
|
||||
|
@@ -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"/>
|
||||
</g>
|
||||
</svg>
|
||||
<h3>Predigten<h3>
|
||||
<h3>Predigten</h3>
|
||||
</a>
|
||||
</LinksGrid>
|
||||
|
@@ -463,19 +463,19 @@ Der unten abgebildete Rosenkranz zeigt die aktuellen Gehmeinisse des Tages nach
|
||||
</g>
|
||||
<g id=lbead5>
|
||||
<circle class=lbead />
|
||||
<circle class=hitbox onclick="" />
|
||||
<circle class=hitbox onclick="{true}" />
|
||||
</g>
|
||||
<g id=lbead4>
|
||||
<circle class=lbead />
|
||||
<circle class=hitbox onclick="" />
|
||||
<circle class=hitbox onclick="{true}" />
|
||||
</g>
|
||||
<g id=lbead3>
|
||||
<circle class=lbead />
|
||||
<circle class=hitbox onclick="" />
|
||||
<circle class=hitbox onclick="{true}" />
|
||||
</g>
|
||||
<g id=lbead6>
|
||||
<circle class=lbead />
|
||||
<circle class=hitbox onclick="" />
|
||||
<circle class=hitbox onclick="{true}" />
|
||||
</g>
|
||||
</g>
|
||||
<g class=beforedecades>
|
||||
@@ -490,11 +490,11 @@ Der unten abgebildete Rosenkranz zeigt die aktuellen Gehmeinisse des Tages nach
|
||||
</g>
|
||||
<g id=lbead1>
|
||||
<circle class=lbead />
|
||||
<circle class=hitbox onclick="" />
|
||||
<circle class=hitbox onclick="{true}" />
|
||||
</g>
|
||||
<g id=lbead2>
|
||||
<circle class=lbead />
|
||||
<circle class=hitbox onclick="" />
|
||||
<circle class=hitbox onclick="{true}" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
@@ -511,7 +511,7 @@ Dieser Plan ist wie folgt:
|
||||
<div class=table >
|
||||
<table>
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Mo</td>
|
||||
<td>Di</td>
|
||||
<td>Mi</td>
|
||||
@@ -519,7 +519,7 @@ Dieser Plan ist wie folgt:
|
||||
<td>Fr</td>
|
||||
<td>Sa</td>
|
||||
<td>So</td>
|
||||
</thead>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>freudenreich</td>
|
||||
<td>schmerzhaft</td>
|
||||
@@ -546,7 +546,7 @@ Der Plan ohne lichtreiche Geheimnisse ist wie folgt:
|
||||
<div class=table>
|
||||
<table>
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Mo</td>
|
||||
<td>Di</td>
|
||||
<td>Mi</td>
|
||||
@@ -554,7 +554,7 @@ Der Plan ohne lichtreiche Geheimnisse ist wie folgt:
|
||||
<td>Fr</td>
|
||||
<td>Sa</td>
|
||||
<td>So</td>
|
||||
</thead>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>freudenreich</td>
|
||||
<td>schmerzhaft</td>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { PageServerLoad } from "./$types"
|
||||
import type { LayoutServerLoad } from "./$types"
|
||||
|
||||
export const load : PageServerLoad = async ({locals}) => {
|
||||
export const load : LayoutServerLoad = async ({locals}) => {
|
||||
return {
|
||||
session: await locals.auth()
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ if(data.session){
|
||||
<li><a href="/rezepte/category">Kategorie</a></li>
|
||||
<li><a href="/rezepte/icon">Icon</a></li>
|
||||
<li><a href="/rezepte/tag">Stichwörter</a></li>
|
||||
<li><a href="/rezepte/tips-and-tricks">Tipps</a></li>
|
||||
</ul>
|
||||
<UserHeader slot=right_side {user}></UserHeader>
|
||||
<slot></slot>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
import Search from '$lib/components/Search.svelte';
|
||||
export let data: PageData;
|
||||
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>
|
||||
<style>
|
||||
h1{
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export async function load({locals}) {
|
||||
const session = await locals.auth();
|
||||
return {
|
||||
user: locals.user
|
||||
user: session?.user
|
||||
};
|
||||
};
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export async function load({ fetch, params, locals}) {
|
||||
let current_month = new Date().getMonth() + 1
|
||||
const res = await fetch(`/api/rezepte/items/${params.name}`);
|
||||
const recipe = await res.json();
|
||||
const session = await locals.auth();
|
||||
return {recipe: recipe,
|
||||
user: locals.user
|
||||
user: session?.user
|
||||
};
|
||||
};
|
||||
|
62
src/routes/rezepte/tips-and-tricks/+page.svelte
Normal file
62
src/routes/rezepte/tips-and-tricks/+page.svelte
Normal 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>
|
68
src/routes/rezepte/tips-and-tricks/Converter.svelte
Normal file
68
src/routes/rezepte/tips-and-tricks/Converter.svelte
Normal 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>
|
1
static/other/.jukit/.jukit_info.json
Normal file
1
static/other/.jukit/.jukit_info.json
Normal file
@@ -0,0 +1 @@
|
||||
{"terminal": "nvimterm"}
|
2
static/other/jellyfin.js
Normal file
2
static/other/jellyfin.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
document.addEventListener('load', function() { alert(1);document.querySelector('.detailImageContainer').addEventListener('click',function(){document.querySelector(".btnPlay[title='Play'],.btnPlay[title='Resume']").click()});});
|
BIN
static/test/1749501265645_angelus0001.jpg
Normal file
BIN
static/test/1749501265645_angelus0001.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
BIN
static/test/1749501265648_Anna_ID.pdf
Normal file
BIN
static/test/1749501265648_Anna_ID.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user