diff --git a/bun.lock b/bun.lock
index 4364b90..29a08bb 100644
--- a/bun.lock
+++ b/bun.lock
@@ -50,6 +50,8 @@
"@radix-ui/react-tabs": "^1.1.13",
"@sprint/shared": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
+ "@tanstack/react-query": "^5.90.19",
+ "@tanstack/react-query-devtools": "^5.91.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"class-variance-authority": "^0.7.1",
@@ -424,6 +426,14 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
+ "@tanstack/query-core": ["@tanstack/query-core@5.90.19", "", {}, "sha512-GLW5sjPVIvH491VV1ufddnfldyVB+teCnpPIvweEfkpRx7CfUmUGhoh9cdcUKBh/KwVxk22aNEDxeTsvmyB/WA=="],
+
+ "@tanstack/query-devtools": ["@tanstack/query-devtools@5.92.0", "", {}, "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ=="],
+
+ "@tanstack/react-query": ["@tanstack/react-query@5.90.19", "", { "dependencies": { "@tanstack/query-core": "5.90.19" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-qTZRZ4QyTzQc+M0IzrbKHxSeISUmRB3RPGmao5bT+sI6ayxSRhn0FXEnT5Hg3as8SBFcRosrXXRFB+yAcxVxJQ=="],
+
+ "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
+
"@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="],
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 075c68e..c140708 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -21,6 +21,8 @@
"@radix-ui/react-tabs": "^1.1.13",
"@sprint/shared": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
+ "@tanstack/react-query": "^5.90.19",
+ "@tanstack/react-query-devtools": "^5.91.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"class-variance-authority": "^0.7.1",
diff --git a/packages/frontend/src/components/query-provider.tsx b/packages/frontend/src/components/query-provider.tsx
new file mode 100644
index 0000000..dd57e57
--- /dev/null
+++ b/packages/frontend/src/components/query-provider.tsx
@@ -0,0 +1,13 @@
+import { QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import type { ReactNode } from "react";
+import { queryClient } from "@/lib/query/client";
+
+export function QueryProvider({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/packages/frontend/src/components/selection-provider.tsx b/packages/frontend/src/components/selection-provider.tsx
new file mode 100644
index 0000000..fd04a84
--- /dev/null
+++ b/packages/frontend/src/components/selection-provider.tsx
@@ -0,0 +1,141 @@
+import type { IssueResponse, OrganisationResponse, ProjectResponse } from "@sprint/shared";
+import type { ReactNode } from "react";
+import { createContext, useCallback, useContext, useMemo, useState } from "react";
+
+type SelectionContextValue = {
+ selectedOrganisationId: number | null;
+ selectedProjectId: number | null;
+ selectedIssueId: number | null;
+ initialParams: {
+ orgSlug: string;
+ projectKey: string;
+ issueNumber: number | null;
+ };
+ selectOrganisation: (organisation: OrganisationResponse | null) => void;
+ selectProject: (project: ProjectResponse | null) => void;
+ selectIssue: (issue: IssueResponse | null) => void;
+};
+
+const SelectionContext = createContext(null);
+
+const readStoredId = (key: string) => {
+ const value = localStorage.getItem(key);
+ if (!value) return null;
+ const parsed = Number(value);
+ return Number.isNaN(parsed) ? null : parsed;
+};
+
+const updateUrlParams = (updates: {
+ orgSlug?: string | null;
+ projectKey?: string | null;
+ issueNumber?: number | null;
+}) => {
+ const params = new URLSearchParams(window.location.search);
+
+ if (updates.orgSlug !== undefined) {
+ if (updates.orgSlug) params.set("o", updates.orgSlug);
+ else params.delete("o");
+ }
+
+ if (updates.projectKey !== undefined) {
+ if (updates.projectKey) params.set("p", updates.projectKey);
+ else params.delete("p");
+ }
+
+ if (updates.issueNumber !== undefined) {
+ if (updates.issueNumber != null) params.set("i", `${updates.issueNumber}`);
+ else params.delete("i");
+ }
+
+ const search = params.toString();
+ const nextUrl = `${window.location.pathname}${search ? `?${search}` : ""}`;
+ window.history.replaceState(null, "", nextUrl);
+};
+
+export function SelectionProvider({ children }: { children: ReactNode }) {
+ const initialParams = useMemo(() => {
+ const params = new URLSearchParams(window.location.search);
+ const orgSlug = params.get("o")?.trim().toLowerCase() ?? "";
+ const projectKey = params.get("p")?.trim().toLowerCase() ?? "";
+ const issueParam = params.get("i")?.trim() ?? "";
+ const issueNumber = issueParam === "" ? null : Number.parseInt(issueParam, 10);
+
+ return {
+ orgSlug,
+ projectKey,
+ issueNumber: issueNumber != null && Number.isNaN(issueNumber) ? null : issueNumber,
+ };
+ }, []);
+
+ const [selectedOrganisationId, setSelectedOrganisationId] = useState(() =>
+ readStoredId("selectedOrganisationId"),
+ );
+ const [selectedProjectId, setSelectedProjectId] = useState(() =>
+ readStoredId("selectedProjectId"),
+ );
+ const [selectedIssueId, setSelectedIssueId] = useState(null);
+
+ const selectOrganisation = useCallback((organisation: OrganisationResponse | null) => {
+ const id = organisation?.Organisation.id ?? null;
+ setSelectedOrganisationId(id);
+ setSelectedProjectId(null);
+ setSelectedIssueId(null);
+ if (id != null) localStorage.setItem("selectedOrganisationId", `${id}`);
+ else localStorage.removeItem("selectedOrganisationId");
+ localStorage.removeItem("selectedProjectId");
+ updateUrlParams({
+ orgSlug: organisation?.Organisation.slug.toLowerCase() ?? null,
+ projectKey: null,
+ issueNumber: null,
+ });
+ }, []);
+
+ const selectProject = useCallback((project: ProjectResponse | null) => {
+ const id = project?.Project.id ?? null;
+ setSelectedProjectId(id);
+ setSelectedIssueId(null);
+ if (id != null) localStorage.setItem("selectedProjectId", `${id}`);
+ else localStorage.removeItem("selectedProjectId");
+ updateUrlParams({
+ projectKey: project?.Project.key.toLowerCase() ?? null,
+ issueNumber: null,
+ });
+ }, []);
+
+ const selectIssue = useCallback((issue: IssueResponse | null) => {
+ const id = issue?.Issue.id ?? null;
+ setSelectedIssueId(id);
+ updateUrlParams({ issueNumber: issue?.Issue.number ?? null });
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ selectedOrganisationId,
+ selectedProjectId,
+ selectedIssueId,
+ initialParams,
+ selectOrganisation,
+ selectProject,
+ selectIssue,
+ }),
+ [
+ selectedOrganisationId,
+ selectedProjectId,
+ selectedIssueId,
+ initialParams,
+ selectOrganisation,
+ selectProject,
+ selectIssue,
+ ],
+ );
+
+ return {children};
+}
+
+export function useSelection() {
+ const context = useContext(SelectionContext);
+ if (!context) {
+ throw new Error("useSelection must be used within SelectionProvider");
+ }
+ return context;
+}
diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx
index 748cc16..4bf0492 100644
--- a/packages/frontend/src/main.tsx
+++ b/packages/frontend/src/main.tsx
@@ -2,6 +2,8 @@ import "./App.css";
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router-dom";
+import { QueryProvider } from "@/components/query-provider";
+import { SelectionProvider } from "@/components/selection-provider";
import { RequireAuth, SessionProvider } from "@/components/session-provider";
import { ThemeProvider } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
@@ -15,37 +17,41 @@ import Test from "@/pages/Test";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
-
-
- {/* public routes */}
- } />
- } />
- } />
+
+
+
+
+
+ {/* public routes */}
+ } />
+ } />
+ } />
- {/* authed routes */}
-
-
-
- }
- />
-
-
-
- }
- />
+ {/* authed routes */}
+
+
+
+ }
+ />
+
+
+
+ }
+ />
- } />
-
-
-
-
+ } />
+
+
+
+
+
+
,
);