├── .env ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package-lock.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.css ├── App.jsx ├── assets │ ├── images │ │ ├── background │ │ │ ├── backgroundwhite.png │ │ │ ├── header-background.jpg │ │ │ ├── index.js │ │ │ └── loginBg.png │ │ ├── index.js │ │ ├── logos │ │ │ ├── fileLogos │ │ │ │ ├── csv.png │ │ │ │ ├── excel.png │ │ │ │ ├── pdf.png │ │ │ │ ├── ppt.png │ │ │ │ ├── text.png │ │ │ │ └── word.png │ │ │ ├── ilovetaguig.jpg │ │ │ ├── index.js │ │ │ ├── logo.png │ │ │ ├── think-big-taguig.png │ │ │ ├── topapp.jpg │ │ │ └── tower-system.jpg │ │ └── other │ │ │ └── passport │ │ │ └── header.JPG │ └── react.svg ├── helpers │ ├── arrObjectFormats.js │ ├── currencyFormat.js │ ├── dateFilters.js │ ├── dateFormats.js │ ├── filters.js │ ├── formatData.js │ ├── getStatus.js │ ├── getUserAgent.js │ ├── itemFormat.jsx │ ├── regExp.js │ └── tobase64.js ├── hooks │ ├── regExp.js │ ├── useTableSearch.jsx │ ├── useTableSearchServerside.jsx │ └── useWindowSize.js ├── index.css ├── main.jsx ├── pages │ └── admin │ │ ├── Dashboard │ │ └── index.jsx │ │ ├── Login.jsx │ │ └── index.jsx ├── routes │ ├── index.jsx │ ├── pageRoutes │ │ └── AdminRoutes.jsx │ └── validateAuth.jsx ├── services │ ├── api │ │ └── admin │ │ │ └── auth.js │ ├── axios.js │ └── requests │ │ └── admin │ │ └── useLoginAuth.js └── store │ ├── admin │ └── useAuth.js │ └── user │ └── useAuth.js ├── tailwind.config.js └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | #DEV - API BASE_URL 2 | VITE_BASE_URL=YOUR_API_URL_HERE 3 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:react/recommended", 7 | "plugin:react/jsx-runtime", 8 | "plugin:react-hooks/recommended", 9 | ], 10 | ignorePatterns: ["dist", ".eslintrc.cjs"], 11 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 12 | settings: { react: { version: "18.2" } }, 13 | plugins: ["react-refresh"], 14 | rules: { 15 | "react/jsx-no-target-blank": "off", 16 | "react-refresh/only-export-components": [ 17 | "warn", 18 | { allowConstantExport: true }, 19 | ], 20 | "react/prop-types": 0, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ATIMS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap'); 2 | 3 | :root { 4 | --scrollbar-color: #F5F5F5; 5 | --scrollbar-thumb-color: #CC0011; 6 | } 7 | 8 | #root { 9 | margin: 0 auto; 10 | } 11 | 12 | body { 13 | font-family: "Montserrat", sans-serif; 14 | } 15 | 16 | ::-webkit-scrollbar { 17 | width: 6px; 18 | height: 6px; 19 | } 20 | 21 | ::-webkit-scrollbar-track { 22 | box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 23 | border-radius: 7px; 24 | background-color: var(--scrollbar-color); 25 | } 26 | 27 | ::-webkit-scrollbar-thumb { 28 | border-radius: 7px; 29 | box-shadow: inset 0 0 6px rgba(0,0,0,.3); 30 | background-color: var(--scrollbar-thumb-color); 31 | cursor: pointer; 32 | } 33 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import RootRoutes from "./routes"; 3 | 4 | function App() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /src/assets/images/background/backgroundwhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/background/backgroundwhite.png -------------------------------------------------------------------------------- /src/assets/images/background/header-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/background/header-background.jpg -------------------------------------------------------------------------------- /src/assets/images/background/index.js: -------------------------------------------------------------------------------- 1 | import bgBackground from "./header-background.jpg"; 2 | import bgBackgroundWhite from "./backgroundwhite.png"; 3 | import loginBg from "./loginBg.png"; 4 | 5 | export { bgBackground, bgBackgroundWhite, loginBg }; 6 | -------------------------------------------------------------------------------- /src/assets/images/background/loginBg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/background/loginBg.png -------------------------------------------------------------------------------- /src/assets/images/index.js: -------------------------------------------------------------------------------- 1 | import loginBg from "./background/loginBg.png"; 2 | import logo from "./logos/logo.png"; 3 | 4 | export { loginBg, logo }; 5 | -------------------------------------------------------------------------------- /src/assets/images/logos/fileLogos/csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/fileLogos/csv.png -------------------------------------------------------------------------------- /src/assets/images/logos/fileLogos/excel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/fileLogos/excel.png -------------------------------------------------------------------------------- /src/assets/images/logos/fileLogos/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/fileLogos/pdf.png -------------------------------------------------------------------------------- /src/assets/images/logos/fileLogos/ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/fileLogos/ppt.png -------------------------------------------------------------------------------- /src/assets/images/logos/fileLogos/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/fileLogos/text.png -------------------------------------------------------------------------------- /src/assets/images/logos/fileLogos/word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/fileLogos/word.png -------------------------------------------------------------------------------- /src/assets/images/logos/ilovetaguig.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/ilovetaguig.jpg -------------------------------------------------------------------------------- /src/assets/images/logos/index.js: -------------------------------------------------------------------------------- 1 | import logo from "./taguig.png"; 2 | 3 | export { logo }; 4 | -------------------------------------------------------------------------------- /src/assets/images/logos/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/logo.png -------------------------------------------------------------------------------- /src/assets/images/logos/think-big-taguig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/think-big-taguig.png -------------------------------------------------------------------------------- /src/assets/images/logos/topapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/topapp.jpg -------------------------------------------------------------------------------- /src/assets/images/logos/tower-system.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/logos/tower-system.jpg -------------------------------------------------------------------------------- /src/assets/images/other/passport/header.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/reactjs-vite-reactquery-zustand-template/d2e32d2742c3495828007013bf6253865f904e45/src/assets/images/other/passport/header.JPG -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/helpers/arrObjectFormats.js: -------------------------------------------------------------------------------- 1 | export const objectToQueryParams = (object) => { 2 | return Object.keys(object) 3 | .map((key) => `${key}=${encodeURIComponent(object[key])}`) 4 | .join("&"); 5 | }; 6 | -------------------------------------------------------------------------------- /src/helpers/currencyFormat.js: -------------------------------------------------------------------------------- 1 | export const format_addCommas = (num) => { 2 | var str = num.toString().split("."); 3 | if (str[0].length >= 4) { 4 | str[0] = str[0].replace(/(\d)(?=(\d{3})+$)/g, "$1,"); 5 | } 6 | return str.join("."); 7 | }; 8 | 9 | const formatCurency = new Intl.NumberFormat("en-US", { 10 | style: "currency", 11 | currency: "PHP", 12 | minimumFractionDigits: 2, 13 | }); 14 | 15 | const formatCurency3 = new Intl.NumberFormat("en-US", { 16 | style: "currency", 17 | currency: "PHP", 18 | minimumFractionDigits: 3, 19 | }); 20 | 21 | export const format_PHCurrency = (num) => formatCurency.format(num); 22 | export const checkFree = (num) => 23 | num === 0 ? "FREE" : formatCurency.format(num); 24 | export const format_PHCurrency3 = (num) => formatCurency3.format(num); 25 | export const unFormat_PHCurrency = (num = "") => 26 | Number(num.replace(/[^0-9\\.]+/g, "")); 27 | 28 | export const getPercentage = (partialValue, totalValue) => { 29 | return ((100 * partialValue) / totalValue).toFixed(2); 30 | }; 31 | -------------------------------------------------------------------------------- /src/helpers/dateFilters.js: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import customParseFormat from "dayjs/plugin/customParseFormat"; 3 | dayjs.extend(customParseFormat); 4 | 5 | const range = (start, end) => { 6 | const result = []; 7 | for (let i = start; i < end; i++) { 8 | result.push(i); 9 | } 10 | return result; 11 | }; 12 | 13 | // eslint-disable-next-line arrow-body-style 14 | export const disabledDate = (current) => { 15 | // Can not select days before today and today 16 | return current && current < dayjs().endOf("day"); 17 | }; 18 | 19 | export const disabledDateTime = () => ({ 20 | disabledHours: () => range(0, 24).splice(4, 20), 21 | disabledMinutes: () => range(30, 60), 22 | disabledSeconds: () => [55, 56], 23 | }); 24 | 25 | export const customDisabledDate = (current, startDate, endDate) => { 26 | if (!startDate || !endDate) { 27 | // Allow all dates if either startDate or endDate is not selected 28 | return false; 29 | } 30 | 31 | // Disable dates before startDate and after endDate 32 | return current < startDate || current > endDate; 33 | }; 34 | 35 | export const disabledRangeTime = (_, type) => { 36 | if (type === "start") { 37 | return { 38 | disabledHours: () => range(0, 60).splice(4, 20), 39 | disabledMinutes: () => range(30, 60), 40 | disabledSeconds: () => [55, 56], 41 | }; 42 | } 43 | return { 44 | disabledHours: () => range(0, 60).splice(20, 4), 45 | disabledMinutes: () => range(0, 31), 46 | disabledSeconds: () => [55, 56], 47 | }; 48 | }; 49 | 50 | //disable dates onwards starts on given date 51 | export const isDateDisabledOnwards = (current, givenDate) => { 52 | return givenDate && current && current >= dayjs(givenDate); 53 | }; 54 | 55 | export const disabledFutureDates = (current) => { 56 | // Disable future dates 57 | return current && current > dayjs().endOf("day"); 58 | }; 59 | 60 | export const enabledRangeDates = (start, end) => { 61 | // Disable future dates 62 | return start && start < dayjs().endOf("day") && end > dayjs().endOf("day"); 63 | }; 64 | -------------------------------------------------------------------------------- /src/helpers/dateFormats.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export const formatDateRange = (start, end) => { 4 | const dateFrom = moment(start); 5 | const dateTo = moment(end); 6 | 7 | const formattedDateFrom = dateFrom.format("MMMM D"); 8 | const formattedDateTo = dateTo.format(dateFrom.month() === dateTo.month() ? "D" : "MMMM D"); 9 | const year = dateFrom.year(); 10 | 11 | return `${formattedDateFrom} - ${formattedDateTo}, ${year}`; 12 | }; 13 | 14 | export const enumerateDates = (startDate, endDate) => { 15 | var now = moment(startDate), 16 | dates = []; 17 | 18 | while (now.isSameOrBefore(endDate)) { 19 | dates.push(now.format("YYYY-MM-DD")); 20 | now.add(1, "days"); 21 | } 22 | return dates; // August 10 - 11, 2023 || August 30 - September 03, 2023 23 | }; 24 | 25 | export const formatTimeRange = (dateFrom, dateTo) => { 26 | const timeFrom = dateFrom.format("h:mmA"); 27 | const timeTo = dateTo.format("h:mmA"); 28 | 29 | return `${timeFrom} - ${timeTo}`; 30 | }; 31 | -------------------------------------------------------------------------------- /src/helpers/filters.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { CARDDATESTATUS, CARDDATESTATUSENUMS } from "../constants/enums"; 3 | 4 | export const filterOption = (input, option) => 5 | (option?.label ?? "").toLowerCase().includes(input.toLowerCase()); 6 | 7 | export const removeDuplicate = (array, key) => { 8 | return array.reduce((acc, item) => { 9 | if (!acc.some((obj) => obj[key] === item[key])) { 10 | acc.push(item); 11 | } 12 | return acc; 13 | }, []); 14 | }; 15 | 16 | export const groupArrayByKey = (arr, key) => { 17 | return Object.groupBy(arr, (obj) => obj[key]); 18 | }; 19 | 20 | // Removes item when existing array 2 21 | export const filterArrayFromArray = (array, array2, arr1Key, arr2Key) => { 22 | return array.filter( 23 | (obj) => !array2.find((obj2) => obj[arr1Key] == obj2[arr2Key]) 24 | ); 25 | }; 26 | 27 | export const getCurrentDataByDate = (arr = [], key = "") => { 28 | return arr.filter((obj) => 29 | moment().isSameOrBefore(moment(obj[key], "YYYY-MM-DD")) 30 | ); 31 | }; 32 | 33 | export const getExpiredDataByDate = (arr = [], key = "") => { 34 | const result = arr.filter((obj) => 35 | moment(obj[key], "YYYY-MM-DD").isAfter(moment()) 36 | ); 37 | return result; 38 | }; 39 | 40 | export const getCardStatusByDate = (date) => { 41 | const currentDate = moment().format("YYYY-MM-DD"); 42 | return moment(currentDate).isBefore(moment(date)) 43 | ? CARDDATESTATUS.ONGOING 44 | : CARDDATESTATUS.EXPIRED; 45 | }; 46 | // export const getCardStatusByDate_1 = (date) => { 47 | // const currentDate = moment().format("YYYY-MM-DD"); 48 | // return moment(moment(currentDate).add(1, "d")).isSame(moment(date)) 49 | // ? CARDDATESTATUS.LASTDAY 50 | // : moment(currentDate).isBefore(moment(date)) 51 | // ? CARDDATESTATUS.ONGOING 52 | // : CARDDATESTATUS.EXPIRED; 53 | // }; 54 | 55 | export const getCountByStatusDateAndKey = (arr = [], key) => { 56 | const currentDate = moment().format("YYYY-MM-DD"); 57 | const result = arr.reduce( 58 | (accumulator, currentValue) => { 59 | if (moment(currentDate).isBefore(moment(currentValue[key]))) { 60 | accumulator[CARDDATESTATUSENUMS.ONGOING].push(currentValue); 61 | } else { 62 | moment(currentDate).isAfter(moment(currentValue[key])); 63 | accumulator[CARDDATESTATUSENUMS.EXPIRED].push(currentValue); 64 | } 65 | return accumulator; 66 | }, 67 | { 68 | [CARDDATESTATUSENUMS.ONGOING]: [], 69 | [CARDDATESTATUSENUMS.EXPIRED]: [], 70 | } 71 | ); 72 | 73 | return result; 74 | }; 75 | 76 | // export const getCountByStatusDateAndKey_V1 = (arr = [], key) => { 77 | // const currentDate = moment().format("YYYY-MM-DD"); 78 | // const result = arr.reduce( 79 | // (accumulator, currentValue) => { 80 | // if ( 81 | // moment(moment(currentDate).add(1, "d")).isSame( 82 | // moment(currentValue[key]) 83 | // ) 84 | // ) { 85 | // accumulator[CARDDATESTATUSENUMS.LASTDAY].push(currentValue); 86 | // } else if (moment(currentDate).isBefore(moment(currentValue[key]))) { 87 | // accumulator[CARDDATESTATUSENUMS.ONGOING].push(currentValue); 88 | // } else { 89 | // moment(currentDate).isAfter(moment(currentValue[key])); 90 | // accumulator[CARDDATESTATUSENUMS.EXPIRED].push(currentValue); 91 | // } 92 | // return accumulator; 93 | // }, 94 | // { 95 | // [CARDDATESTATUSENUMS.LASTDAY]: [], 96 | // [CARDDATESTATUSENUMS.ONGOING]: [], 97 | // [CARDDATESTATUSENUMS.EXPIRED]: [], 98 | // } 99 | // ); 100 | 101 | // return result; 102 | // }; 103 | -------------------------------------------------------------------------------- /src/helpers/formatData.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { MINDAYSRETORECEIVE } from "../constants/static"; 3 | import { viewHandlerCopyable } from "./itemFormat"; 4 | 5 | export const formatNavigations = (arr) => { 6 | return Object.entries(arr) 7 | .reduce((acc, [key, data]) => { 8 | const newData = data.map((obj) => ({ ...obj, module: key })); 9 | acc.push(newData); 10 | return acc; 11 | }, []) 12 | .flat(); 13 | }; 14 | 15 | export const arrayToSelectDropdown = (arr, keyValue, keyLabel) => { 16 | return arr.map((obj) => ({ label: obj[keyLabel], value: obj[keyValue] })); 17 | }; 18 | 19 | export const getBirthdate = (month, day, year) => { 20 | if (month && day && year) { 21 | const date = moment(`${month} ${day}, ${year}`, "MMMM D, YYYY"); 22 | 23 | return date.isValid() ? date.format("YYYY-MM-DD") : ""; 24 | } else { 25 | return ""; 26 | } 27 | }; 28 | 29 | export const decodeHtmlEntities = (html, isCopyable = false) => { 30 | var txt = document.createElement("textarea"); 31 | txt.innerHTML = html; 32 | return isCopyable ? viewHandlerCopyable(txt.value) : txt.value; 33 | }; 34 | 35 | export const sortArrayObjects = (arr, key, type = "desc") => { 36 | return arr.sort((a, b) => { 37 | if (type == "desc") { 38 | if (a[key] > b[key]) return 1; 39 | if (a[key] < b[key]) return -1; 40 | } else { 41 | if (a[key] < b[key]) return 1; 42 | if (a[key] > b[key]) return -1; 43 | } 44 | return 0; 45 | }); 46 | }; 47 | 48 | export const scannerAddStatusOnCitizenMedicines = (data) => { 49 | return { 50 | ...data, 51 | medicines: (data?.medicines || []).map((obj) => { 52 | const monthlyQty = parseInt(obj.quantity || 0); 53 | const receivedQty = parseInt(obj.receivedQty || 0); 54 | let isInvalid = true; 55 | let errorMessage = ""; 56 | let formValue = null; 57 | let maxQuantity = null; 58 | let remainingDays = 0; 59 | 60 | if (obj.status == 0) { 61 | isInvalid = false; 62 | formValue = monthlyQty; 63 | } else if (obj.status == 1) { 64 | const days = moment().diff(moment(obj.dateDistributed), "days"); 65 | if (receivedQty < monthlyQty && days < MINDAYSRETORECEIVE) { 66 | isInvalid = false; 67 | formValue = monthlyQty - receivedQty; 68 | maxQuantity = monthlyQty - receivedQty; 69 | errorMessage = `Have received total of ${receivedQty} last ${moment( 70 | obj.dateDistributed 71 | ).format("ll")}`; 72 | } else if (days < MINDAYSRETORECEIVE) { 73 | remainingDays = MINDAYSRETORECEIVE - days; 74 | errorMessage = `${remainingDays} days left to get another medicine.(${moment() 75 | .add(remainingDays, "day") 76 | .format("ll")})`; 77 | } else { 78 | isInvalid = false; 79 | formValue = monthlyQty; 80 | maxQuantity = monthlyQty; 81 | } 82 | } else { 83 | const days = moment().diff(moment(obj.dateDistributed), "days"); 84 | remainingDays = MINDAYSRETORECEIVE - days; 85 | if (days < MINDAYSRETORECEIVE) { 86 | errorMessage = `${remainingDays} days left to get another medicine.(${moment() 87 | .add(remainingDays, "day") 88 | .format("ll")})`; 89 | } else { 90 | isInvalid = false; 91 | formValue = monthlyQty; 92 | } 93 | } 94 | 95 | return { 96 | ...obj, 97 | isInvalid, 98 | maxQuantity, 99 | errorMessage, 100 | formValue, 101 | remainingDays, 102 | }; 103 | }), 104 | }; 105 | }; 106 | 107 | export const getAllMedicines = (data = []) => { 108 | const result = data 109 | .reduce((acc, item) => { 110 | acc.push(...(item?.medicines || [])); 111 | return acc; 112 | }, []) 113 | .map((obj) => ({ ...obj, quantity: obj.monthlyQuantity })) 114 | .reduce((acc, item) => { 115 | const isExist = acc.find((obj) => obj.productId === item.productId); 116 | if (isExist) { 117 | acc = acc.map((obj) => { 118 | if (obj.productId === item.productId) { 119 | return { ...obj, quantity: obj.quantity + item.quantity }; 120 | } else { 121 | return obj; 122 | } 123 | }); 124 | } else { 125 | acc.push(item); 126 | } 127 | 128 | return acc; 129 | }, []); 130 | 131 | return result; 132 | }; 133 | 134 | export const getAge = (birthdate) => { 135 | return moment().diff(birthdate, "years"); 136 | }; 137 | -------------------------------------------------------------------------------- /src/helpers/getStatus.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | const MINDAYSRETORECEIVE = 25; 3 | 4 | export const getScannerMedicineIsInvalid = (date, total, current) => { 5 | if (total <= 0) { 6 | return true; 7 | } else if (date) { 8 | const days = moment(date).diff(moment(), "days"); 9 | if (current < total && days >= MINDAYSRETORECEIVE) { 10 | return false; 11 | } else { 12 | return true; 13 | } 14 | } else { 15 | return false; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/helpers/getUserAgent.js: -------------------------------------------------------------------------------- 1 | import { USERAGENT } from "../constants/enums"; 2 | 3 | export const getUSERAGENT = () => { 4 | const ua = navigator.USERAGENT; 5 | if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(ua)) { 6 | return USERAGENT.TABLET; 7 | } else if ( 8 | /Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated|(hpw|web)OS|Opera M(obi|ini)/.test( 9 | ua 10 | ) 11 | ) { 12 | return USERAGENT.MOBILE; 13 | } else { 14 | return USERAGENT.WEB; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/helpers/itemFormat.jsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "antd"; 2 | import { motion } from "framer-motion"; 3 | import { Link } from "react-router-dom"; 4 | 5 | export const getItem = (label, key, icon, children, ...props) => { 6 | return { 7 | key, 8 | icon, 9 | children, 10 | label, 11 | ...props, 12 | }; 13 | }; 14 | 15 | export const viewHandler = (value) => (value ? value : "N/A"); 16 | 17 | export const viewHandlerCopyable = (value) => 18 | value ? {value} : "N/A"; 19 | 20 | export const generateItems = (arr = []) => { 21 | return arr.map((obj) => 22 | getItem( 23 | 37 | {obj.label} 38 | , 39 | obj.link, 40 | 41 | {obj.icon} 42 | ) 43 | ); 44 | }; 45 | 46 | export const getSex = (data) => { 47 | if (data == 0) { 48 | return "Male"; 49 | } else if (data == 1) { 50 | return "Female"; 51 | } else { 52 | return "N/A"; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/helpers/regExp.js: -------------------------------------------------------------------------------- 1 | // Password 2 | export const passwordSmallLetter = new RegExp(/[a-z]/g); 3 | export const passwordCapitalLetter = new RegExp(/[A-Z]/g); 4 | export const passwordNumber = new RegExp(/[0-9]/g); 5 | export const passwordSpecialChar = new RegExp(/[!@#$%^&)(+=.-]/g); 6 | export const passwordLength = new RegExp(/[a-zA-Z0-9!@#$%^&)(+=.-]{6}/g); 7 | // 8 | 9 | export const regExp_Names = new RegExp(/^[a-zA-Z\s]+$/); 10 | export const regExp_StudentId = new RegExp(/^\d{4}-\d{2}-\d{6}$/); 11 | export const regExp_Username = new RegExp(/^[a-zA-Z0-9\s]+$/); 12 | export const regExp_MobileNumber = new RegExp(/^[9][0-9]{9}$/); 13 | export const regExp_Email = new RegExp( 14 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/g 15 | ); 16 | -------------------------------------------------------------------------------- /src/helpers/tobase64.js: -------------------------------------------------------------------------------- 1 | import * as xlsx from "xlsx"; 2 | 3 | // File to base64 4 | export const toBase64 = (file) => 5 | new Promise((resolve, reject) => { 6 | const reader = new FileReader(); 7 | reader.readAsDataURL(file); 8 | reader.onload = () => resolve(reader.result); 9 | reader.onerror = (error) => reject(error); 10 | }); 11 | 12 | export const convertToBase64 = async (file) => { 13 | try { 14 | const result = await toBase64(file); 15 | return result; 16 | } catch (error) { 17 | alert("Image convertion failed. Please try again or contact support."); 18 | return; 19 | } 20 | }; 21 | 22 | // URL to base64 23 | export const imageToBase64 = (imageUrl) => { 24 | return new Promise((resolve, reject) => { 25 | const img = new Image(); 26 | img.crossOrigin = "Anonymous"; 27 | img.onload = () => { 28 | const canvas = document.createElement("canvas"); 29 | canvas.width = img.width; 30 | canvas.height = img.height; 31 | const ctx = canvas.getContext("2d"); 32 | ctx.drawImage(img, 0, 0, img.width, img.height); 33 | const base64 = canvas.toDataURL("image/png"); 34 | resolve(base64); 35 | }; 36 | img.onerror = (error) => { 37 | reject(error); 38 | }; 39 | img.src = imageUrl; 40 | }); 41 | }; 42 | 43 | export const fileCSVToJSON = (file) => { 44 | return new Promise((resolve, reject) => { 45 | if (file) { 46 | const reader = new FileReader(); 47 | reader.onload = (e) => { 48 | const data = e.target.result; 49 | const workbook = xlsx.read(data, { type: "array" }); 50 | const sheetName = workbook.SheetNames[0]; 51 | const worksheet = workbook.Sheets[sheetName]; 52 | const json = xlsx.utils.sheet_to_json(worksheet, { defval: "" }); 53 | resolve(json); // Resolve the promise with the JSON data 54 | }; 55 | reader.readAsArrayBuffer(file); 56 | } else { 57 | reject("File is not provided."); // Reject the promise if no file is provided 58 | } 59 | }); 60 | }; 61 | 62 | export const validateSheetJsonKeys = (filePath, keys, data = []) => { 63 | // STATUS 64 | // 0 : invalid file 65 | // -1 : Invalid Data columns 66 | // 1 : valid data 67 | 68 | var allowedExtensions = /(\.xlsx|\.csv|\.xls)$/i; 69 | if (!allowedExtensions.exec(filePath)) { 70 | return 0; 71 | } else { 72 | let errArr = []; 73 | let errKeys = []; 74 | 75 | keys.map((key) => { 76 | // eslint-disable-next-line no-prototype-builtins 77 | if (data?.[0]?.hasOwnProperty(key)) { 78 | errArr.push(true); 79 | errKeys.push(key); 80 | } else { 81 | errArr.push(false); 82 | } 83 | }); 84 | 85 | if (errArr.includes(false)) { 86 | return -1; 87 | } else { 88 | return 1; 89 | } 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /src/hooks/regExp.js: -------------------------------------------------------------------------------- 1 | // Password 2 | export const passwordSmallLetter = new RegExp(/[a-z]/g); 3 | export const passwordCapitalLetter = new RegExp(/[A-Z]/g); 4 | export const passwordNumber = new RegExp(/[0-9]/g); 5 | export const passwordSpecialChar = new RegExp(/[!@#$%^&)(+=.-]/g); 6 | export const passwordLength = new RegExp(/[a-zA-Z0-9!@#$%^&)(+=.-]{6}/g); 7 | // 8 | 9 | export const regExp_Names = new RegExp(/^[a-zA-Z\s]+$/); 10 | export const regExp_StudentId = new RegExp(/^\d{4}-\d{2}-\d{6}$/); 11 | export const regExp_Username = new RegExp(/^[a-zA-Z0-9\s]+$/); 12 | export const regExp_MobileNumber = new RegExp(/^[9][0-9]{9}$/); 13 | export const regExp_Email = new RegExp( 14 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/g 15 | ); 16 | 17 | export const regExp_TrimName = (string) => { 18 | return string 19 | ? string 20 | .replace(/[^A-Za-z ñÑ]+/g, "") 21 | .replace(/^\s+|\s+$/g, "") 22 | .replace(/\s+/g, " ") 23 | : ""; 24 | }; 25 | 26 | export const regExp_Trim = (string) => { 27 | return string ? string.replace(/^\s+|\s+$/g, "").replace(/\s+/g, " ") : ""; 28 | }; 29 | -------------------------------------------------------------------------------- /src/hooks/useTableSearch.jsx: -------------------------------------------------------------------------------- 1 | import { FileSearchOutlined, SearchOutlined } from "@ant-design/icons"; 2 | import { Button, Input, Space, Typography } from "antd"; 3 | import { useRef, useState } from "react"; 4 | import Highlighter from "react-highlight-words"; 5 | import { decodeHtmlEntities } from "../helpers/formatData"; 6 | 7 | const useTableSearch = () => { 8 | const [searchText, setSearchText] = useState(""); 9 | const [searchedColumn, setSearchedColumn] = useState(""); 10 | const searchInput = useRef(null); 11 | 12 | const handleSearch = (selectedKeys, confirm, dataIndex) => { 13 | confirm(); 14 | setSearchText(selectedKeys[0]); 15 | setSearchedColumn(dataIndex); 16 | }; 17 | 18 | const handleReset = (clearFilters, selectedKeys, confirm, dataIndex) => { 19 | clearFilters(); 20 | setSearchText(""); 21 | confirm(); 22 | setSearchedColumn(dataIndex); 23 | }; 24 | 25 | const getColumnSearchProps = (dataIndex) => ({ 26 | filterDropdown: ({ 27 | setSelectedKeys, 28 | selectedKeys, 29 | confirm, 30 | clearFilters, 31 | close, 32 | }) => ( 33 |
e.stopPropagation()} 38 | > 39 | 44 | setSelectedKeys(e.target.value ? [e.target.value] : []) 45 | } 46 | onPressEnter={() => handleSearch(selectedKeys, confirm, dataIndex)} 47 | style={{ 48 | marginBottom: 8, 49 | display: "block", 50 | }} 51 | /> 52 | 53 | 64 | 76 | 89 | 98 | 99 |
100 | ), 101 | filterIcon: (filtered) => 102 | filtered ? ( 103 | 104 | ) : ( 105 | 111 | ), 112 | onFilter: (value, record) => { 113 | return record[dataIndex] 114 | ?.toString() 115 | ?.toUpperCase() 116 | ?.includes(value.toUpperCase()); 117 | }, 118 | onFilterDropdownOpenChange: (visible) => { 119 | if (visible) { 120 | setTimeout(() => searchInput.current?.select(), 100); 121 | } 122 | }, 123 | }); 124 | const renderInputSearch = (dataIndex, isCopyable = false) => ({ 125 | render: (text) => { 126 | return searchedColumn === dataIndex ? ( 127 | isCopyable ? ( 128 | 129 | 138 | 143 | 144 | ) : ( 145 | 154 | ) 155 | ) : ( 156 | decodeHtmlEntities(text, isCopyable) 157 | ); 158 | }, 159 | }); 160 | return { getColumnSearchProps, renderInputSearch }; 161 | }; 162 | 163 | export default useTableSearch; 164 | -------------------------------------------------------------------------------- /src/hooks/useTableSearchServerside.jsx: -------------------------------------------------------------------------------- 1 | import { FileSearchOutlined, SearchOutlined } from "@ant-design/icons"; 2 | import { Button, Input, Space } from "antd"; 3 | import { useRef, useState } from "react"; 4 | import Highlighter from "react-highlight-words"; 5 | const useTableSearch = () => { 6 | const [searchText, setSearchText] = useState(""); 7 | const [searchedColumn, setSearchedColumn] = useState(""); 8 | const searchInput = useRef(null); 9 | 10 | const handleSearch = (selectedKeys, confirm, dataIndex) => { 11 | confirm(); 12 | setSearchText(selectedKeys[0]); 13 | setSearchedColumn(dataIndex); 14 | }; 15 | 16 | const handleReset = (clearFilters, selectedKeys, confirm, dataIndex) => { 17 | clearFilters(); 18 | setSearchText(""); 19 | confirm(); 20 | setSearchedColumn(dataIndex); 21 | }; 22 | 23 | const getColumnSearchProps = (dataIndex) => ({ 24 | filterDropdown: ({ 25 | setSelectedKeys, 26 | selectedKeys, 27 | confirm, 28 | clearFilters, 29 | close, 30 | }) => { 31 | return ( 32 |
e.stopPropagation()} 37 | > 38 | 43 | setSelectedKeys(e.target.value ? [e.target.value] : []) 44 | } 45 | onPressEnter={() => handleSearch(selectedKeys, confirm, dataIndex)} 46 | style={{ 47 | marginBottom: 8, 48 | display: "block", 49 | }} 50 | /> 51 | 52 | 63 | 75 | 88 | 97 | 98 |
99 | ); 100 | }, 101 | filterIcon: (filtered) => 102 | filtered ? ( 103 | 104 | ) : ( 105 | 111 | ), 112 | onFilter: (value, record) => { 113 | return record[dataIndex] 114 | .toString() 115 | .toUpperCase() 116 | .includes(value.toUpperCase()); 117 | }, 118 | onFilterDropdownOpenChange: (visible) => { 119 | if (visible) { 120 | setTimeout(() => searchInput.current?.select(), 100); 121 | } 122 | }, 123 | }); 124 | 125 | const renderInputSearch = (dataIndex) => ({ 126 | render: (text) => { 127 | return searchedColumn === dataIndex ? ( 128 | 137 | ) : ( 138 | text 139 | ); 140 | }, 141 | }); 142 | 143 | return { getColumnSearchProps, renderInputSearch }; 144 | }; 145 | 146 | export default useTableSearch; 147 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.js: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from "react"; 2 | 3 | export const useWindowSize = () => { 4 | const [size, setSize] = useState([0, 0]); 5 | useLayoutEffect(() => { 6 | function updateSize() { 7 | setSize([window.innerWidth, window.innerHeight]); 8 | } 9 | window.addEventListener("resize", updateSize); 10 | updateSize(); 11 | return () => window.removeEventListener("resize", updateSize); 12 | }, []); 13 | return size; 14 | }; 15 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: 'Inter'; 7 | font-synthesis: none; 8 | text-rendering: optimizeLegibility; 9 | } 10 | 11 | body{ 12 | margin: 0; 13 | } 14 | 15 | .customExportModal .ant-modal-body div{ 16 | display: grid; 17 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 6 | import { ConfigProvider } from "antd"; 7 | 8 | const queryClient = new QueryClient(); 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 26 | 27 | 28 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/pages/admin/Dashboard/index.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal } from "antd"; 2 | import { useAdminAuthStore } from "../../../store/admin/useAuth"; 3 | import { ExclamationCircleFilled } from "@ant-design/icons"; 4 | 5 | const { confirm } = Modal; 6 | const Dashboard = () => { 7 | const { reset } = useAdminAuthStore(); 8 | 9 | const handleLogout = () => { 10 | confirm({ 11 | title: "LOGOUT", 12 | icon: , 13 | content: "CONFIRMATION message", 14 | okText: "CONFIRM", 15 | okButtonProps: { 16 | danger: true, 17 | }, 18 | onOk() { 19 | reset(); 20 | }, 21 | }); 22 | }; 23 | 24 | return ( 25 | <> 26 |
27 |

Dashboard

28 | 31 |
32 | 33 | ); 34 | }; 35 | 36 | export default Dashboard; 37 | -------------------------------------------------------------------------------- /src/pages/admin/Login.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, Input } from "antd"; 2 | import { loginBg, logo } from "../../assets/images"; 3 | import { useLoginApi } from "../../services/requests/admin/useLoginAuth"; 4 | 5 | const Login = () => { 6 | const [form] = Form.useForm(); 7 | const { isLoading, mutate } = useLoginApi(); 8 | 9 | const onFinish = (values) => { 10 | mutate(values); 11 | }; 12 | 13 | form.setFieldsValue({ 14 | username: "admin", 15 | password: "Tphr@2024!", 16 | }); 17 | 18 | return ( 19 |
26 |
27 |
28 |
29 | 30 |
31 | ADMIN 32 |
33 |
34 |
35 |
42 | 51 | 52 | 53 | 54 |
55 | 64 | 65 | 66 |
67 | 68 | 69 | 78 | 79 |
80 |
81 |
82 | 83 | Don’t have an account?{" "} 84 | 85 | 86 | Sign Up! 87 | 88 |
89 |
90 |
91 |
92 | ); 93 | }; 94 | 95 | export default Login; 96 | -------------------------------------------------------------------------------- /src/pages/admin/index.jsx: -------------------------------------------------------------------------------- 1 | import Login from "./Login"; 2 | import Dashboard from "./Dashboard"; 3 | 4 | export { Login, Dashboard }; 5 | -------------------------------------------------------------------------------- /src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider, createBrowserRouter } from "react-router-dom"; 2 | import AdminRoutes from "./pageRoutes/AdminRoutes"; 3 | 4 | const RootRoutes = () => { 5 | const router = createBrowserRouter([ 6 | { path: "/admin/*", Component: AdminRoutes }, 7 | { path: "/*", Component: AdminRoutes }, 8 | ]); 9 | 10 | return ; 11 | }; 12 | 13 | export default RootRoutes; 14 | -------------------------------------------------------------------------------- /src/routes/pageRoutes/AdminRoutes.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Route, Routes } from "react-router-dom"; 2 | import { Auth, UnAuth } from "../validateAuth"; 3 | import { useAdminAuthStore } from "../../store/admin/useAuth"; 4 | import { Dashboard, Login } from "../../pages/admin"; 5 | 6 | const AdminRoutes = () => { 7 | return ( 8 | 9 | } /> 10 | 11 | } 13 | > 14 | } /> 15 | 16 | 17 | }> 18 | } /> 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default AdminRoutes; 25 | -------------------------------------------------------------------------------- /src/routes/validateAuth.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet, useLocation } from "react-router-dom"; 2 | 3 | export const Auth = ({ store, redirect }) => { 4 | const { token, userData } = store(); 5 | const location = useLocation(); 6 | 7 | return token && userData ? ( 8 | 9 | ) : ( 10 | 11 | ); 12 | }; 13 | 14 | export const UnAuth = ({ store, redirect }) => { 15 | const { token, userData } = store(); 16 | const location = useLocation(); 17 | 18 | return token && userData ? ( 19 | 20 | ) : ( 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/services/api/admin/auth.js: -------------------------------------------------------------------------------- 1 | import { axiosAuth } from "../../axios"; 2 | 3 | export const loginApi = async (payload) => { 4 | return await axiosAuth.post("/api/v1/cms/auth/login", payload); 5 | }; 6 | -------------------------------------------------------------------------------- /src/services/axios.js: -------------------------------------------------------------------------------- 1 | import { message } from "antd"; 2 | import axios from "axios"; 3 | import { useAdminAuthStore } from "../store/admin/useAuth"; 4 | import { useUserAuthStore } from "../store/user/useAuth"; 5 | 6 | export const axiosAuth = axios.create({ 7 | baseURL: import.meta.env.VITE_BASE_URL, 8 | headers: { 9 | "Content-Type": "application/json", 10 | "Access-Control-Allow-Origin": "*", 11 | }, 12 | }); 13 | 14 | export const getUsersValues = { 15 | admin: "ADMIN", 16 | user: "USER", 17 | }; 18 | 19 | export const getUserToken = (user = getUsersValues.user) => { 20 | if (user === getUsersValues.admin) { 21 | return useAdminAuthStore.getState(); 22 | } else { 23 | return useUserAuthStore.getState(); 24 | } 25 | }; 26 | 27 | export const createAxiosInstanceWithInterceptor = (type = "data", user) => { 28 | const headers = { 29 | "Access-Control-Allow-Origin": "*", 30 | }; 31 | 32 | if (type === "data") { 33 | headers["Content-Type"] = "application/json"; 34 | } else { 35 | headers["content-type"] = "multipart/form-data"; 36 | } 37 | 38 | const instance = axios.create({ 39 | baseURL: import.meta.env.VITE_BASE_URL, 40 | headers, 41 | }); 42 | 43 | instance.interceptors.request.use(async (config) => { 44 | try { 45 | const { token } = getUserToken(user); 46 | if (token) { 47 | config.headers.Authorization = `Bearer ${token}`; 48 | } else { 49 | throw new Error("Authorization token not found."); 50 | } 51 | } catch (error) { 52 | console.error({ error }); 53 | } 54 | return config; 55 | }); 56 | 57 | instance.interceptors.response.use( 58 | (response) => { 59 | // Any status code that lie within the range of 2xx cause this function to trigger 60 | // Do something with response data 61 | return response; 62 | }, 63 | function (error) { 64 | const { reset } = getUserToken(user); 65 | const errMessage = error.response?.data; 66 | if (errMessage?.message === "Invalid token." || errMessage?.code == 300) { 67 | message.warning( 68 | "Unable to process transaction. You have to login again." 69 | ); 70 | reset(); 71 | } 72 | // Any status codes that falls outside the range of 2xx cause this function to trigger 73 | // Do something with response error 74 | return Promise.reject(error); 75 | } 76 | ); 77 | 78 | return instance; 79 | }; 80 | -------------------------------------------------------------------------------- /src/services/requests/admin/useLoginAuth.js: -------------------------------------------------------------------------------- 1 | import { message } from "antd"; 2 | import { loginApi } from "../../api/admin/auth"; 3 | import { useMutation } from "@tanstack/react-query"; 4 | import { useAdminAuthStore } from "../../../store/admin/useAuth"; 5 | 6 | export const useLoginApi = () => { 7 | const { setuserData, setToken } = useAdminAuthStore(); 8 | 9 | return useMutation({ 10 | mutationFn: loginApi, 11 | onSuccess: ({ data }) => { 12 | setuserData(data?.data); 13 | setToken(data.token); 14 | }, 15 | onError: (error) => { 16 | message.warning( 17 | error?.response?.data?.message || 18 | "Login failed. Please check your connection or contact support." 19 | ); 20 | }, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/store/admin/useAuth.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { createJSONStorage, persist } from "zustand/middleware"; 3 | 4 | export const useAdminAuthStore = create( 5 | persist( 6 | (set) => ({ 7 | userData: null, 8 | token: null, 9 | setuserData: (userData) => set({ userData }), 10 | setToken: (token) => set({ token }), 11 | reset: () => set({ userData: null, token: null }), 12 | }), 13 | { 14 | name: "adminAuth", 15 | storage: createJSONStorage(() => sessionStorage), 16 | } 17 | ) 18 | ); 19 | -------------------------------------------------------------------------------- /src/store/user/useAuth.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { createJSONStorage, persist } from "zustand/middleware"; 3 | 4 | export const useUserAuthStore = create( 5 | persist( 6 | (set) => ({ 7 | userData: null, 8 | token: null, 9 | setuserData: (userData) => set({ userData }), 10 | setToken: (token) => set({ token }), 11 | reset: () => set({ userData: null, token: null }), 12 | }), 13 | { 14 | name: "userAuth", 15 | storage: createJSONStorage(() => sessionStorage), 16 | } 17 | ) 18 | ); 19 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | corePlugins: { 4 | preflight: false, 5 | }, 6 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 7 | theme: { 8 | extend: { 9 | colors: { 10 | primaryColor: "#CC0011", 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | host: true, 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------