Compare commits
	
		
			3 Commits
		
	
	
		
			b03ba61599
			...
			a22471a943
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						a22471a943
	
				 | 
					
					
						|||
| 
						
						
							
						
						4b2250ab03
	
				 | 
					
					
						|||
| 
						
						
							
						
						effed784b7
	
				 | 
					
					
						
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import type { Handle } from "@sveltejs/kit"
 | 
					import type { Handle, HandleServerError } from "@sveltejs/kit"
 | 
				
			||||||
import { redirect } from "@sveltejs/kit"
 | 
					import { redirect } from "@sveltejs/kit"
 | 
				
			||||||
import { error } from "@sveltejs/kit"
 | 
					import { error } from "@sveltejs/kit"
 | 
				
			||||||
import { SvelteKitAuth } from "@auth/sveltekit"
 | 
					import { SvelteKitAuth } from "@auth/sveltekit"
 | 
				
			||||||
@@ -7,24 +7,49 @@ import { AUTHENTIK_ID, AUTHENTIK_SECRET, AUTHENTIK_ISSUER } from "$env/static/pr
 | 
				
			|||||||
import { sequence } from "@sveltejs/kit/hooks"
 | 
					import { sequence } from "@sveltejs/kit/hooks"
 | 
				
			||||||
import * as auth from "./auth"
 | 
					import * as auth from "./auth"
 | 
				
			||||||
import { initializeScheduler } from "./lib/server/scheduler"
 | 
					import { initializeScheduler } from "./lib/server/scheduler"
 | 
				
			||||||
 | 
					import fs from 'fs'
 | 
				
			||||||
 | 
					import path from 'path'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initialize the recurring payment scheduler
 | 
					// Initialize the recurring payment scheduler
 | 
				
			||||||
initializeScheduler();
 | 
					initializeScheduler();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function authorization({ event, resolve }) {
 | 
					async function authorization({ event, resolve }) {
 | 
				
			||||||
	// Protect any routes under /authenticated
 | 
						const session = await event.locals.auth();
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						// Protect rezepte routes
 | 
				
			||||||
	if (event.url.pathname.startsWith('/rezepte/edit') || event.url.pathname.startsWith('/rezepte/add')) {
 | 
						if (event.url.pathname.startsWith('/rezepte/edit') || event.url.pathname.startsWith('/rezepte/add')) {
 | 
				
			||||||
   const session = await event.locals.auth();
 | 
					 | 
				
			||||||
		if (!session) {
 | 
							if (!session) {
 | 
				
			||||||
			// Preserve the original URL the user was trying to access
 | 
								// Preserve the original URL the user was trying to access
 | 
				
			||||||
			const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
 | 
								const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
 | 
				
			||||||
			redirect(303, `/login?callbackUrl=${callbackUrl}`);
 | 
								redirect(303, `/login?callbackUrl=${callbackUrl}`);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else if (! session.user.groups.includes('rezepte_users')) {
 | 
							else if (!session.user.groups.includes('rezepte_users')) {
 | 
				
			||||||
			// strip last dir from url
 | 
								error(403, {
 | 
				
			||||||
			// TODO: give indication of why access failed
 | 
									message: 'Zugriff verweigert',
 | 
				
			||||||
			const new_url = event.url.pathname.split('/').slice(0, -1).join('/');
 | 
									details: 'Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
 | 
				
			||||||
			redirect(303, new_url);
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Protect cospend routes and API endpoints
 | 
				
			||||||
 | 
						if (event.url.pathname.startsWith('/cospend') || event.url.pathname.startsWith('/api/cospend')) {
 | 
				
			||||||
 | 
							if (!session) {
 | 
				
			||||||
 | 
								// For API routes, return 401 instead of redirecting
 | 
				
			||||||
 | 
								if (event.url.pathname.startsWith('/api/cospend')) {
 | 
				
			||||||
 | 
									error(401, {
 | 
				
			||||||
 | 
										message: 'Anmeldung erforderlich',
 | 
				
			||||||
 | 
										details: 'Du musst angemeldet sein, um auf diesen Bereich zugreifen zu können.'
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// For page routes, redirect to login
 | 
				
			||||||
 | 
								const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search);
 | 
				
			||||||
 | 
								redirect(303, `/login?callbackUrl=${callbackUrl}`);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else if (!session.user.groups.includes('cospend')) {
 | 
				
			||||||
 | 
								error(403, {
 | 
				
			||||||
 | 
									message: 'Zugriff verweigert',
 | 
				
			||||||
 | 
									details: 'Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.'
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,6 +57,82 @@ async function authorization({ event, resolve }) {
 | 
				
			|||||||
	return resolve(event);
 | 
						return resolve(event);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Bible verse functionality for error pages
 | 
				
			||||||
 | 
					interface BibleVerse {
 | 
				
			||||||
 | 
					  bookName: string;
 | 
				
			||||||
 | 
					  abbreviation: string;
 | 
				
			||||||
 | 
					  chapter: number;
 | 
				
			||||||
 | 
					  verse: number;
 | 
				
			||||||
 | 
					  verseNumber: number;
 | 
				
			||||||
 | 
					  text: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let cachedVerses: BibleVerse[] | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loadVerses(): BibleVerse[] {
 | 
				
			||||||
 | 
					  if (cachedVerses) {
 | 
				
			||||||
 | 
					    return cachedVerses;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const filePath = path.join(process.cwd(), 'static', 'allioli.tsv');
 | 
				
			||||||
 | 
					    const content = fs.readFileSync(filePath, 'utf-8');
 | 
				
			||||||
 | 
					    const lines = content.trim().split('\n');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    cachedVerses = lines.map(line => {
 | 
				
			||||||
 | 
					      const [bookName, abbreviation, chapter, verse, verseNumber, text] = line.split('\t');
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        bookName,
 | 
				
			||||||
 | 
					        abbreviation, 
 | 
				
			||||||
 | 
					        chapter: parseInt(chapter),
 | 
				
			||||||
 | 
					        verse: parseInt(verse),
 | 
				
			||||||
 | 
					        verseNumber: parseInt(verseNumber),
 | 
				
			||||||
 | 
					        text
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return cachedVerses;
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    console.error('Error loading Bible verses:', err);
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getRandomVerse(): BibleVerse | null {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const verses = loadVerses();
 | 
				
			||||||
 | 
					    if (verses.length === 0) return null;
 | 
				
			||||||
 | 
					    const randomIndex = Math.floor(Math.random() * verses.length);
 | 
				
			||||||
 | 
					    return verses[randomIndex];
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    console.error('Error getting random verse:', err);
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatVerse(verse: BibleVerse): string {
 | 
				
			||||||
 | 
					  return `${verse.bookName} ${verse.chapter}:${verse.verseNumber}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const handleError: HandleServerError = async ({ error, event, status, message }) => {
 | 
				
			||||||
 | 
					  console.error('Error occurred:', { error, status, message, url: event.url.pathname });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // Add Bible verse to error context
 | 
				
			||||||
 | 
					  const randomVerse = getRandomVerse();
 | 
				
			||||||
 | 
					  const bibleQuote = randomVerse ? {
 | 
				
			||||||
 | 
					    text: randomVerse.text,
 | 
				
			||||||
 | 
					    reference: formatVerse(randomVerse),
 | 
				
			||||||
 | 
					    book: randomVerse.bookName,
 | 
				
			||||||
 | 
					    chapter: randomVerse.chapter,
 | 
				
			||||||
 | 
					    verse: randomVerse.verseNumber
 | 
				
			||||||
 | 
					  } : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    message: message,
 | 
				
			||||||
 | 
					    bibleQuote
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const handle: Handle = sequence(
 | 
					export const handle: Handle = sequence(
 | 
				
			||||||
	auth.handle,
 | 
						auth.handle,
 | 
				
			||||||
	authorization
 | 
						authorization
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,9 +53,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const ctx = canvas.getContext('2d');
 | 
					    const ctx = canvas.getContext('2d');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Process datasets with colors
 | 
					    // Process datasets with colors and capitalize labels
 | 
				
			||||||
    const processedDatasets = data.datasets.map((dataset, index) => ({
 | 
					    const processedDatasets = data.datasets.map((dataset, index) => ({
 | 
				
			||||||
      ...dataset,
 | 
					      ...dataset,
 | 
				
			||||||
 | 
					      label: dataset.label.charAt(0).toUpperCase() + dataset.label.slice(1),
 | 
				
			||||||
      backgroundColor: getCategoryColor(dataset.label, index),
 | 
					      backgroundColor: getCategoryColor(dataset.label, index),
 | 
				
			||||||
      borderColor: getCategoryColor(dataset.label, index),
 | 
					      borderColor: getCategoryColor(dataset.label, index),
 | 
				
			||||||
      borderWidth: 1
 | 
					      borderWidth: 1
 | 
				
			||||||
@@ -70,16 +71,26 @@
 | 
				
			|||||||
      options: {
 | 
					      options: {
 | 
				
			||||||
        responsive: true,
 | 
					        responsive: true,
 | 
				
			||||||
        maintainAspectRatio: false,
 | 
					        maintainAspectRatio: false,
 | 
				
			||||||
 | 
					        layout: {
 | 
				
			||||||
 | 
					          padding: {
 | 
				
			||||||
 | 
					            top: 40
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        scales: {
 | 
					        scales: {
 | 
				
			||||||
          x: {
 | 
					          x: {
 | 
				
			||||||
            stacked: true,
 | 
					            stacked: true,
 | 
				
			||||||
            grid: {
 | 
					            grid: {
 | 
				
			||||||
              display: false
 | 
					              display: false
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            border: {
 | 
				
			||||||
 | 
					              display: false
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            ticks: {
 | 
					            ticks: {
 | 
				
			||||||
              color: 'var(--nord3)',
 | 
					              color: '#ffffff',
 | 
				
			||||||
              font: {
 | 
					              font: {
 | 
				
			||||||
                family: 'Inter, system-ui, sans-serif'
 | 
					                family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
 | 
					                size: 14,
 | 
				
			||||||
 | 
					                weight: 'bold'
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -87,58 +98,74 @@
 | 
				
			|||||||
            stacked: true,
 | 
					            stacked: true,
 | 
				
			||||||
            beginAtZero: true,
 | 
					            beginAtZero: true,
 | 
				
			||||||
            grid: {
 | 
					            grid: {
 | 
				
			||||||
              color: 'var(--nord4)',
 | 
					              display: false
 | 
				
			||||||
              borderDash: [2, 2]
 | 
					            },
 | 
				
			||||||
 | 
					            border: {
 | 
				
			||||||
 | 
					              display: false
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            ticks: {
 | 
					            ticks: {
 | 
				
			||||||
              color: 'var(--nord3)',
 | 
					              color: 'transparent',
 | 
				
			||||||
              font: {
 | 
					              font: {
 | 
				
			||||||
                family: 'Inter, system-ui, sans-serif'
 | 
					                size: 0
 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              callback: function(value) {
 | 
					 | 
				
			||||||
                return 'CHF ' + value.toFixed(0);
 | 
					 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        plugins: {
 | 
					        plugins: {
 | 
				
			||||||
 | 
					          datalabels: {
 | 
				
			||||||
 | 
					            display: false
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
          legend: {
 | 
					          legend: {
 | 
				
			||||||
            position: 'bottom',
 | 
					            position: 'bottom',
 | 
				
			||||||
            labels: {
 | 
					            labels: {
 | 
				
			||||||
              padding: 20,
 | 
					              padding: 20,
 | 
				
			||||||
              usePointStyle: true,
 | 
					              usePointStyle: true,
 | 
				
			||||||
              color: 'var(--nord1)',
 | 
					              color: '#ffffff',
 | 
				
			||||||
              font: {
 | 
					              font: {
 | 
				
			||||||
                family: 'Inter, system-ui, sans-serif',
 | 
					                family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
                size: 12
 | 
					                size: 14,
 | 
				
			||||||
 | 
					                weight: 'bold'
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          title: {
 | 
					          title: {
 | 
				
			||||||
            display: !!title,
 | 
					            display: !!title,
 | 
				
			||||||
            text: title,
 | 
					            text: title,
 | 
				
			||||||
            color: 'var(--nord0)',
 | 
					            color: '#ffffff',
 | 
				
			||||||
            font: {
 | 
					            font: {
 | 
				
			||||||
              family: 'Inter, system-ui, sans-serif',
 | 
					              family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
              size: 16,
 | 
					              size: 18,
 | 
				
			||||||
              weight: 'bold'
 | 
					              weight: 'bold'
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            padding: 20
 | 
					            padding: 20
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          tooltip: {
 | 
					          tooltip: {
 | 
				
			||||||
            backgroundColor: 'var(--nord1)',
 | 
					            backgroundColor: '#2e3440',
 | 
				
			||||||
            titleColor: 'var(--nord6)',
 | 
					            titleColor: '#ffffff',
 | 
				
			||||||
            bodyColor: 'var(--nord6)',
 | 
					            bodyColor: '#ffffff',
 | 
				
			||||||
            borderColor: 'var(--nord3)',
 | 
					            borderWidth: 0,
 | 
				
			||||||
            borderWidth: 1,
 | 
					            cornerRadius: 12,
 | 
				
			||||||
            cornerRadius: 8,
 | 
					            padding: 12,
 | 
				
			||||||
 | 
					            displayColors: true,
 | 
				
			||||||
 | 
					            titleAlign: 'center',
 | 
				
			||||||
 | 
					            bodyAlign: 'center',
 | 
				
			||||||
            titleFont: {
 | 
					            titleFont: {
 | 
				
			||||||
              family: 'Inter, system-ui, sans-serif'
 | 
					              family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
 | 
					              size: 13,
 | 
				
			||||||
 | 
					              weight: 'bold'
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            bodyFont: {
 | 
					            bodyFont: {
 | 
				
			||||||
              family: 'Inter, system-ui, sans-serif'
 | 
					              family: 'Inter, system-ui, sans-serif',
 | 
				
			||||||
 | 
					              size: 14,
 | 
				
			||||||
 | 
					              weight: '500'
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            titleMarginBottom: 8,
 | 
				
			||||||
 | 
					            usePointStyle: true,
 | 
				
			||||||
 | 
					            boxPadding: 6,
 | 
				
			||||||
            callbacks: {
 | 
					            callbacks: {
 | 
				
			||||||
 | 
					              title: function(context) {
 | 
				
			||||||
 | 
					                return '';
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
              label: function(context) {
 | 
					              label: function(context) {
 | 
				
			||||||
                return context.dataset.label + ': CHF ' + context.parsed.y.toFixed(2);
 | 
					                return context.dataset.label + ': CHF ' + context.parsed.y.toFixed(2);
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
@@ -146,10 +173,53 @@
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        interaction: {
 | 
					        interaction: {
 | 
				
			||||||
          intersect: false,
 | 
					          intersect: true,
 | 
				
			||||||
          mode: 'index'
 | 
					          mode: 'dataset'
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
 | 
					      plugins: [{
 | 
				
			||||||
 | 
					        id: 'monthlyTotals',
 | 
				
			||||||
 | 
					        afterDatasetsDraw: function(chart) {
 | 
				
			||||||
 | 
					          const ctx = chart.ctx;
 | 
				
			||||||
 | 
					          const chartArea = chart.chartArea;
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          ctx.save();
 | 
				
			||||||
 | 
					          ctx.font = 'bold 14px Inter, system-ui, sans-serif';
 | 
				
			||||||
 | 
					          ctx.fillStyle = '#ffffff';
 | 
				
			||||||
 | 
					          ctx.textAlign = 'center';
 | 
				
			||||||
 | 
					          ctx.textBaseline = 'bottom';
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // Calculate and display monthly totals
 | 
				
			||||||
 | 
					          chart.data.labels.forEach((label, index) => {
 | 
				
			||||||
 | 
					            let total = 0;
 | 
				
			||||||
 | 
					            chart.data.datasets.forEach(dataset => {
 | 
				
			||||||
 | 
					              total += dataset.data[index] || 0;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (total > 0) {
 | 
				
			||||||
 | 
					              // Get the x position for this month from any dataset
 | 
				
			||||||
 | 
					              const meta = chart.getDatasetMeta(0);
 | 
				
			||||||
 | 
					              if (meta && meta.data[index]) {
 | 
				
			||||||
 | 
					                const x = meta.data[index].x;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Find the highest point for this month across all datasets
 | 
				
			||||||
 | 
					                let maxY = chartArea.bottom;
 | 
				
			||||||
 | 
					                for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
 | 
				
			||||||
 | 
					                  const datasetMeta = chart.getDatasetMeta(datasetIndex);
 | 
				
			||||||
 | 
					                  if (datasetMeta && datasetMeta.data[index]) {
 | 
				
			||||||
 | 
					                    maxY = Math.min(maxY, datasetMeta.data[index].y);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Display the total above the bar
 | 
				
			||||||
 | 
					                ctx.fillText(`CHF ${total.toFixed(0)}`, x, maxY - 10);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          ctx.restore();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }]
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										393
									
								
								src/routes/+error.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								src/routes/+error.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,393 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
					  import { page } from '$app/stores';
 | 
				
			||||||
 | 
					  import { goto } from '$app/navigation';
 | 
				
			||||||
 | 
					  import Header from '$lib/components/Header.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $: status = $page.status;
 | 
				
			||||||
 | 
					  $: error = $page.error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get session data if available (may not be available in error context)
 | 
				
			||||||
 | 
					  $: session = $page.data?.session;
 | 
				
			||||||
 | 
					  $: user = session?.user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Get Bible quote from SSR via handleError hook
 | 
				
			||||||
 | 
					  $: bibleQuote = $page.error?.bibleQuote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getErrorTitle(status) {
 | 
				
			||||||
 | 
					    switch (status) {
 | 
				
			||||||
 | 
					      case 401:
 | 
				
			||||||
 | 
					        return 'Anmeldung erforderlich';
 | 
				
			||||||
 | 
					      case 403:
 | 
				
			||||||
 | 
					        return 'Zugriff verweigert';
 | 
				
			||||||
 | 
					      case 404:
 | 
				
			||||||
 | 
					        return 'Seite nicht gefunden';
 | 
				
			||||||
 | 
					      case 500:
 | 
				
			||||||
 | 
					        return 'Serverfehler';
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return 'Fehler';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getErrorDescription(status) {
 | 
				
			||||||
 | 
					    switch (status) {
 | 
				
			||||||
 | 
					      case 401:
 | 
				
			||||||
 | 
					        return 'Du musst angemeldet sein, um auf diese Seite zugreifen zu können.';
 | 
				
			||||||
 | 
					      case 403:
 | 
				
			||||||
 | 
					        return 'Du hast keine Berechtigung für diesen Bereich.';
 | 
				
			||||||
 | 
					      case 404:
 | 
				
			||||||
 | 
					        return 'Die angeforderte Seite konnte nicht gefunden werden.';
 | 
				
			||||||
 | 
					      case 500:
 | 
				
			||||||
 | 
					        return 'Es ist ein unerwarteter Fehler aufgetreten. Bitte versuche es später erneut.';
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return 'Es ist ein unerwarteter Fehler aufgetreten.';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function getErrorIcon(status) {
 | 
				
			||||||
 | 
					    switch (status) {
 | 
				
			||||||
 | 
					      case 401:
 | 
				
			||||||
 | 
					        return '🔐';
 | 
				
			||||||
 | 
					      case 403:
 | 
				
			||||||
 | 
					        return '🚫';
 | 
				
			||||||
 | 
					      case 404:
 | 
				
			||||||
 | 
					        return '🔍';
 | 
				
			||||||
 | 
					      case 500:
 | 
				
			||||||
 | 
					        return '⚠️';
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return '❌';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function goHome() {
 | 
				
			||||||
 | 
					    goto('/');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function goBack() {
 | 
				
			||||||
 | 
					    if (window.history.length > 1) {
 | 
				
			||||||
 | 
					      window.history.back();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      goto('/');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  function login() {
 | 
				
			||||||
 | 
					    goto('/login');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:head>
 | 
				
			||||||
 | 
					  <title>{getErrorTitle(status)} - Alexander's Website</title>
 | 
				
			||||||
 | 
					</svelte:head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<Header>
 | 
				
			||||||
 | 
					  <ul class="site_header" slot="links">
 | 
				
			||||||
 | 
					  </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <main class="error-page">
 | 
				
			||||||
 | 
					    <div class="error-container">
 | 
				
			||||||
 | 
					      <div class="error-icon">
 | 
				
			||||||
 | 
					        {getErrorIcon(status)}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <h1 class="error-title">
 | 
				
			||||||
 | 
					        {getErrorTitle(status)}
 | 
				
			||||||
 | 
					      </h1>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <div class="error-code">
 | 
				
			||||||
 | 
					        Fehler {status}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <p class="error-description">
 | 
				
			||||||
 | 
					        {getErrorDescription(status)}
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {#if error?.details}
 | 
				
			||||||
 | 
					        <div class="error-details">
 | 
				
			||||||
 | 
					          {error.details}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="error-actions">
 | 
				
			||||||
 | 
					        {#if status === 401}
 | 
				
			||||||
 | 
					          <button class="btn btn-primary" on:click={login}>
 | 
				
			||||||
 | 
					            Anmelden
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <button class="btn btn-secondary" on:click={goHome}>
 | 
				
			||||||
 | 
					            Zur Startseite
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        {:else if status === 403}
 | 
				
			||||||
 | 
					          <button class="btn btn-primary" on:click={goHome}>
 | 
				
			||||||
 | 
					            Zur Startseite
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <button class="btn btn-secondary" on:click={goBack}>
 | 
				
			||||||
 | 
					            Zurück
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        {:else if status === 404}
 | 
				
			||||||
 | 
					          <button class="btn btn-primary" on:click={goHome}>
 | 
				
			||||||
 | 
					            Zur Startseite
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <button class="btn btn-secondary" on:click={goBack}>
 | 
				
			||||||
 | 
					            Zurück
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        {:else if status === 500}
 | 
				
			||||||
 | 
					          <button class="btn btn-primary" on:click={goHome}>
 | 
				
			||||||
 | 
					            Zur Startseite
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <button class="btn btn-secondary" on:click={goBack}>
 | 
				
			||||||
 | 
					            Erneut versuchen
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        {:else}
 | 
				
			||||||
 | 
					          <button class="btn btn-primary" on:click={goHome}>
 | 
				
			||||||
 | 
					            Zur Startseite
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					          <button class="btn btn-secondary" on:click={goBack}>
 | 
				
			||||||
 | 
					            Zurück
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <!-- Bible Quote Section -->
 | 
				
			||||||
 | 
					      {#if bibleQuote}
 | 
				
			||||||
 | 
					        <div class="bible-quote">
 | 
				
			||||||
 | 
					          <div class="quote-text">
 | 
				
			||||||
 | 
					            „{bibleQuote.text}"
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="quote-reference">
 | 
				
			||||||
 | 
					            — {bibleQuote.reference}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </main>
 | 
				
			||||||
 | 
					</Header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					  .error-page {
 | 
				
			||||||
 | 
					    min-height: calc(100vh - 4rem);
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    background: #fbf9f3;
 | 
				
			||||||
 | 
					    padding: 2rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .error-page {
 | 
				
			||||||
 | 
					      background: var(--background-dark);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error-container {
 | 
				
			||||||
 | 
					    background: var(--nord5);
 | 
				
			||||||
 | 
					    border-radius: 1rem;
 | 
				
			||||||
 | 
					    padding: 3rem;
 | 
				
			||||||
 | 
					    max-width: 600px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					    border: 1px solid var(--nord4);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .error-container {
 | 
				
			||||||
 | 
					      background: var(--nord1);
 | 
				
			||||||
 | 
					      border-color: var(--nord2);
 | 
				
			||||||
 | 
					      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error-icon {
 | 
				
			||||||
 | 
					    font-size: 4rem;
 | 
				
			||||||
 | 
					    margin-bottom: 1.5rem;
 | 
				
			||||||
 | 
					    opacity: 0.8;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error-title {
 | 
				
			||||||
 | 
					    font-size: 2.5rem;
 | 
				
			||||||
 | 
					    color: var(--nord0);
 | 
				
			||||||
 | 
					    margin-bottom: 0.5rem;
 | 
				
			||||||
 | 
					    font-weight: 700;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .error-title {
 | 
				
			||||||
 | 
					      color: var(--nord6);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error-code {
 | 
				
			||||||
 | 
					    font-size: 1.2rem;
 | 
				
			||||||
 | 
					    color: var(--nord3);
 | 
				
			||||||
 | 
					    font-weight: 600;
 | 
				
			||||||
 | 
					    margin-bottom: 1.5rem;
 | 
				
			||||||
 | 
					    opacity: 0.8;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .error-code {
 | 
				
			||||||
 | 
					      color: var(--nord4);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error-description {
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					    color: var(--nord2);
 | 
				
			||||||
 | 
					    margin-bottom: 1rem;
 | 
				
			||||||
 | 
					    line-height: 1.6;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .error-description {
 | 
				
			||||||
 | 
					      color: var(--nord5);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error-details {
 | 
				
			||||||
 | 
					    background: var(--nord4);
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    padding: 1rem;
 | 
				
			||||||
 | 
					    margin: 1.5rem 0;
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					    color: var(--nord0);
 | 
				
			||||||
 | 
					    border-left: 4px solid var(--blue);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .error-details {
 | 
				
			||||||
 | 
					      background: var(--nord2);
 | 
				
			||||||
 | 
					      color: var(--nord6);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .error-actions {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    gap: 1rem;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    margin: 2rem 0;
 | 
				
			||||||
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn {
 | 
				
			||||||
 | 
					    padding: 0.75rem 1.5rem;
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    font-weight: 600;
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    transition: all 0.2s ease;
 | 
				
			||||||
 | 
					    font-size: 1rem;
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    gap: 0.5rem;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn-primary {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, var(--blue), var(--lightblue));
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn-primary:hover {
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, var(--lightblue), var(--blue));
 | 
				
			||||||
 | 
					    transform: translateY(-1px);
 | 
				
			||||||
 | 
					    box-shadow: 0 4px 12px rgba(94, 129, 172, 0.3);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn-secondary {
 | 
				
			||||||
 | 
					    background: var(--nord4);
 | 
				
			||||||
 | 
					    color: var(--nord0);
 | 
				
			||||||
 | 
					    border: 1px solid var(--nord3);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn-secondary:hover {
 | 
				
			||||||
 | 
					    background: var(--nord3);
 | 
				
			||||||
 | 
					    transform: translateY(-1px);
 | 
				
			||||||
 | 
					    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .btn-secondary {
 | 
				
			||||||
 | 
					      background: var(--nord2);
 | 
				
			||||||
 | 
					      color: var(--nord6);
 | 
				
			||||||
 | 
					      border-color: var(--nord3);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .btn-secondary:hover {
 | 
				
			||||||
 | 
					      background: var(--nord3);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .bible-quote {
 | 
				
			||||||
 | 
					    margin: 2.5rem 0;
 | 
				
			||||||
 | 
					    padding: 2rem;
 | 
				
			||||||
 | 
					    background: linear-gradient(135deg, var(--nord5), var(--nord4));
 | 
				
			||||||
 | 
					    border-radius: 0.75rem;
 | 
				
			||||||
 | 
					    border-left: 4px solid var(--blue);
 | 
				
			||||||
 | 
					    font-style: italic;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .bible-quote {
 | 
				
			||||||
 | 
					      background: linear-gradient(135deg, var(--nord2), var(--nord3));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .quote-text {
 | 
				
			||||||
 | 
					    font-size: 1.1rem;
 | 
				
			||||||
 | 
					    line-height: 1.6;
 | 
				
			||||||
 | 
					    color: var(--nord0);
 | 
				
			||||||
 | 
					    margin-bottom: 1rem;
 | 
				
			||||||
 | 
					    text-align: left;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .quote-text {
 | 
				
			||||||
 | 
					      color: var(--nord6);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .quote-reference {
 | 
				
			||||||
 | 
					    font-size: 0.9rem;
 | 
				
			||||||
 | 
					    color: var(--nord2);
 | 
				
			||||||
 | 
					    font-weight: 600;
 | 
				
			||||||
 | 
					    text-align: right;
 | 
				
			||||||
 | 
					    font-style: normal;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (prefers-color-scheme: dark) {
 | 
				
			||||||
 | 
					    .quote-reference {
 | 
				
			||||||
 | 
					      color: var(--nord4);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (max-width: 600px) {
 | 
				
			||||||
 | 
					    .error-container {
 | 
				
			||||||
 | 
					      padding: 2rem;
 | 
				
			||||||
 | 
					      margin: 1rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .error-title {
 | 
				
			||||||
 | 
					      font-size: 2rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .error-actions {
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .btn {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      max-width: 250px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .bible-quote {
 | 
				
			||||||
 | 
					      padding: 1.5rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .quote-text {
 | 
				
			||||||
 | 
					      font-size: 1rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										72
									
								
								src/routes/api/bible-quote/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/routes/api/bible-quote/+server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					import { json, error } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import type { RequestHandler } from './$types';
 | 
				
			||||||
 | 
					import fs from 'fs';
 | 
				
			||||||
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface BibleVerse {
 | 
				
			||||||
 | 
					  bookName: string;
 | 
				
			||||||
 | 
					  abbreviation: string;
 | 
				
			||||||
 | 
					  chapter: number;
 | 
				
			||||||
 | 
					  verse: number;
 | 
				
			||||||
 | 
					  verseNumber: number;
 | 
				
			||||||
 | 
					  text: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Cache for parsed verses to avoid reading file repeatedly
 | 
				
			||||||
 | 
					let cachedVerses: BibleVerse[] | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loadVerses(): BibleVerse[] {
 | 
				
			||||||
 | 
					  if (cachedVerses) {
 | 
				
			||||||
 | 
					    return cachedVerses;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const filePath = path.join(process.cwd(), 'static', 'allioli.tsv');
 | 
				
			||||||
 | 
					    const content = fs.readFileSync(filePath, 'utf-8');
 | 
				
			||||||
 | 
					    const lines = content.trim().split('\n');
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    cachedVerses = lines.map(line => {
 | 
				
			||||||
 | 
					      const [bookName, abbreviation, chapter, verse, verseNumber, text] = line.split('\t');
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        bookName,
 | 
				
			||||||
 | 
					        abbreviation, 
 | 
				
			||||||
 | 
					        chapter: parseInt(chapter),
 | 
				
			||||||
 | 
					        verse: parseInt(verse),
 | 
				
			||||||
 | 
					        verseNumber: parseInt(verseNumber),
 | 
				
			||||||
 | 
					        text
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return cachedVerses;
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    console.error('Error loading Bible verses:', err);
 | 
				
			||||||
 | 
					    throw new Error('Failed to load Bible verses');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getRandomVerse(verses: BibleVerse[]): BibleVerse {
 | 
				
			||||||
 | 
					  const randomIndex = Math.floor(Math.random() * verses.length);
 | 
				
			||||||
 | 
					  return verses[randomIndex];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatVerse(verse: BibleVerse): string {
 | 
				
			||||||
 | 
					  return `${verse.bookName} ${verse.chapter}:${verse.verseNumber}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GET: RequestHandler = async () => {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const verses = loadVerses();
 | 
				
			||||||
 | 
					    const randomVerse = getRandomVerse(verses);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return json({
 | 
				
			||||||
 | 
					      text: randomVerse.text,
 | 
				
			||||||
 | 
					      reference: formatVerse(randomVerse),
 | 
				
			||||||
 | 
					      book: randomVerse.bookName,
 | 
				
			||||||
 | 
					      chapter: randomVerse.chapter,
 | 
				
			||||||
 | 
					      verse: randomVerse.verseNumber
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    console.error('Error fetching random Bible verse:', err);
 | 
				
			||||||
 | 
					    return error(500, 'Failed to fetch Bible verse');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -106,9 +106,23 @@ export const GET: RequestHandler = async ({ url, locals }) => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Convert to arrays for Chart.js
 | 
					    // Convert to arrays for Chart.js
 | 
				
			||||||
    const months = Array.from(monthsMap.keys()).sort();
 | 
					    const allMonths = Array.from(monthsMap.keys()).sort();
 | 
				
			||||||
    const categoryList = Array.from(categories).sort();
 | 
					    const categoryList = Array.from(categories).sort();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Find the first month with any data and trim empty months from the start
 | 
				
			||||||
 | 
					    let firstMonthWithData = 0;
 | 
				
			||||||
 | 
					    for (let i = 0; i < allMonths.length; i++) {
 | 
				
			||||||
 | 
					      const monthData = monthsMap.get(allMonths[i]);
 | 
				
			||||||
 | 
					      const hasData = Object.values(monthData).some(value => value > 0);
 | 
				
			||||||
 | 
					      if (hasData) {
 | 
				
			||||||
 | 
					        firstMonthWithData = i;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Trim the months array to start from the first month with data
 | 
				
			||||||
 | 
					    const months = allMonths.slice(firstMonthWithData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const datasets = categoryList.map((category: string) => ({
 | 
					    const datasets = categoryList.map((category: string) => ({
 | 
				
			||||||
      label: category,
 | 
					      label: category,
 | 
				
			||||||
      data: months.map(month => monthsMap.get(month)[category] || 0)
 | 
					      data: months.map(month => monthsMap.get(month)[category] || 0)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -153,33 +153,38 @@
 | 
				
			|||||||
<main class="cospend-main">
 | 
					<main class="cospend-main">
 | 
				
			||||||
    <h1>Cospend</h1>
 | 
					    <h1>Cospend</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <EnhancedBalance bind:this={enhancedBalanceComponent} initialBalance={data.balance} initialDebtData={data.debtData} />
 | 
					  <!-- Responsive layout for balance and chart -->
 | 
				
			||||||
 | 
					  <div class="dashboard-layout">
 | 
				
			||||||
 | 
					    <div class="balance-section">
 | 
				
			||||||
 | 
					      <EnhancedBalance bind:this={enhancedBalanceComponent} initialBalance={data.balance} initialDebtData={data.debtData} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="actions">
 | 
					      <div class="actions">
 | 
				
			||||||
    {#if balance.netBalance !== 0}
 | 
					        {#if balance.netBalance !== 0}
 | 
				
			||||||
      <a href="/cospend/settle" class="btn btn-settlement">Settle Debts</a>
 | 
					          <a href="/cospend/settle" class="btn btn-settlement">Settle Debts</a>
 | 
				
			||||||
    {/if}
 | 
					        {/if}
 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <DebtBreakdown bind:this={debtBreakdownComponent} />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <!-- Monthly Expenses Chart -->
 | 
					 | 
				
			||||||
  <div class="chart-section">
 | 
					 | 
				
			||||||
    {#if expensesLoading}
 | 
					 | 
				
			||||||
      <div class="loading">Loading monthly expenses chart...</div>
 | 
					 | 
				
			||||||
    {:else if monthlyExpensesData.datasets && monthlyExpensesData.datasets.length > 0}
 | 
					 | 
				
			||||||
      <BarChart
 | 
					 | 
				
			||||||
        data={monthlyExpensesData}
 | 
					 | 
				
			||||||
        title="Monthly Expenses by Category"
 | 
					 | 
				
			||||||
        height="400px"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    {:else}
 | 
					 | 
				
			||||||
      <div class="loading">
 | 
					 | 
				
			||||||
        Debug: expensesLoading={expensesLoading},
 | 
					 | 
				
			||||||
        datasets={monthlyExpensesData.datasets?.length || 0},
 | 
					 | 
				
			||||||
        data={JSON.stringify(monthlyExpensesData)}
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    {/if}
 | 
					
 | 
				
			||||||
 | 
					      <DebtBreakdown bind:this={debtBreakdownComponent} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Monthly Expenses Chart -->
 | 
				
			||||||
 | 
					    <div class="chart-section">
 | 
				
			||||||
 | 
					      {#if expensesLoading}
 | 
				
			||||||
 | 
					        <div class="loading">Loading monthly expenses chart...</div>
 | 
				
			||||||
 | 
					      {:else if monthlyExpensesData.datasets && monthlyExpensesData.datasets.length > 0}
 | 
				
			||||||
 | 
					        <BarChart
 | 
				
			||||||
 | 
					          data={monthlyExpensesData}
 | 
				
			||||||
 | 
					          title="Monthly Expenses by Category"
 | 
				
			||||||
 | 
					          height="400px"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      {:else}
 | 
				
			||||||
 | 
					        <div class="loading">
 | 
				
			||||||
 | 
					          Debug: expensesLoading={expensesLoading},
 | 
				
			||||||
 | 
					          datasets={monthlyExpensesData.datasets?.length || 0},
 | 
				
			||||||
 | 
					          data={JSON.stringify(monthlyExpensesData)}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      {/if}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {#if loading}
 | 
					  {#if loading}
 | 
				
			||||||
@@ -274,7 +279,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
  .cospend-main {
 | 
					  .cospend-main {
 | 
				
			||||||
    max-width: 800px;
 | 
					 | 
				
			||||||
    margin: 0 auto;
 | 
					    margin: 0 auto;
 | 
				
			||||||
    padding: 2rem;
 | 
					    padding: 2rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -355,6 +359,9 @@
 | 
				
			|||||||
    border-radius: 0.75rem;
 | 
					    border-radius: 0.75rem;
 | 
				
			||||||
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
					    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
    border: 1px solid var(--nord4);
 | 
					    border: 1px solid var(--nord4);
 | 
				
			||||||
 | 
					    max-width: 800px;
 | 
				
			||||||
 | 
					    margin-left: auto;
 | 
				
			||||||
 | 
					    margin-right: auto;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .recent-activity h2 {
 | 
					  .recent-activity h2 {
 | 
				
			||||||
@@ -705,8 +712,33 @@
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chart-section {
 | 
					  .dashboard-layout {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    gap: 2rem;
 | 
				
			||||||
    margin-bottom: 2rem;
 | 
					    margin-bottom: 2rem;
 | 
				
			||||||
 | 
					    max-width: 1400px;
 | 
				
			||||||
 | 
					    margin-left: auto;
 | 
				
			||||||
 | 
					    margin-right: auto;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media (min-width: 1200px) {
 | 
				
			||||||
 | 
					    .dashboard-layout {
 | 
				
			||||||
 | 
					      display: grid;
 | 
				
			||||||
 | 
					      grid-template-columns: 1fr 1fr;
 | 
				
			||||||
 | 
					      gap: 3rem;
 | 
				
			||||||
 | 
					      align-items: start;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .balance-section {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 2rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .chart-section {
 | 
				
			||||||
 | 
					    min-height: 400px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .chart-section .loading {
 | 
					  .chart-section .loading {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										31377
									
								
								static/allioli.tsv
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31377
									
								
								static/allioli.tsv
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user