project setup
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import Constants from "expo-constants";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
import { getToken } from "@/auth/token";
|
||||
|
||||
export const getApiUrl = () => {
|
||||
const isDev = process.env.NODE_ENV === "development" || !process.env.NODE_ENV;
|
||||
const prodUrl = "https://your-production-api.com/api";
|
||||
if (!isDev) return prodUrl;
|
||||
|
||||
if (Platform.OS === "web") {
|
||||
return process.env.EXPO_PUBLIC_BACKEND_URL;
|
||||
}
|
||||
|
||||
try {
|
||||
const debuggerHost =
|
||||
Constants.expoConfig?.hostUri ||
|
||||
Constants.manifest2?.extra?.expoGo?.debuggerHost;
|
||||
|
||||
if (debuggerHost) {
|
||||
const hostIp = debuggerHost.split(":")[0];
|
||||
return `http://${hostIp}:5000/api`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get debuggerHost:", error);
|
||||
}
|
||||
|
||||
if (process.env.EXPO_PUBLIC_API_URL) {
|
||||
return process.env.EXPO_PUBLIC_API_URL;
|
||||
}
|
||||
|
||||
return "http://192.168.1.X:5000/api";
|
||||
};
|
||||
|
||||
export const baseApi = createApi({
|
||||
reducerPath: "api",
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: getApiUrl(),
|
||||
prepareHeaders: (headers) => {
|
||||
headers.set("accept", "application/json");
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
headers.set("authorization", `Bearer ${token}`);
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
}),
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
tagTypes: ["Users", "Todos", "UserLogs"],
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { baseApi } from "@/store/api/baseApi";
|
||||
import { Comment } from "@/store/models/Comment.model";
|
||||
|
||||
export const commentsApi = baseApi.injectEndpoints({
|
||||
overrideExisting: false,
|
||||
endpoints: (builder) => ({
|
||||
getTodoComments: builder.query<Comment[], string>({
|
||||
query: (todoId) => ({ url: `/todos/${todoId}/comments` }),
|
||||
providesTags: (_result, _error, todoId) => [
|
||||
{ type: "Todos", id: `comments-${todoId}` },
|
||||
],
|
||||
}),
|
||||
|
||||
addComment: builder.mutation<Comment, { todoId: string; content: string }>({
|
||||
query: ({ todoId, content }) => ({
|
||||
url: `/todos/${todoId}/comments`,
|
||||
method: "POST",
|
||||
body: { content },
|
||||
}),
|
||||
invalidatesTags: (_result, _error, { todoId }) => [
|
||||
{ type: "Todos", id: `comments-${todoId}` },
|
||||
],
|
||||
}),
|
||||
|
||||
updateComment: builder.mutation<
|
||||
Comment,
|
||||
{ todoId: string; commentId: string; content: string }
|
||||
>({
|
||||
query: ({ todoId, commentId, content }) => ({
|
||||
url: `/todos/${todoId}/comments/${commentId}`,
|
||||
method: "PUT",
|
||||
body: { content },
|
||||
}),
|
||||
invalidatesTags: (_result, _error, { todoId }) => [
|
||||
{ type: "Todos", id: `comments-${todoId}` },
|
||||
],
|
||||
}),
|
||||
|
||||
deleteComment: builder.mutation<
|
||||
{ message: string },
|
||||
{ todoId: string; commentId: string }
|
||||
>({
|
||||
query: ({ todoId, commentId }) => ({
|
||||
url: `/todos/${todoId}/comments/${commentId}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: (_result, _error, { todoId }) => [
|
||||
{ type: "Todos", id: `comments-${todoId}` },
|
||||
],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetTodoCommentsQuery,
|
||||
useAddCommentMutation,
|
||||
useUpdateCommentMutation,
|
||||
useDeleteCommentMutation,
|
||||
} = commentsApi;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { baseApi } from "@/store/api/baseApi";
|
||||
|
||||
export type SearchType = "user" | "admin" | "todo";
|
||||
|
||||
export interface SearchResult {
|
||||
id: string;
|
||||
type: SearchType;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
createdAt?: string;
|
||||
score: number;
|
||||
meta?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface QuickSearchResponse {
|
||||
results: SearchResult[];
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
results: SearchResult[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export const searchApi = baseApi.injectEndpoints({
|
||||
overrideExisting: true,
|
||||
endpoints: (builder) => ({
|
||||
quickSearch: builder.query<
|
||||
QuickSearchResponse,
|
||||
{ q: string; limit?: number }
|
||||
>({
|
||||
query: ({ q, limit = 8 }) => ({
|
||||
url: "/search/quick",
|
||||
params: { q, limit },
|
||||
}),
|
||||
}),
|
||||
|
||||
globalSearch: builder.query<
|
||||
SearchResponse,
|
||||
{ q: string; page?: number; limit?: number }
|
||||
>({
|
||||
query: ({ q, page = 1, limit = 25 }) => ({
|
||||
url: "/search",
|
||||
params: { q, page, limit },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useLazyQuickSearchQuery, useGlobalSearchQuery } = searchApi;
|
||||
@@ -0,0 +1,133 @@
|
||||
import { baseApi } from "@/store/api/baseApi";
|
||||
import { Todo } from "@/store/models/Todo.model";
|
||||
import { TodoStatus } from "@/constants/todoStatusEnum";
|
||||
import { PriorityStatus } from "@/constants/priorityStatusEnum";
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
|
||||
interface CreateTodoPayload {
|
||||
title: string;
|
||||
task: string;
|
||||
status: TodoStatus;
|
||||
priority: PriorityStatus;
|
||||
dueDate: Date;
|
||||
createdById: string;
|
||||
assigneeIds: string[];
|
||||
}
|
||||
|
||||
export interface TodosQueryParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
statuses?: TodoStatus[];
|
||||
priorities?: PriorityStatus[];
|
||||
sortBy?: "title" | "dueDate" | "status" | "priority" | "createdAt";
|
||||
sortDir?: TableSortDirection;
|
||||
assigneeUserId?: string;
|
||||
teamOfManagerId?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedTodos {
|
||||
items: Todo[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
const normalizeSortDir = (dir?: TableSortDirection) => {
|
||||
return dir
|
||||
? dir === TableSortDirection.DESCENDING
|
||||
? "desc"
|
||||
: "asc"
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const buildTodoParams = (params?: TodosQueryParams) => {
|
||||
if (!params) return {};
|
||||
|
||||
const {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
statuses,
|
||||
priorities,
|
||||
sortBy,
|
||||
sortDir,
|
||||
assigneeUserId,
|
||||
teamOfManagerId,
|
||||
} = params;
|
||||
|
||||
return {
|
||||
...(page !== undefined && { page }),
|
||||
...(limit !== undefined && { limit }),
|
||||
...(search && { search }),
|
||||
...(Array.isArray(statuses) &&
|
||||
statuses.length && {
|
||||
statuses: statuses.join(","),
|
||||
}),
|
||||
...(Array.isArray(priorities) &&
|
||||
priorities.length && {
|
||||
priorities: priorities.join(","),
|
||||
}),
|
||||
...(sortBy && { sortBy }),
|
||||
...(sortDir && { sortDir: normalizeSortDir(sortDir) }),
|
||||
...(assigneeUserId && { assigneeUserId }),
|
||||
...(teamOfManagerId && { teamOfManagerId }),
|
||||
};
|
||||
};
|
||||
|
||||
export const todosApi = baseApi.injectEndpoints({
|
||||
overrideExisting: false,
|
||||
endpoints: (builder) => ({
|
||||
getTodosByUser: builder.query<Todo[], string>({
|
||||
query: (userId) => ({ url: `/todos/user/${userId}` }),
|
||||
providesTags: (result) => [
|
||||
"Todos",
|
||||
...(result ?? []).map(({ id }) => ({ type: "Todos", id } as const)),
|
||||
],
|
||||
}),
|
||||
|
||||
getTodos: builder.query<PaginatedTodos, TodosQueryParams | undefined>({
|
||||
query: (params) => ({ url: "/todos", params: buildTodoParams(params) }),
|
||||
providesTags: (result, _error, _arg) => [
|
||||
"Todos",
|
||||
...(result?.items ?? []).map(
|
||||
({ id }) => ({ type: "Todos", id } as const)
|
||||
),
|
||||
],
|
||||
}),
|
||||
|
||||
createTodo: builder.mutation<Todo, CreateTodoPayload>({
|
||||
query: (data) => ({
|
||||
url: "/todos",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["Todos"],
|
||||
}),
|
||||
|
||||
updateTodo: builder.mutation<Todo, Partial<Todo>>({
|
||||
query: (todo) => ({
|
||||
url: `/todos/${todo.id}`,
|
||||
method: "PUT",
|
||||
body: todo,
|
||||
}),
|
||||
invalidatesTags: ["Todos"],
|
||||
}),
|
||||
|
||||
deleteTodo: builder.mutation<void, string>({
|
||||
query: (id) => ({
|
||||
url: `/todos/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: ["Todos"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetTodosByUserQuery,
|
||||
useGetTodosQuery,
|
||||
useCreateTodoMutation,
|
||||
useUpdateTodoMutation,
|
||||
useDeleteTodoMutation,
|
||||
} = todosApi;
|
||||
@@ -0,0 +1,150 @@
|
||||
import { baseApi } from "@/store/api/baseApi";
|
||||
import { User } from "@/store/models/User.model";
|
||||
import { RiskEnum } from "@/constants/riskEnum";
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
|
||||
export type UsersQueryParams = {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
risks?: RiskEnum[];
|
||||
sortBy?: "name" | "email" | "title" | "role" | "risk";
|
||||
sortDir?: TableSortDirection;
|
||||
roleType?: "user" | "admin";
|
||||
};
|
||||
|
||||
export type PaginatedUsers = {
|
||||
items: User[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
export type UserLoginLog = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
lastLoginAt: string | Date;
|
||||
activity: string;
|
||||
};
|
||||
|
||||
const normalizeSortDir = (dir?: TableSortDirection) => {
|
||||
return dir
|
||||
? dir === TableSortDirection.DESCENDING
|
||||
? "desc"
|
||||
: "asc"
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const buildUserParams = (params?: UsersQueryParams) => {
|
||||
if (!params) return {};
|
||||
|
||||
const { page, limit, search, risks, sortBy, sortDir, roleType } = params;
|
||||
|
||||
return {
|
||||
...(page !== undefined && { page }),
|
||||
...(limit !== undefined && { limit }),
|
||||
...(search && { search }),
|
||||
...(risks?.length && { risks: risks.join(",") }),
|
||||
...(sortBy && { sortBy }),
|
||||
...(sortDir && { sortDir: normalizeSortDir(sortDir) }),
|
||||
...(roleType && { roleType }),
|
||||
};
|
||||
};
|
||||
|
||||
const buildLogParams = (params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
}) => {
|
||||
if (!params) return {};
|
||||
|
||||
const { page, limit, search } = params;
|
||||
|
||||
return {
|
||||
...(page !== undefined && { page }),
|
||||
...(limit !== undefined && { limit }),
|
||||
...(search && { search }),
|
||||
};
|
||||
};
|
||||
|
||||
export const usersApi = baseApi.injectEndpoints({
|
||||
overrideExisting: true,
|
||||
endpoints: (builder) => ({
|
||||
getUserLogs: builder.query<
|
||||
{ items: UserLoginLog[]; total: number; page: number; limit: number },
|
||||
{ page?: number; limit?: number; search?: string } | undefined
|
||||
>({
|
||||
query: (params) => ({
|
||||
url: "/users/logs",
|
||||
params: buildLogParams(params),
|
||||
}),
|
||||
providesTags: ["UserLogs"],
|
||||
}),
|
||||
|
||||
getUsers: builder.query<PaginatedUsers, UsersQueryParams | undefined>({
|
||||
query: (params) => ({ url: "/users", params: buildUserParams(params) }),
|
||||
providesTags: (result) => [
|
||||
"Users",
|
||||
...(result?.items ?? []).map(
|
||||
({ id }) => ({ type: "Users", id } as const)
|
||||
),
|
||||
],
|
||||
}),
|
||||
|
||||
getUserById: builder.query<User, string>({
|
||||
query: (id) => `/users/${id}`,
|
||||
providesTags: (_res, _err, id) => [{ type: "Users", id }],
|
||||
}),
|
||||
|
||||
deleteUser: builder.mutation<void, string>({
|
||||
query: (id) => ({ url: `/users/${id}`, method: "DELETE" }),
|
||||
invalidatesTags: ["Users"],
|
||||
}),
|
||||
|
||||
deleteUsers: builder.mutation<void, string[]>({
|
||||
query: (ids) => ({ url: "/users", method: "DELETE", body: { ids } }),
|
||||
invalidatesTags: ["Users"],
|
||||
}),
|
||||
|
||||
lockUser: builder.mutation<void, string>({
|
||||
query: (id) => ({ url: `/users/${id}/lock`, method: "PUT" }),
|
||||
invalidatesTags: ["Users"],
|
||||
}),
|
||||
|
||||
updateUser: builder.mutation<User, Partial<User>>({
|
||||
query: (user) => ({
|
||||
url: `/users/${user.id}`,
|
||||
method: "PUT",
|
||||
body: user,
|
||||
}),
|
||||
invalidatesTags: (_res, _err, { id }) => [
|
||||
"Users",
|
||||
{ type: "Users", id },
|
||||
"Todos",
|
||||
],
|
||||
}),
|
||||
|
||||
createUser: builder.mutation<User, Partial<User>>({
|
||||
query: (user) => ({ url: "/users", method: "POST", body: user }),
|
||||
invalidatesTags: ["Users"],
|
||||
}),
|
||||
|
||||
resendInvitation: builder.mutation<{ message: string }, string>({
|
||||
query: (id) => ({ url: `/users/${id}/invite`, method: "POST" }),
|
||||
invalidatesTags: (_res, _err, id) => ["Users", { type: "Users", id }],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetUserLogsQuery,
|
||||
useGetUsersQuery,
|
||||
useGetUserByIdQuery,
|
||||
useDeleteUserMutation,
|
||||
useDeleteUsersMutation,
|
||||
useLockUserMutation,
|
||||
useUpdateUserMutation,
|
||||
useCreateUserMutation,
|
||||
useResendInvitationMutation,
|
||||
} = usersApi;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import type { AppDispatch, RootState } from "./index";
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
|
||||
import { baseApi } from "./api/baseApi";
|
||||
import screenSizeReducer from "./screen/screenSizeSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
screenSize: screenSizeReducer,
|
||||
[baseApi.reducerPath]: baseApi.reducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(baseApi.middleware),
|
||||
});
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { User } from "@/store/models/User.model";
|
||||
|
||||
export interface Comment {
|
||||
id: string;
|
||||
content: string;
|
||||
createdAt: string | Date;
|
||||
todoId: string;
|
||||
userId: string;
|
||||
user: Pick<User, "id" | "firstName" | "lastName" | "imageUrl" | "role">;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TodoStatus } from "@/constants/todoStatusEnum";
|
||||
import { PriorityStatus } from "@/constants/priorityStatusEnum";
|
||||
import { User } from "@/store/models/User.model";
|
||||
|
||||
export interface Todo {
|
||||
id: string;
|
||||
title: string;
|
||||
task: string;
|
||||
status: TodoStatus;
|
||||
priority: PriorityStatus;
|
||||
dueDate: Date;
|
||||
createdAt: Date;
|
||||
assignees: User[];
|
||||
createdBy: User;
|
||||
createdById: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { RoleEnum } from "@/constants/roleEnum";
|
||||
import { Todo } from "@/store/models/Todo.model";
|
||||
import { RiskEnum } from "@/constants/riskEnum";
|
||||
import { SecurityType } from "@/constants/securityTypeEnum";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
firstName: string;
|
||||
middleName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password?: string;
|
||||
todos?: Todo[];
|
||||
riskStatus: RiskEnum;
|
||||
imageUrl?: string;
|
||||
title: string;
|
||||
role: RoleEnum;
|
||||
isLocked: boolean;
|
||||
isActivated: boolean;
|
||||
phoneNumber?: string;
|
||||
department?: string;
|
||||
managerId?: string | null;
|
||||
manager?: User | null;
|
||||
securityType: SecurityType;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Dimensions } from "react-native";
|
||||
|
||||
import type { RootState } from "../index";
|
||||
|
||||
export interface ScreenSizeState {
|
||||
width: number;
|
||||
height: number;
|
||||
isMobile: boolean;
|
||||
sidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
const initialDimensions = Dimensions.get("window");
|
||||
|
||||
const initialState: ScreenSizeState = {
|
||||
width: initialDimensions.width,
|
||||
height: initialDimensions.height,
|
||||
isMobile: initialDimensions.width < 768,
|
||||
sidebarCollapsed: false,
|
||||
};
|
||||
|
||||
export const screenSizeSlice = createSlice({
|
||||
name: "screenSize",
|
||||
initialState,
|
||||
reducers: {
|
||||
setScreenSize: (
|
||||
state,
|
||||
action: PayloadAction<{ width: number; height: number }>
|
||||
) => {
|
||||
state.width = action.payload.width;
|
||||
state.height = action.payload.height;
|
||||
state.isMobile = action.payload.width < 768;
|
||||
},
|
||||
setSidebarCollapsed: (state, action: PayloadAction<boolean>) => {
|
||||
state.sidebarCollapsed = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setScreenSize, setSidebarCollapsed } = screenSizeSlice.actions;
|
||||
|
||||
export const selectScreenSize = (state: RootState) => state.screenSize;
|
||||
export const selectIsMobile = (state: RootState) => state.screenSize.isMobile;
|
||||
export const selectWidth = (state: RootState) => state.screenSize.width;
|
||||
export const selectHeight = (state: RootState) => state.screenSize.height;
|
||||
export const selectSidebarCollapsed = (state: RootState) => state.screenSize.sidebarCollapsed;
|
||||
|
||||
export default screenSizeSlice.reducer;
|
||||
Reference in New Issue
Block a user