Initial commit
This commit is contained in:
90
lib/api/client.ts
Normal file
90
lib/api/client.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000/api/v1';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
message: string,
|
||||
public data?: any
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResponse<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
errorData.message || response.statusText,
|
||||
errorData
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${API_URL}${endpoint}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Add auth token if available
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge with options headers
|
||||
if (options.headers) {
|
||||
Object.assign(headers, options.headers);
|
||||
}
|
||||
|
||||
const config: RequestInit = {
|
||||
...options,
|
||||
headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, config);
|
||||
return handleResponse<T>(response);
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(endpoint: string, options?: RequestInit) =>
|
||||
apiRequest<T>(endpoint, { ...options, method: 'GET' }),
|
||||
|
||||
post: <T>(endpoint: string, data?: any, options?: RequestInit) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
put: <T>(endpoint: string, data?: any, options?: RequestInit) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
patch: <T>(endpoint: string, data?: any, options?: RequestInit) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...options,
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
delete: <T>(endpoint: string, options?: RequestInit) =>
|
||||
apiRequest<T>(endpoint, { ...options, method: 'DELETE' }),
|
||||
};
|
||||
29
lib/api/projects.ts
Normal file
29
lib/api/projects.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { api } from './client';
|
||||
import type { Project, CreateProjectDto, UpdateProjectDto } from '@/types/project';
|
||||
import type { KanbanBoard } from '@/types/kanban';
|
||||
|
||||
export const projectsApi = {
|
||||
getAll: async (page = 1, pageSize = 20): Promise<Project[]> => {
|
||||
return api.get(`/projects?page=${page}&pageSize=${pageSize}`);
|
||||
},
|
||||
|
||||
getById: async (id: string): Promise<Project> => {
|
||||
return api.get(`/projects/${id}`);
|
||||
},
|
||||
|
||||
create: async (data: CreateProjectDto): Promise<Project> => {
|
||||
return api.post('/projects', data);
|
||||
},
|
||||
|
||||
update: async (id: string, data: UpdateProjectDto): Promise<Project> => {
|
||||
return api.put(`/projects/${id}`, data);
|
||||
},
|
||||
|
||||
delete: async (id: string): Promise<void> => {
|
||||
return api.delete(`/projects/${id}`);
|
||||
},
|
||||
|
||||
getKanban: async (id: string): Promise<KanbanBoard> => {
|
||||
return api.get(`/projects/${id}/kanban`);
|
||||
},
|
||||
};
|
||||
27
lib/hooks/use-kanban.ts
Normal file
27
lib/hooks/use-kanban.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { projectsApi } from '@/lib/api/projects';
|
||||
import type { KanbanBoard } from '@/types/kanban';
|
||||
import { api } from '@/lib/api/client';
|
||||
|
||||
export function useKanbanBoard(projectId: string) {
|
||||
return useQuery<KanbanBoard>({
|
||||
queryKey: ['projects', projectId, 'kanban'],
|
||||
queryFn: () => projectsApi.getKanban(projectId),
|
||||
enabled: !!projectId,
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateTaskStatus() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ taskId, newStatus }: { taskId: string; newStatus: string }) =>
|
||||
api.patch(`/tasks/${taskId}/status`, { status: newStatus }),
|
||||
onSuccess: (_, { taskId }) => {
|
||||
// Invalidate kanban board queries to refetch
|
||||
queryClient.invalidateQueries({ queryKey: ['kanban'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks', taskId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
79
lib/hooks/use-projects.ts
Normal file
79
lib/hooks/use-projects.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { projectsApi } from '@/lib/api/projects';
|
||||
import type { Project, CreateProjectDto, UpdateProjectDto } from '@/types/project';
|
||||
|
||||
export function useProjects(page = 1, pageSize = 20) {
|
||||
return useQuery<Project[]>({
|
||||
queryKey: ['projects', page, pageSize],
|
||||
queryFn: () => projectsApi.getAll(page, pageSize),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
export function useProject(id: string) {
|
||||
return useQuery<Project>({
|
||||
queryKey: ['projects', id],
|
||||
queryFn: () => projectsApi.getById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateProject() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateProjectDto) => projectsApi.create(data),
|
||||
onSuccess: (newProject) => {
|
||||
// Invalidate and refetch projects list
|
||||
queryClient.invalidateQueries({ queryKey: ['projects'] });
|
||||
|
||||
// Optimistically update cache
|
||||
queryClient.setQueryData<Project[]>(['projects'], (old) =>
|
||||
old ? [...old, newProject] : [newProject]
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateProject(id: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: UpdateProjectDto) => projectsApi.update(id, data),
|
||||
onMutate: async (updatedData) => {
|
||||
// Optimistic update
|
||||
await queryClient.cancelQueries({ queryKey: ['projects', id] });
|
||||
|
||||
const previousProject = queryClient.getQueryData<Project>(['projects', id]);
|
||||
|
||||
queryClient.setQueryData<Project>(['projects', id], (old) => ({
|
||||
...old!,
|
||||
...updatedData,
|
||||
}));
|
||||
|
||||
return { previousProject };
|
||||
},
|
||||
onError: (err, variables, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousProject) {
|
||||
queryClient.setQueryData(['projects', id], context.previousProject);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projects', id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['projects'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteProject() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => projectsApi.delete(id),
|
||||
onSuccess: (_, id) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['projects'] });
|
||||
queryClient.removeQueries({ queryKey: ['projects', id] });
|
||||
},
|
||||
});
|
||||
}
|
||||
27
lib/providers/query-provider.tsx
Normal file
27
lib/providers/query-provider.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user