251 lines
11 KiB
TypeScript
251 lines
11 KiB
TypeScript
import { type Component, createSignal, onMount, onCleanup, createMemo, createEffect, lazy, Suspense, Show, untrack } from 'solid-js';
|
|
import { Layout } from './components/Layout';
|
|
import { EmbedLayout } from './components/EmbedLayout';
|
|
import { EmbedAuthWrapper } from './components/EmbedAuthWrapper';
|
|
import { CriticalView } from './views/CriticalView';
|
|
import { AuthCallback } from './components/Auth';
|
|
import { pb } from './lib/pocketbase';
|
|
import { initStore, setStore, store, advanceAIHelpTipSectionIndex } from './store';
|
|
import { generateAIHelpTip, hasFireworksApiKey } from './lib/ai-help';
|
|
|
|
// const CriticalView = lazy(() => import('./views/CriticalView').then(m => ({ default: m.CriticalView })));
|
|
const UrgencyView = lazy(() => import('./views/UrgencyView').then(m => ({ default: m.UrgencyView })));
|
|
const PriorityView = lazy(() => import('./views/PriorityView').then(m => ({ default: m.PriorityView })));
|
|
const MatrixView = lazy(() => import('./views/MatrixView').then(m => ({ default: m.MatrixView })));
|
|
const SettingsView = lazy(() => import('./views/SettingsView').then(m => ({ default: m.SettingsView })));
|
|
const SnowballView = lazy(() => import('./views/SnowballView').then(m => ({ default: m.SnowballView })));
|
|
const DigInView = lazy(() => import('./views/DigInView').then(m => ({ default: m.DigInView })));
|
|
const ProgressView = lazy(() => import('./views/ProgressView').then(m => ({ default: m.ProgressView })));
|
|
const HelpView = lazy(() => import('./views/HelpView').then(m => ({ default: m.HelpView })));
|
|
const AIHelpView = lazy(() => import('./views/AIHelpView').then(m => ({ default: m.AIHelpView })));
|
|
const NotepadView = lazy(() => import('./views/NotepadView').then(m => ({ default: m.NotepadView })));
|
|
const QuickEntryForm = lazy(() => import('./components/QuickEntryForm').then(m => ({ default: m.QuickEntryForm })));
|
|
|
|
const App: Component = () => {
|
|
// Basic routing state
|
|
const [currentView, setCurrentView] = createSignal("critical");
|
|
const [lastNonAIView, setLastNonAIView] = createSignal("critical");
|
|
const [isAuthenticated, setIsAuthenticated] = createSignal(pb.authStore.isValid);
|
|
const [authUserId, setAuthUserId] = createSignal<string | null>(pb.authStore.model?.id || null);
|
|
const [location, setLocation] = createSignal(window.location.href);
|
|
const [isDesktop, setIsDesktop] = createSignal(window.matchMedia("(min-width: 768px)").matches);
|
|
const [aiHelpTip, setAiHelpTip] = createSignal<string | null>(null);
|
|
const [tipGeneratedForUserId, setTipGeneratedForUserId] = createSignal<string | null>(null);
|
|
|
|
createEffect(() => {
|
|
const view = currentView();
|
|
if (view !== "help_ai") {
|
|
setLastNonAIView(view);
|
|
}
|
|
});
|
|
|
|
const effectiveView = createMemo(() =>
|
|
currentView() === "help_ai" && isDesktop() ? lastNonAIView() : currentView()
|
|
);
|
|
|
|
createEffect(() => {
|
|
const userId = authUserId();
|
|
const authenticated = isAuthenticated();
|
|
const desktop = isDesktop();
|
|
const initializing = store.isInitializing;
|
|
const hasApiKey = hasFireworksApiKey();
|
|
const alreadyGeneratedForUser = tipGeneratedForUserId() === userId;
|
|
const prefsLoadedForUser = !!userId && store.prefId === userId;
|
|
|
|
if (!authenticated || !userId || !desktop || initializing || !prefsLoadedForUser || !hasApiKey || alreadyGeneratedForUser) return;
|
|
|
|
setTipGeneratedForUserId(userId);
|
|
const sectionIndex = untrack(() => store.aiHelpTipSectionIndex);
|
|
void (async () => {
|
|
try {
|
|
const nextTip = await generateAIHelpTip(sectionIndex);
|
|
if (!nextTip) return;
|
|
setAiHelpTip(nextTip);
|
|
await advanceAIHelpTipSectionIndex();
|
|
} catch (err) {
|
|
console.warn("Tip generation skipped", err);
|
|
}
|
|
})();
|
|
});
|
|
|
|
onMount(() => {
|
|
const mediaQuery = window.matchMedia("(min-width: 768px)");
|
|
const openHelpGuide = (sectionId?: string) => {
|
|
const nextHash = sectionId ? `#help/${sectionId}` : "#help";
|
|
if (window.location.hash !== nextHash) {
|
|
window.location.hash = nextHash;
|
|
}
|
|
setStore('isNotepadMode', false);
|
|
setCurrentView("help");
|
|
window.dispatchEvent(new CustomEvent("taskgrid:help-target", { detail: { sectionId } }));
|
|
};
|
|
|
|
const openAIHelp = () => {
|
|
setStore('isNotepadMode', false);
|
|
setCurrentView("help_ai");
|
|
};
|
|
|
|
const handleMediaChange = (event: MediaQueryListEvent) => {
|
|
setIsDesktop(event.matches);
|
|
};
|
|
|
|
const handleLocChange = () => {
|
|
setLocation(window.location.href);
|
|
|
|
const hash = window.location.hash;
|
|
if (hash === "#help" || hash.startsWith("#help/")) {
|
|
setCurrentView("help");
|
|
}
|
|
};
|
|
|
|
(window as any).setCurrentView = setCurrentView;
|
|
(window as any).openTasGridHelp = openHelpGuide;
|
|
(window as any).openTasGridAIHelp = openAIHelp;
|
|
|
|
window.addEventListener('popstate', handleLocChange);
|
|
window.addEventListener('hashchange', handleLocChange);
|
|
mediaQuery.addEventListener('change', handleMediaChange);
|
|
|
|
let isFirstRun = true;
|
|
const removeListener = pb.authStore.onChange((token) => {
|
|
const wasAuthenticated = isAuthenticated();
|
|
const previousUserId = authUserId();
|
|
const nextUserId = token ? (pb.authStore.model?.id || null) : null;
|
|
setIsAuthenticated(!!token);
|
|
setAuthUserId(nextUserId);
|
|
|
|
if (!token || nextUserId !== previousUserId) {
|
|
setTipGeneratedForUserId(null);
|
|
setAiHelpTip(null);
|
|
}
|
|
|
|
if (token && (isFirstRun || !wasAuthenticated)) {
|
|
initStore();
|
|
}
|
|
isFirstRun = false;
|
|
}, true);
|
|
|
|
onCleanup(() => {
|
|
removeListener();
|
|
delete (window as any).setCurrentView;
|
|
delete (window as any).openTasGridHelp;
|
|
delete (window as any).openTasGridAIHelp;
|
|
window.removeEventListener('popstate', handleLocChange);
|
|
window.removeEventListener('hashchange', handleLocChange);
|
|
mediaQuery.removeEventListener('change', handleMediaChange);
|
|
});
|
|
|
|
// Background preload of other views when idle
|
|
const idleCallback = (window as any).requestIdleCallback || ((cb: any) => setTimeout(cb, 1000));
|
|
idleCallback(() => {
|
|
// CriticalView is now static
|
|
import('./views/UrgencyView');
|
|
import('./views/PriorityView');
|
|
import('./views/MatrixView');
|
|
import('./views/SettingsView');
|
|
import('./views/SnowballView');
|
|
import('./views/DigInView');
|
|
import('./views/ProgressView');
|
|
import('./views/HelpView');
|
|
import('./views/AIHelpView');
|
|
|
|
// Remove splash screen after high priority work is done
|
|
const splash = document.getElementById('splash');
|
|
if (splash) {
|
|
splash.style.opacity = '0';
|
|
setTimeout(() => splash.remove(), 500);
|
|
}
|
|
});
|
|
|
|
handleLocChange();
|
|
});
|
|
|
|
// Determine if we are in an embed route (check both path and hash for compatibility)
|
|
const isEmbed = () => {
|
|
const loc = location(); // Reactive dependency
|
|
const url = new URL(loc);
|
|
const path = url.pathname;
|
|
const hash = url.hash;
|
|
return path.startsWith('/embed/') || hash.startsWith('#/embed/');
|
|
};
|
|
|
|
const getEmbedView = () => {
|
|
const loc = location(); // Reactive dependency
|
|
const url = new URL(loc);
|
|
const fullPath = url.pathname + url.hash;
|
|
let view = null;
|
|
if (fullPath.includes('/notes')) view = 'embed_notes';
|
|
else if (fullPath.includes('/quick-add')) view = 'embed_quick_add';
|
|
return view;
|
|
};
|
|
|
|
return (
|
|
<Show
|
|
when={isEmbed()}
|
|
fallback={
|
|
<Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}>
|
|
<Layout
|
|
currentView={effectiveView()}
|
|
setView={setCurrentView}
|
|
isAIHelpOpen={currentView() === "help_ai" && isDesktop()}
|
|
onToggleAIHelp={() => setCurrentView(currentView() === "help_ai" ? lastNonAIView() : "help_ai")}
|
|
aiHelpTip={aiHelpTip()}
|
|
hideQuickEntry={currentView() === "help_ai" && !isDesktop()}
|
|
>
|
|
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
|
<Show when={effectiveView() === "critical"}><CriticalView /></Show>
|
|
<Show when={effectiveView() === "urgency"}><UrgencyView /></Show>
|
|
<Show when={effectiveView() === "priority"}><PriorityView /></Show>
|
|
<Show when={effectiveView() === "matrix"}><MatrixView /></Show>
|
|
<Show when={effectiveView() === "snowball"}><SnowballView /></Show>
|
|
<Show when={effectiveView() === "dig_in"}><DigInView /></Show>
|
|
<Show when={effectiveView() === "progress"}><ProgressView /></Show>
|
|
<Show when={effectiveView() === "settings"}><SettingsView setView={setCurrentView} /></Show>
|
|
<Show when={effectiveView() === "help_ai"}><AIHelpView /></Show>
|
|
<Show when={effectiveView() === "help"}><HelpView onBack={() => setCurrentView("settings")} /></Show>
|
|
</Suspense>
|
|
</Layout>
|
|
</Show>
|
|
}
|
|
>
|
|
{(() => {
|
|
const isPublicNote = () => {
|
|
const view = getEmbedView();
|
|
const urlObj = new URL(location());
|
|
const noteId = new URLSearchParams(urlObj.search).get('noteId') ||
|
|
(urlObj.hash.includes('?') ? new URLSearchParams(urlObj.hash.substring(urlObj.hash.indexOf('?'))).get('noteId') : null);
|
|
return view === 'embed_notes' && !!noteId;
|
|
};
|
|
|
|
return (
|
|
<EmbedAuthWrapper skipAuth={isPublicNote()}>
|
|
<EmbedLayout>
|
|
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
|
<Show when={getEmbedView() === 'embed_notes'}>
|
|
{(() => {
|
|
const noteIdFromUrl = createMemo(() => {
|
|
const urlObj = new URL(location());
|
|
let noteId = new URLSearchParams(urlObj.search).get('noteId');
|
|
if (!noteId && urlObj.hash.includes('?')) {
|
|
const hashQueryString = urlObj.hash.substring(urlObj.hash.indexOf('?'));
|
|
noteId = new URLSearchParams(hashQueryString).get('noteId');
|
|
}
|
|
return noteId;
|
|
});
|
|
return <NotepadView selectedNoteId={noteIdFromUrl()} setSelectedNoteId={() => { }} hideNavigation={!!noteIdFromUrl()} />;
|
|
})()}
|
|
</Show>
|
|
<Show when={getEmbedView() === 'embed_quick_add'}>
|
|
<QuickEntryForm autoFocus={true} />
|
|
</Show>
|
|
</Suspense>
|
|
</EmbedLayout>
|
|
</EmbedAuthWrapper>
|
|
);
|
|
})()}
|
|
</Show>
|
|
);
|
|
};
|
|
|
|
export default App;
|