routing added
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m12s

This commit is contained in:
2026-03-26 11:06:40 -05:00
parent 613a2c72f5
commit e0e8f4e7e8
3 changed files with 218 additions and 33 deletions
+155 -16
View File
@@ -21,16 +21,93 @@ const AIHelpView = lazy(() => import('./views/AIHelpView').then(m => ({ default:
const NotepadView = lazy(() => import('./views/NotepadView').then(m => ({ default: m.NotepadView }))); const NotepadView = lazy(() => import('./views/NotepadView').then(m => ({ default: m.NotepadView })));
const QuickEntryForm = lazy(() => import('./components/QuickEntryForm').then(m => ({ default: m.QuickEntryForm }))); const QuickEntryForm = lazy(() => import('./components/QuickEntryForm').then(m => ({ default: m.QuickEntryForm })));
const DEFAULT_VIEW = "critical";
const routeToView: Record<string, string> = {
critical: "critical",
focus: "critical",
urgency: "urgency",
priority: "priority",
matrix: "matrix",
snowball: "snowball",
"dig-in": "dig_in",
"dig_in": "dig_in",
progress: "progress",
settings: "settings",
help: "help",
ask: "help_ai",
"help-ai": "help_ai",
"help_ai": "help_ai",
};
const viewToRoute: Record<string, string> = {
critical: "focus",
urgency: "urgency",
priority: "priority",
matrix: "matrix",
snowball: "snowball",
dig_in: "dig-in",
progress: "progress",
settings: "settings",
help: "help",
help_ai: "ask",
};
const normalizeHash = (hash: string) => {
if (hash === "#help" || hash.startsWith("#help/")) {
return `#/${hash.slice(1)}`;
}
return hash;
};
const parseHashRoute = (hash: string): { view: string; sectionId?: string } | null => {
const normalizedHash = normalizeHash(hash);
if (!normalizedHash.startsWith("#/")) return null;
const routePath = normalizedHash.slice(2);
if (!routePath || routePath.startsWith("embed/")) return null;
const [viewSegment, ...rest] = routePath.split("/");
if (decodeURIComponent(viewSegment) === "notes") {
return {
view: "notes",
sectionId: rest.length > 0 ? decodeURIComponent(rest.join("/")) : undefined,
};
}
const view = routeToView[decodeURIComponent(viewSegment)];
if (!view) return null;
if (view === "help") {
return {
view,
sectionId: rest.length > 0 ? decodeURIComponent(rest.join("/")) : undefined,
};
}
return { view };
};
const getHashForView = (view: string, sectionId?: string) => {
if (view === "notes") {
return sectionId ? `#/notes/${encodeURIComponent(sectionId)}` : "#/notes";
}
const route = viewToRoute[view] || viewToRoute[DEFAULT_VIEW];
if (view === "help" && sectionId) {
return `#/${route}/${encodeURIComponent(sectionId)}`;
}
return `#/${route}`;
};
const App: Component = () => { const App: Component = () => {
// Basic routing state // Basic routing state
const [currentView, setCurrentView] = createSignal("critical"); const [currentView, setCurrentView] = createSignal(DEFAULT_VIEW);
const [lastNonAIView, setLastNonAIView] = createSignal("critical"); const [lastNonAIView, setLastNonAIView] = createSignal(DEFAULT_VIEW);
const [isAuthenticated, setIsAuthenticated] = createSignal(pb.authStore.isValid); const [isAuthenticated, setIsAuthenticated] = createSignal(pb.authStore.isValid);
const [authUserId, setAuthUserId] = createSignal<string | null>(pb.authStore.model?.id || null); const [authUserId, setAuthUserId] = createSignal<string | null>(pb.authStore.model?.id || null);
const [location, setLocation] = createSignal(window.location.href); const [location, setLocation] = createSignal(window.location.href);
const [isDesktop, setIsDesktop] = createSignal(window.matchMedia("(min-width: 768px)").matches); const [isDesktop, setIsDesktop] = createSignal(window.matchMedia("(min-width: 768px)").matches);
const [aiHelpTip, setAiHelpTip] = createSignal<string | null>(null); const [aiHelpTip, setAiHelpTip] = createSignal<string | null>(null);
const [tipGeneratedForUserId, setTipGeneratedForUserId] = createSignal<string | null>(null); const [tipGeneratedForUserId, setTipGeneratedForUserId] = createSignal<string | null>(null);
const [selectedNoteId, setSelectedNoteId] = createSignal<string | null>(null);
createEffect(() => { createEffect(() => {
const view = currentView(); const view = currentView();
@@ -43,6 +120,21 @@ const App: Component = () => {
currentView() === "help_ai" && isDesktop() ? lastNonAIView() : currentView() currentView() === "help_ai" && isDesktop() ? lastNonAIView() : currentView()
); );
const syncNotesRoute = (noteId?: string | null, options?: { replace?: boolean }) => {
syncUrlToView("notes", { sectionId: noteId || undefined, replace: options?.replace });
};
const openNotes = (noteId?: string | null, options?: { replace?: boolean }) => {
setStore('isNotepadMode', true);
setSelectedNoteId(noteId || null);
syncNotesRoute(noteId, options);
};
const openTasks = (view?: string, options?: { replace?: boolean }) => {
setStore('isNotepadMode', false);
navigateToView(view || currentView(), options);
};
createEffect(() => { createEffect(() => {
const userId = authUserId(); const userId = authUserId();
const authenticated = isAuthenticated(); const authenticated = isAuthenticated();
@@ -68,21 +160,38 @@ const App: Component = () => {
})(); })();
}); });
const syncUrlToView = (view: string, options?: { sectionId?: string; replace?: boolean }) => {
if (isEmbed()) return;
const nextHash = getHashForView(view, options?.sectionId);
if (window.location.hash === nextHash) return;
if (options?.replace) {
const nextUrl = `${window.location.pathname}${window.location.search}${nextHash}`;
window.history.replaceState(window.history.state, "", nextUrl);
window.dispatchEvent(new HashChangeEvent("hashchange"));
return;
}
window.location.hash = nextHash;
};
const navigateToView = (view: string, options?: { sectionId?: string; replace?: boolean }) => {
setCurrentView(view);
syncUrlToView(view, options);
};
onMount(() => { onMount(() => {
const mediaQuery = window.matchMedia("(min-width: 768px)"); const mediaQuery = window.matchMedia("(min-width: 768px)");
const openHelpGuide = (sectionId?: string) => { const openHelpGuide = (sectionId?: string) => {
const nextHash = sectionId ? `#help/${sectionId}` : "#help";
if (window.location.hash !== nextHash) {
window.location.hash = nextHash;
}
setStore('isNotepadMode', false); setStore('isNotepadMode', false);
setCurrentView("help"); navigateToView("help", { sectionId });
window.dispatchEvent(new CustomEvent("taskgrid:help-target", { detail: { sectionId } })); window.dispatchEvent(new CustomEvent("taskgrid:help-target", { detail: { sectionId } }));
}; };
const openAIHelp = () => { const openAIHelp = () => {
setStore('isNotepadMode', false); setStore('isNotepadMode', false);
setCurrentView("help_ai"); navigateToView("help_ai");
}; };
const handleMediaChange = (event: MediaQueryListEvent) => { const handleMediaChange = (event: MediaQueryListEvent) => {
@@ -92,13 +201,39 @@ const App: Component = () => {
const handleLocChange = () => { const handleLocChange = () => {
setLocation(window.location.href); setLocation(window.location.href);
const hash = window.location.hash; if (isEmbed()) return;
if (hash === "#help" || hash.startsWith("#help/")) {
setCurrentView("help"); const routeState = parseHashRoute(window.location.hash);
if (routeState) {
if (routeState.view === "notes") {
setStore('isNotepadMode', true);
setSelectedNoteId(routeState.sectionId || null);
return;
}
setStore('isNotepadMode', false);
setCurrentView(routeState.view);
return;
}
const normalizedHash = normalizeHash(window.location.hash);
if (normalizedHash !== window.location.hash) {
const nextUrl = `${window.location.pathname}${window.location.search}${normalizedHash}`;
window.history.replaceState(window.history.state, "", nextUrl);
setLocation(window.location.href);
return;
}
if (!window.location.hash) {
if (store.isNotepadMode) {
syncNotesRoute(selectedNoteId(), { replace: true });
} else {
syncUrlToView(currentView(), { replace: true });
}
} }
}; };
(window as any).setCurrentView = setCurrentView; (window as any).setCurrentView = navigateToView;
(window as any).openTasGridHelp = openHelpGuide; (window as any).openTasGridHelp = openHelpGuide;
(window as any).openTasGridAIHelp = openAIHelp; (window as any).openTasGridAIHelp = openAIHelp;
@@ -186,9 +321,13 @@ const App: Component = () => {
<Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}> <Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}>
<Layout <Layout
currentView={effectiveView()} currentView={effectiveView()}
setView={setCurrentView} setView={navigateToView}
selectedNoteId={selectedNoteId()}
setSelectedNoteId={setSelectedNoteId}
onOpenTasks={openTasks}
onOpenNotes={openNotes}
isAIHelpOpen={currentView() === "help_ai" && isDesktop()} isAIHelpOpen={currentView() === "help_ai" && isDesktop()}
onToggleAIHelp={() => setCurrentView(currentView() === "help_ai" ? lastNonAIView() : "help_ai")} onToggleAIHelp={() => navigateToView(currentView() === "help_ai" ? lastNonAIView() : "help_ai")}
aiHelpTip={aiHelpTip()} aiHelpTip={aiHelpTip()}
hideQuickEntry={currentView() === "help_ai" && !isDesktop()} hideQuickEntry={currentView() === "help_ai" && !isDesktop()}
> >
@@ -200,9 +339,9 @@ const App: Component = () => {
<Show when={effectiveView() === "snowball"}><SnowballView /></Show> <Show when={effectiveView() === "snowball"}><SnowballView /></Show>
<Show when={effectiveView() === "dig_in"}><DigInView /></Show> <Show when={effectiveView() === "dig_in"}><DigInView /></Show>
<Show when={effectiveView() === "progress"}><ProgressView /></Show> <Show when={effectiveView() === "progress"}><ProgressView /></Show>
<Show when={effectiveView() === "settings"}><SettingsView setView={setCurrentView} /></Show> <Show when={effectiveView() === "settings"}><SettingsView setView={navigateToView} /></Show>
<Show when={effectiveView() === "help_ai"}><AIHelpView /></Show> <Show when={effectiveView() === "help_ai"}><AIHelpView /></Show>
<Show when={effectiveView() === "help"}><HelpView onBack={() => setCurrentView("settings")} /></Show> <Show when={effectiveView() === "help"}><HelpView onBack={() => navigateToView("settings")} /></Show>
</Suspense> </Suspense>
</Layout> </Layout>
</Show> </Show>
+49 -15
View File
@@ -1,6 +1,6 @@
import { type Component, type JSX, createSignal, createEffect, Show, lazy, Suspense, onMount, onCleanup } from "solid-js"; import { type Component, type JSX, createSignal, createEffect, Show, lazy, Suspense, onMount, onCleanup } from "solid-js";
import { Sidebar, BottomNav, ContextSwitcher } from "./Navigation"; import { Sidebar, BottomNav, ContextSwitcher } from "./Navigation";
import { store, setStore, activeTaskId, setActiveTaskId, requestImmediateNotesMetadataLoad } from "@/store"; import { store, activeTaskId, setActiveTaskId, requestImmediateNotesMetadataLoad, fetchNoteById, updateStoreWithNote } from "@/store";
import { PanelLeftOpen, PanelLeftClose } from "lucide-solid"; import { PanelLeftOpen, PanelLeftClose } from "lucide-solid";
const TaskDetail = lazy(() => import("./TaskDetail").then(m => ({ default: m.TaskDetail }))); const TaskDetail = lazy(() => import("./TaskDetail").then(m => ({ default: m.TaskDetail })));
@@ -15,6 +15,10 @@ interface LayoutProps {
children: JSX.Element; children: JSX.Element;
currentView: string; currentView: string;
setView: (v: string) => void; setView: (v: string) => void;
selectedNoteId: string | null;
setSelectedNoteId: (id: string | null) => void;
onOpenTasks: (view?: string, options?: { replace?: boolean }) => void;
onOpenNotes: (noteId?: string | null, options?: { replace?: boolean }) => void;
isAIHelpOpen?: boolean; isAIHelpOpen?: boolean;
onToggleAIHelp?: () => void; onToggleAIHelp?: () => void;
aiHelpTip?: string | null; aiHelpTip?: string | null;
@@ -25,21 +29,25 @@ export const Layout: Component<LayoutProps> = (props) => {
const [isSidebarLocked, setIsSidebarLocked] = createSignal(true); const [isSidebarLocked, setIsSidebarLocked] = createSignal(true);
const [isSidebarPeeking, setIsSidebarPeeking] = createSignal(false); const [isSidebarPeeking, setIsSidebarPeeking] = createSignal(false);
const [isDropdownOpen, setIsDropdownOpen] = createSignal(false); const [isDropdownOpen, setIsDropdownOpen] = createSignal(false);
const [resolvingRoutedNoteId, setResolvingRoutedNoteId] = createSignal<string | null>(null);
// Lift selectedNoteId up to Layout so it can be passed to NotesSidebar and NotepadView const setActiveNoteId = (id: string | null) => {
const [selectedNoteId, setSelectedNoteId] = createSignal<string | null>(null); props.setSelectedNoteId(id);
if (store.isNotepadMode) {
props.onOpenNotes(id);
}
};
// Make it available globally for QuickEntry auto-tagging without circular deps // Make it available globally for QuickEntry auto-tagging without circular deps
createEffect(() => { createEffect(() => {
(window as any)._activeNoteId = selectedNoteId(); (window as any)._activeNoteId = props.selectedNoteId;
}); });
onMount(() => { onMount(() => {
const handleOpenNote = (e: Event) => { const handleOpenNote = (e: Event) => {
const detail = (e as CustomEvent).detail; const detail = (e as CustomEvent).detail;
if (detail && detail.noteId) { if (detail && detail.noteId) {
setStore('isNotepadMode', true); props.onOpenNotes(detail.noteId);
setSelectedNoteId(detail.noteId);
} }
}; };
window.addEventListener('open-note', handleOpenNote); window.addEventListener('open-note', handleOpenNote);
@@ -65,12 +73,38 @@ export const Layout: Component<LayoutProps> = (props) => {
}); });
createEffect(() => { createEffect(() => {
const noteId = selectedNoteId(); const noteId = props.selectedNoteId;
if (!noteId) return; if (!noteId) return;
const noteExists = store.notes.some(note => note.id === noteId); const noteExists = store.notes.some(note => note.id === noteId);
if (!noteExists) { if (noteExists) {
setSelectedNoteId(null); if (resolvingRoutedNoteId() === noteId) {
setResolvingRoutedNoteId(null);
} }
return;
}
if (store.isNotesLoading || !store.hasLoadedNotesMetadata) return;
if (resolvingRoutedNoteId() === noteId) return;
setResolvingRoutedNoteId(noteId);
void (async () => {
try {
const fetched = await fetchNoteById(noteId);
if (fetched) {
updateStoreWithNote(fetched);
return;
}
} catch (error) {
console.error("Failed to resolve routed note", error);
}
if (props.selectedNoteId === noteId) {
setActiveNoteId(null);
props.onOpenNotes(null, { replace: true });
}
setResolvingRoutedNoteId(current => current === noteId ? null : current);
})();
}); });
return ( return (
@@ -95,7 +129,7 @@ export const Layout: Component<LayoutProps> = (props) => {
class={cn( class={cn(
"hidden md:flex flex-col border-r border-border bg-card h-screen fixed left-0 top-0 transition-all duration-200 ease-in-out z-[50] overflow-hidden", "hidden md:flex flex-col border-r border-border bg-card h-screen fixed left-0 top-0 transition-all duration-200 ease-in-out z-[50] overflow-hidden",
isSidebarLocked() ? `${sidebarWidthClass()} translate-x-0` : `${sidebarWidthClass()} -translate-x-full`, isSidebarLocked() ? `${sidebarWidthClass()} translate-x-0` : `${sidebarWidthClass()} -translate-x-full`,
!isSidebarLocked() && (isSidebarPeeking() || (store.isNotepadMode && !selectedNoteId())) && "translate-x-0 shadow-2xl ring-1 ring-border" !isSidebarLocked() && (isSidebarPeeking() || (store.isNotepadMode && !props.selectedNoteId)) && "translate-x-0 shadow-2xl ring-1 ring-border"
)} )}
onMouseEnter={() => !isSidebarLocked() && setIsSidebarPeeking(true)} onMouseEnter={() => !isSidebarLocked() && setIsSidebarPeeking(true)}
onMouseLeave={() => { onMouseLeave={() => {
@@ -158,8 +192,8 @@ export const Layout: Component<LayoutProps> = (props) => {
</Show> </Show>
</div> </div>
<div class={cn("flex-1 overflow-hidden transition-all duration-200", (!isSidebarLocked() && !isSidebarPeeking() && !(store.isNotepadMode && !selectedNoteId())) ? "opacity-0 pointer-events-none scale-95" : "opacity-100 scale-100")}> <div class={cn("flex-1 overflow-hidden transition-all duration-200", (!isSidebarLocked() && !isSidebarPeeking() && !(store.isNotepadMode && !props.selectedNoteId)) ? "opacity-0 pointer-events-none scale-95" : "opacity-100 scale-100")}>
<NotesSidebar selectedNoteId={selectedNoteId()} setSelectedNoteId={setSelectedNoteId} /> <NotesSidebar selectedNoteId={props.selectedNoteId} setSelectedNoteId={setActiveNoteId} />
</div> </div>
</div> </div>
</div> </div>
@@ -208,7 +242,7 @@ export const Layout: Component<LayoutProps> = (props) => {
"flex-1 h-8 text-[0.7rem] font-bold uppercase tracking-widest z-10 transition-colors duration-200 flex items-center justify-center rounded-lg", "flex-1 h-8 text-[0.7rem] font-bold uppercase tracking-widest z-10 transition-colors duration-200 flex items-center justify-center rounded-lg",
!store.isNotepadMode ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground" !store.isNotepadMode ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground"
)} )}
onClick={() => setStore('isNotepadMode', false)} onClick={() => props.onOpenTasks()}
> >
Tasks Tasks
</button> </button>
@@ -217,7 +251,7 @@ export const Layout: Component<LayoutProps> = (props) => {
"flex-1 h-8 text-[0.7rem] font-bold uppercase tracking-widest z-10 transition-colors duration-200 flex items-center justify-center rounded-lg", "flex-1 h-8 text-[0.7rem] font-bold uppercase tracking-widest z-10 transition-colors duration-200 flex items-center justify-center rounded-lg",
store.isNotepadMode ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground" store.isNotepadMode ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground"
)} )}
onClick={() => setStore('isNotepadMode', true)} onClick={() => props.onOpenNotes(props.selectedNoteId)}
> >
Notes Notes
</button> </button>
@@ -253,7 +287,7 @@ export const Layout: Component<LayoutProps> = (props) => {
</div> </div>
<div class={cn(!store.isNotepadMode ? "hidden" : "block", "h-[calc(100vh-3.5rem)] md:h-[calc(100vh-8rem)] w-full")}> <div class={cn(!store.isNotepadMode ? "hidden" : "block", "h-[calc(100vh-3.5rem)] md:h-[calc(100vh-8rem)] w-full")}>
<Suspense> <Suspense>
<NotepadView selectedNoteId={selectedNoteId()} setSelectedNoteId={setSelectedNoteId} /> <NotepadView selectedNoteId={props.selectedNoteId} setSelectedNoteId={setActiveNoteId} />
</Suspense> </Suspense>
</div> </div>
</div> </div>
+14 -2
View File
@@ -68,8 +68,20 @@ export const HelpView: Component<HelpViewProps> = (props) => {
if (!id) return; if (!id) return;
document.getElementById(`help-${id}`)?.scrollIntoView({ behavior, block: "start" }); document.getElementById(`help-${id}`)?.scrollIntoView({ behavior, block: "start" });
}; };
const getSectionFromHash = () => window.location.hash.startsWith("#help/") ? decodeURIComponent(window.location.hash.slice("#help/".length)) : undefined; const getSectionFromHash = () => {
const jumpTo = (id: string) => { if (window.location.hash !== `#help/${id}`) window.location.hash = `#help/${id}`; scrollToSection(id); }; if (window.location.hash.startsWith("#/help/")) {
return decodeURIComponent(window.location.hash.slice("#/help/".length));
}
if (window.location.hash.startsWith("#help/")) {
return decodeURIComponent(window.location.hash.slice("#help/".length));
}
return undefined;
};
const jumpTo = (id: string) => {
const nextHash = `#/help/${encodeURIComponent(id)}`;
if (window.location.hash !== nextHash) window.location.hash = nextHash;
scrollToSection(id);
};
onMount(() => { onMount(() => {
const syncToHash = () => window.setTimeout(() => scrollToSection(getSectionFromHash(), "auto"), 50); const syncToHash = () => window.setTimeout(() => scrollToSection(getSectionFromHash(), "auto"), 50);