feat(improvement): add pagination to endpoints and pages + sync navbar when data changes

This commit is contained in:
2026-03-13 10:30:27 +08:00
parent a9ebbf6a4a
commit 56404792c6
14 changed files with 543 additions and 210 deletions

View File

@@ -1,30 +1,49 @@
import { API_BASE_URL } from "../config/constants";
import { API_BASE_URL } from "../config/constants"
export const authFetch = async (endpoint: string, options: RequestInit = {}) => {
const token = localStorage.getItem("accessToken");
const isFormData = options.body instanceof FormData;
const token = localStorage.getItem("accessToken")
const isFormData = options.body instanceof FormData
const headers: HeadersInit = {
...(!isFormData && { "Content-Type": "application/json" }),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
};
}
// Safely join URLs preventing double slashes (e.g., "http://api.com//api/..." -> "http://api.com/api/...")
const cleanBaseUrl = API_BASE_URL.replace(/\/+$/, "");
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
const url = `${cleanBaseUrl}${cleanEndpoint}`;
const cleanBaseUrl = API_BASE_URL.replace(/\/+$/, "")
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`
const url = `${cleanBaseUrl}${cleanEndpoint}`
const response = await fetch(url, {
...options,
headers,
});
})
if (response.status === 401) {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
window.location.href = "/auth";
localStorage.removeItem("accessToken")
localStorage.removeItem("refreshToken")
window.location.href = "/auth"
return response
}
return response;
};
const originalJson = response.json.bind(response)
response.json = async () => {
const data = await originalJson()
if (data && typeof data === "object" && "items" in data && "pages_count" in data) {
return {
count: data.total_items || 0,
results: data.items || [],
_meta: {
pages_count: data.pages_count,
items_per_page: data.items_per_page,
current_page: data.current_page
}
}
}
return data
}
return response
}

View File

@@ -1,9 +1,17 @@
import { authFetch } from "./client";
import { type PaginatedClientList } from "../types/client";
export const getClients = async (workspaceId: string, search: string = "", ordering: string = "") => {
const queryParams = new URLSearchParams({ workspace: workspaceId });
export const getClients = async (
workspaceId: string,
search: string = "",
ordering: string = "",
limit: number = 10,
offset: number = 0
) => {
const queryParams = new URLSearchParams({
workspace: workspaceId,
limit: limit.toString(),
offset: offset.toString()
});
if (search) queryParams.append("search", search);
if (ordering) queryParams.append("ordering", ordering);
@@ -12,7 +20,7 @@ export const getClients = async (workspaceId: string, search: string = "", order
if (!response.ok) {
throw new Error("Failed to fetch clients");
}
return response.json();
return response.json();
};
export const createClient = async (workspaceId: string, data: { name: string; notes: string }) => {
@@ -54,7 +62,6 @@ export const deleteClient = async (id: string) => {
throw new Error(errorData?.detail || errorData?.message || "Failed to delete client");
}
// DELETE requests often return 204 No Content, which throws an error on .json()
if (response.status === 204) {
return { success: true };
}

View File

@@ -1,4 +1,3 @@
// src/api/workspaces.ts
import { authFetch } from "./client";
export interface Workspace {
@@ -10,7 +9,31 @@ export interface Workspace {
[key: string]: any;
}
export const fetchWorkspaces = async (params?: Record<string, string>): Promise<Workspace[]> => {
export interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
}
export interface WorkspaceMembership {
id: string;
workspace: string;
user: {
id: string;
email: string;
first_name?: string;
last_name?: string;
[key: string]: any;
};
role: 'owner' | 'admin' | 'member' | 'guest';
is_active: boolean;
joined_at?: string;
[key: string]: any;
}
export const fetchWorkspaces = async (params?: Record<string, string>): Promise<PaginatedResponse<Workspace>> => {
const query = params ? new URLSearchParams(params).toString() : '';
const url = `/api/workspaces/${query ? `?${query}` : ''}`;
const response = await authFetch(url);
@@ -20,7 +43,17 @@ export const fetchWorkspaces = async (params?: Record<string, string>): Promise<
}
const data = await response.json();
return data.results || data;
if (Array.isArray(data)) {
return { count: data.length, next: null, previous: null, results: data };
}
return {
count: data.count || data.results?.length || 0,
next: data.next || null,
previous: data.previous || null,
results: data.results || data
};
};
export const getWorkspace = async (id: string): Promise<Workspace> => {
@@ -65,12 +98,24 @@ export const deleteWorkspace = async (id: string): Promise<void> => {
}
};
export const fetchWorkspaceMemberships = async (workspaceId: string) => {
const response = await authFetch(`/api/workspace-memberships/?workspace=${workspaceId}`);
export const fetchWorkspaceMemberships = async (params?: Record<string, string>): Promise<PaginatedResponse<WorkspaceMembership>> => {
const queryParams = new URLSearchParams((params || {}));
const response = await authFetch(`/api/workspace-memberships/?${queryParams.toString()}`);
if (!response.ok) throw new Error("Failed to fetch workspace memberships");
const data = await response.json();
return data.results || data;
if (Array.isArray(data)) {
return { count: data.length, next: null, previous: null, results: data };
}
return {
count: data.count || data.results?.length || 0,
next: data.next || null,
previous: data.previous || null,
results: data.results || data
};
};
export const addWorkspaceMembership = async (data: { workspace: string; user: string; role: string }) => {