Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.direnv
.DS_Store
.claude
result
bible-verify/target
33 changes: 29 additions & 4 deletions site/src/lib/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export const StopwatchStateSchema = Schema.Struct({
isRunning: Schema.Boolean
});

export const WikiStateSchema = Schema.Struct({
page: Schema.String, // The wiki page name (e.g., "Abraham")
showSidebar: Schema.Boolean
});

// App schema - represents the application running in a tab
export const AppSchema = Schema.Union(
Schema.Struct({
Expand All @@ -48,6 +53,10 @@ export const AppSchema = Schema.Union(
Schema.Struct({
_tag: Schema.Literal("Stopwatch"),
stopwatchState: StopwatchStateSchema
}),
Schema.Struct({
_tag: Schema.Literal("Wiki"),
wikiState: WikiStateSchema
})
);

Expand All @@ -74,6 +83,7 @@ type BibleReferenceReadonly = Schema.Schema.Type<typeof BibleReferenceSchema>;
type BibleSelectionReadonly = Schema.Schema.Type<typeof BibleSelectionSchema>;
type BibleStateReadonly = Schema.Schema.Type<typeof BibleStateSchema>;
type StopwatchStateReadonly = Schema.Schema.Type<typeof StopwatchStateSchema>;
type WikiStateReadonly = Schema.Schema.Type<typeof WikiStateSchema>;
type AppReadonly = Schema.Schema.Type<typeof AppSchema>;
type TabStateReadonly = Schema.Schema.Type<typeof TabStateSchema>;
type TabsStateReadonly = Schema.Schema.Type<typeof TabsStateSchema>;
Expand All @@ -82,16 +92,18 @@ export type BibleReference = DeepWritable<BibleReferenceReadonly>;
export type BibleSelection = DeepWritable<BibleSelectionReadonly>;
export type BibleState = DeepWritable<BibleStateReadonly>;
export type StopwatchState = DeepWritable<StopwatchStateReadonly>;
export type WikiState = DeepWritable<WikiStateReadonly>;
export type App = DeepWritable<AppReadonly>;
export type TabState = DeepWritable<TabStateReadonly>;
export type TabsState = DeepWritable<TabsStateReadonly>;

// Maintain backward compatibility with Data constructors
export const BibleState = Data.case<BibleState>();
export const StopwatchState = Data.case<StopwatchState>();
export const WikiState = Data.case<WikiState>();

// App constructors
export const { Bible, About, ChooseApp, Stopwatch, $match } = Data.taggedEnum<App>()
export const { Bible, About, ChooseApp, Stopwatch, Wiki, $match } = Data.taggedEnum<App>()

// Format time as MM:SS for tab title
const formatTimeForTitle = (milliseconds: number): string => {
Expand Down Expand Up @@ -183,7 +195,8 @@ export namespace App {
},
About: () => "/about",
ChooseApp: () => "/",
Stopwatch: () => "/stopwatch"
Stopwatch: () => "/stopwatch",
Wiki: ({ wikiState }) => `/wiki/${wikiState.page.replace(/ /g, '_')}`
});
};

Expand All @@ -202,7 +215,8 @@ export namespace App {
return formatTimeForTitle(stopwatchState.elapsedTime);
}
return "Stopwatch";
}
},
Wiki: ({ wikiState }) => `Wiki: ${wikiState.page.replace(/_/g, ' ')}`
});
};
}
Expand Down Expand Up @@ -296,7 +310,8 @@ export type CreateTabConfig =
| { app: "Bible", id: string, book: BibleBook, chapter: number, verse?: number | null, selection?: BibleSelection | null, translation: Translation, showCanonExplorer: boolean }
| { app: "Stopwatch", id: string }
| { app: "About", id: string }
| { app: "ChooseApp", id: string };
| { app: "ChooseApp", id: string }
| { app: "Wiki", id: string, page: string, showSidebar?: boolean };

// Unified tab creation function - pure data construction
export const createTab = (config: CreateTabConfig): TabState => {
Expand Down Expand Up @@ -335,6 +350,16 @@ export const createTab = (config: CreateTabConfig): TabState => {
id: config.id,
app: ChooseApp()
};
case "Wiki":
return {
id: config.id,
app: Wiki({
wikiState: WikiState({
page: config.page,
showSidebar: config.showSidebar ?? true
})
})
};
}
};

73 changes: 56 additions & 17 deletions site/src/lib/components/AppContainer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
} else if (initialState.isStopwatch) {
// Create Stopwatch tab if URL is /stopwatch
initialTab = createTab({ app: "Stopwatch", id: "tab1" });
} else if (initialState.isWiki && initialState.wikiPage) {
// Create Wiki tab if URL is /wiki/*
initialTab = createTab({ app: "Wiki", id: "tab1", page: initialState.wikiPage });
} else {
// Create Bible tab with parsed book/chapter/verse and selection
const canonState = ResponsiveService.getInitialCanonState();
Expand Down Expand Up @@ -71,10 +74,12 @@
console.log('📝 updateTabState called with:', updatedTab);
tabsState = TabsStateNS.updateTab(tabsState, updatedTab);

// Update URL hash for scroll position
if (updatedTab.id === tabsState.activeTabId && updatedTab.app._tag === "Bible") {
const url = App.getUrl(updatedTab.app);
NavigationService.navigateToUrl(url);
// Update URL hash for scroll position (Bible) or full URL (Wiki)
if (updatedTab.id === tabsState.activeTabId) {
if (updatedTab.app._tag === "Bible" || updatedTab.app._tag === "Wiki") {
const url = App.getUrl(updatedTab.app);
NavigationService.navigateToUrl(url);
}
}
}

Expand All @@ -97,17 +102,29 @@
}

// Remove tab
function removeTab(tabId: string) {
async function removeTab(tabId: string) {
const wasActiveTab = tabId === tabsState.activeTabId;
tabsState = TabsStateNS.removeTab(tabsState, tabId);

// Update URL if we removed the active tab (we switched to a different one)
if (wasActiveTab) {
const activeTabOption = TabsStateNS.getActiveTab(tabsState);
if (Option.isSome(activeTabOption)) {
const url = App.getUrl(activeTabOption.value.app);
isProgrammaticNavigation = true;
await NavigationService.navigateToUrl(url);
isProgrammaticNavigation = false;
}
}
}

// Set active tab
async function setActiveTab(tabId: string) {
tabsState = TabsStateNS.setActiveTab(tabsState, tabId);

// Update URL only for Bible tabs
// Update URL for the active tab
const activeTabOption = TabsStateNS.getActiveTab(tabsState);
if (Option.isSome(activeTabOption) && activeTabOption.value.app._tag === "Bible") {
if (Option.isSome(activeTabOption)) {
const url = App.getUrl(activeTabOption.value.app);
isProgrammaticNavigation = true;
await NavigationService.navigateToUrl(url);
Expand All @@ -119,7 +136,7 @@
async function goToNextTab() {
tabsState = TabsStateNS.nextTab(tabsState);
const activeTabOption = TabsStateNS.getActiveTab(tabsState);
if (Option.isSome(activeTabOption) && activeTabOption.value.app._tag === "Bible") {
if (Option.isSome(activeTabOption)) {
const url = App.getUrl(activeTabOption.value.app);
isProgrammaticNavigation = true;
await NavigationService.navigateToUrl(url);
Expand All @@ -130,7 +147,7 @@
async function goToPreviousTab() {
tabsState = TabsStateNS.previousTab(tabsState);
const activeTabOption = TabsStateNS.getActiveTab(tabsState);
if (Option.isSome(activeTabOption) && activeTabOption.value.app._tag === "Bible") {
if (Option.isSome(activeTabOption)) {
const url = App.getUrl(activeTabOption.value.app);
isProgrammaticNavigation = true;
await NavigationService.navigateToUrl(url);
Expand All @@ -139,7 +156,7 @@
}

// Handle app choice in ChooseApp tabs
async function handleAppChoice(appType: "bible" | "about" | "stopwatch") {
async function handleAppChoice(appType: "bible" | "about" | "stopwatch" | "wiki") {
const tabIndex = tabsState.tabs.findIndex(tab => tab.id === tabsState.activeTabId);
if (tabIndex === -1) return;

Expand All @@ -159,20 +176,41 @@
newTab = createTab({ app: "About", id: tabsState.activeTabId });
} else if (appType === "stopwatch") {
newTab = createTab({ app: "Stopwatch", id: tabsState.activeTabId });
} else if (appType === "wiki") {
newTab = createTab({ app: "Wiki", id: tabsState.activeTabId, page: "Abraham" });
} else {
console.error(`Unknown app type: ${appType}`);
return;
}

tabsState.tabs = tabsState.tabs.map((tab, index) => index === tabIndex ? newTab : tab);

// Update URL only for Bible tabs
if (newTab.app._tag === "Bible") {
const url = App.getUrl(newTab.app);
isProgrammaticNavigation = true;
await NavigationService.navigateToUrl(url);
isProgrammaticNavigation = false;
}
// Update URL for the new app
const url = App.getUrl(newTab.app);
isProgrammaticNavigation = true;
await NavigationService.navigateToUrl(url);
isProgrammaticNavigation = false;
}

// Handle wiki link click from Bible - create new wiki tab
async function handleWikiLinkClick(page: string) {
const newTab = createTab({
app: "Wiki",
id: `tab${tabsState.nextTabId}`,
page
});

tabsState = {
tabs: [...tabsState.tabs, newTab],
activeTabId: newTab.id,
nextTabId: tabsState.nextTabId + 1
};

// Update URL to wiki page
const url = App.getUrl(newTab.app);
isProgrammaticNavigation = true;
await NavigationService.navigateToUrl(url);
isProgrammaticNavigation = false;
}

// Handle browser navigation - ONLY updates selection, NOT scroll position
Expand Down Expand Up @@ -267,5 +305,6 @@
onSelectionChange={updateTabStateWithSelection}
onAppChoice={handleAppChoice}
onTabRemove={removeTab}
onWikiLinkClick={handleWikiLinkClick}
/>
</div>
30 changes: 30 additions & 0 deletions site/src/lib/components/apps/About.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
</script>

<div class="flex items-center justify-center h-full">
<div class="text-center text-gray-300">
<h1 class="text-3xl font-bold mb-4">About Bible Computer</h1>
<p class="text-lg mb-6">
A modern Bible reading application built with SvelteKit and
Effect.
</p>
<div class="flex justify-center gap-6 text-lg">
<a
href="https://github.com/biblecomputer/bible"
target="_blank"
rel="noopener noreferrer"
class="text-blue-400 hover:text-blue-300 underline"
>
GitHub
</a>
<a
href="https://x.com/biblecomputer"
target="_blank"
rel="noopener noreferrer"
class="text-blue-400 hover:text-blue-300 underline"
>
X
</a>
</div>
</div>
</div>
Loading