Compare commits

...

217 Commits

Author SHA1 Message Date
cd02307a9d initial tests for cospend incl. image upload 2025-06-05 19:40:35 +02:00
dd71832b10 switch from "Unterwegs" to "Snack"
All checks were successful
CI / update (push) Successful in 49s
2025-03-31 17:57:40 +02:00
ce7a542408 fix copilot autocomplete svg messup
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 13:09:15 +01:00
86225d3237 fix docs autolink
All checks were successful
CI / update (push) Successful in 16s
2025-02-02 13:07:42 +01:00
18a5241c1e fix docs autolink
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 12:58:34 +01:00
17a5d6155d fix for svelte 4
All checks were successful
CI / update (push) Successful in 15s
2025-02-02 12:55:33 +01:00
15bf4fd922 remove unused health.bocken.org and papers.bocken.org
All checks were successful
CI / update (push) Successful in 1m26s
2025-02-02 12:44:21 +01:00
aab1f7da9a added tips-and-tricks route
All checks were successful
CI / update (push) Successful in 19s
2024-10-28 17:00:43 +01:00
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
022d727394 OIDC can check for groups now to properly secure users 2024-02-15 04:10:06 +01:00
650a6ce1fc re-protect client paths 2024-02-15 03:13:49 +01:00
3a684a5d5a current state of OIDC integration in README 2024-02-14 18:43:29 +01:00
a781be8d00 initial OIDC setup 2024-02-14 16:07:55 +01:00
1929189187 new svgs 2024-02-14 13:44:37 +01:00
d6f8ab9a17 remove unused files 2024-02-14 13:44:22 +01:00
4c92c1c43d add note about recipes login 2024-02-03 13:03:23 +01:00
ee1008eeea add forgotten css 2024-02-01 17:17:20 +01:00
6ccdfd51de add initial Glaube section 2024-02-01 17:15:51 +01:00
29893f931d install OIDC, update packages 2024-01-28 12:37:30 +01:00
08cc4091b1 add cospend link 2024-01-26 15:51:37 +01:00
9d6e160ec8 update to current state 2024-01-24 16:42:58 +01:00
51381c3f3d only margin-right in MediaScroller 2024-01-24 11:09:54 +01:00
6baaefcfe8 more features 2024-01-22 20:14:36 +01:00
5c009d74fe Search: enable click only result 2024-01-22 16:04:58 +01:00
9ddafaacca fix weird shift in Cards due to double insertion of <a> tag on server 2024-01-22 15:09:08 +01:00
1222fe7487 Card.svelte: placeholder image also blurred if JS disabled 2024-01-22 14:53:26 +01:00
61488a8ce9 fix Search 2024-01-22 14:47:22 +01:00
ef78686432 no blurred image if JS disabled 2024-01-22 14:43:52 +01:00
177e2c8fca update to fix vite vulns 2024-01-22 14:16:24 +01:00
7ec94246f0 Card is now fully loaded in itself
No longer do we have this weird shift of the description to the right of
the Card until some magical JS is loaded to fix it.
Not yet perfect: The now wrapping a-tag is for some reason still weirdly
sent to client until some js cleans it up. Currently results in a too
large gap which is fixed by local js.

Still TODO: do not blur images if no js present
2024-01-22 14:13:56 +01:00
0f45145119 Card.svelte: get rid of :has() and Firefox-specific hacks 2024-01-22 12:40:55 +01:00
6fc2755d87 favorite feature layout 2024-01-21 18:57:00 +01:00
b047034731 fix missing nordtheme mvs 2024-01-21 10:36:32 +01:00
18e26790ce slightly improve js-free Card rendering 2024-01-21 10:34:23 +01:00
27c643ef2b fix Login/PFP falling below viewport 2024-01-20 17:49:20 +01:00
735ce5aecc fix typo 2024-01-20 17:41:19 +01:00
37d2265a3b add new goals 2024-01-20 17:35:09 +01:00
b5878390ad fix mobile view messing up startpage 2024-01-20 17:23:12 +01:00
0d180cc4f9 update startpage 2024-01-20 17:09:07 +01:00
c4c72bd8f0 simplify structure by remove (rezepte) 2024-01-20 16:39:27 +01:00
36e0abb26d Header: add box-shadow 2024-01-20 16:29:47 +01:00
65049e49ec add js-free goals 2024-01-20 14:10:03 +01:00
a3d2e66b5b h1 in rezepte/ also center-aligns for larger screens 2024-01-20 10:01:23 +01:00
b5f6a3d8bd load nordtheme also on rezepte/[name] and rezepte/add 2024-01-20 09:59:14 +01:00
d8f1c06b3a Revert "fix Kategorie -> Stichwörter"
This reverts commit 66721766b8.
2024-01-20 00:48:21 +01:00
c2dc3aa1d5 fixed tag -> Stichwörter 2024-01-20 00:47:16 +01:00
66721766b8 fix Kategorie -> Stichwörter 2024-01-20 00:45:48 +01:00
6362dcaef5 add forgotten Card icon to darktheme 2024-01-20 00:43:31 +01:00
55862e05f9 update to current state 2024-01-20 00:42:02 +01:00
7a7b19c02b dark theme implemented 2024-01-20 00:39:53 +01:00
e2a7de3e90 also search through season, exclude "🍽️"recipes from being in season 2024-01-19 20:12:09 +01:00
09b5e99867 add Frühstück section to main page 2024-01-19 19:58:58 +01:00
42e9a4ceb1 update to current state 2024-01-13 16:01:31 +01:00
9f5c784c6a Search: temporarily disable auto-scroll 2024-01-10 11:03:41 +01:00
95ae0f814c remove shys from Search 2024-01-10 10:55:59 +01:00
c616c9d798 display single month in-season recipes correctly 2024-01-05 22:09:07 +01:00
c6b002a108 update to current state 2024-01-03 11:36:14 +01:00
f263af9abf update ragu img link 2024-01-01 14:29:30 +01:00
91e523e359 move category and icon up in mobile view 2023-12-30 13:07:34 +01:00
59ee898de3 remove shys from pagetitle 2023-12-30 12:47:06 +01:00
9fc8571a18 force line-wrap test 2023-12-30 12:41:25 +01:00
cd5c210d8a add multiplier url parameter 2023-12-28 14:20:19 +01:00
69ff12bcdb do not alert() on img upload/edit 2023-12-18 20:33:56 +01:00
5abd2f7bed seasonality filtering 2023-12-13 14:55:42 +01:00
bdedd4c45c update to current state 2023-12-13 12:15:03 +01:00
2ba62592b3 initial move entries setup 2023-12-11 21:32:16 +01:00
31416870d1 reimplement click_only_result option, scroll results into view 2023-11-21 00:44:17 +01:00
f94bb702bc remove typo 2023-11-20 23:42:32 +01:00
1f5e252b2b fix created at display 2023-11-20 23:32:14 +01:00
aa4a4c4cd5 display create/update date at bottom of recipe 2023-11-20 23:26:50 +01:00
1ff902117e tmp fix for stupid iOS 2023-11-16 15:47:19 +01:00
144a0132fa update services list 2023-11-13 13:07:42 +01:00
29e1b3be76 remove scrollers without results on search 2023-11-08 20:28:13 +01:00
b8c8b73af5 add vh limit for preview thumbnails for mobile 2023-11-07 12:46:25 +01:00
f302c0b234 enlarge current list item 2023-10-29 16:34:15 +01:00
9cd430564f site header in nord0, cards grow on hover 2023-10-27 17:36:09 +02:00
e88b3d307c more theming for jellyfin 2023-10-27 16:10:46 +02:00
53fc4fee04 more settings theming, heart hvoers pale red 2023-10-27 15:31:40 +02:00
e9f40a4e36 add hover effect color for Home and Favorites Tab 2023-10-27 14:39:45 +02:00
c1c1e9bbc1 add initial jellyfin styling 2023-10-27 13:46:30 +02:00
780bca5f96 Safari and Firefox render cards correctly 2023-10-19 14:04:44 +02:00
c22253f4bc actually render correctly for FF 2023-10-19 13:45:12 +02:00
302b5f6a10 title image correctly aligned on firefox 2023-10-19 13:31:16 +02:00
1d7cf03d66 add category button to recipes 2023-10-19 10:46:35 +02:00
7244637ad7 move to cheerio as server side rendering does not support DOMParser 2023-10-10 10:31:56 +02:00
9c5bc00657 remove html tags from meta tags 2023-10-10 10:17:39 +02:00
2f47860046 SEO/ better URL Previews 2023-10-10 10:03:43 +02:00
4a2edd85a1 move from new.bocken.org to bocken.org 2023-10-05 10:09:50 +02:00
b70c236598 finally fix symbol on mobile 2023-10-04 22:53:03 +02:00
797f920074 update favicon and Symbol to more minimal version 2023-10-03 09:13:10 +02:00
6edd635344 update symbol 2023-08-21 21:45:09 +02:00
736d9a2d0a fix searx url 2023-07-29 14:31:15 +02:00
ca427492d5 remove old reference to password 2023-07-28 13:02:04 +02:00
173ea23240 added searx 2023-07-26 23:59:15 +02:00
3852a2f72f add jellyfin link 2023-07-26 15:16:17 +02:00
118d53e64e fix path depth for payments/[item] api 2023-07-24 23:05:50 +02:00
6ecf859102 added missing Payment model 2023-07-24 23:01:01 +02:00
6356456a18 added missing payments api routes 2023-07-24 22:59:09 +02:00
e25c0f9121 add payment route + additional starting blocks 2023-07-24 22:57:12 +02:00
4bcde97eb7 removed lorem ipsum for git 2023-07-24 20:47:18 +02:00
e692ef58fb removed lorem ipsum for rezepte 2023-07-24 16:59:06 +02:00
ea8496b85a slightly improve logout 2023-07-23 15:39:20 +02:00
0d5d936ef3 fix mess-up on hover 2023-07-23 12:48:12 +02:00
41f42eb2b4 get user from cookie, not locals 2023-07-23 12:29:41 +02:00
8ebc407752 add externalized hashPassword.js 2023-07-23 12:22:58 +02:00
4c198e4113 change password possible 2023-07-23 12:21:12 +02:00
82a232a20f fix typo 2023-07-23 10:56:45 +02:00
9814207e0e forms updated 2023-07-22 15:04:18 +02:00
fbf33445d9 menu closes when clicking somwhere else 2023-07-22 13:12:45 +02:00
516ce43025 remove old api routes; 2023-07-22 13:09:28 +02:00
04827b1a39 move API routes as cleanup 2023-07-22 13:09:06 +02:00
9c1002bc5e section scales, not anchor 2023-07-22 12:58:29 +02:00
2f5a71f8e3 update TODO 2023-07-21 15:26:48 +02:00
6abc3e87f4 show username 2023-07-21 12:43:30 +02:00
0506ab9018 login and register designed 2023-07-21 12:25:06 +02:00
3bcf6707a3 fix mobile? 2023-07-21 00:44:37 +02:00
8481654878 less noticable shadows on tags 2023-07-21 00:21:07 +02:00
58d2643908 add Note field in recipe 2023-07-21 00:18:37 +02:00
3796642866 add TODO in readme 2023-07-20 23:41:47 +02:00
c7a2eed3db prettier placeholder landing page 2023-07-20 23:15:28 +02:00
6a9a46ba05 simplify avatar setup via background-image 2023-07-20 15:41:04 +02:00
12db3c9e3c fix removal of ranges 2023-07-20 15:24:10 +02:00
fc46b45386 mobile logout option 2023-07-20 15:05:08 +02:00
662890b044 fix and update 2023-07-20 15:00:52 +02:00
a661f35a3d update depends 2023-07-20 14:58:01 +02:00
af2b513677 less database requests 2023-07-20 14:48:50 +02:00
59ab194b08 test 2023-07-19 15:59:54 +02:00
2b0c954aa5 user displayed in navbar with option to logout 2023-07-19 14:52:50 +02:00
8f045d7111 remove double scale-up 2023-07-18 18:17:47 +02:00
5c4af90456 remove Home as we have symbol 2023-07-18 18:12:54 +02:00
a7c62e776d fix global .icon for action_button 2023-07-18 18:11:47 +02:00
7dfdea4393 fix global .icon for action_button 2023-07-18 18:10:57 +02:00
9d38684b10 symbol in Header 2023-07-18 18:09:31 +02:00
cf88b08ac6 allow for build because of jsonwebtoken quirk 2023-07-18 14:28:39 +02:00
f714ef3fa6 cookie util 2023-07-18 14:20:50 +02:00
7dd52d8890 First fully working user management, move to layout groups 2023-07-18 14:18:52 +02:00
70e640aa9a cleaner login and registration 2023-07-18 12:05:30 +02:00
ffa4496c16 add initial user Management API 2023-07-18 09:14:33 +02:00
0816cbe9e5 custom multiplier and cleaner fractions 2023-07-17 09:14:09 +02:00
3497e2b9a5 rearrange tags 2023-07-14 14:34:15 +02:00
52858e46ff active season/icon highlighted insteas of using text 2023-07-14 14:33:55 +02:00
f8b5160f2d ranges in portions are handled correctly 2023-07-14 14:31:19 +02:00
90895fb957 break word 2023-07-13 23:15:13 +02:00
2bf546ccb5 initial adjust amounts implemented 2023-07-13 20:53:27 +02:00
45942d113b fix modal image screen overflow 2023-07-13 18:57:29 +02:00
fe0c3c6644 less mislicks, switched to :focus where appropriate 2023-07-13 18:27:02 +02:00
be331343e7 bodge img upload on edit if no img available 2023-07-13 18:18:01 +02:00
878aeff52d show zoom-in pointe only when ready 2023-07-13 17:30:36 +02:00
94848e505f update image and season interval on navigation 2023-07-13 17:18:09 +02:00
5769c0cea6 click on title image for full image 2023-07-13 15:26:16 +02:00
bf5c86532a Card hover effect smooth, mobile navbar hides on click 2023-07-13 13:54:42 +02:00
25c3f41b42 add Getränke cateogry in all display 2023-07-13 11:50:29 +02:00
6476419a29 smoother transition 2023-07-12 12:46:33 +02:00
6aaf4ecfb4 reliably unblur, only use unblur if image not already loaded 2023-07-12 12:44:44 +02:00
bfc20ec192 rm jukit stuff 2023-07-12 12:28:34 +02:00
2ae789e6a6 fix img APIs to working standard 2023-07-12 12:23:35 +02:00
385af0401b add initial img API endpoints 2023-07-12 11:35:43 +02:00
c6b82865d4 randomize determined by day alone, not order of execution as well 2023-07-12 09:51:33 +02:00
8441a434d1 randomize moved to API 2023-07-11 22:54:13 +02:00
8e34bf512e do not show progress of downloading full image 2023-07-11 19:36:59 +02:00
08607fafe7 fix placeholder thumbnail misalignment 2023-07-11 19:14:39 +02:00
83542af81d correctly show matching recipes 2023-07-11 19:07:26 +02:00
57017baed8 fix recipe page 2023-07-11 19:00:58 +02:00
915e49352f fix to working state 2023-07-11 18:51:34 +02:00
5ea8502caf initial implementation of placeholder images, thumbnails and blurring between using sharp 2023-07-11 18:47:29 +02:00
abecc0e71f fix imgs 2023-07-10 14:08:58 +02:00
c6248773e9 More image fixes 2023-07-10 14:05:33 +02:00
e740e4ca38 Move imgs 2023-07-10 13:47:44 +02:00
411cfde93d add missing css 2023-07-10 13:25:04 +02:00
bb2cd23dd6 add shake.css 2023-07-10 13:24:42 +02:00
267 changed files with 10151 additions and 1641 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

106
README.md
View File

@ -1,38 +1,96 @@
# create-svelte
# Personal Homepage
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
My own homepage, bocken.org, built with svelte-kit.
## Creating a project
## TODO
### General
- [ ] 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
- [x] get user info from authentik (more than email and name)
- [ ] upload pfp
- [ ] upload/change pfp
- [x] registration only with minimal permissions
- [ ] logout without /logout page
- [ ] preferences page
- [x] change password
- [x] css dark mode `@media (prefers-color-scheme: dark) {}`
- [ ] dark mode toggle
If you're seeing this, you've probably already done this step. Congrats!
### Rezepte
- [x] Do not list recipes that are all-year as "seasonal"
- [ ] nutrition facts
- [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://schema.org/Recipe
- [ ] reference other recipes in recipe
- [ ] add a link to the recipe
- [ ] add ingredients to the ingredients list
- [ ] include steps?
- [ ] add favoriting ability when logged in
- [ ] favorite button on recipe
- [ ] store favorites in DB -> add to user object
- [ ] favorite API endpoint (requires auth of user)
- [ ] set
- [ ] retrieve
- [ ] favorite page/MediaScroller
- [ ] graceful degradation for JS-less browsers
- [ ] use js-only class with display:none and remove it with JS
- [ ] disable search -> use form action instead on submit?
- [x] do not blur images without js
- [x] correct Recipe Card rendering
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
### Glaube
- [ ] just keep it md rendered
- [ ] Google Speech to Text API integration?
- [ ] Gebete
## Developing
### Outside of this sveltekit project but planned to run on the server as well
- [x] create LDAP and OpenID
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
#### E-Mail
- [x] emailwiz setup
- [x] fail2ban
- [ ] LDAP?
```bash
npm run dev
#### Dendrite
- [x] setup dendrite
- [ ] Connect to LDAP/OIDC (waiting on upstream)
- [x] Serve some web-frontend -> Just element?
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
#### Gitea
- [ ] consistent theming
- [x] OpenID Connect
- [x] sane landing page
## Building
#### Jellyfin
- [x] connect to LDAP
- [x] consitent theming
To create a production version of your app:
#### Webtrees
- [x] setup Oauth2proxy -> not necessary, authentik has proxy integrated
- [x] connect to OIDC using Oauth2proxy (using authentik)
- [ ] consistent theming
- [x] auto-login if not logged in
```bash
npm run build
```
#### Jitsi
- [ ] consistent theming
- [ ] move away from docker
- [ ] find a way to improve max video quality without jitsi becoming unreliable
You can preview the production build with `npm run preview`.
#### Searx
- [x] investigate SearxNG as more reliable alternative
- [ ] consistent theming
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
#### Photoprism
- [ ] consistent theming
- [ ] OIDC integration (waiting on upstream)
#### Nextcloud
- [x] consistent theming
- [x] collabora integration
#### Transmission
- [x] move behind authentik

3318
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,18 +11,21 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"svelte-preprocess-import-assets": "^1.0.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.3.0"
"@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",
"tslib": "^2.6.0",
"typescript": "^5.1.6",
"vite": "^5.0.0"
},
"dependencies": {
"@sveltejs/adapter-node": "^1.2.4",
"mongoose": "^7.3.0",
"sass": "^1.63.4"
"@auth/sveltekit": "^0.14.0",
"@sveltejs/adapter-node": "^2.0.0",
"cheerio": "1.0.0-rc.12",
"mongoose": "^7.4.0",
"sharp": "^0.32.3"
}
}

1146
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

4
src/app.d.ts vendored
View File

@ -1,9 +1,5 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare module '@fortawesome/pro-solid-svg-icons/index.es' {
export * from '@fortawesome/pro-solid-svg-icons';
}
declare global {
namespace App {
// interface Error {}

29
src/auth.ts Normal file
View File

@ -0,0 +1,29 @@
import { SvelteKitAuth } from "@auth/sveltekit"
import Authentik from "@auth/core/providers/authentik"
import { AUTHENTIK_ID, AUTHENTIK_SECRET, AUTHENTIK_ISSUER } from "$env/static/private";
export const { handle, signIn, signOut } = SvelteKitAuth({
providers: [
Authentik({
clientId: AUTHENTIK_ID,
clientSecret: AUTHENTIK_SECRET,
issuer: AUTHENTIK_ISSUER,
})],
callbacks: {
// this feels like an extremely hacky way to get nickname and groups into the session object
// TODO: investigate if there's a better way to do this
jwt: async ({token, profile}) => {
if(profile){
token.nickname = profile.nickname;
token.groups = profile.groups;
}
return token;
},
session: async ({session, token}) => {
session.user.nickname = token.nickname;
session.user.groups = token.groups;
return session;
},
}
})

32
src/hooks.server.ts Normal file
View File

@ -0,0 +1,32 @@
import type { Handle } from "@sveltejs/kit"
import { redirect } from "@sveltejs/kit"
import { error } from "@sveltejs/kit"
import { SvelteKitAuth } from "@auth/sveltekit"
import Authentik from "@auth/core/providers/authentik"
import { AUTHENTIK_ID, AUTHENTIK_SECRET, AUTHENTIK_ISSUER } from "$env/static/private";
import { sequence } from "@sveltejs/kit/hooks"
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();
if (!session) {
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('/');
redirect(303, new_url);
}
}
// If the request is still here, just proceed as normally
return resolve(event);
}
export const handle: Handle = sequence(
auth.handle,
authorization
);

View File

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

View File

@ -1,6 +1,6 @@
<script lang='ts'>
export let href
import "$lib/components/nordtheme.css"
import "$lib/css/nordtheme.css"
import "$lib/css/action_button.css"
</script>

View File

@ -2,25 +2,46 @@
export let recipe
export let current_month
export let icon_override = false;
export let search = "search_me"
export let search = true;
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]
}
let isloaded = false
import { onMount } from "svelte";
onMount(() => {
isloaded = document.querySelector("img")?.complete ? true : false
})
const img_name=recipe.short_name + ".webp?v=" + recipe.dateModified
</script>
<style>
.card_anchor{
border-radius: 20px;
}
.card{
--card-width: 300px;
text-decoration: none;
position: relative;
flex-shrink: 0;
transition: 200ms;
text-decoration: none;
box-sizing: border-box;
font-family: sans-serif;
cursor: pointer;
width: var(--card-width);
aspect-ratio: 4/7;
height: 525px;
width: 300px;
border-radius: 20px;
background-size: contain;
display: flex;
@ -28,29 +49,54 @@ if(icon_override){
justify-content: end;
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;
}
#image{
width: 300px;
height: 255px;
object-fit: cover;
transition: 200ms;
}
.blur{
filter: blur(10px);
}
.backdrop_blur{
backdrop-filter: blur(10px);
}
.div_image,
.div_div_image{
width: 300px;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
overflow: hidden;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.div_div_image{
height: 255px;
position: absolute;
width: 300px;
top: 0;
}
.card:hover,
.card:focus-within{
transform: scale(1.02,1.02);
background-color: var(--red);
box-shadow: 0.2em 0.2em 2em 1em rgba(0, 0, 0, 0.3);
}
.card:active{
.card:focus{
scale: 0.95 0.95;
}
.card img{
height: 50%;
object-fit: cover;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.card .title {
position: relative;
box-sizing: border-box;
.card_title {
position: absolute;
padding-top: 0.5em;
height: 50%;
width: 100% ;
height: 262.5px;
width: 300px;
top: 262.5px;
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
display: flex;
@ -58,17 +104,17 @@ if(icon_override){
justify-content: space-between;
transition: 100ms;
}
.card .name{
.name{
font-size: 2em;
color: white;
padding-inline: 0.5em;
padding-block: 0.2em;
}
.card .description{
.description{
padding-inline: 1em;
color: var(--nord4);
}
.card .tags{
.tags{
display: flex;
flex-wrap: wrap-reverse;
overflow: hidden;
@ -78,7 +124,7 @@ if(icon_override){
margin-bottom:0.5em;
flex-grow: 0;
}
.card .tag{
.tag{
cursor: pointer;
text-decoration: unset;
background-color: var(--nord4);
@ -90,18 +136,18 @@ if(icon_override){
transition: 100ms;
box-shadow: 0em 0em 0.2em 0.05em rgba(0, 0, 0, 0.3);
}
.card .tag:hover,
.card .tag:focus-visible
.tag:hover,
.tag:focus-visible
{
transform: scale(1.04, 1.04);
background-color: var(--orange);
box-shadow: 0.2em 0.2em 0.2em 0.1em rgba(0, 0, 0, 0.3);
}
.card .tag:active{
.tag:focus{
transition: 100ms;
scale: 0.8 0.8;
scale: 0.9;
}
.card .title .category{
.card_title .category{
position: absolute;
box-shadow: 0em 0em 1em 0.1em rgba(0, 0, 0, 0.6);
text-decoration: none;
@ -115,33 +161,45 @@ if(icon_override){
transition: 100ms;
}
.card .title .category:hover,
.card .title .category:focus-within
.card_title .category:hover,
.card_title .category:focus-within
{
box-shadow: -0.2em 0.2em 1em 0.1em rgba(0, 0, 0, 0.6);
background-color: var(--nord3);
transform: scale(1.05, 1.05)
}
.card .category:active{
.category:focus{
scale: 0.9 0.9;
}
.card:hover .icon,
.card:focus-visible .icon
{
animation: shake 0.6s
animation: shake 0.6s;
}
.margin_right{
margin-right: 2em;
}
</style>
<a class="card {search}" href="/rezepte/{recipe.short_name}" data-tags=[{recipe.tags}]>
{#if icon_override || recipe.season.includes(current_month)}
<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/{img_name})">
<noscript>
<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={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>
{/if}
<img width=300px height=300px src="/images/{recipe.images[0].mediapath}" alt="{recipe.alt}" />
<div class=title>
{/if}
<div class="card_title">
<a class=category href="/rezepte/category/{recipe.category}" >{recipe.category}</a>
<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}
@ -149,4 +207,5 @@ if(icon_override){
{/each}
</div>
</div>
</div>
</a>

View File

@ -3,9 +3,20 @@
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';
@ -13,9 +24,9 @@ if(!card_data.tags){
card_data.tags = []
}
//locals
let new_tag
let image_preview_url
export function show_local_image(){

View File

@ -1,5 +1,6 @@
<script lang='ts'>
import {flip} from "svelte/animate"
import Pen from '$lib/assets/icons/Pen.svelte'
import Cross from '$lib/assets/icons/Cross.svelte'
import Plus from '$lib/assets/icons/Plus.svelte'
@ -110,10 +111,37 @@ export function edit_ingredient_and_close_modal(){
const modal_el = document.querySelector("#edit_ingredient_modal");
modal_el.close();
}
export function show_keys(event){
console.log(event.ctrlKey, event.key)
export function update_list_position(list_index, direction){
if(direction == 1){
if(list_index == 0){
return
}
ingredients.splice(list_index - 1, 0, ingredients.splice(list_index, 1)[0])
}
else if(direction == -1){
if(list_index == ingredients.length - 1){
return
}
ingredients.splice(list_index + 1, 0, ingredients.splice(list_index, 1)[0])
}
ingredients = ingredients //tells svelte to update dom
}
export function update_ingredient_position(list_index, ingredient_index, direction){
if(direction == 1){
if(ingredient_index == 0){
return
}
ingredients[list_index].list.splice(ingredient_index - 1, 0, ingredients[list_index].list.splice(ingredient_index, 1)[0])
}
else if(direction == -1){
if(ingredient_index == ingredients[list_index].list.length - 1){
return
}
ingredients[list_index].list.splice(ingredient_index + 1, 0, ingredients[list_index].list.splice(ingredient_index, 1)[0])
}
ingredients = ingredients //tells svelte to update dom
}
</script>
<style>
@ -253,12 +281,12 @@ dialog{
box-sizing: content-box;
width: 100%;
height: 100%;
background-color: rgba(255,255,255, 0.001);
background-color: transparent;
border: unset;
margin: 0;
transition: 500ms;
}
dialog[open]{
dialog[open]::backdrop{
animation: show 200ms ease forwards;
}
@keyframes show{
@ -286,7 +314,6 @@ dialog h2{
.mod_icons{
display: flex;
flex-direction: row;
margin-left: 2rem;
}
.button_subtle{
padding: 0em;
@ -298,20 +325,36 @@ dialog h2{
.button_subtle:hover{
scale: 1.2 1.2;
}
.move_buttons_container{
display: flex;
flex-direction: column;
}
.move_buttons_container button{
background-color: transparent;
border: none;
padding: 0;
margin: 0;
transition: 200ms;
}
.move_buttons_container button:hover{
scale: 1.4;
}
h3{
width: fit-content;
display: flex;
flex-direction: row;
align-items: center;
max-width: 1000px;
justify-content: space-between;
user-select: none;
cursor: pointer;
gap: 1em;
}
.ingredients_grid{
box-sizing: border-box;
display: grid;
font-size: 1.1em;
grid-template-columns: 2fr 3fr 1fr;
grid-template-columns: 0.5fr 2fr 3fr 1fr;
grid-template-rows: auto;
grid-auto-flow: row;
align-items: center;
@ -353,17 +396,41 @@ h3{
margin-left: 0;
}
}
.force_wrap{
overflow-wrap: break-word;
}
.button_arrow{
fill: var(--nord1);
}
@media (prefers-color-scheme: dark){
.button_arrow{
fill: var(--nord4);
}
.list_wrapper p[contenteditable]{
background-color: var(--accent-dark);
}
}
</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>
<h2>Zutaten</h2>
{#each ingredients as list, list_index}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<h3 on:click="{() => show_modal_edit_subheading_ingredient(list_index)}">
<div>
<h3>
<div class=move_buttons_container>
<button on:click="{() => update_list_position(list_index, 1)}">
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button on:click="{() => update_list_position(list_index, -1)}">
<svg class="button_arrow" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<div on:click="{() => show_modal_edit_subheading_ingredient(list_index)}">
{#if list.name }
{list.name}
{:else}
@ -378,11 +445,23 @@ h3{
</div>
</h3>
<div class=ingredients_grid>
{#each list.list as ingredient, ingredient_index}
{#each list.list as ingredient, ingredient_index (ingredient_index)}
<div class=move_buttons_container>
<button on:click="{() => update_ingredient_position(list_index, ingredient_index, 1)}">
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button on:click="{() => update_ingredient_position(list_index, ingredient_index, -1)}">
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</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 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

@ -5,7 +5,7 @@ import Cross from '$lib/assets/icons/Cross.svelte'
import Plus from '$lib/assets/icons/Plus.svelte'
import Check from '$lib/assets/icons/Check.svelte'
import '$lib/components/nordtheme.css'
import '$lib/css/nordtheme.css'
import "$lib/css/action_button.css"
import { do_on_key } from '$lib/components/do_on_key.js'
@ -117,13 +117,66 @@ export function add_placeholder(){
el.innerHTML = step_placeholder
}
}
export function update_list_position(list_index, direction){
if(direction == 1){
if(list_index == 0){
return
}
instructions.splice(list_index - 1, 0, instructions.splice(list_index, 1)[0])
}
else if(direction == -1){
if(list_index == instructions.length - 1){
return
}
instructions.splice(list_index + 1, 0, instructions.splice(list_index, 1)[0])
}
instructions = instructions //tells svelte to update dom
}
export function update_step_position(list_index, step_index, direction){
if(direction == 1){
if(step_index == 0){
return
}
instructions[list_index].steps.splice(step_index - 1, 0, instructions[list_index].steps.splice(step_index, 1)[0])
}
else if(direction == -1){
if(step_index == instructions[list_index].steps.length - 1){
return
}
instructions[list_index].steps.splice(step_index + 1, 0, instructions[list_index].steps.splice(step_index, 1)[0])
}
instructions = instructions //tells svelte to update dom
}
</script>
<style>
.move_buttons_container{
display: inline-flex;
flex-direction: column;
}
.move_buttons_container button{
background-color: transparent;
border: none;
padding: 0;
margin: 0;
transition: 200ms;
}
.move_buttons_container button:hover{
scale: 1.4;
}
.step_move_buttons{
position: absolute;
left: -2.5rem;
flex-direction: column;
}
input::placeholder{
all:unset;
}
li {
position: relative;
}
li > div{
display:flex;
flex-direction: row;
@ -248,7 +301,6 @@ dialog{
height: 100%;
background-color: rgba(255,255,255, 0.001);
border: unset;
backdrop-filter: blur(10px);
margin: 0;
transition: 200ms;
}
@ -282,7 +334,7 @@ dialog .adder input::placeholder{
width: 70%;
}
}
dialog[open]{
dialog[open]::backdrop{
animation: show 200ms ease forwards;
}
@keyframes show{
@ -373,6 +425,22 @@ h3{
width: 80%;
}
}
@media (prefers-color-scheme: dark){
.additional_info div{
background-color: var(--accent-dark);
}
.instructions{
background-color: var(--nord6-dark);
}
}
.button_arrow{
fill: var(--nord1);
}
@media (prefers-color-scheme: dark){
.button_arrow{
fill: var(--nord4);
}
}
</style>
<div class=instructions>
@ -406,24 +474,44 @@ h3{
<h2>Zubereitung</h2>
{#each instructions as list, list_index}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<h3 on:click={() => show_modal_edit_subheading_step(list_index)}>
<h3>
<div class=move_buttons_container>
<button on:click="{() => update_list_position(list_index, 1)}">
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button on:click="{() => update_list_position(list_index, -1)}">
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<div on:click={() => show_modal_edit_subheading_step(list_index)}>
{#if list.name}
{list.name}
{:else}
Leer
{/if}
<div>
</div>
<button class="action_button button_subtle" on:click="{() => show_modal_edit_subheading_step(list_index)}">
<Pen fill=var(--nord1)></Pen> </button>
<button class="action_button button_subtle" on:click="{() => remove_list(list_index)}">
<Cross fill=var(--nord1)></Cross>
</button>
</div>
</h3>
<ol>
{#each list.steps as step, step_index}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<li><div><div on:click={() => show_modal_edit_step(list_index, step_index)}>{step}</div>
<li>
<div class="move_buttons_container step_move_buttons">
<button on:click="{() => update_step_position(list_index, step_index, 1)}">
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6 1.41 1.41z"/></svg>
</button>
<button on:click="{() => update_step_position(list_index, step_index, -1)}">
<svg class=button_arrow xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16px" height="16px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</button>
</div>
<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

@ -3,6 +3,7 @@
import Cross from '$lib/assets/icons/Cross.svelte';
import SeasonSelect from '$lib/components/SeasonSelect.svelte';
import '$lib/css/action_button.css'
import '$lib/css/nordtheme.css'
import '$lib/css/shake.css'
import { redirect } from '@sveltejs/kit';
import { RecipeModelType } from '../../types/types';
@ -122,7 +123,6 @@
const item = await res.json();
}
async function doAdd () {
console.log(add_info.total_time)
const res = await fetch('/api/add', {
method: 'POST',
body: JSON.stringify({
@ -215,6 +215,7 @@ h1{
margin-block: 2rem;
margin-inline: auto;
background-color: var(--nord6);
background-color: red;
padding: 1rem 2rem;
}
.title p{
@ -255,6 +256,12 @@ h3{
bottom: 0;
margin: 2rem;
}
@media (prefers-color-scheme: dark){
.title{
background-color: var(--nord6-dark);
background-color: green;
}
}
</style>
<h1>{title}</h1>

View File

@ -0,0 +1,22 @@
<script lang="ts">
</script>
<style>
div{
background-color: var(--red);
color: white;
padding: 1em;
font-size: 1.1rem;
max-width: 400px;
margin-inline: auto;
box-shadow: 0.2em 0.2em 0.5em 0.1em rgba(0, 0, 0, 0.3);
margin-bottom: 1em;
}
h3{
margin-block: 0;
}
</style>
<div>
<h3>Notiz:</h3>
<slot></slot>
</div>

View File

@ -1,41 +1,51 @@
<script>
import "$lib/components/nordtheme.css"
import "$lib/css/nordtheme.css"
import { onMount } from "svelte";
import Symbol from "./Symbol.svelte"
function show_sidebar(){
function toggle_sidebar(state){
// state: force hidden state (optional)
const nav_el = document.querySelector("nav")
nav_el.hidden = !nav_el.hidden
if(state === undefined) nav_el.hidden = !nav_el.hidden
else nav_el.hidden = state
}
onMount( () => {
const link_els = document.querySelectorAll("nav a")
link_els.forEach((el) => {
el.addEventListener("click", () => {toggle_sidebar(true)});
})
})
</script>
<style>
:global(*){
box-sizing: border-box;
font-family: sans-serif;
}
:global(body){
margin:0;
padding:0;
background-color: #fbf9f3;
overflow-x: hidden;
}
nav{
position: sticky;
background-color: var(--nord0);
top: 0;
z-index: 10;
display: flex !important;
flex-direction: row;
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;
}
:global(.site_header li){
:global(.site_header li),
:global(a.entry)
{
list-style-type:none;
transition: 100ms;
color: white;
user-select: none;
}
:global(.site_header li>a){
:global(.site_header li>a),
:global(.entry)
{
text-decoration: none;
font-family: sans-serif;
font-size: 1.2rem;
@ -45,7 +55,9 @@ nav[hidden]{
}
:global(.site_header li:hover),
:global(.site_header li:focus-within)
:global(.site_header li:focus-within),
:global(.entry:hover),
:global(.entry:focus-visible)
{
cursor: pointer;
color: var(--red);
@ -66,11 +78,35 @@ nav[hidden]{
}
.button_wrapper{
display: none;
padding-inline: 0.5rem;
}
@media screen and (max-width: 500px) {
:global(svg.symbol){
height: 4rem;
width: 4rem;
border-radius: 10000px;
}
/*:global(a:has(svg.symbol)){
padding: 0 !important;
width: 4rem;
height: 4rem;
margin-left: 1rem;
}*/
.wrapper{
display:flex;
flex-direction: column;
min-height: 100svh;
}
footer{
padding-block: 1rem;
text-align: center;
margin-top: auto;
}
@media screen and (max-width: 800px) {
.button_wrapper{
box-shadow: 0 1em 1rem 0rem rgba(0,0,0,0.4);
display: flex;
justify-content: right;
justify-content: space-between;
align-items: center;
position: sticky;
background-color: var(--nord0);
@ -84,35 +120,55 @@ nav[hidden]{
background-color: unset;
display: block;
fill: white;
margin-inline: 1rem;
width: 3rem;
margin-inline: 0.5rem;
width: 2rem;
aspect-ratio: 1;
}
.nav_button svg{
width: 100%;
height: 100%;
transition: 100ms;
}
nav{
.nav_button:focus{
fill: var(--red);
scale: 0.9;
}
.nav_site{
position: fixed;
top: 0;
right: 0;
height: 100%;
width: min(80svw, 40em);
height: 100vh; /* dvh does not work, breaks because of transition and only being applied after scroll ends*/
margin-bottom: 50vh;
width: min(95svw, 25em);
transition: 100ms;
z-index: 10;
flex-direction: column;
justify-content: flex-start !important;
align-items: left;
justify-content: space-between!important;
padding-inline: 0.5rem;
}
nav[hidden]{
:global(.nav_site ul){
width: 100% ;
}
.nav_site :first-child{
display:none;
}
.nav_site[hidden]{
transform: translateX(100%);
}
:global(.nav_site a:last-child){
margin-bottom: 2rem;
}
:global(.site_header){
flex-direction: column;
padding-top: min(10rem, 10vh);
}
:global(.site_header li){
:global(.site_header li, .site_header a){
font-size: 4rem;
}
:global(.site_header li > a){
:global(.site_header li > a, .site_header a){
font-size: 2rem;
}
:global(.site_header li:hover),
@ -120,24 +176,17 @@ nav[hidden]{
transform: unset;
}
}
.wrapper{
display:flex;
flex-direction: column;
min-height: 100svh;
}
footer{
padding-block: 1rem;
text-align: center;
margin-top: auto;
}
</style>
<div class=wrapper>
<div class=wrapper lang=de>
<div>
<div class=button_wrapper>
<button class=nav_button on:click={show_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>
<a href="/"><Symbol></Symbol></a>
<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>
<nav hidden class=nav_site>
<a class=entry href="/"><Symbol></Symbol></a>
<slot name=links></slot>
<slot name=right_side></slot>
</nav>
<slot></slot>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import '$lib/css/nordtheme.css';
import "$lib/css/shake.css"
export let icon : string;
</script>
<style>
a{
font-family: "Noto Color Emoji", emoji;
font-size: 2rem;
text-decoration: none;
padding: 0.5em;
background-color: var(--nord4);
border-radius: 1000px;
box-shadow: 0em 0em 0.5em 0.2em rgba(0, 0, 0, 0.2);
}
@media (prefers-color-scheme: dark) {
a{
background-color: var(--accent-dark);
}
}
a:hover{
--angle: 15deg;
animation: shake 0.5s ease forwards;
}
</style>
<a href="/rezepte/icon/{icon}" {...$$restProps} >{icon}</a>

View File

@ -1,12 +1,14 @@
<script lang="ts">
import '$lib/components/nordtheme.css';
import '$lib/css/nordtheme.css';
import Recipes from '$lib/components/Recipes.svelte';
import Search from './Search.svelte';
export let icons
export let active_icon
</script>
<style>
a{
font-family: "Noto Color Emoji", emoji, sans-serif;
font-size: 2rem;
text-decoration: none;
padding: 0.5em;
@ -15,10 +17,12 @@
box-shadow: 0em 0em 0.5em 0.2em rgba(0, 0, 0, 0.2);
}
a:hover,
a:focus-visible
a:focus-visible,
.active
{
--angle: 15deg;
animation: shake 0.5s ease forwards;
background-color: var(--nord2);
}
.flex{
display:flex;
@ -61,11 +65,10 @@
}
</style>
<slot name="test"></slot>
<div class=flex>
{#each icons as icon}
<a href="/rezepte/icon/{icon}">{icon}</a>
{#each icons as icon, i}
<a class:active={active_icon == icon} href="/rezepte/icon/{icon}">{icon}</a>
{/each}
</div>
<section>

View File

@ -0,0 +1,565 @@
<script lang='ts'>
import { onMount } from 'svelte';
import Pen from '$lib/assets/icons/Pen.svelte'
import Cross from '$lib/assets/icons/Cross.svelte'
import Plus from '$lib/assets/icons/Plus.svelte'
import Check from '$lib/assets/icons/Check.svelte'
import "$lib/css/action_button.css"
export let list;
export let list_index;
let edit_ingredient = {
amount: "",
unit: "",
name: "",
sublist: "",
list_index: "",
ingredient_index: "",
}
let edit_heading = {
name:"",
list_index: "",
}
function get_sublist_index(sublist_name, list){
for(var i =0; i < list.length; i++){
if(list[i].name == sublist_name){
return i
}
}
return -1
}
export function show_modal_edit_subheading_ingredient(list_index){
edit_heading.name = ingredients[list_index].name
edit_heading.list_index = list_index
const el = document.querySelector('#edit_subheading_ingredient_modal')
el.showModal()
}
export function edit_subheading_and_close_modal(){
ingredients[edit_heading.list_index].name = edit_heading.name
const el = document.querySelector('#edit_subheading_ingredient_modal')
el.close()
}
export function add_new_ingredient(){
if(!new_ingredient.name){
return
}
let list_index = get_sublist_index(new_ingredient.sublist, ingredients)
if(list_index == -1){
ingredients.push({
name: new_ingredient.sublist,
list: [],
})
list_index = ingredients.length - 1
}
ingredients[list_index].list.push({ ...new_ingredient})
ingredients = ingredients //tells svelte to update dom
}
export function remove_list(list_index){
if(ingredients[list_index].list.length > 1){
const response = confirm("Bist du dir sicher, dass du diese Liste löschen möchtest? Alle Zutaten der Liste werden hiermit auch gelöscht.");
if(!response){
return
}
}
ingredients.splice(list_index, 1);
ingredients = ingredients //tells svelte to update dom
}
export function remove_ingredient(list_index, ingredient_index){
ingredients[list_index].list.splice(ingredient_index, 1)
ingredients = ingredients //tells svelte to update dom
}
export function show_modal_edit_ingredient(list_index, ingredient_index){
edit_ingredient = {...ingredients[list_index].list[ingredient_index]}
edit_ingredient.list_index = list_index
edit_ingredient.ingredient_index = ingredient_index
edit_ingredient.sublist = ingredients[list_index].name
const modal_el = document.querySelector("#edit_ingredient_modal");
modal_el.showModal();
}
export function edit_ingredient_and_close_modal(){
ingredients[edit_ingredient.list_index].list[edit_ingredient.ingredient_index] = {
amount: edit_ingredient.amount,
unit: edit_ingredient.unit,
name: edit_ingredient.name,
}
ingredients[edit_ingredient.list_index].name = edit_ingredient.sublist
const modal_el = document.querySelector("#edit_ingredient_modal");
modal_el.close();
}
let ghost;
let grabbed;
let lastTarget;
let mouseY = 0; // pointer y coordinate within client
let offsetY = 0; // y distance from top of grabbed element to pointer
let layerY = 0; // distance from top of list to top of client
function grab(clientY, element) {
// modify grabbed element
grabbed = element;
grabbed.dataset.grabY = clientY;
// modify ghost element (which is actually dragged)
ghost.innerHTML = grabbed.innerHTML;
// record offset from cursor to top of element
// (used for positioning ghost)
offsetY = grabbed.getBoundingClientRect().y - clientY;
drag(clientY);
}
// drag handler updates cursor position
function drag(clientY) {
if (grabbed) {
mouseY = clientY;
layerY = ghost.parentNode.getBoundingClientRect().y;
}
}
// touchEnter handler emulates the mouseenter event for touch input
// (more or less)
function touchEnter(ev) {
drag(ev.clientY);
// trigger dragEnter the first time the cursor moves over a list item
let target = document.elementFromPoint(ev.clientX, ev.clientY).closest(".item");
if (target && target != lastTarget) {
lastTarget = target;
dragEnter(ev, target);
}
}
function dragEnter(ev, target) {
// swap items in data
if (grabbed && target != grabbed && target.classList.contains("item")) {
moveDatum(parseInt(grabbed.dataset.index), parseInt(target.dataset.index));
}
}
// does the actual moving of items in data
function moveDatum(from, to) {
let temp = list[0].list[from];
list[0].list = [...list[0].list.slice(0, from), ...list[0].list.slice(from + 1)];
list[0].list= [...list[0].list.slice(0, to), temp, ...list[0].list.slice(to)];
}
function release(ev) {
grabbed = null;
}
function removeDatum(index) {
list= [...list.slice(0, index), ...list.slice(index + 1)];
}
</script>
<style>
input::placeholder{
color: inherit;
}
.drag_handle{
cursor: grab;
display:flex;
justify-content: flex-start;
align-items: center;
}
.drag_handle_header{
padding-right: 0.5em;
}
input{
color: unset;
font-size: unset;
padding: unset;
background-color: unset;
}
input.heading{
all: unset;
box-sizing: border-box;
background-color: var(--nord0);
padding: 1rem;
padding-inline: 2rem;
font-size: 1.5rem;
width: 100%;
border-radius: 1000px;
color: white;
justify-content: center;
align-items: center;
transition: 200ms;
}
input.heading:hover{
background-color: var(--nord1);
}
.heading_wrapper{
position: relative;
width: 300px;
margin-inline: auto;
transition: 200ms;
}
.heading_wrapper:hover
{
transform:scale(1.1,1.1);
}
.heading_wrapper button{
position: absolute;
bottom: -1.5rem;
right: -2rem;
}
.adder{
box-sizing: border-box;
margin-inline: auto;
position: relative;
margin-block: 3rem;
width: 90%;
border-radius: 20px;
transition: 200ms;
}
.adder button{
position: absolute;
right: -1.5rem;
bottom: -1.5rem;
}
.category{
border: none;
position: absolute;
--font_size: 1.5rem;
top: -1em;
left: -1em;
font-family: sans-serif;
font-size: 1.5rem;
background-color: var(--nord0);
color: var(--nord4);
border-radius: 1000000px;
width: 23ch;
padding: 0.5em 1em;
transition: 100ms;
box-shadow: 0.5em 0.5em 1em 0.4em rgba(0,0,0,0.3);
}
.category:hover{
background-color: var(--nord1);
transform: scale(1.05,1.05);
}
.adder:hover,
.adder:focus-within
{
transform: scale(1.05, 1.05);
}
.add_ingredient{
font-family: sans-serif;
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: center;
font-size: 1.2rem;
padding: 2rem;
padding-top: 2.5rem;
border-radius: 20px;
background-color: var(--blue);
color: #bbb;
transition: 200ms;
gap: 0.5rem;
}
.add_ingredient input{
border: 2px solid var(--nord4);
color: var(--nord4);
border-radius: 1000px;
padding: 0.5em 1em;
transition: 100ms;
}
.add_ingredient input:hover,
.add_ingredient input:focus-visible
{
border-color: white;
color: white;
transform: scale(1.02, 1.02);
}
.add_ingredient input:nth-of-type(1){
max-width: 8ch;
}
.add_ingredient input:nth-of-type(2){
max-width: 8ch;
}
.add_ingredient input:nth-of-type(3){
max-width: 30ch;
}
dialog{
box-sizing: content-box;
width: 100%;
height: 100%;
background-color: transparent;
border: unset;
margin: 0;
transition: 500ms;
}
dialog[open]::backdrop{
animation: show 200ms ease forwards;
}
@keyframes show{
from {
backdrop-filter: blur(0px);
}
to {
backdrop-filter: blur(10px);
}
}
dialog .adder{
margin-top: 5rem;
}
dialog h2{
font-size: 3rem;
font-family: sans-serif;
color: white;
text-align: center;
margin-top: 30vh;
margin-top: 30dvh;
filter: drop-shadow(0 0 0.4em black)
drop-shadow(0 0 1em black)
;
}
.mod_icons{
display: flex;
flex-direction: row;
margin-left: 2rem;
}
.button_subtle{
padding: 0em;
animation: unset;
margin: 0.2em 0.1em;
background-color: transparent;
box-shadow: unset;
}
.button_subtle:hover{
scale: 1.2 1.2;
}
h3{
width: fit-content;
display: flex;
flex-direction: row;
max-width: 1000px;
justify-content: space-between;
user-select: none;
cursor: pointer;
}
.ingredients_grid > span{
box-sizing: border-box;
display: grid;
font-size: 1.1em;
grid-template-columns: 1em 2fr 3fr 2em;
grid-template-rows: auto;
grid-auto-flow: row;
align-items: center;
row-gap: 0.5em;
column-gap: 0.5em;
}
.ingredients_grid > *{
cursor: pointer;
user-select: none;
}
.ingredients_grid>*:nth-child(3n+1){
min-width: 5ch;
}
.list_wrapper{
padding-inline: 2em;
padding-block: 1em;
}
.list_wrapper p[contenteditable]{
border: 2px solid grey;
border-radius: 1000px;
padding: 0.25em 1em;
background-color: white;
transition: 200ms;
}
@media screen and (max-width: 500px){
dialog h2{
margin-top: 2rem;
}
dialog .heading_wrapper{
width: 80%;
}
.ingredients_grid .mod_icons{
margin-left: 0;
}
}
.list {
cursor: grab;
z-index: 5;
display: flex;
flex-direction: column;
}
.item {
min-height: 3em;
margin-bottom: 0.5em;
border-radius: 2px;
user-select: none;
}
.item:last-child {
margin-bottom: 0;
}
.item:not(#grabbed):not(#ghost) {
z-index: 10;
}
.item > * {
margin: auto;
}
.buttons {
width: 32px;
min-width: 32px;
margin: auto 0;
display: flex;
flex-direction: column;
}
.buttons button {
cursor: pointer;
width: 18px;
height: 18px;
margin: 0 auto;
padding: 0;
border: 1px solid rgba(0, 0, 0, 0);
background-color: inherit;
}
.buttons button:focus {
border: 1px solid black;
}
.delete {
width: 32px;
}
#grabbed {
opacity: 0.0;
}
#ghost {
pointer-events: none;
z-index: -5;
position: absolute;
top: 0;
left: 0;
opacity: 0.0;
}
#ghost * {
pointer-events: none;
}
#ghost.haunting {
z-index: 20;
opacity: 1.0;
}
main {
position: relative;
}
</style>
<main>
<div class=dragdroplist>
<div
bind:this={ghost}
id="ghost"
class={grabbed ? "item haunting" : "item"}
style={"top: " + (mouseY + offsetY - layerY) + "px"}><p></p>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<h3 on:click="{() => show_modal_edit_subheading_ingredient(list_index)}">
<div class="drag_handle drag_handle_header"><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><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></div>
<div>
{#if list.name }
{list.name}
{:else}
Leer
{/if}
</div>
<div class=mod_icons>
<button class="action_button button_subtle" on:click="{() => show_modal_edit_subheading_ingredient(list_index)}">
<Pen fill=var(--nord1)></Pen> </button>
<button class="action_button button_subtle" on:click="{() => remove_list(list_index)}">
<Cross fill=var(--nord1)></Cross></button>
</div>
</h3>
<div class="ingredients_grid list"
on:mousemove={function(ev) {ev.stopPropagation(); drag(ev.clientY);}}
on:touchmove={function(ev) {ev.stopPropagation(); drag(ev.touches[0].clientY);}}
on:mouseup={function(ev) {ev.stopPropagation(); release(ev);}}
on:touchend={function(ev) {ev.stopPropagation(); release(ev.touches[0]);}}
>
{#each list.list as ingredient, ingredient_index}
<span
id={(grabbed && (ingredient.id ? ingredient.id : JSON.stringify(ingredient)) == grabbed.dataset.id) ? "grabbed" : ""}
class="item"
data-index={ingredient_index}
data-id={(ingredient.id ? ingredient.id : JSON.stringify(ingredient))}
data-grabY="0"
on:mousedown={function(ev) {grab(ev.clientY, this);}}
on:touchstart={function(ev) {grab(ev.touches[0].clientY, this);}}
on:mouseenter={function(ev) {ev.stopPropagation(); dragEnter(ev, ev.target);}}
on:touchmove={function(ev) {ev.stopPropagation(); ev.preventDefault(); touchEnter(ev.touches[0]);}}
>
<div class=drag_handle><svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 448 512"><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></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>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div 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>
</span>
{/each}
</div>
</div>
</main>
<dialog id=edit_ingredient_modal>
<h2>Zutat verändern</h2>
<div class=adder>
<input class=category type="text" bind:value={edit_ingredient.sublist} placeholder="Kategorie (optional)">
<div class=add_ingredient on:keydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<input type="text" placeholder="250..." bind:value={edit_ingredient.amount} on:keydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<input type="text" placeholder="mL..." bind:value={edit_ingredient.unit} on:keydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<input type="text" placeholder="Milch..." bind:value={edit_ingredient.name} on:keydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)}>
<button class=action_button on:keydown={(event) => do_on_key(event, 'Enter', false, edit_ingredient_and_close_modal)} on:click={edit_ingredient_and_close_modal}>
<Check fill=white style="width: 2rem; height: 2rem;"></Check>
</button>
</div>
</div>
</dialog>
<dialog id=edit_subheading_ingredient_modal>
<h2>Kategorie umbenennen</h2>
<div class=heading_wrapper>
<input class=heading type="text" bind:value={edit_heading.name} on:keydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} >
<button class=action_button on:keydown={(event) => do_on_key(event, 'Enter', false, edit_subheading_and_close_modal)} on:click={edit_subheading_and_close_modal}>
<Check fill=white style="width:2rem; height:2rem;"></Check>
</button>
</div>
</dialog>

View File

@ -1,5 +1,124 @@
<script>
import { onMount } from 'svelte';
import { onNavigate } from "$app/navigation";
export let data
let multiplier;
let custom_mul = "…"
onMount(() => {
// Apply multiplier from URL
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
const words = inputString.split(' ');
// Define a helper function to check if a number is close to an integer
const isCloseToInt = (num) => Math.abs(num - Math.round(num)) < 0.001;
// Function to convert a float to a fraction
const floatToFraction = (number) => {
let bestNumerator = 0;
let bestDenominator = 1;
let minDifference = Math.abs(number);
for (let denominator = 1; denominator <= 10; denominator++) {
const numerator = Math.round(number * denominator);
const difference = Math.abs(number - numerator / denominator);
if (difference < minDifference) {
bestNumerator = numerator;
bestDenominator = denominator;
minDifference = difference;
}
}
if (bestDenominator == 1) return bestNumerator;
else {
let full_amount = Math.floor(bestNumerator / bestDenominator);
if (full_amount > 0)
return `${full_amount}<sup>${bestNumerator - full_amount * bestDenominator}</sup>/<sub>${bestDenominator}</sub>`;
return `<sup>${bestNumerator}</sup>/<sub>${bestDenominator}</sub>`;
}
};
// Iterate through the words and convert floats to fractions
const result = words.map((word) => {
// Check if the word contains a range (e.g., "300-400")
if (word.includes('-')) {
const rangeNumbers = word.split('-');
const rangeFractions = rangeNumbers.map((num) => {
const number = parseFloat(num);
return !isNaN(number) ? floatToFraction(number) : num;
});
return rangeFractions.join('-');
} else {
const number = parseFloat(word);
return !isNaN(number) ? floatToFraction(number) : word;
}
});
// Join the words back into a string
return result.join(' ');
}
function multiplyNumbersInString(inputString, constant) {
return inputString.replace(/(\d+(?:[\.,]\d+)?)/g, match => {
const number = match.includes(',') ? match.replace(/\./g, '').replace(',', '.') : match;
const multiplied = (parseFloat(number) * constant).toString();
const rounded = parseFloat(multiplied).toFixed(3);
const trimmed = parseFloat(rounded).toString();
return match.includes(',') ? trimmed.replace('.', ',') : trimmed;
});
}
// "1-2 Kuchen (Durchmesser: 26cm", constant=2 -> "2-4 Kuchen (Durchmesser: 26cm)"
function multiplyFirstAndSecondNumbers(inputString, constant) {
const regex = /(\d+(?:[\.,]\d+)?)(\s*-\s*\d+(?:[\.,]\d+)?)?/;
return inputString.replace(regex, (match, firstNumber, secondNumber) => {
const numbersToMultiply = [firstNumber];
if (secondNumber) {
numbersToMultiply.push(secondNumber.replace(/-\s*/, ''));
}
const multipliedNumbers = numbersToMultiply.map(number => {
const multiplied = (parseFloat(number) * constant).toString();
const rounded = parseFloat(multiplied).toString();
const result = number.includes(',') ? rounded.replace('.', ',') : rounded;
return result;
});
return multipliedNumbers.join('-')
});
}
function adjust_amount(string, multiplier){
let temp = multiplyNumbersInString(string, multiplier)
temp = convertFloatsToFractions(temp)
return temp
}
function apply_if_not_NaN(custom){
const multipliers = [0.5, 1, 1.5, 2, 3]
if((!isNaN(custom * 1)) && custom != ""){
if(multipliers.includes(parseFloat(custom))){
multiplier = custom
custom_mul = "…"
}
else{
custom_mul = convertFloatsToFractions(custom)
multiplier = custom
}
}
else{
custom_mul = "…"
}
}
</script>
<style>
*{
@ -20,16 +139,92 @@ font-family: sans-serif;
row-gap: 0.5em;
column-gap: 0.5em;
}
h4{
margin-block: 0;
.multipliers{
display:flex;
gap: 0.5rem;
justify-content: center;
flex-wrap:wrap;
}
.multipliers button{
min-width: 2em;
font-size: 1.1rem;
border-radius: 0.3rem;
border: none;
cursor: pointer;
transition: 100ms;
color: var(--nord0);
background-color: var(--nord5);
box-shadow: 0px 0px 0.4em 0.05em rgba(0,0,0, 0.2);
}
@media (prefers-color-scheme: dark){
.multipliers button{
color: var(--tag-font);
background-color: var(--nord6-dark);
}
}
.multipliers :is(button, div):is(:hover, :focus-within){
scale: 1.2;
background-color: var(--orange);
box-shadow: 0px 0px 0.5em 0.1em rgba(0,0,0, 0.3);
}
.selected{
background-color: var(--nord9) !important;
color: white !important;
font-weight: bold;
scale: 1.2 !important;
box-shadow: 0px 0px 0.4em 0.1em rgba(0,0,0, 0.3) !important;
}
input.selected,
span.selected
{
box-shadow: none !important;
background-color: transparent;
scale: 1 !important;
}
input,
span
{
display: inline;
flex-grow: 1;
min-width: 1.5ch;
background-color: transparent;
border: unset;
padding: 0;
margin: 0;
}
.multipliers button:last-child{
display: flex;
align-items: center;
}
</style>
{#if data.ingredients}
<div class=ingredients>
{#if data.portions}
<h4>Portionen:</h4>
{data.portions}
<h3>Portionen:</h3>
{@html convertFloatsToFractions(multiplyFirstAndSecondNumbers(data.portions, multiplier))}
{/if}
<h3>Menge anpassen:</h3>
<div class=multipliers>
<button class:selected={multiplier==0.5} on:click={() => multiplier=0.5}><sup>1</sup>&frasl;<sub>2</sub>x</button>
<button class:selected={multiplier==1} on:click={() => {multiplier=1; custom_mul="…"}}>1x</button>
<button class:selected={multiplier==1.5} on:click={() => {multiplier=1.5; custom_mul="…"}}><sup>3</sup>&frasl;<sub>2</sub>x</button>
<button class:selected={multiplier==2} on:click="{() => {multiplier=2; custom_mul="…"}}">2x</button>
<button class:selected={multiplier==3} on:click="{() => {multiplier=3; custom_mul="…"}}">3x</button>
<button class:selected={multiplier==custom_mul} on:click={(e) => { const el = e.composedPath()[0].children[0]; if(el){ el.focus()}}}>
<span class:selected={multiplier==custom_mul}
on:focus={() => { custom_mul="" }
}
on:blur="{() => { apply_if_not_NaN(custom_mul);
if(custom_mul == "")
{custom_mul = "…"}
}}"
bind:innerHTML={custom_mul}
contenteditable > </span>
x
</button>
</div>
<h2>Zutaten</h2>
{#each data.ingredients as list}
{#if list.name}
@ -37,7 +232,7 @@ h4{
{/if}
<div class=ingredients_grid>
{#each list.list as item}
<div class=amount>{item.amount} {item.unit}</div><div class=name>{@html item.name}</div>
<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

@ -38,6 +38,14 @@ ol li::marker{
box-shadow: 0.3em 0.3em 1em 0.2em rgba(0,0,0,0.3);
max-width: 30%
}
@media (prefers-color-scheme: dark){
.instructions{
background-color: var(--nord6-dark);
}
.additional_info > *{
background-color: var(--accent-dark);
}
}
@media screen and (max-width: 500px){
.additional_info > *{
max-width: 60%;

View File

@ -0,0 +1,91 @@
<style>
:global(.links_grid a:nth-child(4n)),
:global(.links_grid a:nth-child(4n) svg){
background-color: var(--nord4);
fill: var(--nord11);
}
:global(.links_grid a:nth-child(4n+1)),
:global(.links_grid a:nth-child(4n+1) svg){
background-color: var(--nord6);
fill: var(--nord10);
}
:global(.links_grid a:nth-child(4n+2)){
background-color: var(--nord5);
}
:global(.links_grid a:nth-child(4n+3)){
background-color: var(--nord5);
}
:global(a){
text-decoration: unset;
color: var(--nord0);
transition: 200ms;
}
:global(.links_grid a:hover){
box-shadow: 1em 1em 2em 1em rgba(0,0,0, 0.3);
}
:global(.links_grid a){
box-shadow: 0.2em 0.2em 1em 1em rgba(0,0,0, 0.1);
}
.links_grid{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
max-width: 1000px;
margin-inline: auto;
padding: 2rem 1rem;
}
:global(.links_grid a){
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-decoration: unset;
color: var(--nord0);
transition: 200ms;
width: 100%;
padding: 1rem;
}
:global(.links_grid a:hover){
scale: 1.02;
}
:global(.links_grid a :is(svg, img)){
height: 120px;
fill: var(--nord0);
}
:global(.links_grid h3){
font-size: 1.5rem;
}
@media (prefers-color-scheme: dark){
:global(.links_grid h3){
color: white;
}
:global(.links_grid a:nth-child(4n)),
:global(.links_grid a:nth-child(4n) svg){
background-color: var(--nord6-dark);
fill: var(--nord11);
}
:global(.links_grid a:nth-child(4n+1)),
:global(.links_grid a:nth-child(4n+1) svg){
background-color: var(--accent-dark);
fill: var(--nord9);
}
:global(.links_grid a:nth-child(4n+2)),
:global(.links_grid a:nth-child(4n+2) svg){
background-color: var(--nord1);
fill: var(--nord8);
}
:global(.links_grid a:nth-child(4n+3)),
:global(.links_grid a:nth-child(4n+3) svg){
background-color: var(--background-dark);
fill: var(--nord7);
}
}
</style>
<div class=links_grid>
<slot></slot>
</div>

View File

@ -8,10 +8,10 @@ export let title
flex-direction: row;
flex-wrap:nowrap;
overflow-x: auto;
gap: 2rem;
/*gap: 2rem;*/ /*messes up if js disabled as anchor tag is inserted twice...*/
padding: 3rem;
}
.wrapper{
.media_scroller_wrapper{
background-color: var(--nord2);
}
h2{
@ -23,7 +23,7 @@ h2{
</style>
<div class=wrapper>
<div class=media_scroller_wrapper>
{#if title}
<h2>{title}</h2>
{/if}

View File

@ -47,7 +47,6 @@
const json = await res.json()
result = JSON.stringify(json)
console.log(result)
}
</script>
<style>

View File

@ -0,0 +1,23 @@
<script lang="ts">
export let note : string;
</script>
<style>
div{
background-color: var(--red);
color: white;
padding: 1em;
font-size: 1.1rem;
max-width: 400px;
margin-inline: auto;
box-shadow: 0.2em 0.2em 0.5em 0.1em rgba(0, 0, 0, 0.3);
margin-bottom: 1em;
}
h3{
margin-block: 0;
}
</style>
<div {...$$restProps} >
<h3>Notiz:</h3>
{@html note}
</div>

View File

@ -2,7 +2,6 @@
import {onMount} from "svelte";
import "$lib/css/nordtheme.css";
onMount(() => {
const recipes = document.querySelectorAll(".search_me");
const search = document.getElementById("search");
@ -14,14 +13,41 @@ onMount(() => {
const searchTerms = searchText.split(" ");
const hasFilter = searchText.length > 0;
let scrollers_with_results = [];
let scrollers = [];
// for each recipe hide all but matched
recipes.forEach(recipe => {
const searchString = `${recipe.textContent} ${recipe.dataset.tags}`.toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, "");
const searchString = `${recipe.textContent} ${recipe.dataset.tags}`.toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/&shy;|­/g, '');
const isMatch = searchTerms.every(term => searchString.includes(term));
recipe.style.display = (isMatch ? 'block' : 'none');
recipe.style.display = (isMatch ? 'flex' : 'none');
recipe.classList.toggle("matched-recipe", hasFilter && isMatch);
if(!scrollers.includes(recipe.parentNode)){
scrollers.push(recipe.parentNode)
}
if(!scrollers_with_results.includes(recipe.parentNode) && isMatch){
scrollers_with_results.push(recipe.parentNode)
}
})
scrollers_with_results.forEach( scroller => {
scroller.parentNode.style.display= 'block'
})
scrollers.filter(item => !scrollers_with_results.includes(item)).forEach( scroller => {
scroller.parentNode.style.display= 'none'
})
scroll
let items = document.querySelectorAll(".matched-recipe");
items = [...new Set(items)] // make unique as seasonal mediascroller can lead to duplicates
// if only one result and click_only_result is true, click it
if(click_only_result && scrollers_with_results.length == 1 && items.length == 1){
// add '/rezepte' to history to not force-redirect back to recipe if going back
items[0].click();
}
// if scrollers with results are presenet scroll first result into view
/*if(scrollers_with_results.length > 0){
scrollers_with_results[0].scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
}*/ // For now disabled because it is annoying on mobile
}
search.addEventListener("input", () => {
@ -31,9 +57,12 @@ onMount(() => {
clearSearch.addEventListener("click", () => {
search.value = "";
recipes.forEach(recipe => {
recipe.style.display = 'block';
recipe.style.display = 'flex';
recipe.classList.remove("matched-recipe");
})
document.querySelectorAll(".media_scroller_wrapper").forEach( scroller => {
scroller.style.display= 'block'
})
})
let paramString = window.location.href.split('?')[1];
@ -43,12 +72,11 @@ onMount(() => {
if(pair[0] == 'q'){
const search = document.getElementById("search");
search.value=pair[1];
do_search(click_only_result=true);
do_search(true);
}
}
});
// @license-end
</script>
<style>
input#search {
@ -105,7 +133,7 @@ scale: 0.8 0.8;
}
</style>
<div class="search js-only">
<input type="text" id="search" placeholder="Suche nach Stichwörtern...">
<input type="text" id="search" placeholder="Suche...">
<button id="clear-search">
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Sucheintrag löschen</title><path d="M135.19 390.14a28.79 28.79 0 0021.68 9.86h246.26A29 29 0 00432 371.13V140.87A29 29 0 00403.13 112H156.87a28.84 28.84 0 00-21.67 9.84v0L46.33 256l88.86 134.11z" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"></path><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M336.67 192.33L206.66 322.34M336.67 322.34L206.66 192.33M336.67 192.33L206.66 322.34M336.67 322.34L206.66 192.33"></path></svg></button>
</div>

View File

@ -1,11 +1,12 @@
<script lang="ts">
import '$lib/components/nordtheme.css';
import '$lib/css/nordtheme.css';
import Recipes from '$lib/components/Recipes.svelte';
import Search from './Search.svelte';
let months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
let month : number;
</script>
export let active_index;
</script>
<style>
a.month{
text-decoration: unset;
@ -18,9 +19,11 @@ a.month{
min-width: 4em;
text-align: center;
}
a.month:hover{
transform: scale(1.1,1.1);
background-color: var(--red);
a.month:hover,
.active
{
transform: scale(1.1,1.1) !important;
background-color: var(--red) !important;
}
.months{
display:flex;
@ -32,11 +35,9 @@ a.month:hover{
}
</style>
<slot name="test"></slot>
<div class=months>
{#each months as month, i}
<a class=month href="/rezepte/season/{i+1}">{month}</a>
<a class:active={i == active_index} class=month href="/rezepte/season/{i+1}">{month}</a>
{/each}
</div>
<section>

View File

@ -1,5 +1,5 @@
<script lang=ts>
import "$lib/components/nordtheme.css"
import "$lib/css/nordtheme.css"
import { season } from '$lib/js/season_store.js'
import {onMount} from "svelte";
import {do_on_key} from "./do_on_key";
@ -44,6 +44,8 @@ label{
background-color: var(--nord0);
color: white;
padding: 0.25em 1em;
margin-inline: 0.1em;
line-height: 2em;
border-radius: 1000px;
cursor: pointer;
position: relative;
@ -81,7 +83,8 @@ input[type=checkbox]::after
flex-wrap: wrap;
flex-direction: row;
justify-content: center;
gap: min(1rem, 1dvh);
margin-bottom: 1em;
}
</style>

View File

@ -0,0 +1,79 @@
<script>
import "$lib/css/nordtheme.css";
</script>
<style>
:root{
--icon_fill: var(--nord4);
}
svg{
transition: 100ms;
height: 3em;
}
svg:hover,
svg:focus-visible
{
--icon_fill: var(--red);
}
svg g.leaf path,
.fill
{
fill: var(--icon_fill);
fill-opacity: 1;
fill-rule: nonzero;
stroke: none;
}
svg g.stroke path{
stroke: var(--icon_fill);
fill: none;
stroke-width: 3;
}
</style>
<svg
viewBox="0 0 45.742326 80.310541"
version="1.1"
xmlns="http://www.w3.org/2000/svg">
<g class=stroke
id="branches"
transform="translate(-42.033271,-37.145192)" >
<path
d="m 65.113709,84.638921 c -0.346049,-9.794303 8.85917,-32.693347 8.85917,-32.693347"
/>
<path
d="m 65.108044,84.684262 c 0.346049,-9.794303 -8.85917,-32.693347 -8.85917,-32.693347"
/>
</g>
<g class=leaf
id="g1">
<path
d="M 0,0 C 6.633,-3.91 14.348,-4.302 20.992,-1.732 20.009,5.333 15.93,11.893 9.31,15.795 2.69,19.697 -5.025,20.088 -11.669,17.519 -10.7,10.462 -6.62,3.901 0,0"
transform="matrix(0.35277777,0,0,-0.35277777,4.116564,13.543871)" />
<path
d="m 0,0 c -6.62,3.901 -14.335,4.293 -20.979,1.724 0.97,-7.058 5.049,-13.618 11.669,-17.519 6.633,-3.91 14.348,-4.301 20.992,-1.732 C 10.699,-10.462 6.62,-3.902 0,0"
transform="matrix(0.35277777,0,0,-0.35277777,10.339434,19.278333)" />
<path
d="M 0,0 C 6.633,-3.909 14.348,-4.301 20.992,-1.731 20.009,5.333 15.93,11.894 9.31,15.795 2.69,19.697 -5.026,20.088 -11.669,17.52 -10.7,10.461 -6.62,3.902 0,0"
transform="matrix(0.35277777,0,0,-0.35277777,10.903454,36.572256)" />
<path
d="M 0,0 C 6.644,-2.57 14.358,-2.178 20.992,1.732 27.612,5.633 31.691,12.194 32.661,19.25 26.017,21.82 18.302,21.429 11.682,17.527 5.062,13.625 0.982,7.065 0,0"
transform="matrix(0.35277777,0,0,-0.35277777,32.871328,24.119748)" />
<path
d="M 0,0 C 6.62,3.901 10.699,10.461 11.669,17.519 5.025,20.088 -2.689,19.696 -9.31,15.795 -15.93,11.893 -20.009,5.333 -20.992,-1.732 -14.348,-4.301 -6.633,-3.91 0,0"
transform="matrix(0.35277777,0,0,-0.35277777,35.741597,35.870171)" />
<path
d="m -27.40181,13.441787 c 6.644,-2.57 14.359,-2.178 20.9920004,1.731 6.62000002,3.902 10.699,10.461 11.669,17.519 -6.644,2.569 -14.359,2.178 -20.9790004,-1.724 -6.62,-3.901 -10.7,-10.462 -11.682,-17.526"
transform="matrix(0.35277777,0,0,-0.35277777,43.12113,17.474745)" />
<path
d="m 0,0 c 1.271,7.579 -1.125,14.922 -5.904,20.205 -6.242,-3.433 -10.906,-9.591 -12.178,-17.169 -1.275,-7.594 1.123,-14.937 5.902,-20.22 C -5.936,-13.736 -1.273,-7.578 0,0"
transform="matrix(0.35277777,0,0,-0.35277777,20.082753,7.127875)" />
<path
d="m 0,0 c 1.271,7.579 -1.125,14.922 -5.904,20.206 -6.242,-3.434 -10.906,-9.592 -12.178,-17.17 -1.275,-7.593 1.123,-14.937 5.902,-20.22 C -5.937,-13.736 -1.273,-7.578 0,0"
transform="matrix(0.35277777,0,0,-0.35277777,26.963346,20.756878)" />
<path
d="M 0,0 C 4.779,5.283 7.176,12.627 5.901,20.22 4.629,27.798 -0.035,33.956 -6.277,37.39 -11.055,32.106 -13.453,24.763 -12.18,17.184 -10.908,9.606 -6.244,3.448 0,0"
transform="matrix(0.35277777,0,0,-0.35277777,29.06985,14.051408)" />
</g>
<path class=fill
d="M 0,0 -9.323,10.862 -3.185,17.76 0,21.339 3.173,17.774 9.324,10.862 Z M 41.228,66.513 C 41.168,66.275 38.492,57.729 32.099,49.53 28.9,45.41 24.801,41.388 19.697,38.388 15.112,35.701 9.727,33.802 3.198,33.324 L 0,36.917 -3.195,33.326 c -6.641,0.491 -12.102,2.451 -16.739,5.216 -7.724,4.607 -13.143,11.62 -16.561,17.559 -1.71,2.961 -2.918,5.631 -3.685,7.529 -0.383,0.949 -0.657,1.703 -0.83,2.204 -0.087,0.251 -0.148,0.438 -0.185,0.554 l -0.037,0.12 -0.006,0.017 -1.095,3.699 H -63.598 V 59.868 h 13.769 c 1.509,-3.763 4.398,-9.908 9.196,-16.216 3.801,-4.982 8.828,-10.072 15.38,-13.996 4.034,-2.422 8.662,-4.371 13.847,-5.556 l -3.872,-4.351 -7.799,-8.764 11.537,-13.441 c -5.22,-1.21 -9.868,-3.199 -13.916,-5.657 -9.751,-5.918 -16.085,-14.346 -20.051,-21.22 -2.011,-3.498 -3.414,-6.613 -4.323,-8.872 h -13.768 v -10.356 h 21.265 l 1.097,3.704 c 0.051,0.212 2.714,8.708 9.07,16.892 3.177,4.106 7.248,8.124 12.313,11.137 4.521,2.682 9.822,4.602 16.234,5.143 L 0,-15.9 l 3.619,4.215 c 6.533,-0.549 11.913,-2.527 16.488,-5.287 7.663,-4.624 13.036,-11.615 16.424,-17.525 1.694,-2.946 2.891,-5.601 3.652,-7.486 0.38,-0.943 0.651,-1.693 0.822,-2.19 0.085,-0.249 0.146,-0.435 0.183,-0.55 l 0.037,-0.119 0.004,-0.016 1.094,-3.703 h 21.268 v 10.356 H 49.825 c -1.499,3.743 -4.361,9.841 -9.105,16.111 -3.768,4.964 -8.752,10.044 -15.248,13.981 -4.052,2.46 -8.703,4.452 -13.928,5.661 l 11.534,13.437 -7.8,8.765 -3.867,4.345 c 5.185,1.183 9.812,3.13 13.845,5.55 9.835,5.899 16.218,14.364 20.21,21.275 2.032,3.529 3.447,6.672 4.36,8.948 H 63.591 V 70.224 H 42.323 Z"
transform="matrix(0.35277777,0,0,-0.35277777,23.308833,63.179301)" />
</svg>

View File

@ -1,7 +1,7 @@
<script lang="ts">
export let tag : string;
export let ref: string;
import '$lib/components/nordtheme.css'
import '$lib/css/nordtheme.css'
</script>
<style>
a{

View File

@ -1,5 +1,35 @@
<script>
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();
}
}
function close_dialog_img(){
document.querySelector("#img_carousel").close();
}
import Cross from "$lib/assets/icons/Cross.svelte";
import "$lib/css/action_button.css";
import "$lib/css/shake.css";
import { do_on_key } from "./do_on_key";
</script>
<style>
:root {
@ -46,16 +76,21 @@
z-index: -10;
}
.image-container img {
#image{
display: block;
position: absolute;
top: 0;
width: min(1000px, 100dvw);
z-index: -1;
opacity: 0;
transition: 200ms;
height: max(60dvh,600px);
object-fit: cover;
/*object-position: top;*/
object-position: 50% 20%;
backdrop-filter: blur(20px);
filter: blur(20px);
z-index: -10;
}
.image-container::after {
@ -68,9 +103,97 @@
:global(h1){
width: 100%;
}
.placeholder{
background-repeat: no-repeat;
background-size: cover;
background-position: 50% 20%;
position: absolute;
width: min(1000px, 100dvw);
height: max(60dvh,600px);
z-index: -2;
}
.placeholder_blur{
width: inherit;
height: inherit;
backdrop-filter: blur(20px);
}
div:has(.placeholder){
position: absolute;
top: 0;
width: min(1000px, 100dvw);
height: max(60dvh,600px);
overflow: hidden;
}
.unblur#image{
filter: blur(0px) !important;
opacity: 1;
}
@supports (-moz-appearance:none) {
.placeholder{
translate: -50% -50%;
}
}
/* DIALOG */
dialog{
position: relative;
background-color: unset;
padding:0;
max-height: 90vh;
margin-inline: auto;
overflow: visible;
border: unset;
}
dialog img{
max-width: calc(95vmin - 2rem);
max-height: 95vmin; /* cannot use calc() for some reason */
}
dialog[open]::backdrop{
animation: show 200ms ease forwards;
}
@keyframes show{
from {
backdrop-filter: blur(0px);
}
to {
backdrop-filter: blur(10px);
}
}
dialog button{
position: absolute;
top: -2rem;
right: -2rem;
}
.zoom-in{
cursor: zoom-in;
}
</style>
<section class="section">
<figure class="image-container"><img {src} alt=""/></figure>
<figure class="image-container">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<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=""/>
</div>
</div>
<noscript>
<div class=placeholder style="background-image:url({placeholder_src})" >
<img class="unblur" id=image {src} on:load={() => {isloaded=true}} alt=""/>
</div>
</noscript>
</div>
</figure>
<div class=content><slot></slot></div>
</section>
<dialog id=img_carousel>
<div>
<img class:unblur={isloaded} {src} alt="">
<button class=action_button on:keydown={(event) => do_on_key(event, 'Enter', false, close_dialog_img)} on:click={close_dialog_img}>
<Cross fill=white width=2rem height=2rem></Cross>
</button>
</div>
</dialog>

View File

@ -0,0 +1,148 @@
<script lang="ts">
import { onMount } from "svelte";
export let user;
function toggle_options(){
const el = document.querySelector("#options")
el.hidden = !el.hidden
}
onMount( () => {
document.addEventListener("click", (e) => {
const el = document.querySelector("#button")
if(!el.contains(e.target)){
document.querySelector("#options").hidden = true
}
})
})
</script>
<style>
/* (A) SPEECH BOX */
.speech {
/* (A1) FONT */
font-size: 1.1em;
/* (A2) COLORS */
color: #fff;
background: var(--bg_color);
/* (A3) DIMENSIONS + POSITION */
position: relative;
border-radius: 10px;
}
/* (B) ADD SPEECH "CALLOUT TAIL" */
/* (B1) USE ::AFTER TO CREATE THE "TAIL" */
.speech::after {
/* (B1-1) ATTACH TRANSPARENT BORDERS */
content: "";
border: 20px solid transparent;
/* (B1-2) NECESSARY TO POSITION THE "TAIL" */
position: absolute;
}
/* (C) DIFFERENT TAIL POSITIONS */
/* (C1) TOP */
.top.speech::after {
/* (C1-1) UP TRIANGLE */
border-bottom-color: var(--bg_color);
border-top: 0;
/* (C1-2) POSITION AT TOP */
top: -10px; left:84.5%;
margin-left: -20px;
}
button{
--margin-right: 1rem;
position: relative;
background-color: transparent;
border: none;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
margin-right: var(--margin-right);
background-color: var(--nord4);
background-position: center;
background-size: contain;
}
#options{
--bg_color: var(--nord3);
box-sizing: border-box;
border-radius: 5px;
position: absolute;
right: calc( -1*var(--margin-right) + 0.25rem);
top: calc(100% + 10px);
background-color: var(--bg_color);
width: 30ch;
padding: 1rem;
}
#options ul{
color: white;
font-size: 1.2rem;
width: 100%;
list-style-type: none;
padding: 0;
}
#options li{
margin-block: 0.5rem;
text-align: left;
}
#options li a{
text-decoration: none;
color: white;
text-align: left;
transition: 100ms;
}
#options li:hover a{
color: var(--red);
}
/* (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{
top: unset;
bottom: calc(100% + 15px);
right: -200%;
z-index: 99999999999999999999;
}
.top.speech::after {
/* (B2-1) DOWN TRIANGLE */
border-top-color: #a53d38;
border-bottom: 0;
z-index: 99999999999999999999;
/* (B2-2) POSITION AT BOTTOM */
bottom: -20px; left: 50%;
margin-left: -20px;
}
button{
margin-bottom: 2rem;
}
}
</style>
{#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>{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>
</ul>
</div>
</button>
{:else}
<a class=entry href=/auth/signin>Log In</a>
{/if}

View File

@ -1,25 +0,0 @@
:root{
--nord0: #2E3440;
--nord1: #3B4252;
--nord2: #434C5E;
--nord3: #4C566A;
--nord4: #D8DEE9;
--nord5: #E5E9F0;
--nord6: #ECEFF4;
--nord7: #8FBCBB;
--nord8: #88C0D0;
--nord9: #81A1C1;
--nord10: #5E81AC;
--nord11: #BF616A;
--nord12: #D08770;
--nord13: #EBCB8B;
--nord14: #A3BE8C;
--nord15: #B48EAD;
--lightblue: var(--nord9);
--blue: var(--nord10);
--red: var(--nord11);
--orange: var(--nord12);
--yellow: var(--nord13);
--green: var(--nord14);
--purple: var(--nord15);
}

View File

@ -22,7 +22,7 @@
animation: shake 0.5s ease forwards;
}
.action_button:active{
.action_button:focus{
transition: 50ms;
scale: 0.8 0.8;
}

32
src/lib/css/christ.css Normal file
View File

@ -0,0 +1,32 @@
div.gebet{
text-align: center;
font-size: 1.25em;
}
ul {
font-size: 120%;
}
.gebet v{
margin:0;
}
.gebet v:lang(la) {
color: var(--nord6);
}
.gebet.bilingue v:lang(de){
color: grey;
}
i{
font-style: normal;
color: var(--nord11);
font-weight: 900;
}
i.txt {
font-size: 70%;
font-weight: normal;
}
v{
display: block;
}
.mobile audio{
width:70%;
}

51
src/lib/css/form.css Normal file
View File

@ -0,0 +1,51 @@
form{
background-color: var(--nord5);
display: flex;
flex-direction: column;
max-width: 600px;
gap: 0.5em;
margin-inline: auto;
justify-content: center;
align-items: center;
padding-block: 2rem;
margin-block: 2rem;
}
@media (prefers-color-scheme: dark){
form{
background-color: var(--accent-dark);
}
}
form label{
font-size: 1.2em;
}
form input{
display: block;
font-size: 1.2rem;
}
form button{
background-color: var(--red);
color: white;
border: none;
padding: 0.5em 1em;
font-size: 1.3em;
border-radius: 1000px;
margin-top: 1em;
transition: 100ms;
}
form button:hover,
form button:focus-visible
{
scale: 1.1;
}
form p{
max-width: 400px;
margin-top: 0;
}
form h4{
margin-bottom:0;
}
@media screen and (max-width: 600px){
form{
margin-top: 0;
}
}

52
src/lib/css/icon.css Normal file
View File

@ -0,0 +1,52 @@
.icon:focus{
scale: 0.8 0.8;
rotate: var(--angle, 30deg);
}
a.icon{
--angle: 15deg;
--endscale: 1.2;
text-decoration: unset;
transition: 100ms;
position: absolute;
font-size: 1.5rem;
top:-0.5em;
right:-0.5em;
padding: 0.25em;
background-color: var(--nord6);
border-radius:1000px;
box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.6);
}
@media (prefers-color-scheme: dark) {
a.icon{
background-color: var(--accent-dark);
}
}
input.icon{
z-index: 3;
box-sizing: border-box;
text-decoration: unset;
text-align:center;
width: 2.6rem;
aspect-ratio: 1/1;
transition: 100ms;
position: absolute;
font-size: 1.5rem;
top:-0.5em;
right:-0.5em;
padding: 0.25em;
background-color: var(--nord6);
border-radius:1000px;
box-shadow: 0em 0em 2em 0.1em rgba(0, 0, 0, 0.6);
}
input.icon:hover,
input.icon:focus-visible
{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
}
a.icon:hover,
a.icon:focus-visible
{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
animation: shake 0.6s forwards;
}

View File

@ -22,4 +22,33 @@
--yellow: var(--nord13);
--green: var(--nord14);
--purple: var(--nord15);
--nord6-dark: #292c31;
--accent-dark: #1f1f21;
--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);
}
}

65
src/lib/css/predigten.css Normal file
View File

@ -0,0 +1,65 @@
@font-face {
font-family: 'UnifrakturMaguntia';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/UnifrakturMaguntia20.ttf');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
.bibel{
font-family: 'UnifrakturMaguntia', cursive;
-moz-font-feature-settings: "cv11";
-webkit-font-feature-settings: "cv11";
-ms-font-feature-settings: "cv11";
font-feature-settings: "cv11";
font-size: 1.2rem;
}
li::marker, i, li:lang(la){
font-family: serif;
}
ol{
list-style-position: inside;
}
a {
text-decoration: underline;
}
h1{
font-size: 4rem;
}
h2{
font-size: 2.5rem;
}
h3{
font-size: 2rem;
}
h4{
font-size: 1.5rem;
}
.quote p.title{
font-weight: bold;
}
/*.quote .bibel{
margin-bottom: 1em;
}*/
.quote q{
display: block;
width: 90%;
margin-left: auto;
margin-right: auto;
}
.tod, .grund{
font-size: 1.5rem;
display: block;
text-align: center;
margin-bottom: 1rem;
margin-top: -1rem;
}
.schott q{
quotes: "«" "»";
}
.predigt video {
display: block;
width: 80%;
margin: auto;
}

204
src/lib/css/rosenkranz.css Normal file
View File

@ -0,0 +1,204 @@
@font-face {
font-family: 'crosses';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/crosses.ttf);
}
@font-face {
font-family: 'LibertineMinimal';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/LinLibertine_minimal.ttf);
}
.sbeads{
fill: var(--nord10);
}
.chain{
stroke:black;
stroke-width: 0.7;
stroke-miterlimit: 4;
stroke: gray;
fill: none;
}
.sbeads circle.hitbox{
r: 3.2px;
stroke-width:0;
}
#start1 circle{
cx:15.559271px;
cy: 20.881956px;
}
#start2 circle{
cx:21.633902px;
cy:20.367514px;
}
#start3 circle{
cx:27.96961px;
cy:21.178484px;
}
#lbead5 circle{
cx:118.50725px;
cy:59.477211px;
}
#lbead4 circle{
cx:126.81134px;
cy:15.751753px;
}
#lbead1 circle{
cx:7.6719489px;
cy:25.364584px;
}
#lbead2 circle{
cx:36.798512px;
cy:23.486462px;
}
#lbead3 circle{
cx:84.105789px;
cy:3.0456686px;
}
#lbead6 circle{
cx:72.185097px;
cy:64.006859px;
}
#start1:hover .msg,
#start2:hover .msg,
#start3:hover .msg,
#secret1:hover .msg,
#secret2:hover .msg,
#secret3:hover .msg,
#secret4:hover .msg,
#secret5:hover .msg,
#lbeads .beforedecades:hover .msg,
#lbeads .afterdecade:hover .msg,
#cross:hover .msg
{
display:block;
}
#start1:hover .sbeads circle:not(.hitbox),
#start2:hover .sbeads circle:not(.hitbox),
#start3:hover .sbeads circle:not(.hitbox),
#secret1:hover .sbeads circle,
#secret2:hover .sbeads circle,
#secret3:hover .sbeads circle,
#secret4:hover .sbeads circle,
#secret5:hover .sbeads circle
{
fill: var(--nord11);
r: 1.5px;
}
#lbead1:hover .lbead,
#lbead2:hover .lbead,
#lbead3:hover .lbead,
#lbead4:hover .lbead,
#lbead5:hover .lbead,
#lbead6:hover .lbead{
r: 2.8px;
fill: var(--nord11);
}
#cross:hover .symbol{
fill: var(--nord11);
stroke: var(--nord11);
stroke-width: 0.25;
}
#lbeads.msg{
display:block;
}
.sbeads circle{
r: 1.25px;
}
.msg .diff{
fill: var(--nord11);
}
.msg .b{
font-family: crosses;
font-weight: bold;
}
.msg .title{
fill: var(--nord10);
font-weight: bold;
font-size: 5px;
}
.msg{
font-size: 4px;
stroke: none;
fill: var(--nord4);
display:none;
}
text{
font-family: LibertineMinimal;
}
#lbeads circle.hitbox{
r:5px;
stroke:none;
stroke-width:0;
}
.lbead{
fill: var(--nord12);
r: 2.65px;
}
.hitbox{
opacity:0;
stroke-width: 2;
fill: red;
stroke: red;
}
#coin circle{
r: 2.7px;
fill:darkgray;
}
#coin text{
fill:var(--nord0);
font-size: 4.259px;
line-height:1.25;
font-family: crosses;
}
#cross .symbol{
font-family: crosses;
fill: var(--nord4);
font-size: 17.3637px;
line-height: 1.25;
stroke-width:0.434093
}
table{
width: 100%;
border-collapse: collapse;
}
td{
text-align:center;
border-left: 1px solid;
border-right: 1px solid;
border-color: var(--nord2);
padding-left: 5px;
padding-right: 5px;
}
tr :last-child{
border-right: none;
}
tr :first-child{
border-left: 0px solid;
}
thead td{
color: var(--nord4);
border-bottom-width: 3px;
border-bottom-color: var(--nord10);
border-bottom-style: dotted;
font-size: 110%;
font-weight: bold;
}
.table{
width:100%;
overflow-x: auto;
}

28
src/lib/css/shake.css Normal file
View File

@ -0,0 +1,28 @@
@keyframes shake{
0%{
transform: rotate(0)
scale(1,1);
}
25%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle, 30deg))
scale(1.2,1.2)
;
}
50%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(calc(-1 * var(--angle,30deg)))
scale(1.2,1.2);
}
75%{
box-shadow: 0em 0em 1em 0.2em rgba(0, 0, 0, 0.6);
transform: rotate(var(--angle,30deg))
scale(1.2, 1.2);
}
100%{
transform: rotate(0);
scale: var(--endscale, 1);
}
}

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

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

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;
@ -9,9 +7,11 @@ function mulberry32(a) {
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
}
let rand = mulberry32(seed)
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
}

View File

@ -0,0 +1,7 @@
// Function to strip HTML tags from a string
import {load} from 'cheerio';
export function stripHtmlTags(input) {
const $ = load(input.replace(/&shy;/g, ''));
return $.text();
}

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

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

View File

@ -14,6 +14,7 @@ const RecipeSchema = new mongoose.Schema(
caption: String,
}],
description: {type: String, required: true},
note: {type: String},
tags : [String],
season : [Number],
baking: { temperature: {type:String, default: ""},
@ -38,7 +39,7 @@ const RecipeSchema = new mongoose.Schema(
steps: [String]}],
preamble : String,
addendum : String,
},
}, {timestamps: true}
);
export const Recipe = mongoose.model("Recipe", RecipeSchema);

View File

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

View File

@ -0,0 +1,16 @@
<script>
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
export let data
let user;
if(data.session){
user = data.session.user
}
</script>
<Header>
<ul class=site_header slot=links>
</ul>
<UserHeader {user} slot=right_side></UserHeader>
<slot></slot>
</Header>

View File

@ -0,0 +1,157 @@
<script lang="ts">
import "$lib/css/nordtheme.css";
import LinksGrid from "$lib/components/LinksGrid.svelte";
export let data;
</script>
<style>
.hero{
display: flex;
align-items: center;
max-width: 1400px;
margin-inline: auto;
gap: 2rem;
}
.hero img{
border-radius: 1000px;
margin: 1rem;
width: clamp(100px, 300px, 50vw);
object-fit: cover;
background-color: var(--nord4);
box-shadow: 0.2em 0.2em 1em 1em rgba(0, 0, 0, 0.1);
}
.hero div{
margin-inline: 1rem;
}
section h2{
font-size: 2rem;
text-align: center;
}
@media (prefers-color-scheme: dark){
*{
color: white;
}
.hero img{
box-shadow: 0.1em 0.1em 2em 0.5em rgba(255, 255, 255, 0.1);
}
}
@media (max-width: 600px){
.hero{
flex-direction: column;
gap: 1rem;
}
.hero img{
width: clamp(100px, 200px, 80vw);
}
.hero h1{
text-align: center;
}
}
</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">
<div>
<h1><q>Willkommen auf bocken.org</q></h1>
<p>
Hallo, ich bin Alexander Bocken. Auf dieser Seite findest du einige Softwareprojekte für Freunde, Familie und mich.
Alles ist selbst gehostet bei mir daheim auf einem kleinen Mini-Server (Arch, btw).
</p>
<p>
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.
</p>
</div>
</section>
{/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=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>
<a href="https://stream.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="M448 32H361.9l-1 1-127 127h92.1l1-1L453.8 32.3c-1.9-.2-3.8-.3-5.8-.3zm64 128V96c0-15.1-5.3-29.1-14-40l-104 104H512zM294.1 32H201.9l-1 1L73.9 160h92.1l1-1 127-127zM64 32C28.7 32 0 60.7 0 96v64H6.1l1-1 127-127H64zM512 192H0V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V192z"/></svg>
<h3>Streaming</h3>
</a>
<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>
<a href="https://cloud.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="M0 336c0 79.5 64.5 144 144 144H512c70.7 0 128-57.3 128-128c0-61.9-44-113.6-102.4-125.4c4.1-10.7 6.4-22.4 6.4-34.6c0-53-43-96-96-96c-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32C167.6 32 96 103.6 96 192c0 2.7 .1 5.4 .2 8.1C40.2 219.8 0 273.2 0 336z"/></svg>
<h3>Cloud</h3>
</a>
<a href="https://meet.bocken.org">
<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="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z"/></svg>
<h3>Videokonferenzen</h3>
</a>
<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>
<a href="https://cloud.bocken.org/apps/cospend/">
<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="M0 24C0 10.7 10.7 0 24 0H69.5c22 0 41.5 12.8 50.6 32h411c26.3 0 45.5 25 38.6 50.4l-41 152.3c-8.5 31.4-37 53.3-69.5 53.3H170.7l5.4 28.5c2.2 11.3 12.1 19.5 23.6 19.5H488c13.3 0 24 10.7 24 24s-10.7 24-24 24H199.7c-34.6 0-64.3-24.6-70.7-58.5L77.4 54.5c-.7-3.8-4-6.5-7.9-6.5H24C10.7 48 0 37.3 0 24zM128 464a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zm336-48a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/></svg>
<h3>Einkauf</h3>
</a>
<a href="https://tree.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="M335.5 4l288 160c15.4 8.6 21 28.1 12.4 43.5s-28.1 21-43.5 12.4L320 68.6 47.5 220c-15.4 8.6-34.9 3-43.5-12.4s-3-34.9 12.4-43.5L304.5 4c9.7-5.4 21.4-5.4 31.1 0zM320 160a40 40 0 1 1 0 80 40 40 0 1 1 0-80zM144 256a40 40 0 1 1 0 80 40 40 0 1 1 0-80zm312 40a40 40 0 1 1 80 0 40 40 0 1 1 -80 0zM226.9 491.4L200 441.5V480c0 17.7-14.3 32-32 32H120c-17.7 0-32-14.3-32-32V441.5L61.1 491.4c-6.3 11.7-20.8 16-32.5 9.8s-16-20.8-9.8-32.5l37.9-70.3c15.3-28.5 45.1-46.3 77.5-46.3h19.5c16.3 0 31.9 4.5 45.4 12.6l33.6-62.3c15.3-28.5 45.1-46.3 77.5-46.3h19.5c32.4 0 62.1 17.8 77.5 46.3l33.6 62.3c13.5-8.1 29.1-12.6 45.4-12.6h19.5c32.4 0 62.1 17.8 77.5 46.3l37.9 70.3c6.3 11.7 1.9 26.2-9.8 32.5s-26.2 1.9-32.5-9.8L552 441.5V480c0 17.7-14.3 32-32 32H472c-17.7 0-32-14.3-32-32V441.5l-26.9 49.9c-6.3 11.7-20.8 16-32.5 9.8s-16-20.8-9.8-32.5l36.3-67.5c-1.7-1.7-3.2-3.6-4.3-5.8L376 345.5V400c0 17.7-14.3 32-32 32H296c-17.7 0-32-14.3-32-32V345.5l-26.9 49.9c-1.2 2.2-2.6 4.1-4.3 5.8l36.3 67.5c6.3 11.7 1.9 26.2-9.8 32.5s-26.2 1.9-32.5-9.8z"/></svg>
<h3>Stammbaum</h3>
</a>
<a href=glaube>
<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="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg>
<h3>Glaube</h3>
</a>
<a href=https://transmission.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="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,19 @@
import { redirect } from "@sveltejs/kit"
import type { Actions, PageServerLoad } from "./$types"
export const actions: Actions = {
register: async (event) => {
const data = await event.request.formData();
const res = await event.fetch('/api/user/register',
{method: 'POST',
body: JSON.stringify({
username: data.get('username'),
password: data.get('password'),
})
}
)
throw redirect(303, "/login")
},
}

View File

@ -0,0 +1,18 @@
<script>
import "$lib/css/form.css"
</script>
<form action="?/register" method=POST>
<h1>Registrieren</h1>
<label>
Username
<input type="text" name="username" required>
</label>
<label>
Passwort
<input name="password" type="password" required>
</label>
<button type="submit">Registrieren</button>
<h4>Hinweis:</h4>
<p>Dein Account wird keine Rechte haben bis ein Administrator dir welche gibt. Bis dahin ist der Account relativ nutzlos.</p>
</form>

View File

@ -0,0 +1,34 @@
import { redirect } from "@sveltejs/kit"
import type { Actions, PageServerLoad } from "./$types"
import { error } from "@sveltejs/kit"
export const load: PageServerLoad = async ({ cookies }) => {
const user = await authenticateUser(cookies)
return {user}
}
export const actions: Actions = {
change_password: async (event) => {
console.log("Changin password")
const data = await event.request.formData()
const res = await event.fetch('/api/user/change_pw',
{method: 'POST',
body: JSON.stringify({
username: data.get('username'),
new_password: data.get('new_password'),
new_password_rep: data.get('new_password_rep'),
old_password: data.get('old_password'),
}),
headers: {
credentials: 'include',
}
})
if(res.ok){
console.log("OK response")
}
else{
const item = await res.json()
throw error(401, item.message)
}
}
}

View File

@ -0,0 +1,61 @@
<script>
import {enhance} from '$app/forms';
export let data
let password;
const admin = data.user?.access.includes('admin') ?? false
import "$lib/css/form.css"
</script>
<style>
input:invalid + div{
display: none;
}
input:valid + div{
position: absolute;
color: green;
bottom: 0.25rem;
right: -0.25rem;
font-size: 1.5rem;
width: 1em;
height: 1em;
}
form label,
form label input
{
position: relative;
display: block;
}
input.hide{
display:none;
}
</style>
<section>
<h2>Change Profile pictures</h2>
</section>
<section>
<form action="?/change_password" method=POST use:enhance>
<h2>Passwort ändern</h2>
<input type="text" bind:value={data.user.username} class=hide name="username" required>
<label>
Altes Passwort:
<input type="password" name="old_password" required>
</label>
<label>
Neues Passwort:
<input type="password" name="new_password" required bind:value={password} minlength=10>
<div>✔️</div>
</label>
<label>
Neues Passwort wiederholen:
<input type="password" name="new_password_rep" required pattern={password}>
<div>✔️</div>
</label>
<button type="submit">Ändern</button>
</form>
</section>
{#if admin}
<section>
<h2>Change user permissions</h2>
</section>
{/if}

View File

@ -1,32 +0,0 @@
<script>
import Header from '$lib/components/Header.svelte'
</script>
<style>
</style>
<Header>
<ul class=site_header slot=links>
<li><a href="/rezepte">Rezepte</a></li>
<li><a href="/bilder">Bilder</a></li>
<li><a href="/git">Git</a></li>
<li><a href="/transmission">Transmission</a></li>
</ul>
<section>
<h2><a href="/rezepte">Rezepte</a></h2>
</section>
<section>
<h2><a href="/bilder">Bilder</a></h2>
</section>
<section>
<h2><a href="/git">Git</a></h2>
</section>
<section>
<h2><a href="/transmission">Transmission Web Viewer</a></h2>
</section>
</Header>

View File

@ -1,28 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../utils/db';
import type {RecipeModelType} from '../../../types/types';
import { BEARER_TOKEN } from '$env/static/private'
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request}) => {
let message = await request.json()
const recipe_json = message.recipe
const bearer_token = message.headers.bearer
if(bearer_token === BEARER_TOKEN){
await dbConnect();
try{
await Recipe.create(recipe_json);
} catch(e){
throw error(400, e)
}
await dbDisconnect();
return new Response(JSON.stringify({msg: "Added recipe successfully"}),{
status: 200,
});
}
else{
throw error(403, "Password incorrect")
}
};

View File

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

View File

@ -1,24 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../utils/db';
import type {RecipeModelType} from '../../../types/types';
import { BEARER_TOKEN } from '$env/static/private'
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request}) => {
let message = await request.json()
const short_name = message.old_short_name
const bearer_token = message.headers.bearer
if(bearer_token === BEARER_TOKEN){
await dbConnect();
await Recipe.findOneAndDelete({short_name: short_name});
await dbDisconnect();
return new Response(JSON.stringify({msg: "Deleted recipe successfully"}),{
status: 200,
});
}
else{
throw error(403, "Password incorrect")
}
}

View File

@ -1,26 +0,0 @@
import { writeFileSync } from 'fs';
import path from 'path'
import type { RequestHandler } from '@sveltejs/kit';
import { BEARER_TOKEN } from '$env/static/private'
import { error } from '@sveltejs/kit';
export const POST = (async ({ request }) => {
const data = await request.json();
const filePath = path.join(
process.cwd(),
"static",
"images",
data.filename as string
);
const file = data.image;
if(data.bearer === BEARER_TOKEN){
writeFileSync(filePath, file, 'base64');
return new Response(JSON.stringify({msg: "Added image successfully"}),{
status: 200,
});
}
else{
throw error(403, "Password incorrect")
}
}) satisfies RequestHandler;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request, cookies, locals}) => {
let message = await request.json()
const recipe_json = message.recipe
let auth = await locals.auth();
/*const user = session.user;*/
console.log(auth)
if(!auth){
throw error(401, "Not logged in")
}
/*if(!user.access.includes("rezepte")){
throw error(401, "This user does not have permissions to add recipes")
}*/
await dbConnect();
try{
await Recipe.create(recipe_json);
} catch(e){
throw error(400, e)
}
await dbDisconnect();
return new Response(JSON.stringify({msg: "Added recipe successfully"}),{
status: 200,
});
};

View File

@ -0,0 +1,21 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import type {RecipeModelType} from '../../../../types/types';
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request, locals}) => {
let message = await request.json()
const auth = await locals.auth();
if(!auth) throw error(401, "Need to be logged in")
const short_name = message.old_short_name
await dbConnect();
await Recipe.findOneAndDelete({short_name: short_name});
await dbDisconnect();
return new Response(JSON.stringify({msg: "Deleted recipe successfully"}),{
status: 200,
});
}

View File

@ -1,17 +1,18 @@
import type { RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../utils/db';
import type {RecipeModelType} from '../../../types/types';
import { BEARER_TOKEN } from '$env/static/private'
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import type {RecipeModelType} from '../../../../types/types';
import { error } from '@sveltejs/kit';
// header: use for bearer token for now
// recipe json in body
export const POST: RequestHandler = async ({request}) => {
console.log("AT EDIT API")
export const POST: RequestHandler = async ({request, locals}) => {
let message = await request.json()
const recipe_json = message.recipe
const bearer_token = message.headers.bearer
if(bearer_token === BEARER_TOKEN){
const auth = await locals.auth();
if(!auth){
throw error(403, "Not logged in")
}
else{
await dbConnect();
await Recipe.findOneAndUpdate({short_name: message.old_short_name }, recipe_json);
await dbDisconnect();
@ -20,7 +21,4 @@ export const POST: RequestHandler = async ({request}) => {
});
}
else{
throw error(403, "Password incorrect")
}
};

View File

@ -0,0 +1,44 @@
import path from 'path'
import type { RequestHandler } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
import { IMAGE_DIR } from '$env/static/private'
import sharp from 'sharp';
export const POST = (async ({ request, locals}) => {
const data = await request.json();
const auth = await locals.auth();
if (!auth) throw error(401, "Need to be logged in")
let full_res = new Buffer.from(data.image, 'base64')
// reduce image size if over 500KB
const MAX_SIZE_KB = 500
//const metadata = await sharp(full_res).metadata()
////reduce image size if larger than 500KB
//if(metadata.size > MAX_SIZE_KB*1000){
// full_res = sharp(full_res).
// webp( { quality: 70})
// .toBuffer()
//}
await sharp(full_res)
.toFormat('webp')
.toFile(path.join(IMAGE_DIR,
"rezepte",
"full",
data.name + ".webp"))
await sharp(full_res)
.resize({ width: 800})
.toFormat('webp')
.toFile(path.join(IMAGE_DIR,
"rezepte",
"thumb",
data.name + ".webp"))
await sharp(full_res)
.resize({ width: 20})
.toFormat('webp')
.toFile(path.join(IMAGE_DIR,
"rezepte",
"placeholder",
data.name + ".webp"))
return new Response(JSON.stringify({msg: "Added image successfully"}),{
status: 200,
});
}) satisfies RequestHandler;

View File

@ -0,0 +1,20 @@
import path from 'path'
import type { RequestHandler } from '@sveltejs/kit';
import { IMAGE_DIR } from '$env/static/private'
import { unlink } from 'node:fs';
import { error } from '@sveltejs/kit';
export const POST = (async ({ request, locals}) => {
const data = await request.json();
const auth = await locals.auth()
if(!auth) throw error(401, "You need to be logged in")
[ "full", "thumb", "placeholder"].forEach((folder) => {
unlink(path.join(IMAGE_DIR, "rezepte", folder, data.name + ".webp"), (e) => {
if(e) error(404, "could not delete: " + folder + "/" + data.name + ".webp" + e)
})
})
return new Response(JSON.stringify({msg: "Deleted image successfully"}),{
status: 200,
});
}) satisfies RequestHandler;

View File

@ -0,0 +1,23 @@
import path from 'path'
import type { RequestHandler } from '@sveltejs/kit';
import { IMAGE_DIR } from '$env/static/private'
import { rename } from 'node:fs';
import { error } from '@sveltejs/kit';
export const POST = (async ({ request, locals}) => {
const data = await request.json();
const auth = await locals.auth();
if(!auth ) throw error(401, "need to be logged in")
[ "full", "thumb", "placeholder"].forEach((folder) => {
const old_path = path.join(IMAGE_DIR, "rezepte", folder, data.old_name + ".webp")
rename(old_path, path.join(IMAGE_DIR, "rezepte", folder, data.new_name + ".webp"), (e) => {
console.log(e)
if(e) throw error(500, "could not mv: " + old_path)
})
});
return new Response(JSON.stringify({msg: "Deleted image successfully"}),{
status: 200,
});
}) satisfies RequestHandler;

View File

@ -1,7 +1,7 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import type {RecipeModelType} from '../../../../types/types';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import type {RecipeModelType} from '../../../../../types/types';
import { error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({params}) => {

View File

@ -0,0 +1,12 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import type { BriefRecipeType } from '../../../../../types/types';
import { Recipe } from '../../../../../models/Recipe'
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
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 dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
return json(JSON.parse(JSON.stringify(found_brief)));
};

View File

@ -1,7 +1,7 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import type {BriefRecipeType} from '../../../../types/types';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();

View File

@ -0,0 +1,14 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../../types/types';
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 dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
};

View File

@ -5,9 +5,9 @@ import type {BriefRecipeType} from '../../../../../types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();
let recipes = (await Recipe.find({tags: params.tag}, 'name short_name images tags category icon description season').lean()) as BriefRecipeType[];
let icons = (await Recipe.distinct('icon').lean());
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
icons = JSON.parse(JSON.stringify(icons));
return json(icons);
};

View File

@ -0,0 +1,14 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../../types/types';
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 dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
};

View File

@ -0,0 +1,13 @@
import type {rand_array} from '$lib/js/randomize';
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe'
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
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 dateModified').lean());
await dbDisconnect();
found_in_season = JSON.parse(JSON.stringify(found_in_season));
return json(found_in_season);
};

View File

@ -1,7 +1,7 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../utils/db';
import type {BriefRecipeType} from '../../../../types/types';
import { Recipe } from '../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../types/types';
export const GET: RequestHandler = async ({params}) => {
await dbConnect();

View File

@ -0,0 +1,14 @@
import { json, type RequestHandler } from '@sveltejs/kit';
import { Recipe } from '../../../../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../../../../utils/db';
import type {BriefRecipeType} from '../../../../../../types/types';
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 dateModified').lean()) as BriefRecipeType[];
await dbDisconnect();
recipes = JSON.parse(JSON.stringify(recipes));
return json(recipes);
};

View File

@ -1,411 +0,0 @@
import { json } from '@sveltejs/kit';
import { Recipe } from '../../../models/Recipe';
import { dbConnect, dbDisconnect } from '../../../utils/db';
const test_json = [
{
short_name: "aelplermagronen",
name : "Älplermagronen",
category: "Hauptspeise",
icon: "🍂",
datecreated: 20230619,
datemodified: 20230619,
images: [{
mediapath: "aelplermagronen.webp",
alt: "Älplermagronen serviert mit Apfelmuß",
caption: "",
}],
description: "Alles was das Bauernherz erfreuen lässt in einer Mahlzeit.",
preamble: "Dieser Schweizer Klassiker ist wohl das beste Essen nach einem langen Tag von Skifahren. Die Beilage aus Apfelmus ist ein Muss.",
tags: ["Schweiz", "Käse", "Speck", "Nudeln", "Apfelmuß", "Kartoffeln", "Fleisch"],
season: [10,11,12,1],
portions: "4 Hauptspeisen",
preparation: "10 min",
total_time: "30 min",
ingredients: [ {
name: "",
list: [
{ name: "Speckwürfel",
unit: "g",
amount: "150"
},
{
name: "mittelgroße Zwiebeln",
unit: "",
amount: "3",
},
{
name: "Kartoffeln, festkochend",
unit: "g",
amount: "400",
},
{
name: "Milch",
unit: "L",
amount: "1-2",
},
{
name: "Maccaroni",
unit: "g",
amount: "400",
},
{
name: "Appenzeller",
unit: "g",
amount: "150",
},
{
name: "<a href=apfelmuss>Apfelmuß</a>",
unit: "",
amount: "",
},
]},
],
instructions: [
{name: "",
steps: [
"In einem großen Topf oder tiefer Pfanne Speckwürfel anbraten.",
"Zwiebel in Halbringe schneiden und im gleichen Topf schwitzen lassen.",
"Kartoffeln schälen und in ~1 cm<sup>3</sup> schneiden.",
"Wenn Ziwebeln genügend gekocht sind die Kartoffeln hinzufügen und Milch hinzufügen, sodass alles bedeckt ist. Ca. 10 Minuten kochen lassen.",
"Ca. 1 L Milch hinzugeben. Für den nächsten Schritt wollen wir die Maccaroni hinzufügen. Damit diese nicht zu breiig werden geben wir erst die Milch zu und lassen sie aufkochen.",
"Wenn die der Topf wieder kocht jetzt die Maccaroni hinzugeben.",
"Den Käse zerreiben oder in kleine Würfel schneiden.",
"Ein bis zwei Minuten bevor die Nudeln durchgekocht sind den Käse hinzugeben und schmelzen lassen.",
"Mit Salz und Muskat würzen.",
"Den Topf ein bisschen zu früh vom Herd nehmen und ein bisschen auskühlen lassen.",
"Mit <a href=apfelkompott>Apfelmuß oder Apfelkompott</a> servieren."
]
}
],
addendum: "<p>Man kann das Gericht noch dekanter machen indem man zu Teilen Rahm an Stelle von Milch verwendet. Zudem kann man das ganze auch noch in eine Auflaufform geben und im Ofen eine Kruste anbacken</p>",
},
{
short_name: "baerlauchravioli",
name : "Bärlauchravioli",
category: "Pasta",
icon: "🌷",
datecreated: 20230619,
datemodified: 20230619,
images: [{
mediapath: "baerlauchravioli.webp",
alt: "3 Ravioli mit durchscheinender grüner Bärlauchfüllung auf einem Teller.",
caption: "",
}],
description: "Unwiderstehliche Ravioli mit Bärlauchfüllung.",
tags: ["Bärlauch", "Wald", "Frühling", "Ravioli", "Italien", "Käse"],
season: [ 2,3,4],
baking: {
temperature: "220",
length: "40 Minuten",
mode: "Ober-/Unterhitze",
},
preparation: "20 Minuten",
fermentation: {
bulk: "2.5 Stunden",
final: "2 Stunden"
},
portions: "4 Pizzen",
total_time: "1 Tag",
ingredients: [],
instructions: []
},
{
short_name: "anisbroetli",
name : "Anisbrötli",
category: "Guetzli",
icon: "🎄",
datecreated: 20230619,
datemodified: 20230619,
images: [{
mediapath: "anisbrot.webp",
alt: "Ein ganzes Brot",
caption: "",
}],
description: "Allemannische Weihnachstkekese bekannt durch seine harte Kruste und weiches Inneres.",
tags: ["Backen", "Advent", "schweiz", "Deutschland", "Anis", "Weihnachten", "Kekse"],
season: [ 12,1],
baking: {
temperature: "220",
length: "40 Minuten",
mode: "Ober-/Unterhitze",
},
preparation: "20 Minuten",
fermentation: {
bulk: "2.5 Stunden",
final: "2 Stunden"
},
portions: "4 Pizzen",
total_time: "1 Tag",
ingredients: [],
instructions: []
},
{
short_name: "alragu",
name : "Pasta al Ragù",
category: "Pasta",
icon: "☀️",
datecreated: 20230619,
datemodified: 20230619,
images: [{
mediapath: "al_ragu.webp",
alt: "Ein ganzes Brot",
caption: "",
}],
description: "Eine etwas gehobene Version der klassichen Bolognese.",
tags: ["Pasta", "Fleisch", "Rind", "Italien", "Bolognese", "Linguine"],
season: [ 6,7,8,9],
baking: {
temperature: "220",
length: "40 Minuten",
mode: "Ober-/Unterhitze",
},
preparation: "20 Minuten",
fermentation: {
bulk: "2.5 Stunden",
final: "2 Stunden"
},
portions: "4 Pizzen",
total_time: "1 Tag",
ingredients: [ {
name: "Teig",
list: [
{name: "Mehl",
unit: "g",
amount: 500
} ,
{
name: "Salz",
unit: "g",
amount: 6
}
]},
{
name: "Füllung",
list: [
{
name: "Aprikose",
unit: "Stück",
amount: 10
},
{
name: "Zuckerwürfel",
unit: "Stück",
amount: 10
}
] }
],
instructions: [
{name: "",
steps: [
"Den Rhabarber schälen und in ca. 1 cm große Stücke schneiden",
"Have fun"
]
}
]},
{
short_name: "sauerteigbrot",
name : "Simples Sauerteigbrot",
category: "Brot",
icon: "🍂",
datecreated: 20230619,
datemodified: 20230619,
images: [{
mediapath: "sauerteigbrot.webp",
alt: "Ein ganzes Brot",
caption: "",
}],
description: "Einfach und sehr lecker",
tags: ["Brot", "Backen", "Sauerteig"],
season: [],
baking: {
temperature: "220",
length: "40 Minuten",
mode: "Ober-/Unterhitze",
},
preparation: "20 Minuten",
fermentation: {
bulk: "2.5 Stunden",
final: "2 Stunden"
},
portions: "4 Pizzen",
total_time: "1 Tag",
ingredients: [ {
name: "Teig",
list: [
{name: "Mehl",
unit: "g",
amount: 500
} ,
{
name: "Salz",
unit: "g",
amount: 6
}
]},
{
name: "Füllung",
list: [
{
name: "Aprikose",
unit: "Stück",
amount: 10
},
{
name: "Zuckerwürfel",
unit: "Stück",
amount: 10
}
] }
],
instructions: [
{name: "",
steps: [
"Den Rhabarber schälen und in ca. 1 cm große Stücke schneiden",
"Have fun"
]
}
]},
{ short_name: "rhabarberkonfi",
name: "Rhabarberkonfi",
category: "Aufstrich",
icon: "☀️",
datecreated: 20230610,
datemodified: 20230611,
images:
[ {
mediapath: "rhabarberkonfi.webp",
alt: "Ein Brot mit Marmelade darauf.",
caption: ""
}
],
description: "Die erste Marmelade des Jahres mit säuerlicher Note.",
tags:["Marmelade", "sauer", "Sommer", "süß"],
season: [4,5,6],
baking: {
temperature: "160",
length: "4 Stunden",
mode: "Ober-/Unterhitze"
},
preparation: "20 Minuten",
fermentation: {
bulk: "2.5 Stunden",
final: "2 Stunden"
},
portions: "4 Pizzen",
total_time: "1 Tag",
ingredients: [ {
name: "Teig",
list: [
{name: "Mehl",
unit: "g",
amount: 500
} ,
{
name: "Salz",
unit: "g",
amount: 6
}
]},
{
name: "Füllung",
list: [
{
name: "Aprikose",
unit: "Stück",
amount: 10
},
{
name: "Zuckerwürfel",
unit: "Stück",
amount: 10
}
] }
],
instructions: [
{name: "",
steps: [
"Den Rhabarber schälen und in ca. 1 cm große Stücke schneiden",
"Have fun"
]
}
]
},
{ short_name: "osterkuchen",
name: "Osterkuchen",
category: "Kuchen",
icon: "🐇",
datecreated: 20230610,
datemodified: 20230611,
images:
[ {
mediapath: "osterkuchen.webp",
alt: "Ein Brot mit Marmelade darauf.",
caption: ""
}
],
description: "Ein traditioneller Milchreiskuchen mit Aprikosenmarmelade und Rosinen.",
tags:["Schweiz", "Ostern", "Milchreis", "Aprikosen", 'Backen', 'süß', "Marmelade"],
season: [3,4],
baking: {
temperature: "160",
length: "4 Stunden",
mode: "Ober-/Unterhitze"
},
preparation: "20 Minuten",
fermentation: {
bulk: "2.5 Stunden",
final: "2 Stunden"
},
portions: "4 Pizzen",
total_time: "1 Tag",
ingredients: [ {
name: "Teig",
list: [
{name: "Mehl",
unit: "g",
amount: 500
} ,
{
name: "Salz",
unit: "g",
amount: 6
}
]},
{
name: "Füllung",
list: [
{
name: "Aprikose",
unit: "Stück",
amount: 10
},
{
name: "Zuckerwürfel",
unit: "Stück",
amount: 10
}
] }
],
instructions: [
{name: "",
steps: [
"Den Rhabarber schälen und in ca. 1 cm große Stücke schneiden",
"Have fun"
]
}
]
},
];
// seed data
export const GET = async () => {
await dbConnect();
await Recipe.deleteMany();
await Recipe.insertMany(test_json);
await dbDisconnect();
return json({
message: 'seeded',
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,75 @@
<script>
import LinksGrid from '$lib/components/LinksGrid.svelte';
</script>
<style>
h1{
text-align: center;
font-size: 3em;
}
p{
max-width: 600px;
margin: 0 auto;
font-size: 1.1em;
}
</style>
<h1>Glaube</h1>
<p>
Hier findet man einige Gebete, den Rosenkranz und aufgearbeitete Predigten zum katholischen Glauben.
Ein Fokus auf Latein und den tridentinischen Ritus wird zu bemerken sein.
Diese Seiten sind noch im Aufbau und werden nach und nach erweitert.
Bis jetzt sind nur die Gebete in einem guten Stand.
</p>
<LinksGrid>
<a href="/glaube/gebete">
<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="M351.2 4.8c3.2-2 6.6-3.3 10-4.1c4.7-1 9.6-.9 14.1 .1c7.7 1.8 14.8 6.5 19.4 13.6L514.6 194.2c8.8 13.1 13.4 28.6 13.4 44.4v73.5c0 6.9 4.4 13 10.9 15.2l79.2 26.4C631.2 358 640 370.2 640 384v96c0 9.9-4.6 19.3-12.5 25.4s-18.1 8.1-27.7 5.5L431 465.9c-56-14.9-95-65.7-95-123.7V224c0-17.7 14.3-32 32-32s32 14.3 32 32v80c0 8.8 7.2 16 16 16s16-7.2 16-16V219.1c0-7-1.8-13.8-5.3-19.8L340.3 48.1c-1.7-3-2.9-6.1-3.6-9.3c-1-4.7-1-9.6 .1-14.1c1.9-8 6.8-15.2 14.3-19.9zm-62.4 0c7.5 4.6 12.4 11.9 14.3 19.9c1.1 4.6 1.2 9.4 .1 14.1c-.7 3.2-1.9 6.3-3.6 9.3L213.3 199.3c-3.5 6-5.3 12.9-5.3 19.8V304c0 8.8 7.2 16 16 16s16-7.2 16-16V224c0-17.7 14.3-32 32-32s32 14.3 32 32V342.3c0 58-39 108.7-95 123.7l-168.7 45c-9.6 2.6-19.9 .5-27.7-5.5S0 490 0 480V384c0-13.8 8.8-26 21.9-30.4l79.2-26.4c6.5-2.2 10.9-8.3 10.9-15.2V238.5c0-15.8 4.7-31.2 13.4-44.4L245.2 14.5c4.6-7.1 11.7-11.8 19.4-13.6c4.6-1.1 9.4-1.2 14.1-.1c3.5 .8 6.9 2.1 10 4.1z"/></svg>
<h3>Gebete</h3>
</a>
<a href=/glaube/rosenkranz >
<svg viewBox="0 0 512 512">
<g>
<path class="st0" d="M241.251,145.056c-39.203-17.423-91.472,17.423-104.54,60.982c-13.068,43.558,8.712,117.608,65.337,143.742
c56.626,26.135,108.896-8.712,87.117-39.202c-74.049-8.712-121.963-87.117-100.184-126.319S280.453,162.479,241.251,145.056z"/>
<path class="st0" d="M337.079,271.375c47.914-39.202,21.779-126.319-17.423-135.031c-39.202-8.712-56.626,13.068-26.135,39.202
c39.203,30.491-8.712,91.472-39.202,87.117C254.318,262.663,289.165,310.577,337.079,271.375z"/>
<path class="st0" d="M254.318,119.788c43.558-17.423,74.049-9.579,100.184,16.556c13.068-39.202-30.491-104.54-108.896-113.252
S93.153,118.921,127.999,171.191C136.711,153.767,188.981,106.721,254.318,119.788z"/>
<path class="st0" d="M110.576,245.24C36.527,262.663,28.87,335.248,45.239,380.27c17.423,47.914,4.356,82.761,26.135,91.472
c20.622,8.253,91.472,13.068,152.454,17.423c60.982,4.356,108.896-47.914,91.472-108.896
C141.067,410.761,110.576,284.442,110.576,245.24z"/>
<path class="st0" d="M93.883,235.796c0,0,2.178-28.313,10.89-43.558c-4.356-4.356-8.712-21.779-8.712-21.779
s-4.356-19.601-4.356-34.846c-32.669-6.534-89.295,34.846-91.472,41.38c-2.178,6.534,10.889,80.583,39.202,82.761
C69.927,235.796,93.883,235.796,93.883,235.796z"/>
<path class="st0" d="M489.533,175.546c-39.202-82.761-113.252-65.337-113.252-65.337s4.356,21.779-4.356,34.846
c43.558,47.914,13.067,146.643-24.681,158.265c130.675,56.626,159.712-58.081,164.068-75.504
C515.668,210.393,498.245,197.326,489.533,175.546z"/>
<path class="st0" d="M454.108,332.076c-22.359,15.841-85.663,11.613-121.964-7.265c1.446,14.514-13.067,37.756-20.325,39.202
c27.59,11.621,53.725,62.436,7.265,116.161c18.878,18.87,95.828,4.356,140.842-24.689c7.325-4.722,18.869-52.27,21.779-79.851
C485.56,339.103,488.963,307.387,454.108,332.076z"/>
<path class="st0" d="M257.227,213.294c-18.928,5.164-30.439-6.27-23.234-18.869c5.811-10.167,5.266-20.69-8.712-13.068
c-29.044,17.423-11.612,66.784,24.689,62.428c49.36-17.423,27.581-62.428,14.514-60.982
C251.417,184.249,273.196,208.938,257.227,213.294z"/>
</g>
</svg>
<h3>Rosenkranz</h3>
</a>
<a href="/glaube/predigten">
<svg
enable-background="new 0 0 512 512"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg">
<g>
<path
d="m134.057 149.979v-69.942h-74.41c-8.284 0-15 6.716-15 15v39.94c0 8.284 6.716 15 15 15z"/>
<path d="m437.947 391.026v-211.047h-60.004v150.343c0 4.694-2.197 9.118-5.938 11.954-2.637 1.999-5.826 3.046-9.062 3.046-1.354 0-2.717-.184-4.051-.558l-102.892-28.865-102.892 28.865c-4.521 1.267-9.374.346-13.113-2.489-3.741-2.836-5.938-7.259-5.938-11.954v-150.342h-60.004v211.047z"/>
<path d="m377.943 149.979 74.409-.002c8.284 0 15-6.716 15-15v-39.94c0-8.284-6.716-15-15-15h-74.409z"/>
<path d="m164.057 310.535 87.892-24.657c1.325-.372 2.688-.558 4.052-.558s2.727.186 4.052.558l87.892 24.657v-230.5h-183.888zm106.943-175.06v17.394h15.34c8.284 0 15 6.716 15 15s-6.716 15-15 15h-15.34v57.793c0 8.284-6.716 15-15 15s-15-6.716-15-15v-57.793h-15.34c-8.284 0-15-6.716-15-15s6.716-15 15-15h15.34v-17.394c0-8.284 6.716-15 15-15s15 6.715 15 15z"/>
<path d="m497 482h-18.397v-35.972c0-13.785-11.215-25-25-25h-395.206c-13.785 0-25 11.215-25 25v35.972h-18.397c-8.284 0-15 6.716-15 15s6.716 15 15 15h482c8.284 0 15-6.716 15-15s-6.716-15-15-15z"/>
<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>
</a>
</LinksGrid>

View File

@ -0,0 +1,338 @@
<script>
import "$lib/css/christ.css";
import "$lib/css/nordtheme.css";
import Gebet from "./Gebet.svelte";
</script>
<style>
.ccontainer{
margin: auto;
overflow-x: visible;
max-width: 1000px;
}
.container{
column-count: 2;
column-gap: 1em;
}
@media (max-width: 800px) {
.container{
column-count: 1;
padding-left: calc((100% - 600px ) * 0.5); /* ugly*/
}
}
:global(.container > *){
break-inside: avoid-column; /* Prevent children from splitting across columns */
margin-bottom: 1em;
}
h1{
text-align: center;
font-size: 3rem;
}
</style>
<h1>Gebete</h1>
<div class="ccontainer">
<div class=container>
<Gebet name={"Das heilige Kreuzzeichen"} is_bilingue={true} >
<p>
<v lang=la>In nómine <i></i> Patris, et Fílii, et Spíritus Sancti. Amen.</v>
<v lang=de>Im Namen des <i></i> Vaters und des Sohnes und des Heiligen Geistes. Amen.</v>
</p>
</Gebet>
<Gebet name={"Glória Patri"} is_bilingue={true}>
<p>
<v lang=la>Glória Patri, et Fílio, et Spirítui Sancto.</v>
<v lang=de>Ehre sei dem Vater und dem Sohne und dem Hl. Geiste.</v>
<v lang=la>Sicute erat in princípio, et nunc, et semper:</v>
<v lang=de>Wie es war am Anfang, so auch jetzt und allezeit</v>
<v lang=la>et in sǽcula sæculórum. Amen.</v>
<v lang=de>und in Ewigkeit. Amen.</v>
</p>
</Gebet>
<Gebet name={"Paternoster"} is_bilingue={true} >
<p>
<v lang=la>Pater noster, qui es in cælis</v>
<v lang=de>Vater unser, der Du bist im Himmel,</v>
<v lang=la>Sanctificétur nomen tuum</v>
<v lang=de>geheiligt werde Dein Name;</v>
<v lang=la>Advéniat regnum tuum</v>
<v lang=de>zu uns komme Dein Reich;</v>
<v lang=la>Fiat volúntas tua, sicut in cælo, et in terra.</v>
<v lang=de>Dein Wille geschehe, wie im Himmel, also auch auf Erden!</v>
<v lang=la>Panem nostrum quotidiánum da nobis hódie.</v>
<v lang=de>Unser tägliches Brot gib uns heute;</v>
<v lang=la>Et dimítte nobis debíta nostra,</v>
<v lang=de>und vergib uns unsere Schulden,</v>
<v lang=la>sicut et nos dimíttimus debitóribus nostris.</v>
<v lang=de>wie auch wir vergeben unsern Schuldigern;</v>
<v lang=la>Et ne nos indúcas in tentatiónem.</v>
<v lang=de>und führe uns nicht in Versuchung.</v>
<v lang=la>Sed líbera nos a malo. Amen.</v>
<v lang=de>Sondern erlöse uns von dem Übel. Amen.</v>
</p>
</Gebet>
<Gebet name={"Credo"} is_bilingue={true}>
<p>
<v lang=la>Credo in unum <i><sup></sup></i> Deum, Patrem omnipoténtem,</v>
<v lang=de>Ich glaub an den einen <i><sup></sup></i> Gott. Den allmächtigen Vater,</v>
<v lang=la>factórem cæli et terræ,</v>
<v lang=de>Schöpfer des Himmels und der Erde,</v>
<v lang=la>visibílium ómnium et invisibílium.</v>
<v lang=de>aller sichtbaren und unsichtbaren Dinge.</v>
<v lang=la>Et in unum Dóminum <i><sup></sup></i> Jesum Christum,</v>
<v lang=de>Und an den einen Herrn <i><sup></sup></i> Jesus Christus,</v>
<v lang=la>Fílium Dei unigénitum.</v>
<v lang=de>Gottes eingeborenen Sohn.</v>
<v lang=la>Et ex Patre natum ante ómnia sǽcula.</v>
<v lang=de>Er ist aus dem Vater geboren vor aller Zeit.</v>
<v lang=la>Deum de Deo,</v>
<v lang=de>Gott von Gott,</v>
<v lang=la>lumen de lúmine,</v>
<v lang=de>Licht vom Lichte,</v>
<v lang=la>Deum verum de Deo vero.</v>
<v lang=de>wahrer Gott vom wahren Gott;</v>
<v lang=la>Génitum, non factum,</v>
<v lang=de>Gezeugt, nicht geschaffen,</v>
<v lang=la>consubstantiálem Patri:</v>
<v lang=de>eines Wesens mit dem Vater;</v>
<v lang=la>per quem ómnia facta sunt.</v>
<v lang=de>durch Ihn ist alles geschaffen.</v>
<v lang=la>Qui propter nos hómines</v>
<v lang=de>Für uns Menschen</v>
<v lang=la>et propter nostram salútem</v>
<v lang=de>und um unsres Heiles willen</v>
<v lang=la>descéndit de cælis.</v>
<v lang=de>ist Er vom Himmel herabgestiegen.</v>
</p>
<p>
<v lang=la>Et incarnátus est de Spíritu Sancto</v>
<v lang=de>Er hat Fleisch angenommen durch den Hl. Geist</v>
<v lang=la>ex <i><sup></sup></i> María Vírgine:</v>
<v lang=de>aus <i><sup></sup></i> Maria, der Jungfrau</v>
<v lang=la>Et homo factus est.</v>
<v lang=de>und ist Mensch geworden.</v>
<v lang=la>Crucifíxus étiam pro nobis:</v>
<v lang=de>Gekreuzigt wurde Er sogar für uns;</v>
<v lang=la>sub Póntio Piláto passus, et sepúltus est.</v>
<v lang=de>unter Pontius Pilatus hat Er den Tod erlitten</v>
<v lang=de>und ist begraben worden</v>
</p>
<p>
<v lang=la>Et resurréxit tértia die,</v>
<v lang=de>Er ist auferstanden am dritten Tage,</v>
<v lang=la>secúndum Scriptúras.</v>
<v lang=de>gemäß der Schrift;</v>
<v lang=la>Et ascéndit in cáelum:</v>
<v lang=de>Er ist aufgefahren in den Himmel</v>
<v lang=la>sedet ad déxteram Patris.</v>
<v lang=de>und sitzet zur Rechten des Vaters.</v>
</p>
<p>
<v lang=la>Et íterum ventúrus est cum glória</v>
<v lang=de>Er wird wiederkommen in Herrlichkeit,</v>
<v lang=la>judicáre vivos et mórtuos:</v>
<v lang=de>Gericht zu halten über Lebende und Tote:</v>
<v lang=la>cujus regni non erit finis.</v>
<v lang=de>und Seines Reiches wird kein Endes sein.</v>
</p>
<p>
<v lang=la>Et in Spíritum Sanctum,</v>
<v lang=de>Ich glaube an den Heiligen Geist,</v>
<v lang=la>Dóminum et vivificántem:</v>
<v lang=de>den Herrn und Lebensspender,</v>
<v lang=la>qui ex Patre Filióque procédit.</v>
<v lang=de>der vom Vater und vom Sohne ausgeht.</v>
<v lang=la>Qui cum Patre et Fílio simul <i><sup></sup></i> adorátur et conglorificátur:</v>
<v lang=de>zugleich <i><sup></sup></i> angebetet und verherrlicht;</v>
<v lang=la>qui locútus est per Prophétas.</v>
<v lang=de>Er hat gesprochen durch die Propheten.</v>
<v lang=la>Et unam sanctam cathólicam et apostólicam Ecclésiam.</v>
<v lang=de>Ich glaube an die eine, heilige, katholische und apostolische Kirche.</v>
<v lang=la>Confíteor unum baptísma</v>
<v lang=de>Ich bekenne die eine Taufe</v>
<v lang=la>in remissiónem peccatórum.</v>
<v lang=de>zur Vergebung der Sünden.</v>
<v lang=la>Et exspécto resurrectiónem mortuórum.</v>
<v lang=de>Ich erwarte die Auferstehung der Toten.</v>
<v lang=la><i></i> Et vitam ventúri sǽculi. Amen.</v>
<v lang=de><i></i> Und das Leben der zukünftigen Welt. Amen.</v>
</p>
</Gebet>
<Gebet name={"Ave Maria"} is_bilingue={true}>
<p>
<v lang=la>Ave María, grátia plena. Dóminus tecum,</v>
<v lang=de>Gegrüsset seist du Maria, voll der Gnade; der Herr ist mit dir;</v>
<v lang=la>benedícta tu in muliéribus,</v>
<v lang=de>du bist gebenedeit unter den Weibern,</v>
<v lang=la>et benedíctus fructus ventris tui, Jesus.</v>
<v lang=de>und gebenedeit ist die Frucht deines Leibes, Jesus.</v>
</p>
<p>
<v lang=la>Sancta María, mater Dei, ora pro nobis peccatóribus,</v>
<v lang=de>Heilige Maria, Mutter Gottes, bitte für uns Sünder</v>
<v lang=la>nunc, et in hora mortis nostræ. Amen.</v>
<v lang=de>jetzt und in der Stunde unseres Todes! Amen.</v>
</p>
</Gebet>
<Gebet name={"Salve Regina"} is_bilingue={true}>
<p>
<v lang=la>Salve, Regína,</v>
<v lang=de>Sei gegrüßt, o Königin,</v>
<v lang=la>máter misericórdiae;</v>
<v lang=de>Mutter der Barmherzigkeit,</v>
<v lang=la>Víta, dulcédo et spes nóstra, sálve.</v>
<v lang=de>unser Leben, unsre Wonne</v>
<v lang=de>und unsere Hoffnung, sei gegrüßt!</v>
</p>
<p>
<v lang=la>Ad te clamámus, éxsules fílii Hévae.</v>
<v lang=de>Zu dir rufen wir verbannte Kinder Evas;</v>
<v lang=la>Ad te suspirámus,</v>
<v lang=de>zu dir seufzen wir</v>
<v lang=la>geméntes et fléntes in hac lacrimárum válle.</v>
<v lang=de>trauernd und weinend in diesem Tal der Tränen.</v>
<v lang=la>Eia ergo, Advocáta nóstra,</v>
<v lang=de>Wohlan denn, unsre Fürsprecherin,</v>
<v lang=la>íllos túos misericórdes óculos ad nos convérte.</v>
<v lang=de>deine barmherzigen Augen wende zu uns</v>
<v lang=la>Et Jésum, benedíctum frúctum véntris túi,</v>
<v lang=de>und nach diesem Elend zeige uns Jesus,</v>
<v lang=la>nóbis post hoc exsílíum osténde.</v>
<v lang=de>die gebenedeite Frucht deines Leibes.</v>
<v lang=la>O clémens, o pía, o dúlcis Vírgo María.</v>
<v lang=de>O gütige, o milde, o süße Jungfrau Maria.</v>
</p>
</Gebet>
<Gebet name={"Das Fatimagebet"} is_bilingue={true}>
<v lang=la>Ó mí Jésú, dímitte nóbís débita nostra,</v>
<v lang=de>O mein Jesus, verzeih' uns unsere Sünden,</v>
<v lang=la>líberá nós ab igne ínferní,</v>
<v lang=de>bewahre uns vor den Feuern der Hölle</v>
<v lang=la>condúc in cælum omnés animás, </v>
<v lang=de>und führe alle Seelen in den Himmel,</v>
<v lang=la>præsertim illás,</v>
<v lang=de>besonders jene,</v>
<v lang=la>quæ maximé indigent misericordiá tuá. Amen.</v>
<v lang=de>die Deiner Barmherzigkeit am meisten bedürfen. Amen.</v>
</Gebet>
<Gebet name={"Glória"} is_bilingue={true}>
<p slot="intro">Der uralte Gesang beginnt mit den Worten, mit denen die Engelscharen den neugeborenen Welterlöser feierten. Er preist zunächst Gott Vater, dann Gott Sohn; er schließt mit einer Huldigung an die Heiligste Dreifaltigkeit, wobei man sich mit dem großen Kreuze bezeichnet.</p>
<p>
<v lang=la>Glória in excélsis <i><sup></sup></i> Deo.</v>
<v lang=de>Ehre sei <i><sup></sup></i> Gott in der Höhe.</v>
<v lang=la>Et in terra pax homínibus</v>
<v lang=de>Und auf Erden Friede den Mesnchen,</v>
<v lang=la>bonæ voluntátis.</v>
<v lang=de>die guten Willens sind.</v>
<v lang=la>Laudámus te.</v>
<v lang=de>Wir loben Dich.</v>
<v lang=la>Benedícimus te.</v>
<v lang=de>Wir preisen Dich.</v>
<v lang=la><i><sup></sup></i> Adorámus te.</v>
<v lang=de><i><sup></sup></i> Wir beten Dich an.</v>
<v lang=la>Glorificámus te.</v>
<v lang=de>Wir verherrlichen Dich.</v>
<v lang=la><i><sup></sup></i> Grátias ágimus tibi</v>
<v lang=de><i><sup></sup></i> Wir sagen Dir Dank</v>
<v lang=la>propter magnam glóriam tuam.</v>
<v lang=de>ob Deiner großen Herrlichkeit.</v>
<v lang=la>Dómine Deus, Rex cæléstis,</v>
<v lang=de>Herr und Gott, König des Himmels,</v>
<v lang=la>Deus Pater omnípotens.</v>
<v lang=de>Gott allmächtiger Vater!</v>
<v lang=la>Dómine Fili unigénite, <i><sup></sup></i> Jesu Christe.</v>
<v lang=de>Herr <i><sup></sup></i> Jesus Christus, eingeborener Sohn!</v>
<v lang=la>Dómine Deus, Agnus Dei,</v>
<v lang=de>Herr und Gott, Lamm Gottes,</v>
<v lang=la>Fílius Patris.</v>
<v lang=de>Sohn des Vaters!</v>
<v lang=la>Qui tollis peccáta mundi,</v>
<v lang=de>Du nimmst hinweg die Sünden der Welt:</v>
<v lang=la>miserére nobis.</v>
<v lang=de>erbarme Dich unser.</v>
<v lang=la>Qui tollis peccáta mundi,</v>
<v lang=de>Du nimmst hinwerg die Sünden der Welt.</v>
<v lang=la><i><sup></sup></i> súscipe depreciatiónem nostram.</v>
<v lang=de><i><sup></sup></i> nimm unser Flehen gnädig auf.</v>
<v lang=la>Qui sedes ad déxteram Patris,</v>
<v lang=de>Du sitzt zur Rechten des Vaters:</v>
<v lang=la>miserére nobis.</v>
<v lang=de>erbarme Dich unser.</v>
<v lang=la>Quóniam tu solus Sanctus.</v>
<v lang=de>Denn Du allein bist der Heilige.</v>
<v lang=la>Tu solus Altíssimus,</v>
<v lang=de>Du allein der Höchste,</v>
<v lang=la><i><sup></sup></i> Jesu Christe.</v>
<v lang=de><i><sup></sup></i> Jesus Christus,</v>
<v lang=la>Cum Sancto Spíritu</v>
<v lang=de>Mit dem Hl. Geiste,</v>
<v lang=la><i></i> in glória Dei Patris. Amen.</v>
<v lang=de><i></i> in der Herrlichkeit Gottes des Vaters. Amen.</v>
</p>
</Gebet>
<Gebet name={"Gebet zum hl. Erzengel Michael"} is_bilingue={true}>
<p>
<v lang=la>Sáncte Míchael Archángele,</v>
<v lang=de>Heiliger Erzengel Michael,</v>
<v lang=la>defénde nos in proélio,</v>
<v lang=de>verteidige uns im Kampfe!</v>
<v lang=la>cóntra nequítam et insídias</v>
<v lang=de>Gegen die Bosheit und Nachstellungen</v>
<v lang=la>diáboli ésto præsídium.</v>
<v lang=de>des Teufels sei unser Schutz. </v>
<v lang=la>Ímperet ílli Déus, súpplices deprecámur:</v>
<v lang=de>»Gott gebiete ihm!«, so bitten wir flehentlich.</v>
<v lang=la>tuque, Prínceps milítæ cæléstis,</v>
<v lang=de>Du aber, Fürst der himmlischen Heerscharen,</v>
<v lang=la>Sátanam aliósque spíritus malígnos,</v>
<v lang=de>stoße den Satan und die anderen bösen Geister,</v>
<v lang=la>qui ad perditiónem animárum</v>
<v lang=la>pervagántur in múndo,</v>
<v lang=de>die in der Welt umhergehen,</v>
<v lang=de>um die Seelen zu verderben,</v>
<v lang=la>divína virtúte, in inférnum detrúde. Amen.</v>
<v lang=de>durch die Kraft Gottes in die Hölle. Amen.</v>
</p>
</Gebet>
<Gebet name={"Bruder Klaus Gebet"} is_bilingue={false}>
<p>
<v lang=de>Mein Herr und mein Gott,</v>
<v lang=de>nimm alles von mir,</v>
<v lang=de>was mich hindert zu Dir.</v>
</p>
<p>
<v lang=de>Mein Herr und mein Gott,</v>
<v lang=de>gib alles mir,</v>
<v lang=de>was mich führet zu Dir.</v>
</p>
<p>
<v lang=de>Mein Herr und mein Gott,</v>
<v lang=de>nimm mich mir</v>
<v lang=de>und gib mich ganz zu eigen Dir.</v>
</p>
</Gebet>
<Gebet name={"Josephgebet des hl. Papst Pius X"} is_bilingue={false}>
<p slot="intro">Wenn man mehr zum hl. Joseph als <q>Patrone Morientium</q> wissen möchte kann man <a href="predigten/20220319-hl._joseph">hier</a> die Predigt zum Festtag des hl. Joseph nachlesen.</p>
<p>
<v>Jungfräulicher Vater Jesu,</v>
<v>Reinster Bräutigam Mariä,</v>
<v>Sankt Joseph, bitte Tag für Tag bei Jesus, dem Sohn Gottes.</v>
<v>Seine Kraft und Gnade soll uns stärken,</v>
<v>dass wir siegreich streiten im Leben</v>
<v>und die Krone von Ihm erhalten im Sterben.</v>
</p>
</Gebet>
</div>
</div>

View File

@ -0,0 +1,52 @@
<script>
export let is_bilingue;
export let name;
</script>
<style>
div.gebet{
text-align: center;
font-size: 1.25em;
}
:global(.gebet v){
margin:0;
display: block;
}
:global(.gebet v:lang(la)) {
color: var(--nord6);
}
:global(.bilingue v:lang(de)){
color: grey;
}
.gebet_wrapper h2{
text-align: center;
padding-bottom: 0.5em;
}
:global(.gebet i){
font-style: normal;
color: var(--nord11);
font-weight: 900;
}
.gebet_wrapper{
padding: 1em;
background-color: var(--accent-dark);
box-shadow: 0 0 1em black;
max-width: 600px;
}
@media(prefers-color-scheme: light){
.gebet_wrapper{
background-color: var(--accent-light);
}
:global(.gebet v:lang(la)){
color: black;
}
}
</style>
<div class="gebet_wrapper">
<h2>{name}</h2>
<slot name="intro"></slot>
<div class="gebet" class:bilingue={is_bilingue}>
<slot></slot>
</div>
</div>

View File

@ -0,0 +1,314 @@
<script>
import "$lib/css/nordtheme.css"
import "$lib/css/christ.css"
</script>
<style>
@font-face {
font-family: 'UnifrakturMaguntia';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/UnifrakturMaguntia20.ttf');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
.bibel{
font-family: 'UnifrakturMaguntia', cursive;
-moz-font-feature-settings: "cv11";
-webkit-font-feature-settings: "cv11";
-ms-font-feature-settings: "cv11";
font-feature-settings: "cv11";
font-size: 1.2rem;
}
li::marker, i, li:lang(la){
font-family: serif;
}
ol{
list-style-position: inside;
}
a {
text-decoration: underline;
}
h1{
font-size: 4rem;
}
h2{
font-size: 2.5rem;
}
h3{
font-size: 2rem;
}
h4{
font-size: 1.5rem;
}
.quote p.title{
font-weight: bold;
}
/*.quote .bibel{
margin-bottom: 1em;
}*/
.quote q{
display: block;
width: 90%;
margin-left: auto;
margin-right: auto;
}
.tod, .grund{
font-size: 1.5rem;
display: block;
text-align: center;
margin-bottom: 1rem;
margin-top: -1rem;
}
.schott q{
quotes: "«" "»";
}
.predigt video {
display: block;
width: 80%;
margin: auto;
}
</style>
<h1>13. Februar 2022 - Septuagesima</h1>
<p>Das ganze hier ist noch ein Platzhalter</p>
<h2>Der Osterfestkreis</h2>
<div class=schott>
<p>
Der Weihnachtsfestkreis schließt mit der Woche vor Septuagesima.
Der ersehnte Erlöser ist gekommen und hat in seiner ersten Ankunft zugleich seine zweite, die am Gerichtstage erfolgen wird, begründet und begonnen.
</p>
<p>
Jetzt ist die Zeit des anstrengten Kampfes gegen Sünde, Welt und Fleisch gekommen, die Zeit der mühevollen Aussaat, des sturmumtobten Wachsens.
Durch Kampf zum Sieg, durch Sterben zum Leben, zur Auferstehung, zur Vollherrschaft Christi und schließlich zur Verklärung im Osterlichte!
Christus soll in uns den Thron seiner Herrschaft errichten, einer Herrschaft, die uns nicht erdrückt, sondern erhöht; nicht beraubt, sondern bereichert; nicht einschränkt, sondern innerlich weitet und uns einmal mitherrschen läßt im ewigen Ostern des Himmels.
</p>
<p>
Der Osterfestkreis umfaßt drei Abschnitte:
die Zeit der Vorbereitung: Vorfasten- und Fastenzeit;
die eigentliche Festzeit: Oster- und Pfingstfest;
endlich die Zeit nach Pfingsten
</p>
</div>
<h2> I. Die Zeit der Vorbereitung </h2>
<h3> 1. Die Vorfastenzeit</h3>
<div class=schott >
<p>
Sie umfaßt die Sonntage Septuagesima, Sexagesima und Quinquagesima.
Diese Namen bezeichnen nicht die genauen Abstände bis zum Osterfest, sonder deuten auf die rund berechnete 70tägige, 60tägige, 50tägige Vorbereitungzeit auf Ostern.
</p>
<p>
Der Name Septuagesima weckt die Erinnerung an die 70 Jahre der Gefangenschaft, welche die Juden zur Strafe für ihre Untreue fern von Jerusalem, zu Babylon, verbringen mußten, bevor sie wieder ins Gelobte Land zurückkerhen durften.
So mahnt uns diese Zeit an unsre eigene Pilgerschaft aus der Fremde, aus der gottfernen Welt (Babylon), zum wahren Vaterland (Jerusalem).
Diese Pilgerschaft ist für uns eine beständiger Kampf gegen die Feinde unsres Heiles.
Für den göttlichen Heiland bedeutet das öffentliche Wirken Mühsal und Leiden und schließlich den Tod;
so muß sich auch unser Leben, soll es dem seinen nachgebildet werden, auf Kämpfe, selbst auf ein geistiges Sterben gefaßt machen;
erst dann wird es mit dem Heiland zum endlichen Triumph gelangen.
</p>
</div>
<h2>Sonntag Septuagesima</h2>
<p>13. Februar 2022 - <i>2. Klasse - Farbe violett</i></p>
<h3> Schott </h3>
<p class=schott>
Durch den Kampf zum Sieg, duch Sterben zum Leben, zur Aufersteheung, zur Verklärung: das sind die Gedanken der Vorfastenzeit.
Eine lichtvolle Darstellung dieser Gedanken ist der hl. Laurentius, der Patron der Katechumen in Rom und Patron heutigen Stationskirche.
Larentius - in Todesnöten, auf dem glühenden Roste (Intr.). über ihm die Siegeskrone der Verklärung (vgl. Introituspsalm) - ist ein Wegweiser für die Katechumen und für uns.
Mit ihm treten wir entschlossen und kampfbereit in die Rennbahn und eignen uns Pauli Geist und Grundsätze an (Epistel).
Wir folgen dem Ruf des Hausvaters (Christi) in seinen Weinberg und sind bereit, seinen Willen zu tun (Evang.).
Wir entsagen uns selbst und bringen uns in der Opfergabe dar.
Gestützt auf die Kraft der Gnade Christi, die über uns im hl. Opfer uns besonders ind der hl. Kommunion verklärend aufleuchtet (Comm.), gehen wir neu gestärkt in den Kampf und die Mühsal unseres Christenberufes.
</p>
<h3> Epistel </h3>
<div class="epistel bibel">
<ol><i>1 Cor. 9</i>
<li value=24>Wisset ihr nicht, daß die, welche in der Rennbahn laufen, zwar alle laufen, aber nur einer erlangt den Preis? Laufet fo, daß ihr ihn erlanget!</li>
<li>Jeder aber, der im Kampfspiele ringt, enthält sich von allem, und zwar jene, um eine vergängliche Krone zu empfangen, wir aber eine unvergängliche.</li>
<li>Ich laufe demnach, nicht wie in's Ungewisse; ich kämpfe, nicht indem ich Luftstreiche thue,</li>
<li>sondern ich züchtige meinen Leib, und bringe ihn in die Botmäßigkeit, damit ich nicht etwa, nachdem ich anderen gepredigt habe, selbst verworfen werde.</li>
<i>1 Cor. 10</i>
<li value=1>Denn ich will euch nicht in Unwissenheit lassen, Brüder! Daß unsere Väter alle unter der Wolke waren, und alle durch das Meer hindurch gingen,</li>
<li>und alle auf Moses getauft wurden, in der Wolke und in dem Meere,</li>
<li>und alle dieselbe geistige Speise aßen,</li>
<li>und alle dieselbe geistigen Trank tranken (sie tranken nämlich aus einem geistigen, sie begleitenden Felsen, der Felsen aber war Christus);</li>
<li>aber an der Mehrzahl von ihnen hatte Gott kein Wohlgefallen; denn sie wurden niedergestreckt in der Wüste.</li>
</ol>
</div>
<h3> Evangelium </h3>
<div class="evangelium bibel">
<ol><i>Matth. 20</i>
<li>Das Himmelreich ist gleich einem Hausvater, der am frühen Morgen ausging, um Arbeiter in seinen Weinberg zu dingen.</li>
<li>Nachdem er nun mit den Arbeitern um einen Denar für den Tag übereingekommen war, sandte er sie in seinen Weinberg.</li>
<li>Und als er um die dritte Stunde ausging, sah er andere au dem Markte müßig stehen,</li>
<li>und sprach zu ihnen: Gehet auch ihr in meinen Weinberg, und was recht ist, werde ich euch geben.</li>
<li>Sie aber gingen hin. Abermals ging er um die sechste und neunte Stunde aus, und that ebenso.</li>
<li>Um die elfte Stunde aber ging er aus, und fand andere andere stehen, und sprach zu ihnen: Was stehet ihr hier den ganzen Tag müßig?</li>
<li>Sie antworteten ihm: Weil uns niemand gedungen hat. Da sprach er zu ihnen: Gehet auch ihr in meinen Weinberg,</li>
<li>Als es nun Abend geworden, sagte der Herr des Weinberges zu seinem Verwalter: Rufe die Arbeiter, und gib ihnen den Lohn, von den letzten anfangend, bis zu den ersten.</li>
<li>Da nun die kamen, welche um die elfte Stunde eingetreten waren, empfingen sie jeder einen Denar.</li>
<li>Wie aber auch die ersten kamen, meinten sie, daß sie mehr empfangen würden, aber auch sie erhielten jeder einen Denar.</li>
<li>Und da sie ihn empfingen, murrten sie wider den Hausvater.</li>
<li>und sprachen: Siese letzten haben eine einzige Stunde gearbeitet, und du hast sie uns gleich gehalten, die wir die Last und Hitze des Tages getragen haben.</li>
<li>Er aber antowrtete einem aus ihnen, und sprach: Freund! ich thue dir nicht Unrecht; bist du nicht auf einen Denar mit mir eins geworden?</li>
<li>Nimm, was dein ist, und gehe hin; ich will aber auch diesem letzten geben, wie dir.</li>
<li>Oder ist es mir nicht erlaubt zu thun, was ich will? Ist etwa dein Auge darum böse, weil ich gut bin?</li>
<li>So werden die Letzten die Ersten, und die Ersten die Letzten sein; denn viele sind berufen, aber wenige auserwählt!</li>
</ol>
</div>
<h3> Predigt </h3>
<p class="predigt einleitung" >
Es handelt sich dabei um die Predigt von H. H. Pater Cadiet zu Zaitzkofen welche man <a href="https://www.youtube.com/watch?v=mzqmcbYq9Xk">hier</a> (<a href="https://bocken.org/static/predigten/20220213-Predigt_am_Sonntag_Septuagesima.mp4">Mirror</a>) auch noch nachschauen kann. Die Messe wurde via Livestream mitverfolgt.
</p>
<div class="predigt inhalt">
<p>Es ist nicht einfach in der heutigen Zeit als Gläubiger. Vieles was unnatürlich ist wird als natürlich verlogen und Gottes Werk verneint. Somit ist der Aufruf zum Kampf passender den je.
Wir sollen gleich den Sportlern verzichten für das Heil der Seelen, dem Heil der eigenen Seele.
</p>
<p>
Diese Bildnis der Spiele im Stadion waren vermutlich ein gutes Bildnis für die Korinter. So hatten sie alle zwei Jahre Sportspiele von April bis Anfang Mai, welche wie auch Fußball heute noch, vieles der Gesellschaft lahmlegte.
Auch die Gläubigen gehörten damals wie heute zu den Begeisterten solcher Spiele.
</p>
<p>
Jedoch ist die heutige Lehre, dass es nicht nur eine Medaillie gibt, wie vielleicht die Epistel einen erwarten lässt.
Daher das gewählte Evangelium für den heutigen Tag, um zu zeigen, dass das Relevante ist zu kämpfen als gäbe es nur einen Sieger.
Auch diese, welche erst <q>zur elften Stunde</q> anfangen zu kämpfen werden einen Sieg erreichen können.
<p>
<p>
Diese aber, die nicht kämpfen, werden es bitter bereuen. Dazu gibt es 3 Beispiele aus der heiligen Schrift:
</p>
<h4>1. König David:</h4>
<p>
Sein Leben ist ein Kampf, aber als er seine Macht erreicht hatte und keine Gegner mehr sah, da ruhte er sich aus und sandte seine Kämpfer aus während er auf seiner Terasse zurückblieb.
</p>
<p>
Er hat sich auf seinen Loorbeeren ausgeruht. <q>Was hat er noch zu tun?</q> <q>Hat er nicht alles bereits erreicht?</q>
Somit wird mit einem Blick besiegt.
Nicht von Anderen Menschen, aber vom Teufel durch sich selbst.
So beging er zwei Todsünden durch einen Blick auf eine Frau: Ehebruch und Mord am Manne dieser Frau.
</p>
<p>
Als Kind hat er Löwen und Bären mit bloßen Händen besiegt, aber nun wird dieser einst mutige und starke Mann bezwungen wegen seinem <em>Müßiggang</em>.
Wie man zu sagen pflegt: Wer man kein Beschäftigugn welche Platz einnimmt so wird der Teufel selbst den ganzen Platz einnehmen, den wir freigelassen haben.
Es wäre besser gewesen für David, noch in der Ängstlichkeit der Flucht vor Saulus zu sein, als in seinem Palast in Jerusalem.
</p>
<div class=quote>
<p class=title>
<a href=https://en.wikipedia.org/wiki/Ignatius_of_Loyola>Der Heilige Ignatius von Loyola</a>; Regel zur Unterscheidung der Geister:
</p>
<q>Wenn wir in Schwierigkeiten sind, dann beten wir den lieben Gott treu zu bleiben.
Wenn alles gut geht denken wir schon daran, welche Schwierigkeiten kommen können und beten wir um Vorrat von Mut und von Eifer für die Zeit der Trostlosigkeit, der Prüfung.</q>
</div>
<p>
Müßiggang hat ihn besiegt und der Gedanke, dass er nichts mehr zu erobern hatte.
Es gibt immer etwas weiteres zu erobern. Es gibt immer eine Ecke in unserer Seele die uns, und damit Gott, nicht gehört.
Es gibt immer etwas, was man besser tun kann, es gibt immer schlechte Gewohnheiten die man ablegen muss.
In dieser Vorfastenzeit geht es darum sich diesem deutlicher bewusst zu werden und seinen Kampf gegen diese Müßigkeit für die kommende Fastenzeit zu planen.
</p>
<p>
Als Beispiel hilft hier das Königreich Spanien. Jahrhunderte von Kampf um die Anwesenheit der Muslime zu bekämpfen. Als sie endlich die letzte Stadt, welche unter der Vollmacht der Muslime war, erobert hatten, hat die Vorsehung ihnen noch etwas zu erobern gegeben:
Im Jahre 1492, dem Fall von Granada, wurde Spanien (und Portugal) ein ganzer Kontinent zum Erobern im Namen Christi geschenkt.
Als Spanien endlich von der Versuchung der Apostasie, dem Glaubensabfall, befreit wurde hat sich diese Chance eröffnet.
Es gibt immer noch etwas zu erobern.
Falls wir nichts finden, dann wird die Vorsehung uns etwas zeigen. (Siehe Zitat oben).
</p>
<h4>2. Die Hebräer in der Wüste: <i>4 Mose 13</i></h4>
<p>
Von den Ägyptern durch die Wunder Gottes befreit, sind sie nun auf dem weiten Weg zum versprochenen Land.
Sie schicken Kundschafter in dieses Land. Diese Kundschafter gehen und verbringen 40 Tage dort.
Diese Kundschafter finden ein wunderbares Land. Es fließt Honig und Milch. Aber es ist nicht unbevölkert. Es gibt viele, starke Stämme.
Die Kundschafter haben Angst, sie verbreiteten Lügen über dieses Land da sie Angst haben zu fallen im Versuch es einzunehmen.
Das Volk will murren, beklagen. Sie wollen nicht kämpfen.
</p>
<p>
Warum haben sie gelogen? Sie wollten die eigene Angst rechtfertigen. Der liebe Gott lässt sie daher als Strafe 40 Jahre lang leben in der Wüste, sodass nur die nächst e Generation in Besitzt kommen wird des versprochenem Lande.
Wie konnten sie Gott so verletzen? Als ob Er keine Wunder für sie getätigt hätte? </p>
<div class=quote><p class=title>Hl. Thomas von Aquin in einem Kommentar zur Bibel</p>
<div class=bibel>
<ol><i>Kol. 3</i>
<li value=21>Ihr Väter! reizet eure Kinder nicht zum Zorne, damit sie nicht mutlos werden.</li>
</ol>
</div>
<q>
Der Sinn dieses Ratschlags ist, dass der Mensch den Eindruck seiner Kindheit behält.
Es ist natürlich demjenigen, der in Knechtschaft erzogen wurde, immer kleinmütig, mutlos zu bleiben.
Und daraus haben die Israeliten in der Wüste Angst gehabt vor dem Kampf weil sie immer in Knechtschaft erzogen wurden.
Sie hatten keinen Mut mehr.
</q>
</div>
<p>
Diese Knechtschaft soll abgelegt werden um so zu einem kämpferischen Geist zu kommen. Das wird auch bestätigt im Johannesevangelium wo steht:
<div class=bibel>
<ol><i> Johannes 15</i>
<li value=15>Ich nenne euch nun nicht mehr Knechte, denn der Knecht weiß nicht, was sein Herr tut; euch aber habe ich Freunde genannt; denn alles, was ich von meinem Vater gehört, habe ich euch kundgetan.
</li>
</ol>
</div>
<p>
Was der Herr von seinen Jüngern erwartet ist Liebe, inneres Verständnis und Kühnheit.
Kampf gegen unsere Menschenfurcht.
Dazu erzieht Er seine Jünger zur Freiheit.
So sagt auch Paulus in seinen Briefen an die Galater:
<div class=bibel>
<ol><i>Galater 4</i>
<li value=31>Demnach, Brüder! sind wir nicht Kinder der Magd, sondern der Freien, vermöge der Freiheit, mit der Christus uns befreit hat.</li>
</ol>
</div>
<p>
Eine Lektion für uns:
Jeder Ausbilder erzieht zur Freiheit, zur Autonomie. Jeder Ausbilder arbeitet um nutzlos zu werden, das seine Jünglinge alleine tun können, was er mit ihm erlernt.
Der Jüngling alles selbst tun lassen, damit er lernt.
</p>
<h4>3. Petrus:</h4>
<p>
Der hl. Petrus, der immer mutig ist, Jesus zu verteidigen und seine Treue zu bekennen. Er wird von einer Magd besiegt werden.
</p>
<div class=bibel>
<ol><i>Johannes 18</i>
<li value=10>Simon Petrus also, der sein Schwert hatte, zog es und schlug den Knecht des Hohenpriesters, und hieb ihm sein rechtes Ohr ab. Der Name des Knechtes aber war Malchus.</li>
<i>Matthäus 26</i>
<li value=35>Da sprach Petrus zu ihm: Wenn ich auch mit dir sterben müsste, werde ich dich doch nicht verleugnen. In gleicher Weise sprachen auch alle Jünger.</li>
<li value=69>Petrus aber saß draußen in dem Hofe; und eine Magd trat zu ihm hin, und sprach: Du warest auch bei Jesus, dem Galiläer!</li>
<li>Doch er leugnete vor allen, und sprach: Ich weiss nicht, was du sagst.</li>
</ol>
</div>
<p>
War Petrus nicht kühn genug? War er nicht kämpferisch genug?
Er wird dieses mal gegen sich Selbst kämpfen müssen.
Daraus sollte er beten. Er hat nicht gebetet, er hat geschlafen.
</p>
<p>
Lektion für uns:
Nur im Gebet werden wir die Kraft und Hellsichtigkeit schöpfen, um richtig zu kämpfen.
Nur durch das Gebet hätte Petrus verstanden, was Jesus gesagt hatte nach dem Abendmahl.
</p>
<div class=bibel>
<ol><i>Matthäus 26</i>
<li value=37>Und er nahm den Petrus und die zwei Söhne des Zebedäus mit sich, und fing an, sich zu betrüben und zu bangen.</li>
<li>Da sprach er zu ihnen: Meine Seele ist betrübt bis in den Tod, bleibet hier und wachet mit mir!</li>
<li>Und nachdem er ein wenig vorwärts gegangen war, fiel er auf sein Angesicht, betete, und sprach: Mein Vater! wenn es möglich ist, so gehe dieser Kelch an mir vorüber; jedoch nicht wie ich will, sondern wie du.</li>
<li>Und er kam zu seinen Jüngern, und fand sie schlafend, und sprach zu Petrus: So vermochtet ihr nicht eine Stunde mit mir zu wachen?</li>
<li>Wachet und betet, damit ihr nicht in Versuchung geratet! Der Geist zwar ist willig, das Fleisch aber ist schwach.</li>
</ol>
</div>
<p>
Die neue Lebensweise der Apostel: Es wird Schwert geben, es wird Kampf geben, nimmt euch Beutel Stab usw mit euch.
Drei Beispiele von Leuten die nicht kämpften. Aber: Die Hebräer haben trotzdem 40 Jahre später das Land erobert. König David bekehrte sich, hat das wunderbare Vorbild der Reue gegeben. So auch der hl. Petrus und wurde zum Fürst der Apostel und unser Glaube hängt an seinem.</p>
<p>
Alle bekamen die Kraft Gottes, Petrus sogar so sehr, dass er lehrte bis an seinen eigenen Kreuzestod.
Es ist die Zeit gekommen um sich selbst zu erobern, sich selbst zu besiegen um Gott völlig gefällig zu sein.
</p>
</div>

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,16 +1,22 @@
<script>
import Header from '$lib/components/Header.svelte'
import UserHeader from '$lib/components/UserHeader.svelte';
export let data
let user;
if(data.session){
user = data.session.user
}
</script>
<Header>
<ul class=site_header slot=links>
<li><a href="/">Home</a></li>
<li><a href="/rezepte">Alle Rezepte</a></li>
<li><a href="/rezepte/season">In Saison</a></li>
<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>
</Header>

View File

@ -1,9 +1,9 @@
import type { PageLoad } from "./$types";
import type { PageServerLoad } from "./$types";
export async function load({ fetch }) {
let current_month = new Date().getMonth() + 1
const res_season = await fetch(`/api/items/in_season/` + current_month);
const res_all_brief = await fetch(`/api/items/all_brief`);
const res_season = await fetch(`/api/rezepte/items/in_season/` + current_month);
const res_all_brief = await fetch(`/api/rezepte/items/all_brief`);
const item_season = await res_season.json();
const item_all_brief = await res_all_brief.json();
return {

View File

@ -6,34 +6,45 @@
import Search from '$lib/components/Search.svelte';
export let data: PageData;
export let current_month = new Date().getMonth() + 1
const all_categories = [ ...new Set (data.all_brief.map(item => item.category))];
import { rand_array } from '$lib/js/randomize';
const categories = ["Hauptspeise", "Nudel", "Brot", "Dessert", "Suppe", "Beilage", "Salat", "Kuchen", "Frühstück", "Sauce", "Zutat", "Getränk", "Aufstrich", "Guetzli", "Snack"]
</script>
<style>
h1{
box-sizing: border-box;
max-width: 1000px;
padding-left: 5rem;
text-align: center;
margin-bottom: 0;
font-size: 4rem;
}
.subheading{
text-align: center;
margin-top: 0;
font-size: 1.5rem;
}
</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>Rezepte</h1>
<section>
<MediaScroller title="In Saison:">
{#each rand_array(data.season) as recipe}
<Card {recipe} {current_month} search=""></Card>
{/each}
</MediaScroller>
</section>
<p class=subheading>{data.all_brief.length} Rezepte und stetig wachsend...</p>
<Search></Search>
{#each all_categories as category}
<MediaScroller title="In Saison">
{#each data.season as recipe}
<Card {recipe} {current_month} loading_strat={"eager"} do_margin_right={true}></Card>
{/each}
</MediaScroller>
{#each categories as category}
<MediaScroller title={category}>
{#each rand_array(data.all_brief.filter(recipe => recipe.category == category)) as recipe}
<Card {recipe} {current_month}></Card>
{#each data.all_brief.filter(recipe => recipe.category == category) as recipe}
<Card {recipe} {current_month} do_margin_right={true}></Card>
{/each}
</MediaScroller>
{/each}
<p>{data.all_brief.length}</p>
<AddButton></AddButton>

View File

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

View File

@ -3,15 +3,20 @@
export const multiplier = writable(0);
import type { PageData } from './$types';
import "$lib/components/nordtheme.css"
import "$lib/css/nordtheme.css"
import EditButton from '$lib/components/EditButton.svelte';
import InstructionsPage from '$lib/components/InstructionsPage.svelte';
import IngredientsPage from '$lib/components/IngredientsPage.svelte';
import TitleImgParallax from '$lib/components/TitleImgParallax.svelte';
import { afterNavigate } from '$app/navigation';
import {season} from '$lib/js/season_store';
import RecipeNote from '$lib/components/RecipeNote.svelte';
import {stripHtmlTags} from '$lib/js/stripHtmlTags';
export let data: PageData;
let hero_img_src = "/images/" + data.images[0].mediapath
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 = []
@ -47,6 +52,24 @@
return interval_arr
}
export let season_iv = season_intervals();
afterNavigate(() => {
hero_img_src = "https://bocken.org/static/rezepte/full/" + data.short_name + ".webp"
placeholder_src = "https://bocken.org/static/rezepte/placeholder/" + data.short_name + ".webp"
season_iv = season_intervals();
})
let display_date = new Date(data.dateCreated);
if (data.updatedAt){
display_date = new Date(data.updatedAt);
}
const options = {
day: '2-digit',
month: 'short', // German abbreviation for the month
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
};
const formatted_display_date = display_date.toLocaleDateString('de-DE', options)
</script>
<style>
*{
@ -58,6 +81,28 @@ h1{
border-radius: 10000px;
margin:0;
font-size: 3rem;
overflow-wrap: break-word;
hyphens: auto;
text-wrap: balance;
}
.category{
--size: 1.75rem;
position: absolute;
top: calc(-1* var(--size) );
left:calc(-3/2 * var(--size));
background-color: var(--nord0);
color: var(--nord6);
text-decoration: none;
font-size: var(--size);
padding: calc(var(--size) * 2/3);
border-radius: 1000px;
transition: 100ms;
box-shadow: 0em 0em 1em 0.3em rgba(0,0,0,0.4);
}
.category:hover,
.category:focus-visible{
background-color: var(--nord1);
scale: 1.1;
}
.tags{
margin-block: 1rem;
@ -67,6 +112,9 @@ h1{
flex-wrap: wrap;
gap: 1em;
}
.center{
justify-content: center;
}
.tag{
all:unset;
color: var(--nord0);
@ -75,7 +123,13 @@ h1{
border-radius: 10000px;
padding: 0.25em 1em;
transition: 100ms;
box-shadow: 0.2em 0.2em 0.4em 0.1em rgba(0,0,0,0.3);
box-shadow: 0em 0em 0.5em 0.05em rgba(0,0,0,0.3);
}
@media (prefers-color-scheme: dark) {
.tag{
background-color: var(--nord0);
color: white;
}
}
.tag:hover,
.tag:focus-visible
@ -83,7 +137,7 @@ h1{
cursor: pointer;
transform: scale(1.1,1.1);
background-color: var(--orange);
box-shadow: 0.1em 0.1em 0.2em 0.2em rgba(0,0,0,0.3);
box-shadow: 0.1em 0.1em 0.5em 0.1em rgba(0,0,0,0.3);
}
.wrapper_wrapper{
@ -96,6 +150,11 @@ h1{
transform: translateY(-7rem);
z-index: -2;
}
@media (prefers-color-scheme: dark) {
.wrapper_wrapper{
background-color: var(--background-dark);
}
}
.wrapper{
display: flex;
@ -110,7 +169,6 @@ h1{
flex-direction:column;
}
}
.title{
position: relative;
width: min(800px, 80vw);
@ -120,7 +178,14 @@ h1{
translate: 0 1px; /*bruh*/
z-index: 1;
}
@media (prefers-color-scheme: dark) {
.title{
background-color: var(--nord6-dark);
}
}
.icon{
font-family: "Noto Color Emoji", emoji;
position: absolute;
top: -1em;
right: -0.75em;
@ -130,8 +195,14 @@ h1{
font-size: 1.5rem;
border-radius: 100000px;
transition: 100ms;
box-shadow: 0em 0em 1em 0.5em rgba(0,0,0,0.5);
box-shadow: 0em 0em 1em 0.3em rgba(0,0,0,0.4);
}
@media (prefers-color-scheme: dark) {
.icon{
background-color: var(--accent-dark);
}
}
.icon:hover,
.icon:focus-visible{
scale: 1.2 1.2;
@ -152,6 +223,11 @@ h4{
}
.icon{
right: 1rem;
top: -1.75rem;
}
.category{
left: 1rem;
top: calc(var(--size) * -1.5);
}
}
@keyframes shake{
@ -183,14 +259,32 @@ h4{
}
}
</style>
.description{
text-align: center;
margin-bottom: 2em;
margin-top: -0.5em;
}
.date{
margin-bottom: 0;
}
<TitleImgParallax src=/images/{data.images[0].mediapath}>
</style>
<svelte:head>
<title>{stripHtmlTags(data.name)} - Bocken'sche Rezepte</title>
<meta name="description" content="{stripHtmlTags(data.description)}" />
<meta property="og:image" content="https://bocken.org/static/rezepte/thumb/{data.short_name}.webp" />
<meta property="og:image:secure_url" content="https://bocken.org/static/rezepte/thumb/{data.short_name}.webp" />
<meta property="og:image:type" content="image/webp" />
<meta property="og:image:alt" content="{stripHtmlTags(data.name)}" />
</svelte:head>
<TitleImgParallax src={hero_img_src} {placeholder_src}>
<div class=title>
<a class="category" href='/rezepte/category/{data.category}'>{data.category}</a>
<a class="icon" href='/rezepte/icon/{data.icon}'>{data.icon}</a>
<h1>{@html data.name}</h1>
{#if data.description && ! data.preamble}
<p>{data.description}</p>
<p class=description>{data.description}</p>
{/if}
{#if data.preamble}
<p>{@html data.preamble}</p>
@ -198,17 +292,26 @@ h4{
<div class=tags>
<h4>Saison:</h4>
{#each season_iv as season}
<a class=tag href="/rezepte/season/{season[0]}">{months[season[0] - 1]}-{months[season[1] - 1]}</a>
<a class=tag href="/rezepte/season/{season[0]}">
{#if season[0]}
{months[season[0] - 1]}
{/if}
{#if season[1]}
- {months[season[1] - 1]}
{/if}
</a>
{/each}
</div>
<div class=tags>
<h4>Stichwörter:</h4>
<div class="tags center">
{#each data.tags as tag}
<a class=tag href="/rezepte/tag/{tag}">{tag}</a>
{/each}
</div>
</div>
{#if data.note}
<RecipeNote note={data.note}></RecipeNote>
{/if}
</div>
<div class=wrapper_wrapper>
<div class=wrapper>
@ -220,6 +323,7 @@ h4{
{@html data.addendum}
{/if}
</div>
<p class=date>Letzte Änderung: {formatted_display_date}</p>
</div>
</TitleImgParallax>

Some files were not shown because too many files have changed in this diff Show More