61 Commits

Author SHA1 Message Date
7f06717615 Revert to clean Authentik provider configuration
- Use official Authentik provider instead of generic OIDC
- Issue was resolved by fixing callback URL in Authentik configuration
- Cleaner and more maintainable auth setup

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:46:19 +02:00
4f34ff5329 Update @auth/sveltekit to latest stable version 1.10.0
- Upgraded @auth/sveltekit from 0.14.0 to 1.10.0
- Updated session API from event.locals.getSession() to event.locals.auth()
- Fixed TypeScript definitions for new auth API in app.d.ts
- Updated layout server load functions to use LayoutServerLoad type
- Fixed session callbacks with proper token type casting
- Switched to generic OIDC provider config to resolve issuer validation issues
- All auth functionality now working with latest Auth.js version

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:45:14 +02:00
69e719780c Card.svelte: fix top-right icon offset 2025-08-31 21:09:34 +02:00
e2a76d4080 Fix Card.svelte icon positioning and styling
- Restored icon to top-right position with absolute positioning
- Added proper circular background with nord0 color
- Set correct dimensions (50px × 50px) and border-radius for circular shape
- Added shadow and hover effects to match original design
- Fixed z-index to ensure icon appears above other elements
- Maintained shake animation on card hover for visual feedback

The icon now appears correctly in the top-right corner with a round
background instead of being positioned at bottom center with transparent
background.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:07:10 +02:00
b847b8f1c8 Add missing Payment model and database connection utilities
- Created Payment model with mongoose schema for cospend functionality
- Added database connection utilities with proper connection caching
- Fixed build errors related to missing imports
- Build now succeeds and dev server starts correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:03:15 +02:00
b861f9aeec Upgrade SvelteKit 4 to SvelteKit 5 with latest dependencies
Major changes:
- Upgraded Svelte from v4 to v5.38.6 (latest stable)
- Upgraded SvelteKit from v2.0.0 to v2.37.0 (latest)
- Upgraded Vite from v5 to v7.1.3 for better performance
- Updated all related packages to latest compatible versions
- Added pnpm as package manager with packageManager field
- Fixed Card.svelte nested anchor tags issue by converting inner links to buttons
- Updated component styling to maintain visual consistency
- Removed incompatible svelte-preprocess-import-assets package

Dependencies updated:
- @sveltejs/kit: ^2.0.0 → ^2.37.0
- @sveltejs/vite-plugin-svelte: ^3.0.0 → ^6.1.3
- svelte: ^4.0.0 → ^5.38.6
- vite: ^5.0.0 → ^7.1.3
- @sveltejs/adapter-auto: ^3.0.0 → ^6.1.0
- @sveltejs/adapter-node: ^2.0.0 → ^5.0.0
- svelte-check: ^3.4.6 → ^4.0.0
- mongoose: ^7.4.0 → ^8.0.0
- sharp: ^0.32.3 → ^0.33.0

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-31 21:01:19 +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
367ea7a17e render HTML in Recipe Card description
All checks were successful
CI / update (push) Successful in 17s
2024-10-22 17:29:29 +02:00
154a8f5efe fix typo 2024-10-22 17:28:44 +02:00
87768b539f fix trim()
All checks were successful
CI / update (push) Successful in 14s
2024-08-27 18:13:52 +02:00
470a74099f add https://audio.bocken.org
All checks were successful
CI / update (push) Successful in 8s
2024-08-27 18:10:31 +02:00
1dd47824c7 trim spaces from short_name, otherwise recipes become unnavigatable if ending on spaces
All checks were successful
CI / update (push) Successful in 7s
2024-08-18 21:20:03 +02:00
176ffae32c jellyfin: markers correct color
All checks were successful
CI / update (push) Successful in 16s
2024-08-17 13:16:51 +02:00
2523ca3d31 add forgotten link title for health.bocken.org
All checks were successful
CI / update (push) Successful in 13s
2024-08-11 18:38:46 +02:00
c82b3334c3 add link to health.bocken.org
All checks were successful
CI / update (push) Successful in 13s
2024-08-11 18:35:38 +02:00
b1e05888c9 bump packages
All checks were successful
CI / update (push) Successful in 14s
2024-07-30 14:50:52 +02:00
b6e61caa29 no crawling of /static/
All checks were successful
CI / update (push) Successful in 14s
2024-07-30 14:08:41 +02:00
aeee16078d disallow GPTBot/Openai from crawling website
All checks were successful
CI / update (push) Successful in 1m7s
2024-07-30 14:03:32 +02:00
a1460f5ee3 worker: change to new service name
All checks were successful
CI / update (push) Successful in 31s
2024-06-22 09:27:28 +02:00
c10f622a78 jellyinf: correct theming for progress ticks
Some checks failed
CI / update (push) Failing after 36s
2024-06-22 09:23:57 +02:00
78925d287c fix for not logged in
All checks were successful
CI / update (push) Successful in 16s
2024-03-27 22:09:15 +01:00
a5020be145 conditional redirect for Dokumente
All checks were successful
CI / update (push) Successful in 19s
2024-03-27 22:07:49 +01:00
a1d7420d09 add action to build on push
All checks were successful
CI / update (push) Successful in 14s
2024-03-22 14:54:58 +01:00
8d50e84488 add papers 2024-03-22 13:20:49 +01:00
632be44fe8 update jellyfin css to current state 2024-03-19 10:49:37 +01:00
dda25edd4b add paperless to links grid 2024-03-18 17:40:47 +01:00
3db9f01e1b bump @auth/sveltekit to 0.14.0 2024-03-09 13:52:11 +01:00
b71a7072e7 Render html also in edit panels 2024-03-02 12:38:03 +01:00
5ee48fa733 emoji font for icon 2024-02-26 12:00:38 +01:00
abddf4b201 eager image loading for top recipes on page 2024-02-25 12:54:30 +01:00
b4dc4d194f specify emoji font for icons 2024-02-25 11:15:06 +01:00
687063f216 update multiplier on navgation 2024-02-21 13:47:30 +01:00
0bfbb6da10 apply ingredient amount to multiplier 2024-02-21 13:26:27 +01:00
8b5e089792 attempt to fix rand_array() 2024-02-21 10:29:17 +01:00
bda44e4647 invalidate image cache on /edit properly 2024-02-21 09:38:55 +01:00
68973dbec7 "fix" symbol in header on mobile 2024-02-20 20:27:33 +01:00
5cf21c7d75 finally buildable without jwt 2024-02-19 23:38:08 +01:00
c22a7f0e99 remove user admin routes 2024-02-19 23:24:28 +01:00
4fdfacd7be simplify boolean assignemnt 2024-02-19 23:22:29 +01:00
db391bc383 remove Users from db 2024-02-19 23:18:31 +01:00
2b6499e602 remove unnecesarry deps since moving to authjs 2024-02-19 23:17:16 +01:00
c21fbc7f1e migration to Sveltekit 2 2024-02-19 21:09:39 +01:00
4c5826e1b5 migration to Svelte 4 2024-02-19 21:02:51 +01:00
e3680da1ad move globals out of component into css file 2024-02-18 23:34:14 +01:00
cf79f75a5d do not display placeholder image in edit/adding recipe 2024-02-18 19:58:20 +01:00
794817f69d first attempt in disabling image coursel on redirect 2024-02-18 19:53:47 +01:00
e556e65707 add recipe counter 2024-02-18 17:35:17 +01:00
2b18176310 update links to domain-separated ones 2024-02-18 16:39:12 +01:00
0bf8d50f1e update README to current state 2024-02-18 15:23:14 +01:00
8e9ca091ef add link styling 2024-02-18 15:09:25 +01:00
d5228aab60 add forgotten updated api routes 2024-02-18 14:45:01 +01:00
bd5fdbd7c3 first attempt of img cache invalidation 2024-02-18 14:43:42 +01:00
bb4383f212 fix image url 2024-02-15 09:59:49 +01:00
71aabfb9ba add title metadata to main page, remove link clutter 2024-02-15 04:32:26 +01:00
e7944b9aa0 Correctly display user with pfp if available 2024-02-15 04:28:31 +01:00
71 changed files with 6781 additions and 3217 deletions

View File

@@ -0,0 +1,33 @@
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push to master (including merged PRs)
push:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
update:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- name: Updating website.
uses: appleboy/ssh-action@master
with:
host: bocken.org
username: homepage
key: ${{ secrets.homepage_ssh }}
passphrase: ${{ secrets.homepage_pass }}
port: 22
script: |
cd /usr/share/webapps/homepage
git pull --force https://Alexander:${{ secrets.homepage_gitea_token }}@git.bocken.org/Alexander/homepage
npm run build
sudo systemctl restart homepage.service

View File

@@ -7,7 +7,7 @@ My own homepage, bocken.org, built with svelte-kit.
- [ ] Admin user management -> move to authentik via oIDC
- [x] login to authentik
- [x] only let rezepte_users edit recipes -> currently only letting them log in, should be changed
- [ ] get user info from authentik (more than email and name)
- [x] get user info from authentik (more than email and name)
- [ ] upload pfp
- [ ] upload/change pfp
- [x] registration only with minimal permissions
@@ -23,7 +23,7 @@ My own homepage, bocken.org, built with svelte-kit.
- [x] verify randomize arrays based on day
- [x] notes for next time
- [ ] refactor, like, a lot
- [ ] expose json-ld for recipes https://json-ld.org/ https://github.com/flauschtrud/broccoli
- [ ] expose json-ld for recipes https://json-ld.org/ https://schema.org/Recipe
- [ ] reference other recipes in recipe
- [ ] add a link to the recipe
- [ ] add ingredients to the ingredients list
@@ -63,7 +63,7 @@ My own homepage, bocken.org, built with svelte-kit.
#### Gitea
- [ ] consistent theming
- [x] OpenID Connect
- [ ] sane landing page
- [x] sane landing page
#### Jellyfin
- [x] connect to LDAP
@@ -73,7 +73,7 @@ My own homepage, bocken.org, built with svelte-kit.
- [x] setup Oauth2proxy -> not necessary, authentik has proxy integrated
- [x] connect to OIDC using Oauth2proxy (using authentik)
- [ ] consistent theming
- [ ] auto-login if not logged in
- [x] auto-login if not logged in
#### Jitsi
- [ ] consistent theming
@@ -89,8 +89,8 @@ My own homepage, bocken.org, built with svelte-kit.
- [ ] OIDC integration (waiting on upstream)
#### Nextcloud
- [ ] consistent theming
- [ ] collabora integration
- [x] consistent theming
- [x] collabora integration
#### Transmission
- [ ] move behind authentik
- [x] move behind authentik

88
README_DEV_AUTH.md Normal file
View 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

2213
package-lock.json generated

File diff suppressed because it is too large Load Diff

3318
package-lock_old.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,25 +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": "^2.1.0",
"@sveltejs/kit": "^1.22.3",
"svelte": "^3.59.2",
"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": "^4.4.4"
"vite": "^7.1.3"
},
"dependencies": {
"@auth/sveltekit": "^0.12.3",
"@sveltejs/adapter-node": "^1.3.1",
"argon2": "^0.30.3",
"@auth/sveltekit": "^1.10.0",
"@sveltejs/adapter-node": "^5.0.0",
"cheerio": "1.0.0-rc.12",
"jsonwebtoken": "^9.0.1",
"mongoose": "^7.4.0",
"sharp": "^0.32.3",
"svelte-cookie": "^1.0.1",
"sveltekit-oidc": "^0.0.8"
"mongoose": "^8.0.0",
"sharp": "^0.33.0"
}
}

3211
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

18
src/app.d.ts vendored
View File

@@ -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 {};

View File

@@ -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;
},

View File

@@ -1,4 +1,3 @@
import { authenticateUser } from "$lib/js/authenticate"
import type { Handle } from "@sveltejs/kit"
import { redirect } from "@sveltejs/kit"
import { error } from "@sveltejs/kit"
@@ -11,15 +10,15 @@ 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) {
throw redirect(303, '/auth/signin');
redirect(303, '/auth/signin');
}
else if (! session.user.groups.includes('rezepte_users')) {
// strip last dir from url
// TODO: give indication of why access failed
const new_url = event.url.pathname.split('/').slice(0, -1).join('/');
throw redirect(303, new_url);
redirect(303, new_url);
}
}

View File

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

View File

@@ -7,6 +7,11 @@ import "$lib/css/nordtheme.css";
import "$lib/css/shake.css";
import "$lib/css/icon.css";
export let do_margin_right = false;
// to manually override lazy loading for top cards
export let loading_strat : "lazy" | "eager" | undefined;
if(loading_strat === undefined){
loading_strat = "lazy"
}
if(icon_override){
current_month = recipe.season[0]
@@ -20,6 +25,7 @@ onMount(() => {
isloaded = document.querySelector("img")?.complete ? true : false
})
const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
</script>
<style>
.card_anchor{
@@ -44,6 +50,27 @@ onMount(() => {
background-color: var(--blue);
box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.3);
}
.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;
height: 255px;
@@ -126,6 +153,7 @@ onMount(() => {
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
@@ -150,7 +178,8 @@ onMount(() => {
padding-inline: 1em;
border-radius: 1000px;
transition: 100ms;
border: none;
cursor: pointer;
}
.card_title .category:hover,
.card_title .category:focus-within
@@ -163,6 +192,18 @@ onMount(() => {
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
{
@@ -176,25 +217,25 @@ onMount(() => {
<a class=card_anchor href="/rezepte/{recipe.short_name}" class:search_me={search} data-tags=[{recipe.tags}] >
<div class="card" class:margin_right={do_margin_right}>
<div class=div_div_image >
<div class=div_image style="background-image:url({'https://bocken.org/static/rezepte/placeholder/' + recipe.short_name + '.webp'})">
<div class=div_image style="background-image:url(https://bocken.org/static/rezepte/placeholder/{img_name})">
<noscript>
<img id=image class="backdrop_blur" src={'https://bocken.org/static/rezepte/thumb/' + recipe.short_name + '.webp'} loading=lazy alt="{recipe.alt}"/>
<img id=image class="backdrop_blur" src="https://bocken.org/static/rezepte/thumb/{img_name}" loading={loading_strat} alt="{recipe.alt}"/>
</noscript>
<img class:blur={!isloaded} id=image class="backdrop_blur" src={'https://bocken.org/static/rezepte/thumb/' + recipe.short_name + '.webp'} loading=lazy alt="{recipe.alt}" on:load={() => isloaded=true}/>
<img class:blur={!isloaded} id=image class="backdrop_blur" src={'https://bocken.org/static/rezepte/thumb/' + recipe.short_name + '.webp'} loading={loading_strat} alt="{recipe.alt}" on:load={() => isloaded=true}/>
</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>{recipe.description}</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>

View File

@@ -3,11 +3,21 @@
import Cross from '$lib/assets/icons/Cross.svelte'
import "$lib/css/shake.css"
import "$lib/css/icon.css"
import { onMount } from 'svelte'
// all data shared with rest of page in card_data
export let card_data
export let image_preview_url
onMount( () => {
fetch(image_preview_url, { method: 'HEAD' })
.then(response => {
if(response.redirected){
image_preview_url = ""
}
})
})
import { img } from '$lib/js/img_store';
if(!card_data.tags){

View File

@@ -413,7 +413,7 @@ h3{
}
</style>
<div class=list_wrapper>
<div class=list_wrapper >
<h4>Portionen:</h4>
<p contenteditable type="text" bind:innerText={portions_local} on:blur={set_portions}></p>
@@ -455,9 +455,13 @@ h3{
</button>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<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.amount} {ingredient.unit}
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class=force_wrap on:click={() => show_modal_edit_ingredient(list_index, ingredient_index)} >{ingredient.name}</div>
<div class=force_wrap on:click={() => show_modal_edit_ingredient(list_index, ingredient_index)} >
{@html 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>

View File

@@ -509,7 +509,9 @@ h3{
</button>
</div>
<div>
<div on:click={() => show_modal_edit_step(list_index, step_index)}>{step}</div>
<div on:click={() => show_modal_edit_step(list_index, step_index)}>
{@html 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>

View File

@@ -19,23 +19,6 @@ link_els.forEach((el) => {
</script>
<style>
:global(*){
box-sizing: border-box;
font-family: sans-serif;
}
:global(body){
margin:0;
padding:0;
background-color: #fbf9f3;
overflow-x: hidden;
}
@media (prefers-color-scheme: dark) {
:global(body){
color: white;
background-color: var(--background-dark);
}
}
nav{
position: sticky;
background-color: var(--nord0);
@@ -46,6 +29,7 @@ nav{
justify-content: space-between !important;
align-items: center;
box-shadow: 0 1em 1rem 0rem rgba(0,0,0,0.4);
height: 4rem;
}
nav[hidden]{
display:block;
@@ -97,16 +81,16 @@ nav[hidden]{
padding-inline: 0.5rem;
}
:global(svg.symbol){
height: 3.5rem;
width: 3.5rem;
height: 4rem;
width: 4rem;
border-radius: 10000px;
}
:global(a:has(svg.symbol)){
/*:global(a:has(svg.symbol)){
padding: 0 !important;
width: 3.5rem;
height: 3.5rem;
width: 4rem;
height: 4rem;
margin-left: 1rem;
}
}*/
.wrapper{
display:flex;
flex-direction: column;
@@ -136,8 +120,8 @@ footer{
background-color: unset;
display: block;
fill: white;
margin-inline: 1rem;
width: 2.5rem;
margin-inline: 0.5rem;
width: 2rem;
aspect-ratio: 1;
}
.nav_button svg{
@@ -197,7 +181,7 @@ footer{
<div>
<div class=button_wrapper>
<a href="/"><Symbol></Symbol></a>
<button class=nav_button on:click={() => {toggle_sidebar()}}><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></button>
<button class=nav_button on:click={() => {toggle_sidebar()}}><svg xmlns="http://www.w3.org/2000/svg" height="0.5em" viewBox="0 0 448 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg></button>
</div>
<nav hidden class=nav_site>
<a class=entry href="/"><Symbol></Symbol></a>

View 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>

View File

@@ -5,6 +5,7 @@
</script>
<style>
a{
font-family: "Noto Color Emoji", emoji;
font-size: 2rem;
text-decoration: none;
padding: 0.5em;

View File

@@ -8,6 +8,7 @@
<style>
a{
font-family: "Noto Color Emoji", emoji, sans-serif;
font-size: 2rem;
text-decoration: none;
padding: 0.5em;

View File

@@ -1,7 +1,8 @@
<script>
import { onMount } from 'svelte';
import { onNavigate } from "$app/navigation";
export let data
let multiplier = 1
let multiplier;
let custom_mul = "…"
onMount(() => {
@@ -9,6 +10,10 @@ onMount(() => {
const urlParams = new URLSearchParams(window.location.search);
multiplier = urlParams.get('multiplier') || 1;
})
onNavigate(() => {
const urlParams = new URLSearchParams(window.location.search);
multiplier = urlParams.get('multiplier') || 1;
})
function convertFloatsToFractions(inputString) {
// Split the input string into individual words
@@ -227,7 +232,7 @@ span
{/if}
<div class=ingredients_grid>
{#each list.list as item}
<div class=amount>{@html adjust_amount(item.amount, multiplier)} {item.unit}</div><div class=name>{@html item.name.replace("{{multiplier}}", multiplier)}</div>
<div class=amount>{@html adjust_amount(item.amount, multiplier)} {item.unit}</div><div class=name>{@html item.name.replace("{{multiplier}}", multiplier * item.amount)}</div>
{/each}
</div>
{/each}

View File

@@ -7,6 +7,7 @@
}
svg{
transition: 100ms;
height: 3em;
}
svg:hover,
svg:focus-visible
@@ -29,14 +30,9 @@
</style>
<svg
width="calc(45.742325px/ 1.3)"
height="calc(80.310539px / 1.3)"
viewBox="0 0 45.742326 80.310541"
version="1.1"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg">
<g class=stroke
id="branches"
transform="translate(-42.033271,-37.145192)" >

View File

@@ -2,15 +2,23 @@
export let src
export let placeholder_src
let isloaded=false
let isredirected=false
import { onMount } from "svelte";
onMount(() => {
const el = document.querySelector("img")
if(el.complete){
isloaded = true
}
fetch(src, { method: 'HEAD' })
.then(response => {
isredirected = response.redirected
})
})
function show_dialog_img(){
if(isredirected){
return
}
if(document.querySelector("img").complete){
document.querySelector("#img_carousel").showModal();
}
@@ -160,11 +168,12 @@ dialog button{
.zoom-in{
cursor: zoom-in;
}
</style>
<section class="section">
<figure class="image-container">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class:zoom-in={isloaded} on:click={show_dialog_img}>
<div class:zoom-in={isloaded && !isredirected} on:click={show_dialog_img}>
<div class=placeholder style="background-image:url({placeholder_src})" >
<div class=placeholder_blur>
<img class:unblur={isloaded} id=image {src} on:load={() => {isloaded=true}} alt=""/>

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import { onMount } from "svelte";
export let username;
export let user;
function toggle_options(){
const el = document.querySelector("#options")
el.hidden = !el.hidden
}
let src="https://bocken.org/static/user/thumb/" + username + ".webp"
onMount( () => {
document.addEventListener("click", (e) => {
const el = document.querySelector("#button")
@@ -101,6 +102,12 @@
/* (B2) BOTTOM "CALLOUT TAIL" */
h2{
margin-block: 0;
font-size: 1.2rem;
}
h2 + p{
padding-top: 0;
margin-top: 0;
font-size: 1.2rem;
}
@media screen and (max-width: 800px){
#options{
@@ -125,10 +132,11 @@ h2{
}
</style>
{#if username}
<button on:click={toggle_options} style="background-image: url({src})" id=button>
{#if user}
<button on:click={toggle_options} style="background-image: url(https://bocken.org/static/user/thumb/{user.nickname}.webp)" id=button>
<div id=options class="speech top" hidden>
<h2>{username}</h2>
<h2>{user.name}</h2>
<p>({user.nickname})</p>
<ul>
<li><a href="https://sso.bocken.org/if/user/#/settings" >Einstellungen</a></li>
<li><a href="/auth/signout" >Log Out</a></li>

View File

@@ -27,3 +27,28 @@
--background-dark: #21201b;
--font-default-dark: #ffffff;
}
a:not(:visited){
color: var(--red);
}
a:visited{
color: var(--purple);
}
*{
box-sizing: border-box;
font-family: Helvetica, Arial, "Noto Sans", sans-serif
}
body{
margin:0;
padding:0;
background-color: #fbf9f3;
overflow-x: hidden;
}
@media (prefers-color-scheme: dark) {
body{
color: white;
background-color: var(--background-dark);
}
}

44
src/lib/db/db.ts Normal file
View 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;
}
}

View File

@@ -1,41 +0,0 @@
import type { RequestEvent } from "@sveltejs/kit";
import { COOKIE_SECRET } from "$env/static/private";
import pkg from 'jsonwebtoken';
const { verify } = pkg;
import { error } from "@sveltejs/kit";
import { dbConnect, dbDisconnect } from "../../utils/db";
import { User } from "../../models/User";;
export async function authenticateUser(cookies){
// Set your master secret key (replace with your own secret)
const masterSecret = COOKIE_SECRET;
const secretKey = masterSecret
let decoded
try{
const cookie : string = cookies.get("UserSession")
if(cookie){
decoded = await verify(cookie, secretKey);
}
}
catch(e){
return null
}
if(decoded){
await dbConnect()
let res = await User.findOne({username: decoded.username}, 'access').lean();
await dbDisconnect()
if(!res){
throw error(404, "User for this Cookie does no longer exist")
}
return {
username: decoded.username,
access: res.access,
_id: res._id.toString(),
}
}
else{
return null
}
}

View File

@@ -1,33 +0,0 @@
import type { RequestEvent } from "@sveltejs/kit";
import { COOKIE_SECRET } from "$env/static/private";
import pkg from 'jsonwebtoken';
const { verify } = pkg;
import { error } from "@sveltejs/kit";
import { dbConnect, dbDisconnect } from "../../utils/db";
import { User } from "../../models/User";;
export async function get_username(cookies){
// Set your master secret key (replace with your own secret)
const masterSecret = COOKIE_SECRET;
const secretKey = masterSecret
let decoded
try{
const cookie : string = cookies.get("UserSession")
if(cookie){
decoded = await verify(cookie, secretKey);
}
}
catch(e){
return null
}
if(decoded){
return {
username: decoded.username,
}
}
else{
return null
}
}

View File

@@ -1,10 +0,0 @@
import { hash } from 'argon2'
export async function hashPassword(password, salt) {
try {
const hashedPassword = await hash(password, salt); // Hash the password with the salt and pepper
return hashedPassword;
} catch (error) {
console.error('Error hashing password:', error);
}
}

View File

@@ -1,6 +1,4 @@
const time = new Date()
const MS_PER_DAY = 86400000
let seed = Math.floor(time.getTime()/MS_PER_DAY)
function mulberry32(a) {
return function() {
var t = a += 0x6D2B79F5;
@@ -11,6 +9,8 @@ function mulberry32(a) {
}
export function rand_array(array){
let time = new Date()
const seed = Math.floor(time.getTime()/MS_PER_DAY)
let rand = mulberry32(seed)
array.sort((a,b) => 0.5 - rand())
return array

14
src/lib/models/Payment.ts Normal file
View 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);

View File

@@ -1,12 +0,0 @@
import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema(
{
username: {type: String, required: true, unique: true},
pass_hash: {type: String, required: true},
salt : {type: String, required: true},
access: [String], //rezepte, flims, abrechnung, ...
}, {timestamps: true}
);
export const User = mongoose.model("User", UserSchema);

View File

@@ -1,8 +1,6 @@
import { get_username } from '$lib/js/get_username';;
import type { Actions, PageServerLoad } from "./$types"
import { error } from "@sveltejs/kit"
import type { LayoutServerLoad } from "./$types"
export const load = (async ({cookies, locals}) => {
export const load : LayoutServerLoad = (async ({locals}) => {
return {
session: await locals.auth(),
}

View File

@@ -2,23 +2,15 @@
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
export let data
let username = ""
let user;
if(data.session){
username = data.session.user.name
user = data.session.user
}
console.log(data)
</script>
<Header>
<ul class=site_header slot=links>
<li><a href="/rezepte">Rezepte</a></li>
<li><a href="/git">Git</a></li>
<li><a href="https://stream.bocken.org">Streaming</a></li>
<li><a href="/bilder">Bilder</a></li>
<li><a href="https://meet.bocken.org">Jitsi</a></li>
<li><a href="https://cloud.bocken.org">Cloud</a></li>
<li><a href="/searx">Searx</a></li>
</ul>
<UserHeader {username} slot=right_side></UserHeader>
<UserHeader {user} slot=right_side></UserHeader>
<slot></slot>
</Header>

View File

@@ -1,11 +1,7 @@
<script>
<script lang="ts">
import "$lib/css/nordtheme.css";
import LinksGrid from "$lib/components/LinksGrid.svelte";
export let data;
import { SignIn, SignOut } from "@auth/sveltekit/components"
import { page } from "$app/stores"
/*console.log($page)*/
/*console.log($page.daja.session.user)*/
</script>
<style>
.hero{
@@ -52,11 +48,18 @@ section h2{
}
}
</style>
<svelte:head>
<title>Bocken</title>
<meta name="description" content="Die persönliche Website von Alexander Bocken" />
<meta property="og:image" content="https://bocken.org/static/favicon.png" />
<meta property="og:image:secure_url" content="https://bocken.org/favicon.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:alt" content="Das Familienwappen simplifiziert" />
</svelte:head>
{#if ! data.session}
<section class=hero>
<img src="https://bocken.org/static/user/full/Alexander.webp" alt="Smiling Alexander Bocken">
<img src="https://bocken.org/static/user/full/alexander.webp" alt="Smiling Alexander Bocken">
<div>
<h1><q>Willkommen auf bocken.org</q></h1>
<p>
@@ -64,7 +67,7 @@ section h2{
Alles ist selbst gehostet bei mir daheim auf einem kleinen Mini-Server (Arch, btw).
</p>
<p>
Zu empfeheln ist meine stetig wachsende Rezeptsammlung. Dort findest du viele leckere Rezepte, die ich selbst ausprobiert habe und ständig weiterfeilsche.
Zu empfehlen ist meine stetig wachsende Rezeptsammlung. Dort findest du viele leckere Rezepte, die ich selbst ausprobiert habe und ständig weiterfeilsche.
Zudem kannst du gerne meine Suchmaschine oder auch Jitsi-instanz für Videokonferenzen nutzen.
Einiges ist hinter einem Login versteckt, anderes ist öffentlich zugänglich.
Wer sich ein bisschen mit Programmieren auskennt, kann auch gerne in meinen Git-Repositories stöbern.
@@ -74,13 +77,14 @@ section h2{
{/if}
<section>
<h2>Seiten</h2>
<LinksGrid>
<a href="rezepte">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M240 144A96 96 0 1 0 48 144a96 96 0 1 0 192 0zm44.4 32C269.9 240.1 212.5 288 144 288C64.5 288 0 223.5 0 144S64.5 0 144 0c68.5 0 125.9 47.9 140.4 112h71.8c8.8-9.8 21.6-16 35.8-16H496c26.5 0 48 21.5 48 48s-21.5 48-48 48H392c-14.2 0-27-6.2-35.8-16H284.4zM144 80a64 64 0 1 1 0 128 64 64 0 1 1 0-128zM400 240c13.3 0 24 10.7 24 24v8h96c13.3 0 24 10.7 24 24s-10.7 24-24 24H280c-13.3 0-24-10.7-24-24s10.7-24 24-24h96v-8c0-13.3 10.7-24 24-24zM288 464V352H512V464c0 26.5-21.5 48-48 48H336c-26.5 0-48-21.5-48-48zM48 320h80 16 32c26.5 0 48 21.5 48 48s-21.5 48-48 48H160c0 17.7-14.3 32-32 32H64c-17.7 0-32-14.3-32-32V336c0-8.8 7.2-16 16-16zm128 64c8.8 0 16-7.2 16-16s-7.2-16-16-16H160v32h16zM24 464H200c13.3 0 24 10.7 24 24s-10.7 24-24 24H24c-13.3 0-24-10.7-24-24s10.7-24 24-24z"/></svg>
<h3>Rezepte</h3>
</a>
<a href=git>
<a href=https://git.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="M392.8 1.2c-17-4.9-34.7 5-39.6 22l-128 448c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l128-448c4.9-17-5-34.7-22-39.6zm80.6 120.1c-12.5 12.5-12.5 32.8 0 45.3L562.7 256l-89.4 89.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l112-112c12.5-12.5 12.5-32.8 0-45.3l-112-112c-12.5-12.5-32.8-12.5-45.3 0zm-306.7 0c-12.5-12.5-32.8-12.5-45.3 0l-112 112c-12.5 12.5-12.5 32.8 0 45.3l112 112c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256l89.4-89.4c12.5-12.5 12.5-32.8 0-45.3z"/></svg>
<h3>Git</h3>
</a>
@@ -90,7 +94,7 @@ section h2{
<h3>Streaming</h3>
</a>
<a href="/bilder">
<a href="https://bilder.bocken.org">
<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="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6h96 32H424c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"/></svg>
<h3>Familienbilder</h3>
</a>
@@ -105,7 +109,7 @@ section h2{
<h3>Videokonferenzen</h3>
</a>
<a href="/searx">
<a href="https://searx.bocken.org">
<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="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>
<h3>Suchmaschine</h3>
</a>
@@ -127,5 +131,27 @@ 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>
<!-- 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>
</a>
</LinksGrid>
</section>

View File

@@ -0,0 +1,3 @@
import { signIn } from "../../../auth"
import type { Actions } from "./$types"
export const actions: Actions = { default: signIn }

View File

@@ -0,0 +1,3 @@
import { signIn } from "../../../auth"
import type { Actions } from "./$types"
export const actions: Actions = { default: signIn }

View 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 });
}
};

View 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);
};

View 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);
};

View File

@@ -6,7 +6,7 @@ import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let found_brief = rand_array(await Recipe.find({}, 'name short_name tags category icon description season').lean()) as BriefRecipeType[];
let found_brief = rand_array(await Recipe.find({}, 'name short_name tags category icon description season dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
return json(JSON.parse(JSON.stringify(found_brief)));
};

View File

@@ -6,7 +6,7 @@ import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({category: params.category}, 'name short_name images tags category icon description season').lean()) as BriefRecipeType[];
let recipes = rand_array(await Recipe.find({category: params.category}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));

View File

@@ -6,7 +6,7 @@ import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({icon: params.icon}, 'name short_name images tags category icon description season').lean()) as BriefRecipeType[];
let recipes = rand_array(await Recipe.find({icon: params.icon}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));

View File

@@ -6,7 +6,7 @@ import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let found_in_season = rand_array(await Recipe.find({season: params.month, icon: {$ne: "🍽️"}}, 'name short_name images tags category icon description season').lean());
let found_in_season = rand_array(await Recipe.find({season: params.month, icon: {$ne: "🍽️"}}, 'name short_name images tags category icon description season dateModified').lean());
await dbDisconnect();
found_in_season = JSON.parse(JSON.stringify(found_in_season));
return json(found_in_season);

View File

@@ -6,7 +6,7 @@ import { rand_array } from '$lib/js/randomize';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = rand_array(await Recipe.find({tags: params.tag}, 'name short_name images tags category icon description season').lean()) as BriefRecipeType[];
let recipes = rand_array(await Recipe.find({tags: params.tag}, 'name short_name images tags category icon description season dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));

View File

@@ -1,23 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import { User } from '../../../../../models/User';
import { get_username } from '$lib/js/get_username';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({cookies}) => {
const requesting_user = await get_username(cookies)
await dbConnect()
let res = await User.findOne({username: requesting_user}, 'access').lean()
if(!res.access.contains("admin")){
await dbDisconnect()
throw error(401, {message: "Your user does not have the permissions to do this"})
}
else{
let res = await User.find({}, 'username access').lean()
await dbDisconnect()
return { res }
}
};

View File

@@ -1,36 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
import { verify } from 'argon2';
import { hashPassword } from '$lib/js/hashPassword'
import {randomBytes} from 'crypto'
import { PEPPER } from '$env/static/private';
import { User } from '../../../../models/User';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request}) => {
const {username, old_password, new_password, new_password_rep} = await request.json()
if(new_password != new_password_rep){
throw error(400, 'new passwords do not match!')
}
await dbConnect();
const user = await User.findOne({username: username});
console.log("Found user:", user)
const isMatch = await verify(user.pass_hash, old_password + PEPPER, {salt: user.salt})
console.log("isMatch:", isMatch)
if(isMatch){
const salt = randomBytes(32).toString('hex'); // Generate a random salt
const pass_hash = await hashPassword(new_password + PEPPER, salt)
await User.findOneAndUpdate({username: username}, {pass_hash: pass_hash, salt: salt})
await dbDisconnect()
return new Response(JSON.stringify({message: "Password updated successfully"}),
{status: 200})
}
else{
await dbDisconnect();
throw error(401, "Wrong old password")
}
};

View File

@@ -1,46 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
import pkg from 'jsonwebtoken';
const { sign } = pkg;
import { verify} from 'argon2';
import { COOKIE_SECRET } from '$env/static/private'
import { PEPPER } from '$env/static/private'
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import { User } from '../../../../models/User';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request}) => {
const {username, password} = await request.json()
await dbConnect()
let res = await User.findOne({username: username}, 'pass_hash salt').lean()
await dbDisconnect()
if(!res){
console.log("NOT FOUND")
throw error(401, {message: "wrong password or user does not exist"})
}
const stored_pw = res.pass_hash
const salt = res.salt
const isMatch = await verify(stored_pw, password + PEPPER, {salt})
if(!isMatch){
throw error(401, {message: "wrong password or user does not exist"})
}
res = await createJWT(username)
return new Response(JSON.stringify(res))
};
async function createJWT(username) {
const payload = {
username: username,
};
const masterSecret = COOKIE_SECRET;
const secretKey = masterSecret;
const jwt = sign(payload, secretKey);
console.log(jwt)
return jwt
}

View File

@@ -1,41 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
import { randomBytes } from 'crypto';
import { ALLOW_REGISTRATION } from '$env/static/private';
import { PEPPER } from '$env/static/private';
import {hashPassword} from '$lib/js/hashPassword'
import { User } from '../../../../models/User';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request}) => {
if(ALLOW_REGISTRATION){
const {username, password} = await request.json()
const salt = randomBytes(32).toString('hex'); // Generate a random salt
const pass_hash = await hashPassword(password + PEPPER, salt)
await dbConnect();
try{
await User.create({
username: username,
pass_hash: pass_hash,
salt: salt,
access: [],
})
}catch(e){
await dbDisconnect();
throw error(400, e);
}
await dbDisconnect();
return new Response(JSON.stringify({message: "User added successfully"}),
{status: 200}
);
}
else{
throw error(401, "user registration currently closed")
}
};

View 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
};
}

View File

@@ -1,7 +1,7 @@
import { get_username } from '$lib/js/get_username';;
import type { Actions, PageServerLoad } from "./$types"
import { error } from "@sveltejs/kit"
import type { LayoutServerLoad } from "./$types"
export const load = (async ({cookies}) => {
return { user: await get_username(cookies) }
export const load : LayoutServerLoad = (async ({locals}) => {
return {
session: await locals.auth(),
}
});

View File

@@ -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>

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"/>
</g>
</svg>
<h3>Predigten<h3>
<h3>Predigten</h3>
</a>
</LinksGrid>

View File

@@ -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>

View File

@@ -1,8 +1,7 @@
import type { Actions, PageServerLoad } from "./$types"
import { error } from "@sveltejs/kit"
import type { LayoutServerLoad } from "./$types"
export const load = (async ({cookies, locals}) => {
export const load : LayoutServerLoad = async ({locals}) => {
return {
session: await locals.getSession()
session: await locals.auth()
}
});
};

View File

@@ -2,9 +2,9 @@
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
export let data
let username = ""
let user;
if(data.session){
username = data.session.user.name
user = data.session.user
}
</script>
@@ -15,7 +15,8 @@ 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 {username}></UserHeader>
<UserHeader slot=right_side {user}></UserHeader>
<slot></slot>
</Header>

View File

@@ -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{
@@ -14,6 +14,11 @@ h1{
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
</style>
<svelte:head>
<title>Bocken Rezepte</title>
@@ -25,12 +30,13 @@ h1{
</svelte:head>
<h1>Rezepte</h1>
<p class=subheading>{data.all_brief.length} Rezepte und stetig wachsend...</p>
<Search></Search>
<MediaScroller title="In Saison">
{#each data.season as recipe}
<Card {recipe} {current_month} do_margin_right={true}></Card>
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true}></Card>
{/each}
</MediaScroller>

View File

@@ -15,8 +15,8 @@
export let data: PageData;
let hero_img_src = "https://bocken.org/static/rezepte/full/" + data.short_name + ".webp"
let placeholder_src = "https://bocken.org/static/rezepte/placeholder/" + data.short_name + ".webp"
let hero_img_src = "https://bocken.org/static/rezepte/full/" + data.short_name + ".webp?v=" + data.dateModified
let placeholder_src = "https://bocken.org/static/rezepte/placeholder/" + data.short_name + ".webp?v=" + data.dateModified
export let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
function season_intervals() {
let interval_arr = []
@@ -185,6 +185,7 @@ h1{
}
.icon{
font-family: "Noto Color Emoji", emoji;
position: absolute;
top: -1em;
right: -0.75em;
@@ -292,7 +293,9 @@ h4{
<h4>Saison:</h4>
{#each season_iv as season}
<a class=tag href="/rezepte/season/{season[0]}">
{months[season[0] - 1]}
{#if season[0]}
{months[season[0] - 1]}
{/if}
{#if season[1]}
- {months[season[1] - 1]}
{/if}

View File

@@ -1,5 +1,6 @@
export async function load({locals}) {
const session = await locals.auth();
return {
user: locals.user
user: session?.user
};
};

View File

@@ -85,7 +85,7 @@
console.log(img_local)
const data = {
image: img_local,
name: short_name,
name: short_name.trim(),
}
await fetch(`/api/rezepte/img/add`, {
method: 'POST',
@@ -108,9 +108,9 @@
recipe: {
...card_data,
...add_info,
images: {mediapath: short_name + '.webp', alt: "", caption: ""}, // TODO
images: {mediapath: short_name.trim() + '.webp', alt: "", caption: ""}, // TODO
season: season_local,
short_name,
short_name : short_name.trim(),
portions: portions_local,
datecreated,
datemodified,

View File

@@ -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
};
};

View File

@@ -10,7 +10,7 @@
export let data: PageData;
let preamble = data.recipe.preamble
let addendum = data.recipe.addendum
let image_preview_url="https://bocken.org/static/rezepte/thumb/" + data.recipe.short_name + ".webp"
let image_preview_url="https://bocken.org/static/rezepte/thumb/" + data.recipe.short_name + ".webp?v=" + data.recipe.dateModified;
let note = data.recipe.note
import { season } from '$lib/js/season_store';
@@ -160,7 +160,7 @@
async function upload_img(){
const data = {
image: img_local,
name: short_name,
name: short_name.trim(),
}
const res = await fetch(`/api/rezepte/img/add`, {
method: 'POST',
@@ -191,7 +191,7 @@
},
body: JSON.stringify({
old_name: old_short_name,
new_name: short_name,
new_name: short_name.trim(),
})
})
if(!res_img.ok){
@@ -208,7 +208,7 @@
...add_info,
images, // TODO
season: season_local,
short_name,
short_name: short_name.trim(),
datecreated,
portions: portions_local,
datemodified,
@@ -228,7 +228,7 @@
if(res.ok){
const url = location.href.split('/');
url.splice(url.length -2, 2);
url.push(short_name);
url.push(short_name.trim());
location.assign(url.join('/'))
}
else{

View File

@@ -10,6 +10,7 @@
</script>
<style>
a{
font-family: "Noto Color Emoji", emoji, sans-serif;
--padding: 0.5em;
font-size: 3rem;
text-decoration: none;

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>

View File

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

View File

@@ -3,12 +3,6 @@ This file styles jellyfin and can be imported by adding:
@import url("https://bocken.org/other/jellyfin.css");
under Server -> General -> Custom CSS Code
*/
/* jellyscrub default: width: 15vw, annoying for smaller screen sizes */
.chapterThumbContainer{
width: min(80vw, 500px, 50vh);
}
/* Attempt to change accent color */
:root{
--nord0: #2E3440;
--blue: #5E81AC;
@@ -86,8 +80,10 @@ border-color: var(--blue);
.defaultCardBackground3,
.defaultCardBackground4,
.defaultCardBackground5,
.emby-checkbox:checked + span + .checkboxOutline, .itemProgressBarForeground,
.taskProgressInner
.emby-checkbox:checked + span + .checkboxOutline,
.itemProgressBarForeground,
.taskProgressInner,
.sliderMarker.watched
{
background-color: var(--blue) !important;
}
@@ -131,6 +127,9 @@ scale: 1.05;
font-size: initial !important;
}
}
.emby-scroller{
overflow: visible !important;
.backgroundContainer{
background-color: #21201b;
}
.pageTitleWithDefaultLogo {
background-image: url(https://bocken.org/static/css/logos/logo_text_light.png);
}

2
static/other/jellyfin.js Normal file
View 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()});});

5
static/robots.txt Normal file
View File

@@ -0,0 +1,5 @@
User-agent: GPTBot
Disallow: /
User-agent: *
Disallow: /static/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

View File

@@ -1,5 +1,5 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {