routing added
This commit is contained in:
+155
-16
@@ -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 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 = () => {
|
||||
// Basic routing state
|
||||
const [currentView, setCurrentView] = createSignal("critical");
|
||||
const [lastNonAIView, setLastNonAIView] = createSignal("critical");
|
||||
const [currentView, setCurrentView] = createSignal(DEFAULT_VIEW);
|
||||
const [lastNonAIView, setLastNonAIView] = createSignal(DEFAULT_VIEW);
|
||||
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);
|
||||
const [selectedNoteId, setSelectedNoteId] = createSignal<string | null>(null);
|
||||
|
||||
createEffect(() => {
|
||||
const view = currentView();
|
||||
@@ -43,6 +120,21 @@ const App: Component = () => {
|
||||
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(() => {
|
||||
const userId = authUserId();
|
||||
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(() => {
|
||||
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");
|
||||
navigateToView("help", { sectionId });
|
||||
window.dispatchEvent(new CustomEvent("taskgrid:help-target", { detail: { sectionId } }));
|
||||
};
|
||||
|
||||
const openAIHelp = () => {
|
||||
setStore('isNotepadMode', false);
|
||||
setCurrentView("help_ai");
|
||||
navigateToView("help_ai");
|
||||
};
|
||||
|
||||
const handleMediaChange = (event: MediaQueryListEvent) => {
|
||||
@@ -92,13 +201,39 @@ const App: Component = () => {
|
||||
const handleLocChange = () => {
|
||||
setLocation(window.location.href);
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash === "#help" || hash.startsWith("#help/")) {
|
||||
setCurrentView("help");
|
||||
if (isEmbed()) return;
|
||||
|
||||
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).openTasGridAIHelp = openAIHelp;
|
||||
|
||||
@@ -186,9 +321,13 @@ const App: Component = () => {
|
||||
<Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}>
|
||||
<Layout
|
||||
currentView={effectiveView()}
|
||||
setView={setCurrentView}
|
||||
setView={navigateToView}
|
||||
selectedNoteId={selectedNoteId()}
|
||||
setSelectedNoteId={setSelectedNoteId}
|
||||
onOpenTasks={openTasks}
|
||||
onOpenNotes={openNotes}
|
||||
isAIHelpOpen={currentView() === "help_ai" && isDesktop()}
|
||||
onToggleAIHelp={() => setCurrentView(currentView() === "help_ai" ? lastNonAIView() : "help_ai")}
|
||||
onToggleAIHelp={() => navigateToView(currentView() === "help_ai" ? lastNonAIView() : "help_ai")}
|
||||
aiHelpTip={aiHelpTip()}
|
||||
hideQuickEntry={currentView() === "help_ai" && !isDesktop()}
|
||||
>
|
||||
@@ -200,9 +339,9 @@ const App: Component = () => {
|
||||
<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() === "settings"}><SettingsView setView={navigateToView} /></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>
|
||||
</Layout>
|
||||
</Show>
|
||||
|
||||
+49
-15
@@ -1,6 +1,6 @@
|
||||
import { type Component, type JSX, createSignal, createEffect, Show, lazy, Suspense, onMount, onCleanup } from "solid-js";
|
||||
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";
|
||||
|
||||
const TaskDetail = lazy(() => import("./TaskDetail").then(m => ({ default: m.TaskDetail })));
|
||||
@@ -15,6 +15,10 @@ interface LayoutProps {
|
||||
children: JSX.Element;
|
||||
currentView: string;
|
||||
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;
|
||||
onToggleAIHelp?: () => void;
|
||||
aiHelpTip?: string | null;
|
||||
@@ -25,21 +29,25 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
const [isSidebarLocked, setIsSidebarLocked] = createSignal(true);
|
||||
const [isSidebarPeeking, setIsSidebarPeeking] = 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 [selectedNoteId, setSelectedNoteId] = createSignal<string | null>(null);
|
||||
const setActiveNoteId = (id: string | null) => {
|
||||
props.setSelectedNoteId(id);
|
||||
if (store.isNotepadMode) {
|
||||
props.onOpenNotes(id);
|
||||
}
|
||||
};
|
||||
|
||||
// Make it available globally for QuickEntry auto-tagging without circular deps
|
||||
createEffect(() => {
|
||||
(window as any)._activeNoteId = selectedNoteId();
|
||||
(window as any)._activeNoteId = props.selectedNoteId;
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const handleOpenNote = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (detail && detail.noteId) {
|
||||
setStore('isNotepadMode', true);
|
||||
setSelectedNoteId(detail.noteId);
|
||||
props.onOpenNotes(detail.noteId);
|
||||
}
|
||||
};
|
||||
window.addEventListener('open-note', handleOpenNote);
|
||||
@@ -65,12 +73,38 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const noteId = selectedNoteId();
|
||||
const noteId = props.selectedNoteId;
|
||||
if (!noteId) return;
|
||||
|
||||
const noteExists = store.notes.some(note => note.id === noteId);
|
||||
if (!noteExists) {
|
||||
setSelectedNoteId(null);
|
||||
if (noteExists) {
|
||||
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 (
|
||||
@@ -95,7 +129,7 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
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",
|
||||
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)}
|
||||
onMouseLeave={() => {
|
||||
@@ -158,8 +192,8 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
</Show>
|
||||
</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")}>
|
||||
<NotesSidebar selectedNoteId={selectedNoteId()} setSelectedNoteId={setSelectedNoteId} />
|
||||
<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={props.selectedNoteId} setSelectedNoteId={setActiveNoteId} />
|
||||
</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",
|
||||
!store.isNotepadMode ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
onClick={() => setStore('isNotepadMode', false)}
|
||||
onClick={() => props.onOpenTasks()}
|
||||
>
|
||||
Tasks
|
||||
</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",
|
||||
store.isNotepadMode ? "text-primary-foreground" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
onClick={() => setStore('isNotepadMode', true)}
|
||||
onClick={() => props.onOpenNotes(props.selectedNoteId)}
|
||||
>
|
||||
Notes
|
||||
</button>
|
||||
@@ -253,7 +287,7 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
</div>
|
||||
<div class={cn(!store.isNotepadMode ? "hidden" : "block", "h-[calc(100vh-3.5rem)] md:h-[calc(100vh-8rem)] w-full")}>
|
||||
<Suspense>
|
||||
<NotepadView selectedNoteId={selectedNoteId()} setSelectedNoteId={setSelectedNoteId} />
|
||||
<NotepadView selectedNoteId={props.selectedNoteId} setSelectedNoteId={setActiveNoteId} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+14
-2
@@ -68,8 +68,20 @@ export const HelpView: Component<HelpViewProps> = (props) => {
|
||||
if (!id) return;
|
||||
document.getElementById(`help-${id}`)?.scrollIntoView({ behavior, block: "start" });
|
||||
};
|
||||
const getSectionFromHash = () => window.location.hash.startsWith("#help/") ? decodeURIComponent(window.location.hash.slice("#help/".length)) : undefined;
|
||||
const jumpTo = (id: string) => { if (window.location.hash !== `#help/${id}`) window.location.hash = `#help/${id}`; scrollToSection(id); };
|
||||
const getSectionFromHash = () => {
|
||||
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(() => {
|
||||
const syncToHash = () => window.setTimeout(() => scrollToSection(getSectionFromHash(), "auto"), 50);
|
||||
|
||||
Reference in New Issue
Block a user