mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
added tanstack query packages and provider components
This commit is contained in:
13
packages/frontend/src/components/query-provider.tsx
Normal file
13
packages/frontend/src/components/query-provider.tsx
Normal file
@@ -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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
141
packages/frontend/src/components/selection-provider.tsx
Normal file
141
packages/frontend/src/components/selection-provider.tsx
Normal file
@@ -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<SelectionContextValue | null>(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<number | null>(() =>
|
||||
readStoredId("selectedOrganisationId"),
|
||||
);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState<number | null>(() =>
|
||||
readStoredId("selectedProjectId"),
|
||||
);
|
||||
const [selectedIssueId, setSelectedIssueId] = useState<number | null>(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<SelectionContextValue>(
|
||||
() => ({
|
||||
selectedOrganisationId,
|
||||
selectedProjectId,
|
||||
selectedIssueId,
|
||||
initialParams,
|
||||
selectOrganisation,
|
||||
selectProject,
|
||||
selectIssue,
|
||||
}),
|
||||
[
|
||||
selectedOrganisationId,
|
||||
selectedProjectId,
|
||||
selectedIssueId,
|
||||
initialParams,
|
||||
selectOrganisation,
|
||||
selectProject,
|
||||
selectIssue,
|
||||
],
|
||||
);
|
||||
|
||||
return <SelectionContext.Provider value={value}>{children}</SelectionContext.Provider>;
|
||||
}
|
||||
|
||||
export function useSelection() {
|
||||
const context = useContext(SelectionContext);
|
||||
if (!context) {
|
||||
throw new Error("useSelection must be used within SelectionProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -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(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<SessionProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* public routes */}
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="/font" element={<Font />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<QueryProvider>
|
||||
<SessionProvider>
|
||||
<SelectionProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* public routes */}
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="/font" element={<Font />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/* authed routes */}
|
||||
<Route
|
||||
path="/app"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<App />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/test"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Test />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
{/* authed routes */}
|
||||
<Route
|
||||
path="/app"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<App />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/test"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Test />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<Toaster visibleToasts={1} duration={2000} />
|
||||
</SessionProvider>
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<Toaster visibleToasts={1} duration={2000} />
|
||||
</SelectionProvider>
|
||||
</SessionProvider>
|
||||
</QueryProvider>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user