@@ -172,9 +184,9 @@ export default function Tags() {
updateListParams({ search: value, page: 1 })}
ordering={ordering}
- setOrdering={setOrdering}
+ setOrdering={(value) => updateListParams({ ordering: value, page: 1 })}
orderingOptions={orderingOptions}
searchPlaceholder={t.tags?.searchPlaceholder || "Search tags..."}
/>
@@ -238,9 +250,12 @@ export default function Tags() {
})}
{tags.length === 0 && (
-
-
-
{t.tags?.emptyState || "No tags found"}
+
+
+
{t.tags?.emptyState || "No tags found"}
+
+ {searchQuery ? t.tags?.noTagsSearch : t.tags?.emptyState}
+
)}
@@ -249,8 +264,8 @@ export default function Tags() {
currentPage={currentPage}
totalCount={totalItems}
limit={limit}
- onPageChange={setCurrentPage}
- onLimitChange={setLimit}
+ onPageChange={(page) => updateListParams({ page })}
+ onLimitChange={(pageLimit) => updateListParams({ limit: pageLimit, page: 1 })}
/>
)}
diff --git a/src/pages/Timesheet.tsx b/src/pages/Timesheet.tsx
index 45b6b4b..07f92f2 100644
--- a/src/pages/Timesheet.tsx
+++ b/src/pages/Timesheet.tsx
@@ -1,5 +1,6 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { createPortal } from "react-dom";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { createPortal } from "react-dom";
+import { useSearchParams } from "react-router-dom";
import { CalendarDays, Check, ChevronDown, Clock3, DollarSign, MoreVertical, Pencil, Play, Plus, Search, Square, Tag as TagIcon, Trash2 } from "lucide-react";
import { toast } from "sonner";
@@ -25,6 +26,11 @@ import { Input } from "../components/ui/input";
import { SearchableSelect } from "../components/ui/SearchableSelect";
import { useWorkspace } from "../context/WorkspaceContext";
import { useTranslation } from "../hooks/useTranslation";
+import {
+ readArrayParam,
+ readStringParam,
+ updateQueryParams,
+} from "../lib/queryParams";
type EntryModalMode = "manual" | "edit" | null;
@@ -2009,9 +2015,10 @@ function TimesheetSkeleton({ loadingLabel }: { loadingLabel: string }) {
}
export default function Timesheet() {
- const { t, lang } = useTranslation();
- const { activeWorkspace } = useWorkspace();
- const isRtl = lang === "fa";
+ const { t, lang } = useTranslation();
+ const { activeWorkspace } = useWorkspace();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const isRtl = lang === "fa";
const extendedTimesheet = (t.timesheet as {
deleteTitle?: string;
deleteConfirmMessage?: string;
@@ -2047,10 +2054,19 @@ export default function Timesheet() {
const [activeRunningEntry, setActiveRunningEntry] = useState
(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
- const [searchQuery, setSearchQuery] = useState("");
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState("");
- const [filters, setFilters] = useState(DEFAULT_ENTRY_FILTERS);
- const [hasMoreHistory, setHasMoreHistory] = useState(false);
+ const searchQuery = readStringParam(searchParams, "search", "");
+ const filters = useMemo(
+ () => ({
+ projectId: readStringParam(searchParams, "project", DEFAULT_ENTRY_FILTERS.projectId),
+ clientId: readStringParam(searchParams, "client", DEFAULT_ENTRY_FILTERS.clientId),
+ tagIds: readArrayParam(searchParams, "tags"),
+ startedAfter: readStringParam(searchParams, "from", DEFAULT_ENTRY_FILTERS.startedAfter),
+ startedBefore: readStringParam(searchParams, "to", DEFAULT_ENTRY_FILTERS.startedBefore),
+ }),
+ [searchParams],
+ );
+ const [hasMoreHistory, setHasMoreHistory] = useState(false);
const [nextOffset, setNextOffset] = useState(0);
const [limit] = useState(20);
const [ticker, setTicker] = useState(Date.now());
@@ -2134,9 +2150,6 @@ export default function Timesheet() {
}, [activeWorkspace?.id, t.timesheet?.optionsError]);
useEffect(() => {
- setSearchQuery("");
- setDebouncedSearchQuery("");
- setFilters(DEFAULT_ENTRY_FILTERS);
setGroupedHistory([]);
setNextOffset(0);
setHasMoreHistory(false);
@@ -2163,10 +2176,14 @@ export default function Timesheet() {
(project) => project.id === filters.projectId && project.client?.id === filters.clientId,
);
- if (!projectStillMatchesClient) {
- setFilters((current) => ({ ...current, projectId: "" }));
- }
- }, [filters.clientId, filters.projectId, projects]);
+ if (!projectStillMatchesClient) {
+ setSearchParams(
+ (current) =>
+ updateQueryParams(current, { project: "" }, { project: "" }),
+ { replace: true },
+ );
+ }
+ }, [filters.clientId, filters.projectId, projects, setSearchParams]);
const loadHistory = useCallback(async ({ offset = 0, append = false }: { offset?: number; append?: boolean } = {}) => {
if (!activeWorkspace?.id) return;
@@ -2523,14 +2540,53 @@ export default function Timesheet() {
}, []);
const handleApplyFilters = useCallback((nextFilters: TimeEntryFilters) => {
- setFilters(nextFilters);
- }, []);
+ setSearchParams(
+ (current) =>
+ updateQueryParams(
+ current,
+ {
+ project: nextFilters.projectId,
+ client: nextFilters.clientId,
+ tags: nextFilters.tagIds,
+ from: nextFilters.startedAfter,
+ to: nextFilters.startedBefore,
+ },
+ {
+ project: "",
+ client: "",
+ from: "",
+ to: "",
+ },
+ ),
+ { replace: true },
+ );
+ }, [setSearchParams]);
const handleClearFilters = useCallback(() => {
- setSearchQuery("");
+ setSearchParams(
+ (current) =>
+ updateQueryParams(
+ current,
+ {
+ search: "",
+ project: "",
+ client: "",
+ tags: [],
+ from: "",
+ to: "",
+ },
+ {
+ search: "",
+ project: "",
+ client: "",
+ from: "",
+ to: "",
+ },
+ ),
+ { replace: true },
+ );
setDebouncedSearchQuery("");
- setFilters(DEFAULT_ENTRY_FILTERS);
- }, []);
+ }, [setSearchParams]);
const handleLoadMore = useCallback(() => {
if (!hasMoreHistory || nextOffset === null || isLoadingMore) return;
@@ -2789,7 +2845,13 @@ export default function Timesheet() {
+ setSearchParams(
+ (current) =>
+ updateQueryParams(current, { search: value }, { search: "" }),
+ { replace: true },
+ )
+ }
onApply={handleApplyFilters}
onClearFilters={handleClearFilters}
projects={projects}
diff --git a/src/pages/Workspaces.tsx b/src/pages/Workspaces.tsx
index 67142b5..f3c673b 100644
--- a/src/pages/Workspaces.tsx
+++ b/src/pages/Workspaces.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { Plus, Trash2, Pencil, Eye } from 'lucide-react';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import { Plus, Trash2, Pencil, Eye, LayoutDashboard } from 'lucide-react';
import { toast } from 'sonner';
import { fetchWorkspaces, deleteWorkspace, type Workspace } from '../api/workspaces';
import { useTranslation } from '../hooks/useTranslation';
@@ -17,6 +17,7 @@ import { Input } from '../components/ui/input';
import { Card, CardContent, CardTitle } from '../components/ui/card';
import { Pagination } from '../components/Pagination';
import { Modal } from '../components/Modal';
+import { readNumberParam, readStringParam, updateQueryParams } from '../lib/queryParams';
const RoleBadge = ({ role }: { role?: WorkspaceRole }) => {
const { t } = useTranslation();
@@ -39,18 +40,18 @@ const RoleBadge = ({ role }: { role?: WorkspaceRole }) => {
export default function Workspaces() {
const [workspaces, setWorkspaces] = useState([]);
const [isLoading, setIsLoading] = useState(true);
- const [searchQuery, setSearchQuery] = useState('');
- const [ordering, setOrdering] = useState('-created_at');
-
- const [currentPage, setCurrentPage] = useState(1);
const [totalItems, setTotalItems] = useState(0);
- const [limit, setLimit] = useState(10);
const [deleteModal, setDeleteModal] = useState<{isOpen: boolean; workspace: Workspace | null}>({isOpen: false, workspace: null});
const [deleteInput, setDeleteInput] = useState('');
const navigate = useNavigate();
+ const [searchParams, setSearchParams] = useSearchParams();
const { t } = useTranslation();
+ const searchQuery = readStringParam(searchParams, 'search', '');
+ const ordering = readStringParam(searchParams, 'ordering', '-created_at');
+ const currentPage = Math.max(1, readNumberParam(searchParams, 'page', 1));
+ const limit = Math.max(1, readNumberParam(searchParams, 'limit', 10));
const orderingOptions = [
{ value: '-created_at', label: t.ordering?.createdAtDesc || 'Newest First' },
@@ -60,10 +61,6 @@ export default function Workspaces() {
{ value: '-updated_at', label: t.ordering?.updatedAtDesc || 'Recently Updated' },
];
- useEffect(() => {
- setCurrentPage(1);
- }, [searchQuery, ordering]);
-
useEffect(() => {
const timer = setTimeout(() => {
loadWorkspaces();
@@ -116,6 +113,19 @@ export default function Workspaces() {
}
};
+ const updateListParams = (updates: Record) => {
+ setSearchParams(
+ (current) =>
+ updateQueryParams(current, updates, {
+ search: '',
+ ordering: '-created_at',
+ page: 1,
+ limit: 10,
+ }),
+ { replace: true },
+ );
+ };
+
return (
@@ -139,9 +149,9 @@ export default function Workspaces() {
updateListParams({ search: value, page: 1 })}
ordering={ordering}
- setOrdering={setOrdering}
+ setOrdering={(value) => updateListParams({ ordering: value, page: 1 })}
orderingOptions={orderingOptions}
searchPlaceholder={t.workspace?.searchPlaceholder || 'Search...'}
/>
@@ -220,10 +230,12 @@ export default function Workspaces() {
})}
{workspaces.length === 0 && (
-
-
-
{t.workspace?.emptyState || 'No workspaces found'}
-
+
+
+
{t.workspace.noWorkspace}
+
+ {searchQuery ? t.workspace.noWorkspaceSearch : t.workspace?.emptyState}
+
)}
@@ -232,8 +244,8 @@ export default function Workspaces() {
currentPage={currentPage}
totalCount={totalItems}
limit={limit}
- onPageChange={setCurrentPage}
- onLimitChange={setLimit}
+ onPageChange={(page) => updateListParams({ page })}
+ onLimitChange={(pageLimit) => updateListParams({ limit: pageLimit, page: 1 })}
pageSizeOptions={[10, 20, 50]}
/>