diff --git a/site/src/app.css b/site/src/app.css index f1d8c73..b480fdf 100644 --- a/site/src/app.css +++ b/site/src/app.css @@ -1 +1,9 @@ @import "tailwindcss"; + +html, body { + overflow: hidden; + height: 100%; + width: 100%; + position: fixed; + overscroll-behavior: none; +} diff --git a/site/src/lib/app.ts b/site/src/lib/app.ts index ea0e048..fa84cae 100644 --- a/site/src/lib/app.ts +++ b/site/src/lib/app.ts @@ -1,10 +1,10 @@ -import { Data, Effect, Schema } from "effect"; +import { Data, Effect, Schema, Option } from "effect"; import type { BibleBook } from "$lib/book"; import { BibleBookSchema, getDisplayName as getBibleBookDisplayName, getShortName } from "$lib/book"; import type { Translation } from "$lib/translations/translation"; import { TranslationSchema } from "$lib/translations/translation"; -// Effect Schema definitions - App content without IDs +// Effect Schema definitions - App-specific state export const BibleStateSchema = Schema.Struct({ currentBook: BibleBookSchema, currentChapter: Schema.Number, @@ -17,8 +17,8 @@ export const StopwatchStateSchema = Schema.Struct({ isRunning: Schema.Boolean }); -// App content schema - just the app-specific data -export const AppContentSchema = Schema.Union( +// App schema - represents the application running in a tab +export const AppSchema = Schema.Union( Schema.Struct({ _tag: Schema.Literal("Bible"), bibleState: BibleStateSchema @@ -35,30 +35,43 @@ export const AppContentSchema = Schema.Union( }) ); -// Tab schema - combines ID with app content -export const TabSchema = Schema.Struct({ +// TabState schema - combines ID with app +export const TabStateSchema = Schema.Struct({ id: Schema.String, - app: AppContentSchema + app: AppSchema }); -// Type exports -export type BibleState = Schema.Schema.Type; -export type StopwatchState = Schema.Schema.Type; -export type AppContent = Schema.Schema.Type; -export type Tab = Schema.Schema.Type; +// TabsState schema - the complete tabs system state +export const TabsStateSchema = Schema.Struct({ + tabs: Schema.Array(TabStateSchema), + activeTabId: Schema.String, + nextTabId: Schema.Number +}); + +// Helper type to make deeply readonly types writable (for Svelte $state compatibility) +type DeepWritable = T extends object + ? { -readonly [P in keyof T]: DeepWritable } + : T; + +// Type exports - Schema.Type generates readonly types, make them writable for $state +type BibleStateReadonly = Schema.Schema.Type; +type StopwatchStateReadonly = Schema.Schema.Type; +type AppReadonly = Schema.Schema.Type; +type TabStateReadonly = Schema.Schema.Type; +type TabsStateReadonly = Schema.Schema.Type; +export type BibleState = DeepWritable; +export type StopwatchState = DeepWritable; +export type App = DeepWritable; +export type TabState = DeepWritable; +export type TabsState = DeepWritable; // Maintain backward compatibility with Data constructors export const BibleState = Data.case(); export const StopwatchState = Data.case(); -// AppContent constructors -export const { Bible, About, ChooseApp, Stopwatch, $match } = Data.taggedEnum() - -// Tab ID management using Effect -export const getTabId = (tab: Tab): string => { - return tab.id; -}; +// App constructors +export const { Bible, About, ChooseApp, Stopwatch, $match } = Data.taggedEnum() // Format time as MM:SS for tab title const formatTimeForTitle = (milliseconds: number): string => { @@ -68,77 +81,172 @@ const formatTimeForTitle = (milliseconds: number): string => { return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; }; -// Get display name for Tab (tab title) using $match -export const getDisplayName = (tab: Tab): string => { - return $match(tab.app, { - Bible: ({ bibleState }) => `${getBibleBookDisplayName(bibleState.currentBook)} ${bibleState.currentChapter} (${bibleState.translation.metadata.shortName})`, - About: () => "About", - ChooseApp: () => "Choose App", - Stopwatch: ({ stopwatchState }) => { - if (stopwatchState.elapsedTime > 0 || stopwatchState.isRunning) { - return formatTimeForTitle(stopwatchState.elapsedTime); +// App namespace - operations on App type +export namespace App { + export const getUrl = (app: App): string => { + return $match(app, { + Bible: ({ bibleState }) => { + const bookShort = getShortName(bibleState.currentBook); + return `/${bookShort}/${bibleState.currentChapter}`; + }, + About: () => "/about", + ChooseApp: () => "/", + Stopwatch: () => "/stopwatch" + }); + }; + + export const getTitle = (app: App): string => { + return $match(app, { + Bible: ({ bibleState }) => `${getBibleBookDisplayName(bibleState.currentBook)} ${bibleState.currentChapter} (${bibleState.translation.metadata.shortName})`, + About: () => "About", + ChooseApp: () => "Choose App", + Stopwatch: ({ stopwatchState }) => { + if (stopwatchState.elapsedTime > 0 || stopwatchState.isRunning) { + return formatTimeForTitle(stopwatchState.elapsedTime); + } + return "Stopwatch"; } - return "Stopwatch"; - } - }); + }); + }; +} + +// TabsState namespace - operations on TabsState type +export namespace TabsState { + export const addTab = (state: TabsState, app: App): Effect.Effect => { + return Effect.sync(() => { + const tabId = `tab${state.nextTabId}`; + const newTab: TabState = { id: tabId, app }; + + return { + tabs: [...state.tabs, newTab], + activeTabId: tabId, + nextTabId: state.nextTabId + 1 + }; + }); + }; + + export const removeTab = (state: TabsState, tabId: string): Effect.Effect => { + return Effect.sync(() => { + // Don't allow removing last tab + if (state.tabs.length === 1) { + return state; + } + + const newTabs = state.tabs.filter(tab => tab.id !== tabId); + + // If removing active tab, switch to first tab + const newActiveTabId = state.activeTabId === tabId + ? newTabs[0]?.id ?? state.activeTabId + : state.activeTabId; + + return { + tabs: newTabs, + activeTabId: newActiveTabId, + nextTabId: state.nextTabId + }; + }); + }; + + export const setActiveTab = (state: TabsState, tabId: string): Effect.Effect => { + return Effect.sync(() => { + // Verify tab exists + const tabExists = state.tabs.some(tab => tab.id === tabId); + if (!tabExists) { + console.error(`Tab ${tabId} not found`); + return state; + } + + return { + ...state, + activeTabId: tabId + }; + }); + }; + + export const nextTab = (state: TabsState): Effect.Effect => { + return Effect.sync(() => { + const currentIndex = state.tabs.findIndex(tab => tab.id === state.activeTabId); + if (currentIndex === -1) return state; + + const nextIndex = (currentIndex + 1) % state.tabs.length; + return { + ...state, + activeTabId: state.tabs[nextIndex].id + }; + }); + }; + + export const previousTab = (state: TabsState): Effect.Effect => { + return Effect.sync(() => { + const currentIndex = state.tabs.findIndex(tab => tab.id === state.activeTabId); + if (currentIndex === -1) return state; + + const prevIndex = currentIndex === 0 ? state.tabs.length - 1 : currentIndex - 1; + return { + ...state, + activeTabId: state.tabs[prevIndex].id + }; + }); + }; + + export const updateTab = (state: TabsState, updatedTab: TabState): Effect.Effect => { + return Effect.sync(() => { + return { + ...state, + tabs: state.tabs.map(tab => tab.id === updatedTab.id ? updatedTab : tab) + }; + }); + }; + + export const getActiveTab = (state: TabsState): Option.Option => { + const tab = state.tabs.find(t => t.id === state.activeTabId); + return tab ? Option.some(tab) : Option.none(); + }; +} + + +// Create tab configuration types +export type CreateTabConfig = + | { app: "Bible", id: string, book: BibleBook, chapter: number, translation: Translation, showCanonExplorer: boolean } + | { app: "Stopwatch", id: string } + | { app: "About", id: string } + | { app: "ChooseApp", id: string }; + +// Unified tab creation function +export const createTab = (config: CreateTabConfig): Effect.Effect => { + switch (config.app) { + case "Bible": + return Effect.succeed({ + id: config.id, + app: Bible({ + bibleState: BibleState({ + currentBook: config.book, + currentChapter: config.chapter, + translation: config.translation as any, // Translation types are readonly but compatible + showCanonExplorer: config.showCanonExplorer + }) + }) + }); + case "Stopwatch": + return Effect.succeed({ + id: config.id, + app: Stopwatch({ + stopwatchState: StopwatchState({ + elapsedTime: 0, + isRunning: false + }) + }) + }); + case "About": + return Effect.succeed({ + id: config.id, + app: About() + }); + case "ChooseApp": + return Effect.succeed({ + id: config.id, + app: ChooseApp() + }); + } }; -// Tab state transformations using Effect -export const transformTab = (tab: Tab, transform: (tab: Tab) => Tab): Effect.Effect => - Effect.succeed(transform(tab)); - -export const createBibleTab = ( - id: string, - book: BibleBook, - chapter: number, - translation: Translation, - showCanonExplorer: boolean -): Effect.Effect => - Effect.succeed({ - id, - app: Bible({ - bibleState: BibleState({ - currentBook: book, - currentChapter: chapter, - translation, - showCanonExplorer - }) - }) - }); - -export const createAboutTab = (id: string): Effect.Effect => - Effect.succeed({ - id, - app: About() - }); - -export const createChooseTab = (id: string): Effect.Effect => - Effect.succeed({ - id, - app: ChooseApp() - }); - -export const createStopwatchTab = (id: string): Effect.Effect => - Effect.succeed({ - id, - app: Stopwatch({ - stopwatchState: StopwatchState({ - elapsedTime: 0, - isRunning: false - }) - }) - }); - - -// Map Tab instances to their corresponding URLs -export const getTabUrl = (tab: Tab): string => { - return $match(tab.app, { - Bible: ({ bibleState }) => { - const bookShort = getShortName(bibleState.currentBook); - return `/${bookShort}/${bibleState.currentChapter}`; - }, - About: () => "/about", - ChooseApp: () => "/", // Default to home for choose app - Stopwatch: () => "/stopwatch" - }); -}; \ No newline at end of file diff --git a/site/src/lib/components/AppContainer.svelte b/site/src/lib/components/AppContainer.svelte index 5001ac8..69fbe52 100644 --- a/site/src/lib/components/AppContainer.svelte +++ b/site/src/lib/components/AppContainer.svelte @@ -2,14 +2,12 @@ import type { Translation } from "$lib/translations/translation"; import { BibleBook } from "$lib/book"; import { - type Tab, - getTabId, - getTabUrl, - getDisplayName, - createBibleTab, - createAboutTab, - createChooseTab, - createStopwatchTab + type TabsState, + type TabState, + App, + TabsState as TabsStateNS, + createTab, + ChooseApp } from "$lib/app"; import { NavigationServiceLive } from "$lib/services/NavigationService"; import { ResponsiveServiceLive } from "$lib/services/ResponsiveService"; @@ -17,134 +15,138 @@ import TabContent from "$lib/components/tabs/TabContent.svelte"; import { page } from "$app/stores"; import { onMount } from "svelte"; - import { Effect } from "effect"; + import { Effect, Option } from "effect"; let { translation }: { translation: Translation } = $props(); - // State management - let tabs = $state([]); - let activeTabId = $state("tab1"); - let nextTabId = $state(2); + // State management - single source of truth + let tabsState = $state({ + tabs: [], + activeTabId: "tab1", + nextTabId: 2 + }); // Initialize the first tab using Effect async function initializeTab() { - try { - const initialState = await Effect.runPromise(NavigationServiceLive.getInitialState()); - - let initialTab: Tab; - if (initialState.isAbout) { - // Create About tab if URL is /about - initialTab = await Effect.runPromise(createAboutTab("tab1")); - } else if (initialState.isStopwatch) { - // Create Stopwatch tab if URL is /stopwatch - initialTab = await Effect.runPromise(createStopwatchTab("tab1")); - } else { - // Create Bible tab with parsed book/chapter - const canonState = await Effect.runPromise(ResponsiveServiceLive.getInitialCanonState()); - initialTab = await Effect.runPromise( - createBibleTab( - "tab1", - initialState.book, - initialState.chapter, - translation, - canonState - ) - ); - } - - tabs = [initialTab]; - } catch (error) { - console.error("Failed to initialize tab:", error); - // Fallback to default - const fallbackTab = await Effect.runPromise( - createBibleTab("tab1", BibleBook.John, 1, translation, true) + const initialState = await Effect.runPromise(NavigationServiceLive.getInitialState()) + .catch(error => { + console.error("Failed to get initial state:", error); + return { book: BibleBook.John, chapter: 1, isAbout: false, isStopwatch: false }; + }); + + let initialTab: TabState; + if (initialState.isAbout) { + // Create About tab if URL is /about + initialTab = await Effect.runPromise(createTab({ app: "About", id: "tab1" })); + } else if (initialState.isStopwatch) { + // Create Stopwatch tab if URL is /stopwatch + initialTab = await Effect.runPromise(createTab({ app: "Stopwatch", id: "tab1" })); + } else { + // Create Bible tab with parsed book/chapter + const canonState = await Effect.runPromise(ResponsiveServiceLive.getInitialCanonState()); + initialTab = await Effect.runPromise( + createTab({ + app: "Bible", + id: "tab1", + book: initialState.book, + chapter: initialState.chapter, + translation, + showCanonExplorer: canonState + }) ); - tabs = [fallbackTab]; } + + tabsState.tabs = [initialTab]; } - // Get active tab reference - let activeTab = $derived(tabs.find(tab => getTabId(tab) === activeTabId)); + // Get active tab reference - derived from tabsState + let activeTabOption = $derived(TabsStateNS.getActiveTab(tabsState)); // Update browser title when active tab changes $effect(() => { - if (activeTab && typeof document !== 'undefined') { - document.title = getDisplayName(activeTab); + if (Option.isSome(activeTabOption) && typeof document !== 'undefined') { + document.title = App.getTitle(activeTabOption.value.app); } }); // Update tab state - function updateTabState(updatedTab: Tab) { - const tabId = getTabId(updatedTab); - tabs = tabs.map(tab => getTabId(tab) === tabId ? updatedTab : tab); - - // Update URL if this is the active tab - if (tabId === activeTabId) { - const url = getTabUrl(updatedTab); - Effect.runPromise(NavigationServiceLive.navigateToUrl(url)); - } + function updateTabState(updatedTab: TabState) { + Effect.runPromise(TabsStateNS.updateTab(tabsState, updatedTab)) + .then(newState => { + tabsState = newState; + + // Update URL if this is the active tab + if (updatedTab.id === tabsState.activeTabId) { + const url = App.getUrl(updatedTab.app); + Effect.runPromise(NavigationServiceLive.navigateToUrl(url)) + .catch(error => console.error("Failed to navigate:", error)); + } + }) + .catch(error => console.error("Failed to update tab:", error)); } // Add new tab - async function addTab() { - try { - const tabId = `tab${nextTabId}`; - const newTab = await Effect.runPromise(createChooseTab(tabId)); - tabs = [...tabs, newTab]; - activeTabId = tabId; - nextTabId++; - } catch (error) { - console.error("Failed to add tab:", error); - } + function addTab() { + Effect.runPromise(TabsStateNS.addTab(tabsState, ChooseApp())) + .then(newState => { + tabsState = newState; + }) + .catch(error => console.error("Failed to add tab:", error)); } // Remove tab - async function removeTab(tabId: string) { - if (tabs.length === 1) return; - - // Clean up background tasks for the removed tab (no longer needed with simplified approach) - - tabs = tabs.filter(tab => getTabId(tab) !== tabId); - - if (activeTabId === tabId) { - const firstTab = tabs[0]; - if (firstTab) { - activeTabId = getTabId(firstTab); - } - } + function removeTab(tabId: string) { + Effect.runPromise(TabsStateNS.removeTab(tabsState, tabId)) + .then(newState => { + tabsState = newState; + }) + .catch(error => console.error("Failed to remove tab:", error)); } // Set active tab function setActiveTab(tabId: string) { - activeTabId = tabId; - - // Update URL for any tab type using the mapping function - const tab = tabs.find(tab => getTabId(tab) === tabId); - if (tab) { - const url = getTabUrl(tab); - Effect.runPromise(NavigationServiceLive.navigateToUrl(url)); - } + Effect.runPromise(TabsStateNS.setActiveTab(tabsState, tabId)) + .then(newState => { + tabsState = newState; + + // Update URL + const activeTabOption = TabsStateNS.getActiveTab(newState); + if (Option.isSome(activeTabOption)) { + const url = App.getUrl(activeTabOption.value.app); + Effect.runPromise(NavigationServiceLive.navigateToUrl(url)) + .catch(error => console.error("Failed to navigate:", error)); + } + }) + .catch(error => console.error("Failed to set active tab:", error)); } // Tab navigation functions function goToNextTab() { - const currentIndex = tabs.findIndex(tab => getTabId(tab) === activeTabId); - if (currentIndex !== -1 && currentIndex < tabs.length - 1) { - setActiveTab(getTabId(tabs[currentIndex + 1])); - } else if (tabs.length > 1) { - // Wrap to first tab - setActiveTab(getTabId(tabs[0])); - } + Effect.runPromise(TabsStateNS.nextTab(tabsState)) + .then(newState => { + tabsState = newState; + const activeTabOption = TabsStateNS.getActiveTab(newState); + if (Option.isSome(activeTabOption)) { + const url = App.getUrl(activeTabOption.value.app); + Effect.runPromise(NavigationServiceLive.navigateToUrl(url)) + .catch(error => console.error("Failed to navigate:", error)); + } + }) + .catch(error => console.error("Failed to navigate to next tab:", error)); } function goToPreviousTab() { - const currentIndex = tabs.findIndex(tab => getTabId(tab) === activeTabId); - if (currentIndex > 0) { - setActiveTab(getTabId(tabs[currentIndex - 1])); - } else if (tabs.length > 1) { - // Wrap to last tab - setActiveTab(getTabId(tabs[tabs.length - 1])); - } + Effect.runPromise(TabsStateNS.previousTab(tabsState)) + .then(newState => { + tabsState = newState; + const activeTabOption = TabsStateNS.getActiveTab(newState); + if (Option.isSome(activeTabOption)) { + const url = App.getUrl(activeTabOption.value.app); + Effect.runPromise(NavigationServiceLive.navigateToUrl(url)) + .catch(error => console.error("Failed to navigate:", error)); + } + }) + .catch(error => console.error("Failed to navigate to previous tab:", error)); } // Handle keyboard shortcuts @@ -166,78 +168,93 @@ goToPreviousTab(); } else if (event.key === 'w') { event.preventDefault(); - removeTab(activeTabId); + removeTab(tabsState.activeTabId); } } // Handle app choice in ChooseApp tabs async function handleAppChoice(appType: "bible" | "about" | "stopwatch") { - try { - const tabIndex = tabs.findIndex(tab => getTabId(tab) === activeTabId); - if (tabIndex === -1) return; - - let newTab: Tab; - if (appType === "bible") { - const canonState = await Effect.runPromise(ResponsiveServiceLive.getInitialCanonState()); - newTab = await Effect.runPromise( - createBibleTab(activeTabId, BibleBook.John, 1, translation, canonState) - ); - } else if (appType === "about") { - newTab = await Effect.runPromise(createAboutTab(activeTabId)); - } else if (appType === "stopwatch") { - newTab = await Effect.runPromise(createStopwatchTab(activeTabId)); - } else { - throw new Error(`Unknown app type: ${appType}`); - } - - tabs = tabs.map((tab, index) => index === tabIndex ? newTab : tab); - - // Update URL using the mapping function - const url = getTabUrl(newTab); - Effect.runPromise(NavigationServiceLive.navigateToUrl(url)); - } catch (error) { - console.error("Failed to handle app choice:", error); + const tabIndex = tabsState.tabs.findIndex(tab => tab.id === tabsState.activeTabId); + if (tabIndex === -1) return; + + let newTab: TabState; + if (appType === "bible") { + const canonState = await Effect.runPromise(ResponsiveServiceLive.getInitialCanonState()); + newTab = await Effect.runPromise( + createTab({ + app: "Bible", + id: tabsState.activeTabId, + book: BibleBook.John, + chapter: 1, + translation, + showCanonExplorer: canonState + }) + ); + } else if (appType === "about") { + newTab = await Effect.runPromise(createTab({ app: "About", id: tabsState.activeTabId })); + } else if (appType === "stopwatch") { + newTab = await Effect.runPromise(createTab({ app: "Stopwatch", id: tabsState.activeTabId })); + } else { + console.error(`Unknown app type: ${appType}`); + return; } + + tabsState.tabs = tabsState.tabs.map((tab, index) => index === tabIndex ? newTab : tab); + + // Update URL using the mapping function + const url = App.getUrl(newTab.app); + Effect.runPromise(NavigationServiceLive.navigateToUrl(url)) + .catch(error => console.error("Failed to navigate:", error)); } // Handle browser navigation async function syncFromURL() { - try { - const currentPage = $page; - const params = currentPage.params; - if (params.book && params.chapter) { - const urlState = await Effect.runPromise( - NavigationServiceLive.parseURL(currentPage.url.pathname) - ); - - if (urlState && activeTab && activeTab.app._tag === "Bible") { - const currentBook = activeTab.app.bibleState.currentBook; - const currentChapter = activeTab.app.bibleState.currentChapter; - - if (currentBook !== urlState.book || currentChapter !== urlState.chapter) { - const updatedTab = await Effect.runPromise( - createBibleTab( - activeTab.id, - urlState.book, - urlState.chapter, - activeTab.app.bibleState.translation, - activeTab.app.bibleState.showCanonExplorer - ) - ); - updateTabState(updatedTab); - } - } - } - } catch (error) { - console.error("Failed to sync from URL:", error); - } + const currentPage = $page; + const params = currentPage.params; + + if (!params.book || !params.chapter) return; + + const urlStateOption = await Effect.runPromise( + NavigationServiceLive.parseURL(currentPage.url.pathname) + ).catch(error => { + console.error("Failed to parse URL:", error); + return Option.none(); + }); + + if (Option.isNone(urlStateOption)) return; + if (Option.isNone(activeTabOption)) return; + + const activeTab = activeTabOption.value; + if (activeTab.app._tag !== "Bible") return; + + const urlState = urlStateOption.value; + const currentBook = activeTab.app.bibleState.currentBook; + const currentChapter = activeTab.app.bibleState.currentChapter; + + if (currentBook === urlState.book && currentChapter === urlState.chapter) return; + + const updatedTab = await Effect.runPromise( + createTab({ + app: "Bible", + id: activeTab.id, + book: urlState.book, + chapter: urlState.chapter, + translation: activeTab.app.bibleState.translation, + showCanonExplorer: activeTab.app.bibleState.showCanonExplorer + }) + ).catch(error => { + console.error("Failed to create updated tab:", error); + return activeTab; // Return unchanged on error + }); + + updateTabState(updatedTab); } // Initialize on mount - onMount(async () => { - await initializeTab(); - await syncFromURL(); - + onMount(() => { + // Start async initialization (don't await in onMount) + initializeTab().then(() => syncFromURL()); + // Add global keyboard event listener if (typeof window !== 'undefined') { window.addEventListener('keydown', handleGlobalKeydown); @@ -253,18 +270,17 @@
- - diff --git a/site/src/lib/components/bible/BibleChapterViewer.svelte b/site/src/lib/components/bible/BibleChapterViewer.svelte index fb6ebdc..4087253 100644 --- a/site/src/lib/components/bible/BibleChapterViewer.svelte +++ b/site/src/lib/components/bible/BibleChapterViewer.svelte @@ -1,23 +1,49 @@ -
+
{#if Option.isSome(chapterData)} + +
+
-
-

{Option.getOrThrow(chapterData).name}

-
-
- + + {#if showBookHeader && chapterRef} +
+

{getDisplayName(chapterRef.book)}

+
+
+ {/if} + + + {#if chapterRef} +
+

— {chapterRef.chapter} —

+
+ {/if} + + +
+
{#each Option.getOrThrow(chapterData).verses as verse}
@@ -32,6 +58,9 @@
+ + +
{:else}
diff --git a/site/src/lib/components/bible/BibleReader.svelte b/site/src/lib/components/bible/BibleReader.svelte index 91cad90..b47e860 100644 --- a/site/src/lib/components/bible/BibleReader.svelte +++ b/site/src/lib/components/bible/BibleReader.svelte @@ -1,12 +1,10 @@ + +
+ {#if renderedChaptersArray.length > 0} + {#each renderedChaptersArray as chapterData (chapterData.ref.key)} + + {/each} + {:else} +
+
+

Loading...

+

Please wait while the content loads.

+
+
+ {/if} +
diff --git a/site/src/lib/components/tabs/Tab.svelte b/site/src/lib/components/tabs/Tab.svelte index 2e3243d..6cb5025 100644 --- a/site/src/lib/components/tabs/Tab.svelte +++ b/site/src/lib/components/tabs/Tab.svelte @@ -1,17 +1,19 @@
- {#each tabs as tab (getTabId(tab))} -
- + {#key tab.id} + + {/key}
{/each}
\ No newline at end of file diff --git a/site/src/lib/components/ui/Stopwatch.svelte b/site/src/lib/components/ui/Stopwatch.svelte index 0a235b4..f417dca 100644 --- a/site/src/lib/components/ui/Stopwatch.svelte +++ b/site/src/lib/components/ui/Stopwatch.svelte @@ -1,19 +1,22 @@ -
+
- {#if data.bibleData} - + {#if Option.isSome(data.bibleData)} + {:else}
diff --git a/site/src/routes/+layout.ts b/site/src/routes/+layout.ts index d019e69..8a68e88 100644 --- a/site/src/routes/+layout.ts +++ b/site/src/routes/+layout.ts @@ -1,31 +1,27 @@ -import { Effect } from "effect"; +import { Effect, Option } from "effect"; import { loadBibleData } from "$lib/translations/loadBibleData"; export const prerender = false; export const ssr = false; export async function load() { - try { - console.log('Starting to load bible data...'); - const bibleData = await Effect.runPromise(loadBibleData()); + const bibleDataOption = await Effect.runPromise(loadBibleData()) + .then(bibleData => { + // Check if content is loaded + if (bibleData.content._tag === "Local") { + console.log('Bible data loaded successfully:', bibleData.content.data.books?.length, 'books'); + } else { + console.log('Bible data metadata loaded, content will be fetched from:', bibleData.content.url); + } + return Option.some(bibleData); + }) + .catch(error => { + console.error('Failed to load bible data:', error); + console.error('Error details:', JSON.stringify(error, null, 2)); + return Option.none(); + }); - // Check if content is loaded - if (bibleData.content._tag === "Local") { - console.log('Bible data loaded successfully:', bibleData.content.data.books?.length, 'books'); - } else { - console.log('Bible data metadata loaded, content will be fetched from:', bibleData.content.url); - } - - return { - bibleData - }; - } catch (error) { - console.error('Failed to load bible data:', error); - console.error('Error details:', JSON.stringify(error, null, 2)); - - // Return null to indicate failure - return { - bibleData: null - }; - } + return { + bibleData: bibleDataOption + }; } diff --git a/site/src/routes/[book]/[chapter]/+page.svelte b/site/src/routes/[book]/[chapter]/+page.svelte index ddeedbf..7dd9c98 100644 --- a/site/src/routes/[book]/[chapter]/+page.svelte +++ b/site/src/routes/[book]/[chapter]/+page.svelte @@ -1,5 +1,4 @@