feat: complete day 20 react task
This commit is contained in:
+11
-5
@@ -16,9 +16,9 @@ const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_1",
|
||||
DB_NAME: "day_19",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
@@ -26,7 +26,7 @@ const config = {
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_DATABASE,
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
@@ -51,8 +51,14 @@ let sequelize = new Sequelize(
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize.sync();
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
console.log("Database & tables created!");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"dev": "node --watch --env-file=.env ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dev-dist
|
||||
|
||||
*.local
|
||||
release
|
||||
config.php
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,7 @@
|
||||
# Day 20
|
||||
|
||||
Read:
|
||||
|
||||
- https://www.notion.so/How-to-Use-Baas-00f549dda3a84dc48b352c79222f1a3a
|
||||
- https://www.notion.so/Create-Manage-Projects-With-Wireframe-Tool-df67b882f0c14735a0192d69dc3ff777
|
||||
|
||||
@@ -27,4 +28,3 @@ Read:
|
||||
10. Clone backend repo on src/backend/custom
|
||||
|
||||
11. Write APIs, and test locally.
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="/src/favicon.svg"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>inventorylynx</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="portal"></div>
|
||||
|
||||
<script
|
||||
type="module"
|
||||
src="/src/index.jsx"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"Components/*": ["src/components/*"],
|
||||
"Pages/*": ["src/pages/*"],
|
||||
"Utils/*": ["src/utils/*"],
|
||||
"Assets/*": ["src/assets/*"],
|
||||
"Context/*": ["src/context/*"],
|
||||
"Routes/*": ["src/routes/*"],
|
||||
"Hooks/*": ["src/hooks/*"],
|
||||
"Src/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
|
||||
{
|
||||
"name": "adminportal",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"tw": "npx tailwindcss -i ./src/index.css -o ./src/output.css --watch",
|
||||
"build": "vite build",
|
||||
"commit": "git add . && git commit -m \"Update\" && git pull && git push",
|
||||
"commit:script": "node git-script add,commit,pull,push message=\"Update | API SECTION RESTRUCTURE IN PROGRESS\" origin=wireframe",
|
||||
"preview": "vite preview",
|
||||
"generate-pwa-assets": "pwa-assets-generator --preset minimal public/mkd_logo.png"
|
||||
},
|
||||
"dependencies": {
|
||||
"@craftjs/core": "^0.2.0-beta.11",
|
||||
"@editorjs/attaches": "^1.3.0",
|
||||
"@editorjs/checklist": "^1.5.0",
|
||||
"@editorjs/code": "^2.8.0",
|
||||
"@editorjs/delimiter": "^1.3.0",
|
||||
"@editorjs/editorjs": "^2.26.5",
|
||||
"@editorjs/embed": "^2.5.3",
|
||||
"@editorjs/header": "^2.7.0",
|
||||
"@editorjs/image": "^2.8.1",
|
||||
"@editorjs/inline-code": "^1.4.0",
|
||||
"@editorjs/link": "^2.5.0",
|
||||
"@editorjs/list": "^1.8.0",
|
||||
"@editorjs/marker": "^1.3.0",
|
||||
"@editorjs/nested-list": "^1.3.0",
|
||||
"@editorjs/paragraph": "^2.9.0",
|
||||
"@editorjs/personality": "^2.0.2",
|
||||
"@editorjs/quote": "^2.5.0",
|
||||
"@editorjs/raw": "^2.4.0",
|
||||
"@editorjs/simple-image": "^1.5.1",
|
||||
"@editorjs/table": "^2.2.1",
|
||||
"@editorjs/underline": "^1.1.0",
|
||||
"@editorjs/warning": "^1.3.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/serialize": "^1.1.2",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
"@fontsource/poppins": "^4.5.10",
|
||||
"@fontsource/roboto-mono": "^5.0.16",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@fullcalendar/core": "^5.11.3",
|
||||
"@fullcalendar/daygrid": "^5.11.3",
|
||||
"@fullcalendar/interaction": "^5.11.3",
|
||||
"@fullcalendar/list": "^5.11.3",
|
||||
"@fullcalendar/react": "^5.11.2",
|
||||
"@fullcalendar/timegrid": "^5.11.3",
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@legendapp/state": "^0.23.4",
|
||||
"@mantine/core": "^6.0.19",
|
||||
"@mantine/hooks": "^6.0.19",
|
||||
"@react-google-maps/api": "^2.19.2",
|
||||
"@react-pdf-viewer/core": "^3.12.0",
|
||||
"@splidejs/react-splide": "^0.7.12",
|
||||
"@stripe/react-stripe-js": "^2.1.0",
|
||||
"@stripe/stripe-js": "^1.52.1",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@uppy/audio": "^1.1.1",
|
||||
"@uppy/aws-s3": "^3.2.1",
|
||||
"@uppy/aws-s3-multipart": "^3.4.1",
|
||||
"@uppy/compressor": "^1.0.2",
|
||||
"@uppy/core": "^3.7.1",
|
||||
"@uppy/dashboard": "^3.4.1",
|
||||
"@uppy/drag-drop": "^3.0.2",
|
||||
"@uppy/drop-target": "^2.0.1",
|
||||
"@uppy/dropbox": "^3.1.1",
|
||||
"@uppy/facebook": "^3.1.3",
|
||||
"@uppy/file-input": "^3.0.3",
|
||||
"@uppy/golden-retriever": "^3.1.0",
|
||||
"@uppy/google-drive": "^3.1.1",
|
||||
"@uppy/image-editor": "^2.1.2",
|
||||
"@uppy/instagram": "^3.1.3",
|
||||
"@uppy/onedrive": "^3.1.1",
|
||||
"@uppy/progress-bar": "^3.0.3",
|
||||
"@uppy/react": "^3.1.2",
|
||||
"@uppy/remote-sources": "^1.0.3",
|
||||
"@uppy/screen-capture": "^3.1.1",
|
||||
"@uppy/tus": "^3.4.0",
|
||||
"@uppy/webcam": "^3.3.1",
|
||||
"@uppy/xhr-upload": "^3.5.0",
|
||||
"apexcharts": "^3.40.0",
|
||||
"axios": "^1.5.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"codemirror": "^5.65.11",
|
||||
"codemirror-console": "^3.0.4",
|
||||
"codemirror-console-ui": "^3.0.4",
|
||||
"emoji-picker-textarea": "^1.0.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^10.16.4",
|
||||
"fullcalendar": "^5.11.3",
|
||||
"html-to-image": "^1.11.11",
|
||||
"jodit-react": "^1.3.39",
|
||||
"jszip": "^3.10.1",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "^4.0.2",
|
||||
"openai": "^4.24.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"pdfjs-dist": "^3.4.120",
|
||||
"pluralize": "^8.0.0",
|
||||
"pretty-rating-react": "^2.2.0",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-addons-update": "^15.6.3",
|
||||
"react-apexcharts": "^1.4.0",
|
||||
"react-calendar": "^4.2.1",
|
||||
"react-codemirror2": "^7.2.1",
|
||||
"react-confirm-alert": "^3.0.6",
|
||||
"react-contenteditable": "^3.3.7",
|
||||
"react-dnd": "^10.0.2",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-google-autocomplete": "^2.7.3",
|
||||
"react-google-maps": "^9.4.5",
|
||||
"react-hook-form": "^7.46.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-input-emoji": "^5.4.1",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-outside-click-handler": "^1.3.0",
|
||||
"react-pdf": "^7.6.0",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-ratings-declarative": "^3.4.1",
|
||||
"react-router": "^6.15.0",
|
||||
"react-router-dom": "^6.11.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-slick": "^0.29.0",
|
||||
"react-spinners": "^0.13.8",
|
||||
"react-timeago": "^7.2.0",
|
||||
"react-toggle": "^4.1.3",
|
||||
"redux": "^4.2.1",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"swiper": "^9.3.1",
|
||||
"tw-elements": "^1.0.0-beta2",
|
||||
"twilio-video": "^2.27.0",
|
||||
"uppy": "^3.20.0",
|
||||
"use-debounce": "^9.0.4",
|
||||
"xlsx": "^0.18.5",
|
||||
"yup": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@editorjs/link-autocomplete": "^0.1.0",
|
||||
"@editorjs/opensea": "^1.0.2",
|
||||
"@editorjs/translate-inline": "^1.0.0-rc.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@vite-pwa/assets-generator": "^0.0.8",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite": "^4.3.5",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-pwa": "^0.16.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
import React from "react";
|
||||
import {AuthProvider} from "Context/Auth";
|
||||
import {GlobalProvider} from "Context/Global";
|
||||
import Main from "./routes/Routes";
|
||||
import "@uppy/core/dist/style.css";
|
||||
import "@uppy/dashboard/dist/style.css";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import "react-loading-skeleton/dist/skeleton.css";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
|
||||
|
||||
const stripePromise = loadStripe("pk_test_51Ll5ukBgOlWo0lDUrBhA2W7EX2MwUH9AR5Y3KQoujf7PTQagZAJylWP1UOFbtH4UwxoufZbInwehQppWAq53kmNC00UIKSmebO");
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
||||
<AuthProvider>
|
||||
<GlobalProvider>
|
||||
<Router>
|
||||
<Elements stripe={stripePromise}>
|
||||
<Main />
|
||||
</Elements>
|
||||
</Router>
|
||||
</GlobalProvider>
|
||||
</AuthProvider>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
// export { default as LoginBg } from "./login-bg.jpg";
|
||||
export { default as LoginBgNew } from "./login-new-bg.png";
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 669 KiB |
@@ -0,0 +1,18 @@
|
||||
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
import React from "react";
|
||||
|
||||
export const CaretLeft = ({ className = "" }) => {
|
||||
return (
|
||||
<svg
|
||||
className={`${className}`}
|
||||
width="8"
|
||||
height="14"
|
||||
viewBox="0 0 8 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 13L1 7L7 1"
|
||||
stroke="black"
|
||||
stroke-width="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
export const CloseIcon = ({ className = "" }) => {
|
||||
return (
|
||||
<svg className={`${className}`} width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M19.4059 16.6337C20.198 17.4257 20.198 18.6139 19.4059 19.4059C19.0099 19.802 18.5149 20 18.0198 20C17.5248 20 17.0297 19.802 16.6337 19.4059L10 12.7723L3.36634 19.4059C2.9703 19.802 2.47525 20 1.9802 20C1.48515 20 0.990099 19.802 0.594059 19.4059C-0.19802 18.6139 -0.19802 17.4257 0.594059 16.6337L7.22772 10L0.594059 3.36634C-0.19802 2.57426 -0.19802 1.38614 0.594059 0.594059C1.38614 -0.19802 2.57426 -0.19802 3.36634 0.594059L10 7.22772L16.6337 0.594059C17.4257 -0.19802 18.6139 -0.19802 19.4059 0.594059C20.198 1.38614 20.198 2.57426 19.4059 3.36634L12.7723 10L19.4059 16.6337Z"
|
||||
fill="#636363"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const DangerIcon = ( { className } ) => {
|
||||
return (
|
||||
<svg className={ `${ className }` } width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M40 80C62.1333 80 80 62.1333 80 40C80 17.8667 62.1333 0 40 0C17.8667 0 0 17.8667 0 40C0 62.1333 17.8667 80 40 80ZM36.1932 46.9993V18.1818H43.9169V46.9993H36.1932ZM44.3697 54.4567C44.3697 55.6108 43.9879 56.5607 43.2244 57.3065C42.4432 58.0522 41.3867 58.4251 40.055 58.4251C38.7411 58.4251 37.6935 58.0522 36.9123 57.3065C36.131 56.5607 35.7404 55.6108 35.7404 54.4567C35.7404 53.2848 36.1399 52.326 36.9389 51.5803C37.7202 50.8345 38.7589 50.4616 40.055 50.4616C41.3512 50.4616 42.3988 50.8345 43.1978 51.5803C43.979 52.326 44.3697 53.2848 44.3697 54.4567Z" fill="#CF2A2A" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
import React, { useId } from "react";
|
||||
import MoonLoader from "react-spinners/MoonLoader";
|
||||
|
||||
const override = {
|
||||
borderColor: "red",
|
||||
};
|
||||
|
||||
export const Spinner = ({ size = 20, color = "#ffffff" }) => {
|
||||
const id = useId();
|
||||
return (
|
||||
<MoonLoader
|
||||
color={color}
|
||||
loading={true}
|
||||
cssOverride={override}
|
||||
size={size}
|
||||
// aria-label="Loading Spinner"
|
||||
data-testid={id}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
import { lazy } from "react";
|
||||
|
||||
export const CloseIcon = lazy(() => import("./CloseIcon").then((module) => ({ default: module.CloseIcon })));
|
||||
export const DangerIcon = lazy(() => import("./DangerIcon").then((module) => ({ default: module.DangerIcon })));
|
||||
export const Spinner = lazy(() => import("./Spinner").then((module) => ({ default: module.Spinner })));
|
||||
export const CaretLeft = lazy(() => import("./CaretLeft").then((module) => ({ default: module.CaretLeft })));
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
|
||||
import React, { useState } from "react";
|
||||
import classes from "./AddButton.module.css";
|
||||
|
||||
const AddButton = ({
|
||||
onClick,
|
||||
children = "Add New",
|
||||
showPlus = true,
|
||||
className,
|
||||
showChildren = true,
|
||||
}) => {
|
||||
const [animate, setAnimate] = useState(false);
|
||||
|
||||
const onClickHandle = () => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
setAnimate(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onAnimationEnd={() => setAnimate(false)}
|
||||
onClick={onClickHandle}
|
||||
className={`${animate && "animate-wiggle"} ${
|
||||
classes.button
|
||||
} relative flex h-[2.125rem] w-fit min-w-fit items-center justify-center overflow-hidden rounded-md border border-primaryBlue bg-indigo-600 px-[.6125rem] py-[.5625rem] text-sm font-medium leading-none text-white shadow-md shadow-indigo-600 ${className}`}
|
||||
>
|
||||
{showPlus ? "+" : null} {showChildren ? children : null}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButton;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
-webkit-transition-duration: 0.4s; /* Safari */
|
||||
transition-duration: 0.4s;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
.button:after {
|
||||
content: "";
|
||||
background-color: #6752e0;
|
||||
/* background: #4f46e5; */
|
||||
display: block;
|
||||
position: absolute;
|
||||
padding-top: 300%;
|
||||
padding-left: 350%;
|
||||
margin-left: -20px !important;
|
||||
margin-top: -120%;
|
||||
opacity: 0;
|
||||
transition: all 0.8s;
|
||||
}
|
||||
|
||||
.button:active:after {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
export {default as AddButton } from "./AddButton";
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const AddTags = ({tags, tag, setTagData}) => {
|
||||
// console.log('addtag--->',{tags, tag, setTagData})
|
||||
return (
|
||||
<li className="px-2 py-1 text-sm inline-flex items-center gap-2 rounded bg-blue-500 text-white"><span>{tag}</span> <button onClick={() => setTagData(tags.filter(tags=> tags.name !== tag))} className="flex items-center justify-center h-5 w-5 rounded-full bg-blue-100 hover:bg-blue-200 duration-300"><span className="leading-0 -mt-1 text-black">×</span></button></li>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTags;
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const AddTagsTemplate = lazy(()=> import("./AddTagsTemplate"))
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
|
||||
import React from "react";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import { PiUsersThreeFill } from "react-icons/pi";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import { MdDashboard } from "react-icons/md";
|
||||
import { GlobalContext } from "Context/Global";
|
||||
import { AuthContext, tokenExpireError } from "Context/Auth";
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{
|
||||
to: "/admin/dashboard",
|
||||
text: "Dashboard",
|
||||
icon: <MdDashboard className="text-xl text-[#A8A8A8]" />,
|
||||
value: "admin",
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
to: "/admin/cms",
|
||||
text: " Cms",
|
||||
icon: <MdDashboard className="text-xl text-[#A8A8A8]" />,
|
||||
value: "cms",
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
to: "/admin/email",
|
||||
text: " Emails",
|
||||
icon: <MdDashboard className="text-xl text-[#A8A8A8]" />,
|
||||
value: "email",
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
to: "/admin/photo",
|
||||
text: " Photos",
|
||||
icon: <MdDashboard className="text-xl text-[#A8A8A8]" />,
|
||||
value: "photo",
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
to: "/admin/users",
|
||||
text: " Users",
|
||||
icon: <MdDashboard className="text-xl text-[#A8A8A8]" />,
|
||||
value: "users",
|
||||
},
|
||||
|
||||
|
||||
|
||||
{
|
||||
to: "/admin/profile",
|
||||
text: "Profile",
|
||||
icon: <PiUsersThreeFill className="text-xl text-[#A8A8A8]" />,
|
||||
value: "profile",
|
||||
},
|
||||
];
|
||||
|
||||
export const AdminHeader = () => {
|
||||
const {
|
||||
state: { isOpen, path },
|
||||
dispatch: gobalDispatch,
|
||||
} = React.useContext(GlobalContext);
|
||||
const { state: authState, dispatch } = React.useContext(AuthContext);
|
||||
const [openDropdown, setOpenDropdown] = React.useState(false);
|
||||
const [isHovering, setIsHovering] = React.useState(false);
|
||||
|
||||
// const handleMouseOver = () => {
|
||||
// setIsHovering(true);
|
||||
// };
|
||||
|
||||
// const handleMouseOut = () => {
|
||||
// setIsHovering(false);
|
||||
// };
|
||||
let toggleOpen = (open) => {
|
||||
gobalDispatch({
|
||||
type: "OPEN_SIDEBAR",
|
||||
payload: { isOpen: open },
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const result = await sdk.getProfile();
|
||||
dispatch({
|
||||
type: "UPDATE_PROFILE",
|
||||
payload: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
tokenExpireError(
|
||||
dispatch,
|
||||
error.response.data.message
|
||||
? error.response.data.message
|
||||
: error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
// sidebar-holder
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`z-50 flex max-h-screen flex-1 flex-col border border-[#E0E0E0] bg-white py-4 text-[#A8A8A8] transition-all ${
|
||||
isOpen
|
||||
? "fixed h-screen w-[15rem] min-w-[15rem] max-w-[15rem] md:relative"
|
||||
: "relative min-h-screen w-[4.2rem] min-w-[4.2rem] max-w-[4.2rem] bg-black text-white"
|
||||
} `}
|
||||
>
|
||||
<div
|
||||
className={`text-[#393939] ${
|
||||
isOpen ? "flex w-full" : "flex items-center justify-center"
|
||||
} `}
|
||||
>
|
||||
<div></div>
|
||||
{isOpen && (
|
||||
<div className="text-2xl font-bold">
|
||||
<Link to="/">
|
||||
<h4 className="flex cursor-pointer items-center px-4 pb-4 font-sans font-bold">
|
||||
Baas Brand{" "}
|
||||
</h4>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-fit w-auto flex-1">
|
||||
<div className="sidebar-list w-auto">
|
||||
<ul className="flex flex-wrap px-2 text-sm">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<li className="block w-full list-none" key={item.value}>
|
||||
<NavLink
|
||||
to={item.to}
|
||||
className={`${ path == item.value ? "active-nav" : "" } `}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{item.icon}
|
||||
{isOpen && <span>{item.text}</span>}
|
||||
</div>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="mr-3 cursor-pointer rounded-lg border border-[#E0E0E0] bg-white p-2 text-2xl text-gray-400">
|
||||
<span onClick={() => toggleOpen(!isOpen)}>
|
||||
<svg
|
||||
className={`transition-transform ${
|
||||
!isOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
xmlns="http:www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12ZM10.4142 11L11.7071 9.70711C12.0976 9.31658 12.0976 8.68342 11.7071 8.29289C11.3166 7.90237 10.6834 7.90237 10.2929 8.29289L7.82322 10.7626C7.13981 11.446 7.13981 12.554 7.82322 13.2374L10.2929 15.7071C10.6834 16.0976 11.3166 16.0976 11.7071 15.7071C12.0976 15.3166 12.0976 14.6834 11.7071 14.2929L10.4142 13H16C16.5523 13 17 12.5523 17 12C17 11.4477 16.5523 11 16 11H10.4142Z"
|
||||
fill="#A8A8A8"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminHeader;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const AdminHeader = lazy(()=> import("./AdminHeader"))
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
|
||||
|
||||
import React, { Suspense, memo } from "react";
|
||||
|
||||
import {AdminHeader} from "Components/AdminHeader";
|
||||
import {TopHeader} from "Components/TopHeader";
|
||||
import { Spinner } from "Assets/svgs";
|
||||
const navigation = []
|
||||
const AdminWrapper = ({ children }) => {
|
||||
return (
|
||||
<div id="admin_wrapper" className={`flex w-full max-w-full flex-col bg-white`}>
|
||||
<div className={`flex min-h-screen w-full max-w-full `}>
|
||||
<AdminHeader
|
||||
|
||||
/>
|
||||
<div className={`mb-20 w-full overflow-hidden`}>
|
||||
<TopHeader />
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
className={`flex h-screen w-full items-center justify-center`}
|
||||
>
|
||||
<Spinner size={100} color="#2CC9D5" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-full overflow-y-auto overflow-x-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AdminWrapper);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
import {lazy} from 'react'
|
||||
export const AdminWrapper = lazy( ()=> import('./AdminWrapper'))
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
import React from "react";
|
||||
import { CaretLeft } from "Assets/svgs";
|
||||
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
const BackButton = ({ text = "back", link }) => {
|
||||
return (
|
||||
<div>
|
||||
<NavLink className="flex items-center gap-3 " to={link ? link : -1}>
|
||||
<CaretLeft />
|
||||
{text}
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackButton;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from "react";
|
||||
|
||||
export const BackButton = lazy(()=> import("./BackButton"))
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
|
||||
import { useState } from "react";
|
||||
// import { formatDate } from '@fullcalendar/core'
|
||||
import '@fullcalendar/react/dist/vdom';
|
||||
import FullCalendar, { formatDate } from "@fullcalendar/react";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import { Modal } from 'Components/Modal/Modal';
|
||||
import ModalPrompt from 'Components/Modal/ModalPrompt';
|
||||
import "./calendar.css"
|
||||
|
||||
|
||||
const Calendar = ({defaulEvents, setEvents, isCalendarFullWidth = true}) => {
|
||||
const [currentEvents, setCurrentEvents] = useState([]);
|
||||
const [showAddEventmodal, setShowAddEventmodal] = useState(false);
|
||||
const [showDeleteEventmodal, setShowDeleteEventmodal] = useState(false);
|
||||
const [newEventName, setNewEventName] = useState('');
|
||||
const [currentSelectedDate, setCurrentSelectedDate] = useState();
|
||||
const [currentSelectedEvent, setCurrentSelectedEvent] = useState();
|
||||
|
||||
const handleDateClick = (selected) => {
|
||||
setShowAddEventmodal(true)
|
||||
setCurrentSelectedDate(selected)
|
||||
// console.log(selected)
|
||||
};
|
||||
|
||||
|
||||
const handleEventsSet = (events) => {
|
||||
setCurrentEvents(events)
|
||||
if(setEvents){
|
||||
setEvents(events)
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddModalSubmit = () =>{
|
||||
const calendarApi = currentSelectedDate.view.calendar;
|
||||
calendarApi.unselect();
|
||||
|
||||
if (newEventName) {
|
||||
setShowAddEventmodal(false)
|
||||
calendarApi.addEvent({
|
||||
id: `${currentSelectedDate.dateStr}-${newEventName}`,
|
||||
title: newEventName,
|
||||
start: currentSelectedDate.startStr,
|
||||
end: currentSelectedDate.endStr,
|
||||
allDay: currentSelectedDate.allDay,
|
||||
});
|
||||
}
|
||||
setNewEventName('')
|
||||
|
||||
}
|
||||
|
||||
const handleEventClick = (selected) => {
|
||||
// console.log(selected)
|
||||
setCurrentSelectedEvent(selected)
|
||||
setShowDeleteEventmodal(true)
|
||||
};
|
||||
|
||||
const handleDeletEventClick = () => {
|
||||
currentSelectedEvent.event.remove()
|
||||
setShowDeleteEventmodal(false)
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<div className="m-5">
|
||||
<div className="flex justify-center items-center">
|
||||
{/* CALENDAR SIDEBAR */}
|
||||
<div className="flex-col w-[15%] p-4 rounded-md bg-slate-100">
|
||||
<h5>Events</h5>
|
||||
<ul className="space-y-2 mt-2 rounded-md text-center">
|
||||
{currentEvents.map((event) => (
|
||||
<li
|
||||
key={event.id}
|
||||
className="mx-3 rounded-md bg-slate-300 border text-slate-500 font-medium hover:border-slate-100 hover:shadow p-4"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
{event.title}
|
||||
</div>
|
||||
<p>
|
||||
{formatDate(event.start, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* CALENDAR */}
|
||||
<div className={`flex w-[85%] ml-4 ${isCalendarFullWidth ? 'calendar-full-width' : ''}`}>
|
||||
<FullCalendar
|
||||
height="500px"
|
||||
// aspectRatio={5}
|
||||
viewHeight={"200px"}
|
||||
eventBorderColor="purple"
|
||||
eventBackgroundColor="purple"
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
listPlugin,
|
||||
]}
|
||||
headerToolbar={{
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: "dayGridMonth,timeGridWeek,timeGridDay,listMonth",
|
||||
}}
|
||||
initialView="dayGridMonth"
|
||||
editable={true}
|
||||
selectable={true}
|
||||
selectMirror={true}
|
||||
dayMaxEvents={true}
|
||||
select={handleDateClick}
|
||||
eventClick={handleEventClick}
|
||||
eventsSet={(events) => handleEventsSet(events)}
|
||||
initialEvents={defaulEvents}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={"Add Event"}
|
||||
isOpen={showAddEventmodal}
|
||||
modalCloseClick={() => setShowAddEventmodal(false)}
|
||||
modalHeader={true}
|
||||
classes={'w-1/2 '}
|
||||
>
|
||||
<div className="flex flex-col flex-wrap justify-center items-center">
|
||||
<input className="mb-3" type="text" name="addEvent" value={newEventName} onChange={(e)=> setNewEventName(e.target.value)} />
|
||||
<button className="p-4 bg-slate-800 text-white rounded-md" onClick={handleAddModalSubmit}>Add</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{
|
||||
showDeleteEventmodal &&
|
||||
<ModalPrompt
|
||||
closeModalFunction={()=>{setShowDeleteEventmodal(false)}}
|
||||
message="Are you sure you want to delete this event?"
|
||||
title = "Delete Event"
|
||||
loading = {false}
|
||||
actionHandler={handleDeletEventClick}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calendar;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
.calendar-full-width .fc.fc-media-screen.fc-direction-ltr{
|
||||
width:100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
export {default as Calendar} from "./Calendar"
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
|
||||
import React, { memo, useEffect, useRef, useState } from "react";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import { AuthContext, tokenExpireError } from "Context/Auth";
|
||||
import { GlobalContext, showToast } from "Context/Global";
|
||||
import { Spinner } from "Assets/svgs";
|
||||
|
||||
const sdk = new MkdSDK();
|
||||
|
||||
const CameraToUpload = ({ onSave = undefined, uploadSuccess = undefined }) => {
|
||||
const { dispatch, state } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch, state: globalState } =
|
||||
React.useContext(GlobalContext);
|
||||
|
||||
const photoTrayRef = useRef(null);
|
||||
const videoRef = useRef(null);
|
||||
const canvasRef = useRef(null);
|
||||
const [profilePictures, setProfileProfilePictures] = useState();
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [showCamera, setShowCamera] = useState(false);
|
||||
const [useFrontCam, setUseFrontCam] = useState(true);
|
||||
const [photos, setPhotos] = useState([]);
|
||||
|
||||
const constraints = {
|
||||
video: {
|
||||
facingMode: { exact: useFrontCam ? "user" : "environment" },
|
||||
advanced: [{ zoom: 1 }],
|
||||
},
|
||||
};
|
||||
|
||||
const handleCapture = () => {
|
||||
const video = videoRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
const aspectRatio = video.videoWidth / video.videoHeight;
|
||||
|
||||
// Calculate the aspect ratio of the video
|
||||
let canvasWidth = video.offsetWidth;
|
||||
let canvasHeight = canvasWidth / aspectRatio;
|
||||
|
||||
if (canvasHeight > video.offsetHeight) {
|
||||
canvasHeight = video.offsetHeight;
|
||||
canvasWidth = canvasHeight * aspectRatio;
|
||||
}
|
||||
|
||||
// Set the canvas size based on the aspect ratio of the video
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
|
||||
// Draw the current video frame on the canvas
|
||||
const context = canvas.getContext("2d");
|
||||
context.drawImage(video, 0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// Get the data URL of the canvas
|
||||
// const dataUrl = canvasRef.current.toDataURL("image/png");
|
||||
// setProfileProfilePictures(dataUrl);
|
||||
// fileUpload(dataUrl);
|
||||
// stopCapture();
|
||||
// Draw the video frame on the canvas
|
||||
// const context = canvas.getContext("2d");
|
||||
// context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Convert the canvas data to a blob
|
||||
canvas.toBlob((blob) => {
|
||||
// Upload the blob to the server
|
||||
// const formData = new FormData();
|
||||
setPhotos((prev) => [...prev, blob]);
|
||||
if (!photos.length) {
|
||||
photoTrayRef.current.style.maxHeight = `150px`;
|
||||
}
|
||||
// formData.append("file", blob);
|
||||
// fileUpload(blob);
|
||||
// console.log(formData);
|
||||
// stopCapture();
|
||||
// fetch('/upload', { method: 'POST', body: formData });
|
||||
});
|
||||
};
|
||||
|
||||
const handleStream = (stream) => {
|
||||
const video = videoRef.current;
|
||||
video.style.display = "block";
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
};
|
||||
async function uploadFunction(formData) {
|
||||
try {
|
||||
let uploadResult = await sdk.uploadImage(formData);
|
||||
|
||||
if (!uploadResult.error) {
|
||||
return uploadResult?.url; // Return the response data from the server
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!uploadSuccess) {
|
||||
throw new Error("uploadSuccess is not a function");
|
||||
}
|
||||
setSubmitLoading(true);
|
||||
try {
|
||||
if (photos && photos.length) {
|
||||
const uploadPromises = photos.map(async (item) => {
|
||||
let formData = new FormData();
|
||||
formData.append("file", item);
|
||||
|
||||
// Perform the upload operation for 'item' and return the result
|
||||
return uploadFunction(formData); // Replace 'uploadFunction' with your actual upload logic
|
||||
});
|
||||
const uploadResults = await Promise.all(uploadPromises);
|
||||
// Process uploadResults if needed
|
||||
console.log(uploadResults);
|
||||
showToast(globalDispatch, "Upload Successful", 5000, "success");
|
||||
uploadSuccess(uploadResults);
|
||||
}
|
||||
setSubmitLoading(false);
|
||||
} catch (error) {
|
||||
setSubmitLoading(false);
|
||||
tokenExpireError(
|
||||
dispatch,
|
||||
error?.response?.data?.message
|
||||
? error?.response?.data?.message
|
||||
: error?.message
|
||||
);
|
||||
showToast(
|
||||
globalDispatch,
|
||||
error?.response?.data?.message
|
||||
? error?.response?.data?.message
|
||||
: error?.message,
|
||||
5000,
|
||||
"error"
|
||||
);
|
||||
console.log(error.message);
|
||||
// Handle errors
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (onSave) {
|
||||
onSave(photos);
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (error) => {
|
||||
console.error("Error accessing camera:", error);
|
||||
};
|
||||
|
||||
const startCapture = async () => {
|
||||
try {
|
||||
// Request access to the camera
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
setShowCamera(true);
|
||||
// Display the camera feed on the video element
|
||||
handleStream(stream);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const stopCapture = () => {
|
||||
// Stop all tracks in the stream
|
||||
if (videoRef.current.srcObject) {
|
||||
videoRef.current.srcObject.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
setPhotos(() => []);
|
||||
// // Clear the stream from state
|
||||
// setStream(null);
|
||||
setShowCamera(false);
|
||||
// Release the camera resources
|
||||
videoRef.current.srcObject = null;
|
||||
};
|
||||
const removeItem = (index) => {
|
||||
const tempPhotos = [...photos];
|
||||
tempPhotos.splice(index, 1);
|
||||
setPhotos(() => [...tempPhotos]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (photos.length === 0) {
|
||||
photoTrayRef.current.style.maxHeight = null;
|
||||
}
|
||||
// console.log(photos);
|
||||
}, [photos.length]);
|
||||
useEffect(() => {
|
||||
startCapture(true);
|
||||
}, [useFrontCam]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset
|
||||
className={`cus-input mt-5 block w-full cursor-pointer md:w-[23rem] `}
|
||||
>
|
||||
<div className="relative mb-2 flex h-[4.125rem] w-full items-center rounded-[1.25rem_1.25rem_0rem_1.25rem] border border-blue-600 ">
|
||||
<div
|
||||
id="profile_picture"
|
||||
// {...register("profile_picture")}
|
||||
className={`flex h-full grow items-center justify-center rounded-[1.25rem] bg-white text-[1.375rem] text-black`}
|
||||
>
|
||||
<span className="block md:hidden">Take picture now</span>
|
||||
<span className="hidden text-xs md:block">
|
||||
Switch to Mobile to take picture
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => startCapture()}
|
||||
className={`flex h-full w-[5.625rem] min-w-[5.625rem] items-center justify-center rounded-[0rem_1.25rem] bg-blue-600 md:hidden`}
|
||||
>
|
||||
camera
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-field-error mb-5 italic text-red-500"></p>
|
||||
</fieldset>
|
||||
|
||||
<div
|
||||
className={`${
|
||||
showCamera
|
||||
? "fixed left-0 right-0 top-0 z-[99999999] m-auto block h-screen w-full"
|
||||
: "hidden"
|
||||
}`}
|
||||
>
|
||||
<div className={`relative h-screen w-full bg-black`}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className={`relative z-[99999999] h-screen w-full object-cover`}
|
||||
style={{ transform: "rotateY(180deg)" }}
|
||||
/>
|
||||
<div
|
||||
className={`${
|
||||
submitLoading ? "flex" : "hidden"
|
||||
} absolute inset-0 z-[999999992] m-auto h-full w-full items-center justify-center `}
|
||||
>
|
||||
<Spinner size={100} color="#0EA5E9" />
|
||||
</div>
|
||||
{!submitLoading ? (
|
||||
<div
|
||||
className={`absolute inset-x-0 top-0 z-[999999991] m-auto flex h-fit w-full cursor-pointer flex-col flex-wrap items-center justify-center gap-5 pr-2 text-[2rem] text-black`}
|
||||
>
|
||||
<div
|
||||
onClick={() => stopCapture()}
|
||||
className={`relative flex h-[50px] w-[50px] cursor-pointer flex-col items-center justify-center self-end rounded-[50%] bg-white text-lg leading-[50px] text-black`}
|
||||
>
|
||||
x
|
||||
</div>
|
||||
<div className="flex w-full justify-end gap-5">
|
||||
<div
|
||||
className={`${
|
||||
photos.length && onSave ? "block" : "hidden"
|
||||
} text-sm`}
|
||||
onClick={() => handleSave()}
|
||||
>
|
||||
save {photos.length > 1 ? "photos" : "photo"}
|
||||
</div>
|
||||
<div
|
||||
className={`${photos.length ? "block" : "hidden"} text-sm`}
|
||||
onClick={() => handleUpload()}
|
||||
>
|
||||
upload {photos.length > 1 ? "photos" : "photo"}
|
||||
</div>
|
||||
<div
|
||||
className={`${photos.length ? "block" : "hidden"} text-sm`}
|
||||
onClick={() => setPhotos(() => [])}
|
||||
>
|
||||
clear
|
||||
</div>
|
||||
<div
|
||||
className="text-sm"
|
||||
onClick={() => setUseFrontCam(!useFrontCam)}
|
||||
>
|
||||
{useFrontCam ? "Use Rare Cam" : "Use Front Cam"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
onClick={() => handleCapture()}
|
||||
className={`absolute inset-x-0 ${
|
||||
photos.length ? "bottom-[5.5625rem]" : "bottom-8"
|
||||
} z-[999999991] m-auto flex h-[3.75rem] w-[3.75rem] cursor-pointer items-center justify-center rounded-[50%] bg-white text-[1rem] text-black transition-all`}
|
||||
>
|
||||
<div
|
||||
className={`relative inset-x-0 z-[999999991] m-auto flex h-[50%] w-[50%] cursor-pointer items-center justify-center rounded-md bg-black`}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
ref={photoTrayRef}
|
||||
className={`absolute bottom-0 left-0 right-0 z-[999999991] m-auto flex max-h-0 w-full items-center justify-start gap-2 overflow-x-auto overflow-y-hidden transition-all`}
|
||||
>
|
||||
{photos.length
|
||||
? photos.map((photo, photoKey) => (
|
||||
<div
|
||||
key={photoKey}
|
||||
className="relative h-full w-[5rem] min-w-[5rem] max-w-[5rem] rounded-md bg-white"
|
||||
>
|
||||
<img
|
||||
style={{ transform: "rotateY(180deg)" }}
|
||||
className="h-full w-full rounded-md"
|
||||
src={URL.createObjectURL(photo)}
|
||||
/>
|
||||
{!submitLoading ? (
|
||||
<div
|
||||
onClick={() => removeItem(photoKey)}
|
||||
className={`absolute right-0 top-0 z-[999999991] m-auto flex h-[1rem] w-[1rem] cursor-pointer items-center justify-center rounded-[50%] text-sm leading-[0.5rem_!important] text-red-600`}
|
||||
>
|
||||
x
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{ display: "none", transform: "rotateY(180deg)" }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CameraToUpload);
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
import {lazy} from "react";
|
||||
|
||||
export const CameraToUpload = lazy(()=> import("./CameraToUpload"))
|
||||
@@ -0,0 +1,509 @@
|
||||
|
||||
import React from "react";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
|
||||
import moment from "moment";
|
||||
import { ImagePreviewModal } from "Components/ImagePreviewModal";
|
||||
import { CreateNewRoomModal } from "Components/CreateNewRoomModal";
|
||||
import {
|
||||
UserCircleIcon,
|
||||
PaperAirplaneIcon,
|
||||
ArrowLeftIcon,
|
||||
PaperClipIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { FaceSmileIcon } from "@heroicons/react/24/outline";
|
||||
import { AuthContext, tokenExpireError } from "Context/Auth";
|
||||
import InputEmoji from "react-input-emoji";
|
||||
import { renderName } from "Components/CreateNewRoomModal";
|
||||
|
||||
const Chat = ({roles=[]}) => {
|
||||
const { state } = React.useContext(AuthContext);
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const [rooms, setRooms] = React.useState([]);
|
||||
const [messages, setMessages] = React.useState([]);
|
||||
const [chatId, setChatId] = React.useState();
|
||||
const otherUserId = React.useRef();
|
||||
const currentRooms = React.useRef();
|
||||
const inputRef = React.useRef(null);
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [roomId, setRoomId] = React.useState();
|
||||
const [file, setFile] = React.useState(null);
|
||||
const [previewModal, showPreviewModal] = React.useState(false);
|
||||
const [screenSize, setScreenSize] = React.useState(window.innerWidth);
|
||||
const [showContacts, setShowContacts] = React.useState(true);
|
||||
const [createRoom, setCreateRoom] = React.useState(false);
|
||||
const [filteredRooms, setFilteredRooms] = React.useState([]);
|
||||
|
||||
function setDimension(e) {
|
||||
if (e.currentTarget.innerWidth > 1024) {
|
||||
setShowContacts(true);
|
||||
}
|
||||
setScreenSize(e.currentTarget.innerWidth);
|
||||
}
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const handleClick = () => {
|
||||
inputRef.current.click();
|
||||
};
|
||||
|
||||
const cancelFileUpload = () => {
|
||||
showPreviewModal(false);
|
||||
setFile(null);
|
||||
inputRef.current.value = "";
|
||||
};
|
||||
|
||||
const formatDate = (time) => {
|
||||
let currentTime = moment(new Date());
|
||||
let messageDate = moment(time);
|
||||
if (currentTime.diff(messageDate, "days") > 1) {
|
||||
return moment(messageDate).format("Do MMMM");
|
||||
} else {
|
||||
return moment(messageDate).format("hh:mm A");
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < file.length; i++) {
|
||||
formData.append("file", file[i]);
|
||||
}
|
||||
try {
|
||||
const upload = await sdk.uploadImage(formData);
|
||||
await sendImageAsMessage(upload);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
async function handleKeyDown(event) {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
async function getRooms(filter) {
|
||||
try {
|
||||
if (filter) {
|
||||
return setFilteredRooms(
|
||||
rooms.filter((user) =>
|
||||
`${renderName(user).toLowerCase()}`.includes(filter.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const chats = await sdk.getMyRoom();
|
||||
if (chats && chats.list && chats.list[0]) {
|
||||
setRooms(chats.list);
|
||||
setFilteredRooms(chats.list);
|
||||
currentRooms.current = chats.list;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewRoom(otherUser) {
|
||||
try {
|
||||
const createdRoom = await sdk.createRoom({
|
||||
user_id: state.user,
|
||||
other_user_id: otherUser.id,
|
||||
});
|
||||
let newRoom = {
|
||||
chat_id: createdRoom.chat_id,
|
||||
create_at: new Date(),
|
||||
email: otherUser.email,
|
||||
first_name: otherUser.first_name,
|
||||
id: createdRoom.room_id,
|
||||
last_name: otherUser.last_name,
|
||||
other_user_id: otherUser.id,
|
||||
other_user_update_at: new Date(),
|
||||
photo: otherUser.photo,
|
||||
unread: 0,
|
||||
update_at: new Date(),
|
||||
user_id: state.user,
|
||||
user_update_at: new Date(),
|
||||
};
|
||||
|
||||
const updatedRoomList = [newRoom, ...rooms];
|
||||
setRooms(updatedRoomList);
|
||||
setFilteredRooms(updatedRoomList);
|
||||
currentRooms.current = updatedRoomList;
|
||||
setCreateRoom(false);
|
||||
|
||||
if (document.getElementById(`user-${otherUser.id}`)) {
|
||||
document.getElementById(`user-${otherUser.id}`).click();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
document.querySelector(".container-chat").firstChild.click();
|
||||
}, 200);
|
||||
}
|
||||
} catch (err) {
|
||||
setCreateRoom(false);
|
||||
document.getElementById(`user-${otherUser.id}`).click();
|
||||
}
|
||||
}
|
||||
|
||||
async function getChats(room_id, chat_id) {
|
||||
try {
|
||||
setRoomId(room_id);
|
||||
setChatId(chat_id);
|
||||
let date = new Date().toISOString().split("T")[0];
|
||||
const messages = await sdk.getChats(room_id, chat_id, date);
|
||||
if (messages && messages.model) {
|
||||
setMessages(messages.model.reverse());
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
try {
|
||||
let date = new Date().toISOString().split("T")[0];
|
||||
await sdk.postMessage({
|
||||
room_id: roomId,
|
||||
chat_id: chatId,
|
||||
user_id: state.user,
|
||||
message,
|
||||
date,
|
||||
});
|
||||
let newMessageObj = {
|
||||
message: message,
|
||||
user_id: state.user,
|
||||
is_image: false,
|
||||
timeStamp: new Date(),
|
||||
};
|
||||
const updatedMessages = [...messages, newMessageObj];
|
||||
setMessages(updatedMessages);
|
||||
setMessage("");
|
||||
} catch (err) {
|
||||
console.log("Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendImageAsMessage(upload) {
|
||||
try {
|
||||
let date = new Date().toISOString().split("T")[0];
|
||||
await sdk.postMessage({
|
||||
room_id: roomId,
|
||||
chat_id: chatId,
|
||||
user_id: state.user,
|
||||
message: upload.url,
|
||||
date,
|
||||
is_image: true,
|
||||
});
|
||||
let newMessageObj = {
|
||||
message: upload.url,
|
||||
user_id: state.user,
|
||||
is_image: true,
|
||||
timeStamp: new Date(),
|
||||
};
|
||||
const updatedMessages = [...messages, newMessageObj];
|
||||
setMessages(updatedMessages);
|
||||
showPreviewModal(false);
|
||||
setFile(null);
|
||||
inputRef.current.value = "";
|
||||
setMessage("");
|
||||
} catch (err) {
|
||||
console.log("Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function startPooling() {
|
||||
try {
|
||||
const pool = await sdk.startPooling(state.user);
|
||||
if (pool.message) {
|
||||
let newMessageObj = {
|
||||
message: pool.message,
|
||||
user_id: pool.user_id,
|
||||
is_image: false,
|
||||
timeStamp: new Date(),
|
||||
};
|
||||
if (pool.user_id === otherUserId.current) {
|
||||
setMessages((prevMessages) => [...prevMessages, newMessageObj]);
|
||||
setTimeout(async () => {
|
||||
await startPooling();
|
||||
}, 2000);
|
||||
} else {
|
||||
setTimeout(async () => {
|
||||
await startPooling();
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
setTimeout(async () => {
|
||||
startPooling();
|
||||
}, 1000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err.message);
|
||||
tokenExpireError(dispatch, err.message);
|
||||
if (err.message === "TOKEN_EXPIRED")
|
||||
window.location.replace(`/${state.role}/login/`);
|
||||
else
|
||||
setTimeout(async () => {
|
||||
startPooling();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
(async function () {
|
||||
await getRooms();
|
||||
await startPooling();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("resize", setDimension);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", setDimension);
|
||||
};
|
||||
}, [screenSize]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex-1 pt-4">
|
||||
<div className="main-body container m-auto flex h-full w-11/12 flex-col">
|
||||
<div className="main flex flex-1 flex-col">
|
||||
<div className="heading flex-2 hidden lg:block">
|
||||
<h1 className="mb-4 text-3xl text-gray-700">Chat</h1>
|
||||
</div>
|
||||
<div className="flex h-full flex-1">
|
||||
{showContacts && (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
className="inline-block h-10 w-10 rounded-full bg-blue-400 text-white"
|
||||
onClick={() => setCreateRoom(true)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-2 w-full flex-col pr-6 lg:flex lg:w-1/3">
|
||||
<div className="flex-2 px-2 pb-6">
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full border-b-2 border-gray-200 bg-transparent py-2 outline-none"
|
||||
placeholder="Search"
|
||||
onChange={(e) => getRooms(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container-chat h-full max-h-[70vh] flex-1 overflow-y-auto overflow-x-hidden px-2">
|
||||
{filteredRooms &&
|
||||
filteredRooms.map((room, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
id={`user-${room.other_user_id}`}
|
||||
className="entry mb-4 flex transform cursor-pointer items-center rounded bg-white p-4 shadow-md transition-transform duration-300 hover:scale-105"
|
||||
onClick={() => {
|
||||
getChats(room.id, room.chat_id);
|
||||
otherUserId.current = room.other_user_id;
|
||||
otherUserId.currentRoom = room;
|
||||
if (screenSize < 1024) {
|
||||
setShowContacts(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex-2">
|
||||
<div className="relative h-12 w-12">
|
||||
{room.photo ? (
|
||||
<img
|
||||
className="mx-auto h-12 w-12 rounded-full"
|
||||
src={room.photo}
|
||||
alt="user-photo"
|
||||
/>
|
||||
) : (
|
||||
<UserCircleIcon className="h-10 w-10" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 px-2">
|
||||
<div className="w-32 truncate">
|
||||
<span className="text-gray-800">
|
||||
{renderName(room)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-2 text-right">
|
||||
<div>
|
||||
<small className="text-gray-500">
|
||||
{formatDate(room.update_at)}
|
||||
</small>
|
||||
</div>
|
||||
{room.unread > 0 && (
|
||||
<div>
|
||||
<small className="inline-block h-6 w-6 rounded-full bg-green-500 text-center text-xs leading-6 text-white">
|
||||
{room.unread}
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screenSize > 1023 || (screenSize < 1024 && !showContacts) ? (
|
||||
<div className="flex max-h-[80vh] w-full flex-col lg:max-h-[60vh]">
|
||||
{otherUserId?.current ? (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex-3">
|
||||
<h2 className="mb-8 flex border-b-2 border-gray-200 py-1 text-xl">
|
||||
<span
|
||||
className="my-auto mr-4 lg:hidden"
|
||||
onClick={() => setShowContacts(true)}
|
||||
>
|
||||
<ArrowLeftIcon className="h-6 w-6" />
|
||||
</span>
|
||||
Chatting with{" "}
|
||||
<b className="ml-2">{`${renderName(
|
||||
otherUserId.currentRoom
|
||||
)}`}</b>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{messages && (
|
||||
<div className="max-h-[80vh] min-h-[60vh] flex-1 overflow-y-auto lg:max-h-[60vh]">
|
||||
{messages.map((message, idx) => (
|
||||
<div key={idx} className=" mb-4 flex">
|
||||
{message?.user_id !== state.user && (
|
||||
<div className="flex-2">
|
||||
<div className="relative h-12 w-12">
|
||||
<span className="absolute bottom-0 right-0 h-4 w-4 rounded-full border-2 border-white bg-gray-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex-1 px-2 ${
|
||||
message?.user_id === state.user && "text-right"
|
||||
}`}
|
||||
>
|
||||
<div className="inline-block">
|
||||
{message.is_image ? (
|
||||
<img
|
||||
src={message?.message}
|
||||
className="h-40 md:h-52 lg:h-80"
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
className={`${
|
||||
message?.user_id === state.user
|
||||
? "bg-gray-300 text-gray-700 "
|
||||
: "bg-blue-600 text-white"
|
||||
} whitespace-pre-line rounded-xl p-2 px-6 `}
|
||||
>
|
||||
{message?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="pl-4">
|
||||
<small className="text-gray-500">
|
||||
{moment(message.timestamp).format("hh:mm A")}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-2 items-end pb-10 pt-4">
|
||||
<div className="write flex rounded-lg bg-white shadow">
|
||||
{/* <div className="flex-3 flex content-center items-center text-center p-4 pr-0">
|
||||
<span className="block text-center text-gray-400 hover:text-gray-800">
|
||||
<FaceSmileIcon className="h-6 w-6" />
|
||||
</span>
|
||||
</div> */}
|
||||
|
||||
<div className="flex-1">
|
||||
{/* <textarea
|
||||
name="message"
|
||||
className="w-full block outline-none py-4 px-4 bg-transparent h-full max-"
|
||||
rows="1"
|
||||
placeholder="Type a message..."
|
||||
autoFocus
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
></textarea> */}
|
||||
<InputEmoji
|
||||
value={message}
|
||||
onChange={setMessage}
|
||||
// cleanOnEnter
|
||||
// onEnter={(e) => {}}
|
||||
placeholder="Type a message"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-2 flex w-32 content-center items-center p-2">
|
||||
<div className="flex-1 text-center">
|
||||
<span className="text-gray-400 hover:text-gray-800">
|
||||
<input
|
||||
className="hidden"
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/png, image/gif, image/jpeg"
|
||||
name="file"
|
||||
onChange={(e) => {
|
||||
setFile(e.target.files);
|
||||
showPreviewModal(true);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="inline-block align-text-bottom"
|
||||
>
|
||||
<PaperClipIcon className="h-6 w-6 text-black" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<button
|
||||
className="inline-block h-10 w-10 rounded-full bg-blue-400"
|
||||
onClick={() => sendMessage()}
|
||||
>
|
||||
<span className="inline-block align-text-bottom">
|
||||
<PaperAirplaneIcon className="h-6 w-6 text-white" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[70vh] w-full items-center justify-center text-7xl text-gray-700 ">
|
||||
Select a Chat to view
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{previewModal && file ? (
|
||||
<ImagePreviewModal
|
||||
file={file}
|
||||
handleFileUpload={handleFileUpload}
|
||||
cancelFileUpload={cancelFileUpload}
|
||||
/>
|
||||
) : null}
|
||||
{createRoom && (
|
||||
<CreateNewRoomModal
|
||||
roles={[...roles]}
|
||||
createNewRoom={createNewRoom}
|
||||
setCreateRoom={setCreateRoom}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const Chat = lazy(()=> import("./Chat"))
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { FiSend } from "react-icons/fi";
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { RxHamburgerMenu } from "react-icons/rx";
|
||||
|
||||
import Message from "./Message";
|
||||
import { homePage } from "./ChatUtils";
|
||||
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import { GlobalContext } from "Context/Global";
|
||||
import { AuthContext, tokenExpireError } from "Context/Auth";
|
||||
|
||||
const Chat = (props) => {
|
||||
const { toggleComponentVisibility, index, currentRoom } = props;
|
||||
|
||||
const { state, dispatch } = useContext(GlobalContext);
|
||||
const { dispatch: authDispatch } = useContext(AuthContext);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [showEmptyChat, setShowEmptyChat] = useState(true);
|
||||
const [conversation, setConversation] = useState([]);
|
||||
const [message, setMessage] = useState("");
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const bottomOfChatRef = useRef(null);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
useEffect(() => {
|
||||
if (bottomOfChatRef.current) {
|
||||
bottomOfChatRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [conversation]);
|
||||
|
||||
const sendMessage = async (e) => {
|
||||
// Don't send empty messages
|
||||
if (message.length < 1) {
|
||||
setErrorMessage("Please enter a message.");
|
||||
return;
|
||||
} else {
|
||||
setErrorMessage("");
|
||||
dispatch({
|
||||
type: "SETROOM",
|
||||
payload: { position: props.index, value: message },
|
||||
});
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Add the message to the conversation
|
||||
setConversation([
|
||||
...conversation,
|
||||
{ content: message, role: "user", content2: null, role2: "system" },
|
||||
]);
|
||||
|
||||
// Clear the message & remove empty chat
|
||||
setMessage("");
|
||||
setShowEmptyChat(false);
|
||||
|
||||
try {
|
||||
setGenerating(true);
|
||||
const response = await sdk.chatGPT(message);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setGenerating(false);
|
||||
|
||||
// Add the message to the conversation
|
||||
setConversation([
|
||||
...conversation,
|
||||
{
|
||||
content: message,
|
||||
role: "user",
|
||||
content2: data.Answer,
|
||||
role2: "system",
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
console.error(response);
|
||||
setGenerating(false);
|
||||
|
||||
setErrorMessage(response.statusText);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setGenerating(false);
|
||||
setErrorMessage(error.message);
|
||||
tokenExpireError(
|
||||
authDispatch,
|
||||
error?.response?.data?.messsage
|
||||
? error?.response?.data?.messsage
|
||||
: error?.message
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeypress = (e) => {
|
||||
// It's triggers by pressing the enter key
|
||||
if (e.keyCode == 13 && !e.shiftKey) {
|
||||
sendMessage(e);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
index !== currentRoom && "hidden"
|
||||
} flex max-w-full flex-1 flex-col dark:bg-gray-800`}
|
||||
>
|
||||
<div className="sticky top-0 z-10 flex items-center border-b border-white/20 bg-gray-800 pl-1 pt-1 text-gray-200 sm:pl-3 md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white dark:hover:text-white"
|
||||
onClick={toggleComponentVisibility}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<RxHamburgerMenu className="h-6 w-6 text-white" />
|
||||
</button>
|
||||
<h1 className="flex-1 text-center text-base font-normal">New chat</h1>
|
||||
<button type="button" className="px-3">
|
||||
<BsPlusLg className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="transition-width relative mx-auto flex h-full w-full flex-1 flex-col items-stretch overflow-hidden">
|
||||
<div className="overflow-scroll sm:h-[700px]">
|
||||
<div className="react-scroll-to-bottom--css-ikyem-79elbk dark:bg-gray-800">
|
||||
<div
|
||||
className={`react-scroll-to-bottom--css-ikyem-1n7m0yu ${
|
||||
!showEmptyChat && conversation.length ? " sm:mt-2" : "sm:mt-20"
|
||||
}`}
|
||||
>
|
||||
{!showEmptyChat && conversation.length > 0 ? (
|
||||
<div className="flex flex-col items-center bg-gray-800 text-sm">
|
||||
<Message
|
||||
generating={generating}
|
||||
conversation={conversation}
|
||||
message={message}
|
||||
genert
|
||||
/>
|
||||
<div className="h-32 w-full flex-shrink-0 md:h-48"></div>
|
||||
<div ref={bottomOfChatRef}></div>
|
||||
</div>
|
||||
) : null}
|
||||
{showEmptyChat ? (
|
||||
<div className="relative mx-auto mb-28 h-full max-w-[90%] py-10 sm:mb-0 sm:max-w-[80%]">
|
||||
<h1 className="flex items-center justify-center gap-2 text-center text-4xl font-bold text-[white]">
|
||||
ChatGPT
|
||||
</h1>
|
||||
<div className="mt-5 items-center gap-4 sm:mt-10 sm:flex sm:flex-wrap sm:justify-center">
|
||||
{homePage.map((item, index) => (
|
||||
<div key={index}>
|
||||
<div className="mt-4 flex items-center justify-center gap-2 sm:mt-0 sm:grid">
|
||||
<span className="flex justify-center text-center text-2xl text-[white] sm:mt-4">
|
||||
{item.icon}
|
||||
</span>
|
||||
<h3 className="text-lg text-gray-100 sm:my-3">
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-4 sm:mt-10">
|
||||
{item.details.map((details, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="mb-4 w-full rounded-md bg-gray-500 px-2 py-4 text-sm text-gray-100 sm:my-4 sm:w-[250px]"
|
||||
>
|
||||
{details}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col items-center text-sm dark:bg-gray-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient absolute bottom-0 left-0 w-full border-t bg-white pt-2 dark:border-white/20 dark:bg-gray-800 md:border-t-0 md:border-transparent md:!bg-transparent md:dark:border-transparent">
|
||||
<form className="stretch mx-2 flex flex-row gap-3 last:mb-2 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-2xl xl:max-w-3xl">
|
||||
<div className="relative flex h-full flex-1 flex-col items-stretch md:flex-col">
|
||||
<div className="relative flex h-[55px] w-full flex-grow items-center rounded-md border border-black/10 bg-white px-4 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-gray-700 dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]">
|
||||
<textarea
|
||||
// ref={textAreaRef}
|
||||
value={message}
|
||||
tabIndex={0}
|
||||
data-id="root"
|
||||
style={{
|
||||
height: "24px",
|
||||
maxHeight: "200px",
|
||||
overflowY: "hidden",
|
||||
}}
|
||||
// rows={1}
|
||||
placeholder="Send a message"
|
||||
className="m-0 flex h-6 w-full resize-none items-center border-0 bg-transparent focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent"
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={handleKeypress}
|
||||
/>
|
||||
<button
|
||||
disabled={isLoading || message?.length === 0}
|
||||
onClick={() => sendMessage()}
|
||||
className={`absolute right-1 rounded-md bg-green-500 bg-transparent p-1 disabled:bg-gray-500 disabled:opacity-40 md:right-2`}
|
||||
>
|
||||
<FiSend className="mr-1 h-4 w-4 text-white " />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{errorMessage ? (
|
||||
<div className="mb-2 md:mb-0">
|
||||
<div className="ml-1 flex h-full justify-center gap-0 md:m-auto md:mb-2 md:w-full md:gap-2">
|
||||
<span className="text-sm text-red-500">{errorMessage}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="px-3 pb-3 pt-2 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pb-6 md:pt-3">
|
||||
<span>
|
||||
ChatGPT may produce inaccurate information about people, places,
|
||||
or facts.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { useState } from "react";
|
||||
import Chat from "./Chat";
|
||||
import MobileSiderbar from "./MobileSidebar";
|
||||
import Sidebar from "./Sidebar";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import { BiRefresh } from "react-icons/bi";
|
||||
|
||||
function ChatBot() {
|
||||
const [chatRooms, setChatRooms] = React.useState(["old"]);
|
||||
const [runPod, setRunPod] = React.useState("");
|
||||
const [currentRoom, setCurrentRoom] = React.useState(0);
|
||||
const [isComponentVisible, setIsComponentVisible] = useState(false);
|
||||
const d = chatRooms.filter((room, index) => index === currentRoom);
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const toggleComponentVisibility = () => {
|
||||
setIsComponentVisible(!isComponentVisible);
|
||||
};
|
||||
|
||||
const runPodStatus = async () => {
|
||||
try {
|
||||
const response = await sdk.runPodStatus();
|
||||
|
||||
if (!response.error) {
|
||||
setRunPod(response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
runPodStatus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative text-center">
|
||||
<main className="overflow-hidde relative flex h-screen w-full">
|
||||
{isComponentVisible ? (
|
||||
<MobileSiderbar
|
||||
setCurrentRoom={setCurrentRoom}
|
||||
chatRooms={chatRooms}
|
||||
setChatRooms={setChatRooms}
|
||||
toggleComponentVisibility={toggleComponentVisibility}
|
||||
/>
|
||||
) : null}
|
||||
<div className="dark hidden flex-shrink-0 bg-gray-900 md:flex md:w-[260px] md:flex-col">
|
||||
<div className="flex h-full min-h-0 flex-col ">
|
||||
<Sidebar
|
||||
setCurrentRoom={setCurrentRoom}
|
||||
chatRooms={chatRooms}
|
||||
setChatRooms={setChatRooms}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{chatRooms.map((chatRoom, index) => (
|
||||
<Chat
|
||||
key={index}
|
||||
index={index}
|
||||
currentRoom={currentRoom}
|
||||
toggleComponentVisibility={toggleComponentVisibility}
|
||||
/>
|
||||
))}
|
||||
</main>
|
||||
<div className="absolute right-2 top-12 flex items-center gap-2 sm:right-4 sm:top-7">
|
||||
<span className="rounded-md bg-white p-2 text-xs font-bold">
|
||||
RunPod: {runPod}
|
||||
</span>
|
||||
<BiRefresh
|
||||
onClick={() => runPodStatus()}
|
||||
className="text-xl text-green-600 hover:cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatBot;
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { BsFillSunFill } from "react-icons/bs";
|
||||
import { AiOutlineThunderbolt } from "react-icons/ai";
|
||||
import { IoWarningOutline } from "react-icons//io5";
|
||||
|
||||
export const homePage = [
|
||||
{
|
||||
title: "Examples",
|
||||
icon: <BsFillSunFill />,
|
||||
details: [
|
||||
"Explain Quantum Computing in simple terms",
|
||||
"Got any creative ideas for a ten year old birthday",
|
||||
"How do I make an HTTP request using Javascript",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Capabilities",
|
||||
icon: <AiOutlineThunderbolt />,
|
||||
details: [
|
||||
"Explain Quantum Computing in simple terms",
|
||||
"Got any creative ideas for a ten year old birthday",
|
||||
"How do I make an HTTP request using Javascript",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Limitations",
|
||||
icon: <IoWarningOutline />,
|
||||
details: [
|
||||
"Explain Quantum Computing in simple terms",
|
||||
"Got any creative ideas for a ten year old birthday",
|
||||
"How do I make an HTTP request using Javascript",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function consoleWithEllipsis(phrase) {
|
||||
const words = phrase.split(" ");
|
||||
let delay = 0;
|
||||
|
||||
words.forEach((word) => {
|
||||
setTimeout(() => {
|
||||
console.log("..." + word);
|
||||
}, delay);
|
||||
|
||||
// Increment the delay for the next word
|
||||
delay += 1000; // You can adjust the delay time (in milliseconds) as per your preference
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { SiOpenai } from "react-icons/si";
|
||||
import { FaUserAlt } from "react-icons/fa";
|
||||
import { TbCursorText } from "react-icons/tb";
|
||||
import TypingEffect from "./TypingEffect";
|
||||
|
||||
const Message = (props) => {
|
||||
const { conversation, generating } = props;
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentIndex(conversation.length - 1);
|
||||
}, [conversation]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`mb- group w-full border-b border-black/10 text-gray-800 dark:text-gray-100 `}
|
||||
>
|
||||
<div className="m-auto flex w-full items-center justify-center gap-4 py-2 text-base dark:border-gray-900/50 dark:bg-gray-500 sm:py-0 md:gap-6 lg:px-0">
|
||||
<div className="ml-4 flex gap-1 text-start text-xs">
|
||||
<button
|
||||
className="text-gray-300 dark:text-gray-400"
|
||||
disabled={currentIndex + 1 === 1}
|
||||
onClick={() => setCurrentIndex(currentIndex - 1)}
|
||||
>
|
||||
<
|
||||
</button>
|
||||
<span className="flex-shrink-0 flex-grow">
|
||||
{currentIndex + 1} / {conversation?.length}
|
||||
</span>
|
||||
<button
|
||||
disabled={currentIndex + 1 === conversation.length}
|
||||
className="text-gray-300 dark:text-gray-400"
|
||||
onClick={() => setCurrentIndex(currentIndex + 1)}
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div className="my-auto flex w-full flex-row gap-4 pl-4 md:max-w-2xl md:gap-6 md:py-6 lg:max-w-xl lg:px-0 xl:max-w-3xl">
|
||||
<div className="relative flex w-8 flex-col items-end">
|
||||
<div className="text-opacity-100r relative flex h-7 w-7 items-center justify-center rounded-sm bg-green-600 p-1 text-white">
|
||||
<FaUserAlt className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 text-start md:gap-3 lg:w-[calc(100%-115px)]">
|
||||
<div className="flex flex-grow flex-col gap-3">
|
||||
<div className="min-h-20 flex flex-col items-start gap-4 whitespace-pre-wrap break-words">
|
||||
<div className="markdown prose dark:prose-invert dark w-full break-words">
|
||||
<p>{conversation[currentIndex]["content"]}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mr m-auto mt-2 flex w-full gap-4 text-base md:max-w-2xl md:gap-6 lg:max-w-xl lg:px-0 xl:max-w-3xl">
|
||||
<div className="m-auto flex w-full flex-row gap-4 p-4 md:max-w-2xl md:gap-6 md:py-6 lg:max-w-xl lg:px-0 xl:max-w-3xl">
|
||||
<div className="relative flex w-8 flex-col items-end">
|
||||
<div className="text-opacity-100r relative flex h-7 w-7 items-center justify-center rounded-sm bg-green-600 p-1 text-white">
|
||||
<SiOpenai className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 text-start md:gap-3 lg:w-[calc(100%-115px)]">
|
||||
<div className="flex flex-grow flex-col gap-3">
|
||||
<div className="min-h-20 flex flex-col items-start gap-4 whitespace-pre-wrap break-words">
|
||||
<div className="markdown prose dark:prose-invert dark w-full break-words">
|
||||
{generating ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<TbCursorText className="h-6 w-6 animate-pulse" />
|
||||
<span className="text-lg font-bold">
|
||||
Generating Response...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
<TypingEffect
|
||||
phrase={conversation[currentIndex]["content2"]}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Message;
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import { IoMdClose } from "react-icons/io";
|
||||
import Sidebar from "./Sidebar";
|
||||
|
||||
const MobileSiderbar = (props) => {
|
||||
const { toggleComponentVisibility, setCurrentRoom, chatRooms, setChatRooms } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<div id="headlessui-portal-root display:hidden">
|
||||
<div data-headlessui-portal="">
|
||||
<button
|
||||
type="button"
|
||||
aria-hidden="true"
|
||||
className="fixed left-[1px] top-[1px] m-[-1px] h-0 w-[1px] overflow-hidden whitespace-nowrap border-0 p-0"
|
||||
></button>
|
||||
<div>
|
||||
<div
|
||||
className="relative z-40"
|
||||
id="headlessui-dialog-:re:"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
data-headlessui-state="open"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-75 opacity-100"></div>
|
||||
<div className="fixed inset-0 z-40 flex">
|
||||
<div
|
||||
className="relative flex w-full max-w-xs flex-1 translate-x-0 flex-col bg-gray-900"
|
||||
id="headlessui-dialog-panel-:rf:"
|
||||
data-headlessui-state="open"
|
||||
>
|
||||
<div className="absolute right-0 top-0 -mr-12 pt-2 opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 flex h-10 w-10 items-center justify-center focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||
tabIndex={0}
|
||||
onClick={toggleComponentVisibility}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<IoMdClose className="h-6 w-6 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<Sidebar
|
||||
setCurrentRoom={setCurrentRoom}
|
||||
chatRooms={chatRooms}
|
||||
setChatRooms={setChatRooms}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-14 flex-shrink-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-hidden="true"
|
||||
className="fixed left-[1px] top-[1px] m-[-1px] h-0 w-[1px] overflow-hidden whitespace-nowrap border-0 p-0"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileSiderbar;
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { GlobalContext } from "Context/Global";
|
||||
import React, { useContext } from "react";
|
||||
import { AiOutlineMessage, AiOutlinePlus } from "react-icons/ai";
|
||||
import { FiMessageSquare } from "react-icons/fi";
|
||||
|
||||
const Sidebar = ({ setChatRooms, chatRooms, setCurrentRoom }) => {
|
||||
const { state } = useContext(GlobalContext);
|
||||
console.log(state);
|
||||
return (
|
||||
<div className="scrollbar-trigger flex h-screen w-full flex-1 items-start border-white/20">
|
||||
<nav className="flex h-full flex-1 flex-col space-y-1 p-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setChatRooms([...chatRooms, "new room"]);
|
||||
setCurrentRoom(chatRooms.length);
|
||||
}}
|
||||
className="mb-1 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||
>
|
||||
<AiOutlinePlus className="h-4 w-4" />
|
||||
New chat
|
||||
</button>
|
||||
|
||||
{chatRooms.map((chatRoom, index) => (
|
||||
<div
|
||||
onClick={() => setCurrentRoom(index)}
|
||||
className="flex-0 flex-col overflow-y-auto border-b border-white/20"
|
||||
>
|
||||
<div className="flex flex-col gap-2 pb-2 text-sm text-gray-100">
|
||||
<a className="group relative flex cursor-pointer items-center gap-3 break-all rounded-md px-3 py-3 hover:bg-[#2A2B32] hover:pr-4">
|
||||
<FiMessageSquare className="h-4 w-4" />
|
||||
<div className="flex- relative max-h-5 overflow-hidden text-ellipsis break-all">
|
||||
{state.rooms[index]?.value ?? "New Conversation"}
|
||||
<div className="absolute inset-y-0 right-0 z-10 w-8 bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]"></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* <a className="flex py-3 px-3 items-center absolute bottom-10 gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm">
|
||||
<AiOutlineMessage className="h-4 w-4" />
|
||||
Clear conversations
|
||||
</a> */}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
const TypingEffect = ({ phrase }) => {
|
||||
const words = phrase !== null && phrase.split(" ");
|
||||
const [currentWordIndex, setCurrentWordIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (phrase !== null) {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentWordIndex((prevIndex) => prevIndex + 1);
|
||||
}, 100); // Change the interval to control the speed of the animation
|
||||
|
||||
// Cleanup the interval on component unmount
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{phrase !== null ? (
|
||||
<span>
|
||||
{words.map((word, index) => (
|
||||
<span key={index}>
|
||||
{index < currentWordIndex ? `${word} ` : ""}
|
||||
</span>
|
||||
))}
|
||||
{currentWordIndex < words.length && "..."}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm font-bold">No Response, Try Again.</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypingEffect;
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
export { default as Chat } from "./Chat";
|
||||
export { default as ChatBot } from "./ChatBot";
|
||||
export { default as Message } from "./Message";
|
||||
export { default as MobileSidebar } from "./MobileSidebar";
|
||||
export { default as Sidebar } from "./Sidebar";
|
||||
export { default as TypingEffect } from "./TypingEffect";
|
||||
export { consoleWithEllipsis, homePage } from "./ChatUtils";
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
|
||||
import React from "react";
|
||||
|
||||
|
||||
|
||||
const CollapsibleMenu = ({label, children}) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const toggle = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<li className="list-none block w-full" >
|
||||
<a className="cursor-pointer" onClick={toggle}>{label}</a>
|
||||
{open && <div className="ml-2 bg-gray-800">{children}</div>}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollapsibleMenu;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const CollapsibleMenu = lazy(()=> import("./CollapsibleMenu"))
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
|
||||
import React from "react";
|
||||
import { AuthContext } from "Context/Auth";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
|
||||
export const renderName = (user) => {
|
||||
if (user?.first_name && user?.last_name) {
|
||||
return `${user?.first_name} ${user.last_name}`;
|
||||
} else if (user?.first_name || user?.last_name) {
|
||||
if (user?.first_name) {
|
||||
return user?.first_name;
|
||||
} else {
|
||||
return user?.last_name;
|
||||
}
|
||||
} else {
|
||||
return user?.email;
|
||||
}
|
||||
};
|
||||
|
||||
const CreateNewRoomModal = ({ roles, createNewRoom, setCreateRoom }) => {
|
||||
const [otherUsers, setOtherUsers] = React.useState();
|
||||
const [unfilteredUsers, setUnfilteredUsers] = React.useState();
|
||||
const [search, setSearch] = React.useState("");
|
||||
const { state } = React.useContext(AuthContext);
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
const getAllUsers = async () => {
|
||||
try {
|
||||
let users = await sdk.getAllUsers();
|
||||
|
||||
if (!users.error) {
|
||||
console.log(roles);
|
||||
let filtered = [];
|
||||
if (roles.length) {
|
||||
filtered = users?.list.filter(
|
||||
(user) => roles.includes(user.role) && state.user !== user?.id
|
||||
);
|
||||
} else {
|
||||
filtered = users?.list.filter((user) => state.user !== user?.id);
|
||||
}
|
||||
console.log("filtered >> ", filtered);
|
||||
setOtherUsers(filtered);
|
||||
setUnfilteredUsers(
|
||||
users?.list.filter((user) => user.id !== state.user)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Error", err);
|
||||
}
|
||||
};
|
||||
|
||||
const filterList = (value) => {
|
||||
setSearch(value);
|
||||
if (value.length > 0) {
|
||||
setOtherUsers(
|
||||
[...unfilteredUsers].filter((user) =>
|
||||
`${user.first_name.toLowerCase()} ${user.last_name.toLowerCase()}`.includes(
|
||||
value.toLowerCase()
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setOtherUsers(unfilteredUsers);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
(async function () {
|
||||
await getAllUsers();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div
|
||||
className="fixed inset-0 h-full w-full bg-black opacity-40"
|
||||
onClick={() => setCreateRoom(false)}
|
||||
></div>
|
||||
<div className="flex min-h-screen items-center px-4 py-8">
|
||||
<div className="relative mx-auto w-full max-w-lg rounded-md bg-white p-4 shadow-lg">
|
||||
<div className="mt-3 sm:flex">
|
||||
<div className="mt-2 w-full px-2 text-center sm:ml-4 sm:text-left">
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full border-b-2 border-gray-200 bg-transparent py-2 outline-none"
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => filterList(e.target.value)}
|
||||
/>
|
||||
<ul className="scrollbar-hide mt-4 h-[50vh] w-full overflow-y-scroll text-sm font-medium text-gray-900">
|
||||
{otherUsers &&
|
||||
otherUsers.map((user) => (
|
||||
<li
|
||||
key={user.id}
|
||||
onClick={() => createNewRoom(user)}
|
||||
className={`w-full cursor-pointer bg-white px-4 py-2 hover:bg-gray-200 user-${user.id}`}
|
||||
>
|
||||
{renderName(user)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateNewRoomModal;
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
export {
|
||||
default as CreateNewRoomModal,
|
||||
renderName,
|
||||
} from "./CreateNewRoomModal";
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
|
||||
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Bars3Icon, CalendarIcon, HomeIcon, MagnifyingGlassCircleIcon, MapIcon, MegaphoneIcon, UserGroupIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import PieChart from "./PieChart";
|
||||
import LineChart from "./LineChart";
|
||||
import NumberLabelCard from "./NumberLabelCard";
|
||||
import IconCards from "./IconCards";
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export default function DashboardUI({ navigation, user, aside }) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [navList, setNavList] = useState(navigation);
|
||||
|
||||
const setNewUI = (nav) => {
|
||||
const newList = navList.map((navIndex) => (navIndex.id === nav.id ? { ...nav, current: true } : { ...navIndex, current: false }));
|
||||
setNavList(newList);
|
||||
setSidebarOpen(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-[100vh]">
|
||||
<Transition.Root
|
||||
show={sidebarOpen}
|
||||
as={Fragment}
|
||||
>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-40 lg:hidden"
|
||||
onClose={setSidebarOpen}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-40 flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-white focus:outline-none">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-in-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute top-0 right-0 -mr-12 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon
|
||||
className="h-6 w-6 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
<div className="h-0 flex-1 pt-5 pb-4">
|
||||
<div className="flex flex-shrink-0 items-center px-4">
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
|
||||
alt="Your Company"
|
||||
/>
|
||||
</div>
|
||||
<nav
|
||||
aria-label="Sidebar"
|
||||
className="mt-5"
|
||||
>
|
||||
<div className="space-y-1 px-2">
|
||||
{navList.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current ? "bg-gray-100 text-gray-900" : "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
"group flex items-center px-2 py-2 text-base font-medium rounded-md"
|
||||
)}
|
||||
onClick={() => setNewUI(item)}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(item.current ? "text-gray-500" : "text-gray-400 group-hover:text-gray-500", "mr-4 h-6 w-6")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 border-t border-gray-200 p-4">
|
||||
<a
|
||||
href="#"
|
||||
className="group block flex-shrink-0"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<img
|
||||
className="inline-block h-10 w-10 rounded-full"
|
||||
src="https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
className="inline-block h-10 w-10 rounded-full"
|
||||
src={user?.profileImg}
|
||||
alt={"alt"}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-base font-medium text-gray-700 group-hover:text-gray-900">{user?.username}</p>
|
||||
<p className="text-sm font-medium text-gray-500 group-hover:text-gray-700">View profile</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<div
|
||||
className="w-14 flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Force sidebar to shrink to fit close icon */}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden lg:flex lg:flex-shrink-0">
|
||||
<div className="flex w-64 flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex min-h-0 flex-1 flex-col border-r border-gray-200 bg-gray-100">
|
||||
<div className="flex flex-1 flex-col pt-5 pb-4">
|
||||
{/* <div className="flex flex-shrink-0 items-center px-4">
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
|
||||
alt="Your Company"
|
||||
/>
|
||||
</div> */}
|
||||
<nav
|
||||
className="mt-5 flex-1"
|
||||
aria-label="Sidebar"
|
||||
>
|
||||
<div className="space-y-1 px-2">
|
||||
{navList.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current ? "bg-gray-200 text-gray-900" : "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
"group flex items-center px-2 py-2 text-sm font-medium rounded-md"
|
||||
)}
|
||||
onClick={() => setNewUI(item)}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(item.current ? "text-gray-500" : "text-gray-400 group-hover:text-gray-500", "mr-3 h-6 w-6")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 border-t border-gray-200 p-4">
|
||||
<a
|
||||
href="#"
|
||||
className="group block w-full flex-shrink-0"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<img
|
||||
className="inline-block h-10 w-10 rounded-full"
|
||||
src={user?.profileImg}
|
||||
alt={"alt"}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-gray-700 group-hover:text-gray-900">{user?.username}</p>
|
||||
<p className="text-xs font-medium text-gray-500 group-hover:text-gray-700">View profile</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col ">
|
||||
<div className="lg:hidden">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-1.5">
|
||||
<div>
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
|
||||
alt="Your Company"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="-mr-3 inline-flex h-12 w-12 items-center justify-center rounded-md text-gray-500 hover:text-gray-900"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon
|
||||
className="h-6 w-6"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-0 flex flex-1 ">
|
||||
<main className="relative z-0 flex-1 focus:outline-none">
|
||||
{/* Start main area*/}
|
||||
<div className="absolute inset-0 py-6 px-4 sm:px-6 lg:px-8">
|
||||
{/* <div className="h-full rounded-lg border-2 border-dashed border-gray-200"> */}
|
||||
<div className="h-full rounded-lg ">
|
||||
{navList?.map((nav) => (nav.current ? nav.mainComponent : null)) || ""}
|
||||
<PieChart
|
||||
series={[44, 55, 41, 17, 15]}
|
||||
labels={["A", "B", "C", "D", "E"]}
|
||||
/>{" "}
|
||||
<LineChart
|
||||
options={{
|
||||
chart: {
|
||||
id: "basic-bar"
|
||||
}
|
||||
}}
|
||||
categories={[1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998]}
|
||||
series={[
|
||||
{
|
||||
name: "Series-1",
|
||||
data: [30, 40, 45, 50, 49, 60, 70, 91]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<NumberLabelCard
|
||||
data={[
|
||||
{
|
||||
name: "Jane Cooper",
|
||||
title: "Regional Paradigm Technician",
|
||||
role: "Admin",
|
||||
email: "janecooper@example.com",
|
||||
telephone: "+1-202-555-0170",
|
||||
imageUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<IconCards
|
||||
projects={[
|
||||
{ name: "Graph API", initials: "GA", href: "#", members: 16, bgColor: "bg-pink-600" }
|
||||
// { name: "Component Design", initials: "CD", href: "#", members: 12, bgColor: "bg-purple-600" },
|
||||
// { name: "Templates", initials: "T", href: "#", members: 16, bgColor: "bg-yellow-500" },
|
||||
// { name: "React Components", initials: "RC", href: "#", members: 8, bgColor: "bg-green-500" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* End main area */}
|
||||
</main>
|
||||
{aside ? (
|
||||
<aside className="relative hidden w-96 flex-shrink-0 border-l border-gray-200 xl:flex xl:flex-col">
|
||||
{/* Start secondary column (hidden on smaller screens) */}
|
||||
<div className="absolute inset-0 py-6 px-4 sm:px-6 lg:px-8">
|
||||
<div className="h-full rounded-lg border-2 border-dashed border-gray-200">{navList?.map((nav) => (nav.current ? nav.asideComponent : null)) || ""}</div>
|
||||
</div>
|
||||
{/* End secondary column */}
|
||||
</aside>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
|
||||
|
||||
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export default function IconCards({ projects }) {
|
||||
return (
|
||||
<div>
|
||||
<ul
|
||||
role="list"
|
||||
className="mt-3 grid grid-cols-1 gap-5 sm:grid-cols-2 sm:gap-6 lg:grid-cols-4"
|
||||
>
|
||||
{projects.map((project) => (
|
||||
<li
|
||||
key={project.name}
|
||||
className="col-span-1 flex rounded-md shadow-sm"
|
||||
>
|
||||
{/* <div className={classNames(project.bgColor, "flex-shrink-0 flex items-center justify-center w-16 text-white text-sm font-medium rounded-l-md")}>{project.initials}</div> */}
|
||||
<div className="flex-shrink-0 pr-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-white bg-transparent text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<span className="sr-only">Open options</span>
|
||||
<EllipsisVerticalIcon
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-between truncate rounded-r-md border-t border-r border-b border-gray-200 bg-white">
|
||||
<div className="flex-1 truncate px-4 py-2 text-sm">
|
||||
<a
|
||||
href={project.href}
|
||||
className="font-medium text-gray-900 hover:text-gray-600"
|
||||
>
|
||||
{project.name}
|
||||
</a>
|
||||
{/* <p className="text-gray-500">{project.members} Members</p> */}
|
||||
</div>
|
||||
{/* <div className="flex-shrink-0 pr-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-white bg-transparent text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<span className="sr-only">Open options</span>
|
||||
<EllipsisVerticalIcon
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
|
||||
import React, { Component } from "react";
|
||||
import Chart from "react-apexcharts";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
class LineChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
chart: {
|
||||
id: "basic-bar"
|
||||
},
|
||||
...props.options,
|
||||
xaxis: {
|
||||
categories: props.categories
|
||||
}
|
||||
},
|
||||
series: props.series
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="row">
|
||||
<div className="mixed-chart">
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.state.series}
|
||||
type="line"
|
||||
width="500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LineChart.defaultProps = {
|
||||
options: { chart: { id: "" } },
|
||||
categories: [],
|
||||
series: []
|
||||
};
|
||||
|
||||
LineChart.propTypes = {
|
||||
options: PropTypes.shape({}),
|
||||
categories: PropTypes.shape([]),
|
||||
series: PropTypes.shape([])
|
||||
};
|
||||
|
||||
export default LineChart;
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
|
||||
|
||||
import { EnvelopeIcon, PhoneIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export default function NumberLabelCard({ data }) {
|
||||
return (
|
||||
<ul
|
||||
role="list"
|
||||
className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
{data.map((item) => (
|
||||
<li
|
||||
key={item.email}
|
||||
className="col-span-1 divide-y divide-gray-200 rounded-lg bg-white shadow"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between space-x-6 p-6">
|
||||
<div className="flex-1 truncate">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h3 className="truncate text-sm font-medium text-gray-900">{item.name}</h3>
|
||||
{/* <span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">{person.role}</span> */}
|
||||
</div>
|
||||
<p className="mt-1 truncate text-sm text-gray-500">{item.title}</p>
|
||||
</div>
|
||||
{/* <img
|
||||
className="h-10 w-10 flex-shrink-0 rounded-full bg-gray-300"
|
||||
src={person.imageUrl}
|
||||
alt=""
|
||||
/> */}
|
||||
</div>
|
||||
<div>
|
||||
<div className="-mt-px flex divide-x divide-gray-200">
|
||||
<div className="flex w-0 flex-1">
|
||||
<a
|
||||
href={`mailto:${item.email}`}
|
||||
className="relative -mr-px inline-flex w-0 flex-1 items-center justify-center rounded-bl-lg border border-transparent py-4 text-sm font-medium text-gray-700 hover:text-gray-500"
|
||||
>
|
||||
<EnvelopeIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="ml-3">Email</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="-ml-px flex w-0 flex-1">
|
||||
<a
|
||||
href={`tel:${item.telephone}`}
|
||||
className="relative inline-flex w-0 flex-1 items-center justify-center rounded-br-lg border border-transparent py-4 text-sm font-medium text-gray-700 hover:text-gray-500"
|
||||
>
|
||||
<PhoneIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="ml-3">Call</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
|
||||
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Chart from "react-apexcharts";
|
||||
|
||||
class PieChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: props.options,
|
||||
series: props.series,
|
||||
labels: props.labels
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="donut">
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.state.series}
|
||||
type="donut"
|
||||
width="380"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PieChart.defaultProps = {
|
||||
options: {},
|
||||
series: [],
|
||||
labels: []
|
||||
};
|
||||
|
||||
PieChart.propTypes = {
|
||||
options: PropTypes.shape({}),
|
||||
series: PropTypes.shape([]),
|
||||
labels: PropTypes.shape([])
|
||||
};
|
||||
|
||||
export default PieChart;
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
import React, {useEffect, useState} from "react";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
|
||||
const sdk = new MkdSDK();
|
||||
|
||||
const Stats = ({heading, request, unit, unitPosition}) => {
|
||||
|
||||
const [data, setData] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
( async () => {
|
||||
let res = await sdk.callRawAPI(request?.route, request?.payload ?? {}, request?.method)
|
||||
.catch(err => console.error(err));
|
||||
setData(res.model);
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const getStat = (stat, unit = '', unitPosition = "left") => {
|
||||
return unitPosition == "left" ? `${unit}${stat}` : `${stat}${unit}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<article className="rounded-lg border border-gray-100 bg-white p-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">{heading}</p>
|
||||
|
||||
<p className="text-2xl font-medium text-gray-900">{getStat(data, unit, unitPosition)}</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default Stats;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export {default as IconCards} from './IconCards'
|
||||
export {default as DashboardUI} from './DashboardUI'
|
||||
export {default as LineChart} from './LineChart'
|
||||
export {default as NumberLabelCard} from './NumberLabelCard'
|
||||
export {default as PieChart} from './PieChart'
|
||||
export {default as Stats} from './Stats'
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
import React from 'react';
|
||||
import { memo } from 'react'
|
||||
import "@hassanmojab/react-modern-calendar-datepicker/lib/DatePicker.css";
|
||||
import { Calendar, utils } from "@hassanmojab/react-modern-calendar-datepicker";
|
||||
|
||||
const DateRange = ( { selectedDayRange, setSelectedDayRange } ) => {
|
||||
return (
|
||||
<Calendar
|
||||
value={ selectedDayRange }
|
||||
onChange={ setSelectedDayRange }
|
||||
shouldHighlightWeekends={ true }
|
||||
minimumDate={ utils().getToday() }
|
||||
colorPrimary="#0fbcf9" // added this
|
||||
colorPrimaryLight="rgba(75, 207, 250, 0.4)"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo( DateRange )
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const DateRangeTemplate = lazy(()=> import("./DateRangeTemplate"))
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
|
||||
|
||||
import React from "react";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import {empty} from "Utils/utils";
|
||||
|
||||
const sdk = new MkdSDK();
|
||||
const defaultImage = 'https://via.placeholder.com/150?text=%20';
|
||||
const DynamicContentType = ({contentType, contentValue, setContentValue}) => {
|
||||
const [previewUrl, setPreviewUrl] = React.useState(defaultImage);
|
||||
const handleImageChange = async (e) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
try {
|
||||
const result = await sdk.uploadImage(formData);
|
||||
setPreviewUrl(result.url);
|
||||
setContentValue(result.url);
|
||||
}catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
}
|
||||
switch (contentType) {
|
||||
case "text":
|
||||
return (
|
||||
<>
|
||||
<textarea className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
rows={15}
|
||||
placeholder="Content"
|
||||
onChange={e => setContentValue(e.target.value)}
|
||||
defaultValue={contentValue}
|
||||
></textarea>
|
||||
</>
|
||||
)
|
||||
|
||||
case "image":
|
||||
return (
|
||||
<>
|
||||
<img src={empty(contentValue) ? previewUrl : contentValue } alt="preview" height={150} width={150} />
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleImageChange}
|
||||
className={`shadow appearance-none border block rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
case "number":
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
className={`shadow appearance-none border block rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
onChange={e => setContentValue(e.target.value)}
|
||||
defaultValue={contentValue}
|
||||
/>
|
||||
)
|
||||
|
||||
case "team-list":
|
||||
return (
|
||||
<TeamList setContentValue={setContentValue} contentValue={contentValue} />
|
||||
)
|
||||
|
||||
case "image-list":
|
||||
return (
|
||||
<ImageList setContentValue={setContentValue} contentValue={contentValue} />
|
||||
)
|
||||
|
||||
case "captioned-image-list":
|
||||
return (
|
||||
<CaptionedImageList setContentValue={setContentValue} contentValue={contentValue} />
|
||||
)
|
||||
|
||||
case "kvp":
|
||||
return (
|
||||
<KeyValuePair setContentValue={setContentValue} contentValue={contentValue} />
|
||||
)
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicContentType;
|
||||
|
||||
const ImageList = ({contentValue, setContentValue}) => {
|
||||
let itemsObj = [
|
||||
{key: '', value_type: 'image', value: null}
|
||||
];
|
||||
if (!empty(contentValue)) {
|
||||
itemsObj = JSON.parse(contentValue);
|
||||
}
|
||||
const [items, setItems] = React.useState(itemsObj);
|
||||
|
||||
|
||||
const handleImageChange = async (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
try {
|
||||
const result = await sdk.uploadImage(formData);
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.value = result.url;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.key = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block">
|
||||
{items.map( (item, index) => <div key={index*0.23}>
|
||||
<img src={item.value !== null ? item.value : defaultImage} alt="preview" height={150} width={150} />
|
||||
<div className="flex">
|
||||
<input
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text" placeholder="key" listkey={index} onChange={handleKeyChange} defaultValue={item.key} />
|
||||
<input
|
||||
listkey={index}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-400 hover:bg-blue-700 text-white font-bold py-1 px-2 my-4 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={ e => setItems(old => [...old, {key: '', value_type: 'image', value: null}])}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const CaptionedImageList = ({setContentValue, contentValue}) => {
|
||||
let itemsObj = [
|
||||
{key: '', value_type: 'image', value: null, caption: ''}
|
||||
]
|
||||
|
||||
if (!empty(contentValue)) {
|
||||
itemsObj = JSON.parse(contentValue);
|
||||
}
|
||||
const [items, setItems] = React.useState(itemsObj);
|
||||
|
||||
const handleImageChange = async (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
try {
|
||||
const result = await sdk.uploadImage(formData);
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.value = result.url;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.key = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
const handleCaptionChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.caption = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block">
|
||||
{items.map( (item, index) => <div key={index*0.23} >
|
||||
<img src={item.value !== null ? item.value : defaultImage} alt="preview" height={150} width={150} />
|
||||
<div className="flex">
|
||||
<input
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 mr-2 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text" placeholder="Key" listkey={index} onChange={handleKeyChange} defaultValue={item.key}
|
||||
/>
|
||||
<input
|
||||
listkey={index}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
className={`shadow block appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text"
|
||||
placeholder="Caption" listkey={index} onChange={handleCaptionChange} defaultValue={item.caption}
|
||||
/>
|
||||
|
||||
</div>)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-400 hover:bg-blue-700 text-white font-bold py-1 px-2 my-4 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={ e => setItems(old => [...old, {key: '', value_type: 'image', value: null, caption: ''}])}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
const TeamList = ({setContentValue, contentValue}) => {
|
||||
let itemsObj = [
|
||||
{name: '', image: null, title: ''}
|
||||
]
|
||||
|
||||
if (!empty(contentValue)) {
|
||||
itemsObj = JSON.parse(contentValue);
|
||||
}
|
||||
const [items, setItems] = React.useState(itemsObj);
|
||||
|
||||
const handleImageChange = async (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
const formData = new FormData();
|
||||
formData.append('file', e.target.files[0]);
|
||||
try {
|
||||
const result = await sdk.uploadImage(formData);
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.image = result.url;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const handleNameChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.name = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
const handleTitleChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.title = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block my-4">
|
||||
{items.map( (item, index) => <div key={index*0.23} className="my-4" >
|
||||
|
||||
{/* <div className="flex"> */}
|
||||
<input
|
||||
className={`shadow block appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text"
|
||||
placeholder="Title" listkey={index} onChange={handleTitleChange} defaultValue={item.title}
|
||||
/>
|
||||
<input
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 mr-2 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text" placeholder="Name" listkey={index} onChange={handleNameChange} defaultValue={item.name}
|
||||
/>
|
||||
<img src={item.image !== null ? item.image : defaultImage} alt="preview" height={150} width={150} />
|
||||
<input
|
||||
listkey={index}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
/>
|
||||
{/* </div> */}
|
||||
|
||||
|
||||
</div>)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-400 hover:bg-blue-700 text-white font-bold py-1 px-2 my-4 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={ e => setItems(old => [...old, {name: '', image: null, title: ''}])}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
const KeyValuePair = ({setContentValue, contentValue}) => {
|
||||
let itemsObj = [
|
||||
{key: '', value_type: 'text', value: ''}
|
||||
]
|
||||
|
||||
if (!empty(contentValue)) {
|
||||
itemsObj = JSON.parse(contentValue);
|
||||
}
|
||||
|
||||
const [items, setItems] = React.useState(itemsObj);
|
||||
const valueTypeMap = [
|
||||
{
|
||||
key: "text",
|
||||
value: "Text"
|
||||
},
|
||||
{
|
||||
key: "number",
|
||||
value: "Number"
|
||||
},
|
||||
{
|
||||
key: "json",
|
||||
value: "JSON Object"
|
||||
},
|
||||
{
|
||||
key: "url",
|
||||
value: "URL"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const handleKeyChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.key = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
const handleValueChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.value = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
const handleValueTypeChange = (e) => {
|
||||
const listKey = e.target.getAttribute('listkey');
|
||||
setItems(oldItems => {
|
||||
let updatedItems = oldItems.map((item, index) => {
|
||||
if (index == listKey) {
|
||||
item.value_type = e.target.value;
|
||||
return item;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
return updatedItems;
|
||||
})
|
||||
setContentValue(JSON.stringify(items))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block">
|
||||
{items.map( (item, index) => <div key={index*0.23} className="my-4" >
|
||||
<input
|
||||
className={`shadow appearance-none border rounded w-full py-2 px-3 mr-2 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text" placeholder="Key" listkey={index} onChange={handleKeyChange} defaultValue={item.key}
|
||||
/>
|
||||
<select
|
||||
className={`shadow block border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
listkey={index} onChange={handleValueTypeChange} defaultValue={item.value_type}>
|
||||
{valueTypeMap.map( (type, index) => <option key={index*122} value={type.key}>{type.value}</option>)}
|
||||
</select>
|
||||
<input
|
||||
className={`shadow block appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline`}
|
||||
type="text"
|
||||
required
|
||||
placeholder="Value" listkey={index} onChange={handleValueChange} defaultValue={item.value}
|
||||
/>
|
||||
|
||||
|
||||
</div>)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-400 hover:bg-blue-700 text-white font-bold py-1 px-2 my-4 rounded focus:outline-none focus:shadow-outline"
|
||||
onClick={ e => setItems(old => [...old, {key: '', value_type: 'text', value: ''}])}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const DynamicContentType = lazy(()=> import("./DynamicContentType"))
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import ReactQuill from "react-quill";
|
||||
import EditorToolbar, { modules, formats } from "./EditorToolbars";
|
||||
const Editor = ({
|
||||
setValue,
|
||||
errors,
|
||||
name,
|
||||
placeholder = "Write something awesome...",
|
||||
initialContent = "",
|
||||
}) => {
|
||||
const [content, setContent] = useState(initialContent);
|
||||
const editorStyle = {
|
||||
// maxheight: '500px',
|
||||
// minheight: '500px',
|
||||
height: "500px",
|
||||
// overFlow: 'auto',
|
||||
|
||||
// set the height to 500 pixels
|
||||
};
|
||||
const onSetContent = (content) => {
|
||||
setContent(content);
|
||||
setValue(name, content);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorToolbar />
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={content}
|
||||
onChange={(content) => onSetContent(content)}
|
||||
placeholder={placeholder}
|
||||
modules={modules}
|
||||
formats={formats}
|
||||
style={editorStyle}
|
||||
/>
|
||||
{errors && errors?.content && (
|
||||
<p className="text-field-error italic text-red-500">
|
||||
{errors?.content?.message}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
@@ -0,0 +1,160 @@
|
||||
|
||||
import React from "react";
|
||||
import { Quill } from "react-quill";
|
||||
|
||||
// Custom Undo button icon component for Quill editor. You can import it directly
|
||||
// from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not
|
||||
// handle them correctly
|
||||
const CustomUndo = () => (
|
||||
<svg viewBox="0 0 18 18">
|
||||
<polygon className="ql-fill ql-stroke" points="6 10 4 12 2 10 6 10" />
|
||||
<path
|
||||
className="ql-stroke"
|
||||
d="M8.09,13.91A4.6,4.6,0,0,0,9,14,5,5,0,1,0,4,9"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Redo button icon component for Quill editor
|
||||
const CustomRedo = () => (
|
||||
<svg viewBox="0 0 18 18">
|
||||
<polygon className="ql-fill ql-stroke" points="12 10 14 12 16 10 12 10" />
|
||||
<path
|
||||
className="ql-stroke"
|
||||
d="M9.91,13.91A4.6,4.6,0,0,1,9,14a5,5,0,1,1,5-5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Undo and redo functions for Custom Toolbar
|
||||
function undoChange () {
|
||||
this.quill.history.undo();
|
||||
}
|
||||
function redoChange () {
|
||||
this.quill.history.redo();
|
||||
}
|
||||
|
||||
// Add sizes to whitelist and register them
|
||||
const Size = Quill.import( "formats/size" );
|
||||
Size.whitelist = [ "extra-small", "small", "medium", "large" ];
|
||||
Quill.register( Size, true );
|
||||
|
||||
// Add fonts to whitelist and register them
|
||||
const Font = Quill.import( "formats/font" );
|
||||
Font.whitelist = [
|
||||
"arial",
|
||||
"comic-sans",
|
||||
"courier-new",
|
||||
"georgia",
|
||||
"helvetica",
|
||||
"lucida"
|
||||
];
|
||||
Quill.register( Font, true );
|
||||
|
||||
// Modules object for setting up the Quill editor
|
||||
export const modules = {
|
||||
toolbar: {
|
||||
container: "#toolbar",
|
||||
handlers: {
|
||||
undo: undoChange,
|
||||
redo: redoChange
|
||||
}
|
||||
},
|
||||
history: {
|
||||
delay: 500,
|
||||
maxStack: 100,
|
||||
userOnly: true
|
||||
}
|
||||
};
|
||||
|
||||
// Formats objects for setting up the Quill editor
|
||||
export const formats = [
|
||||
"header",
|
||||
"font",
|
||||
"size",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"align",
|
||||
"strike",
|
||||
"script",
|
||||
"blockquote",
|
||||
"background",
|
||||
"list",
|
||||
"bullet",
|
||||
"indent",
|
||||
"link",
|
||||
"image",
|
||||
"color",
|
||||
"code-block"
|
||||
];
|
||||
|
||||
// Quill Toolbar component
|
||||
const QuillToolbar = () => (
|
||||
<div id="toolbar">
|
||||
<span className="ql-formats">
|
||||
<select className="ql-font" defaultValue="arial">
|
||||
<option value="arial">Arial</option>
|
||||
<option value="comic-sans">Comic Sans</option>
|
||||
<option value="courier-new">Courier New</option>
|
||||
<option value="georgia">Georgia</option>
|
||||
<option value="helvetica">Helvetica</option>
|
||||
<option value="lucida">Lucida</option>
|
||||
</select>
|
||||
<select className="ql-size" defaultValue="medium">
|
||||
<option value="extra-small">Size 1</option>
|
||||
<option value="small">Size 2</option>
|
||||
<option value="medium">Size 3</option>
|
||||
<option value="large">Size 4</option>
|
||||
</select>
|
||||
<select className="ql-header" defaultValue="3">
|
||||
<option value="1">Heading</option>
|
||||
<option value="2">Subheading</option>
|
||||
<option value="3">Normal</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-bold" />
|
||||
<button className="ql-italic" />
|
||||
<button className="ql-underline" />
|
||||
<button className="ql-strike" />
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-list" value="ordered" />
|
||||
<button className="ql-list" value="bullet" />
|
||||
<button className="ql-indent" value="-1" />
|
||||
<button className="ql-indent" value="+1" />
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-script" value="super" />
|
||||
<button className="ql-script" value="sub" />
|
||||
<button className="ql-blockquote" />
|
||||
<button className="ql-direction" />
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<select className="ql-align" />
|
||||
<select className="ql-color" />
|
||||
<select className="ql-background" />
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-link" />
|
||||
<button className="ql-image" />
|
||||
<button className="ql-video" />
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-formula" />
|
||||
<button className="ql-code-block" />
|
||||
<button className="ql-clean" />
|
||||
</span>
|
||||
<span className="ql-formats">
|
||||
<button className="ql-undo">
|
||||
<CustomUndo />
|
||||
</button>
|
||||
<button className="ql-redo">
|
||||
<CustomRedo />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default QuillToolbar;
|
||||
@@ -0,0 +1,2 @@
|
||||
export{default as Editor} from './Editor'
|
||||
// export{default as EditorToolbars} from './EditorToolbars'
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
|
||||
|
||||
import React from "react";
|
||||
|
||||
const ExportButton = ({ onClick, className }) => {
|
||||
return (
|
||||
<>
|
||||
<button onClick={onClick} className={`relative flex h-[2.125rem] w-fit min-w-fit items-center justify-center overflow-hidden rounded-md border border-primaryBlue bg-indigo-600 px-[.6125rem] py-[.5625rem] font-['Inter'] text-sm font-medium leading-none text-white shadow-md shadow-indigo-600 ${className}`}>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
|
||||
</path>
|
||||
</svg>
|
||||
<span>Export</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportButton;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {default as ExportButton} from "./ExportButton"
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
|
||||
import React from "react";
|
||||
|
||||
const ImagePreviewModal = ({ file, handleFileUpload, cancelFileUpload }) => {
|
||||
const [imageSrc, setImageSrc] = React.useState()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (file.length > 0) {
|
||||
setImageSrc(URL.createObjectURL(file[0]))
|
||||
}
|
||||
}, [file])
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div
|
||||
className="fixed inset-0 w-full h-full bg-black opacity-40"
|
||||
onClick={cancelFileUpload}
|
||||
></div>
|
||||
<div className="flex items-center min-h-screen px-4 py-8">
|
||||
<div className="relative w-full max-w-lg p-4 mx-auto bg-white rounded-md shadow-lg">
|
||||
<div className="mt-3 sm:flex">
|
||||
<div className="mt-2 text-center sm:ml-4 sm:text-left">
|
||||
<h4 className="text-lg font-medium text-gray-800">
|
||||
Send Image{file.length > 1 && 's'}
|
||||
</h4>
|
||||
<img className="block" src={imageSrc} />
|
||||
<div className="items-center gap-2 mt-3 sm:flex">
|
||||
<button
|
||||
className="w-full mt-2 p-2.5 flex-1 mr-4 text-white bg-blue-600 whitespace-nowrap rounded-md outline-none ring-offset-2 ring-blue-600 focus:ring-2"
|
||||
onClick={handleFileUpload}
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
<button
|
||||
className="w-full mt-2 p-2.5 flex-1 text-gray-800 rounded-md outline-none border ring-offset-2 ring-indigo-600 focus:ring-2"
|
||||
onClick={cancelFileUpload}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImagePreviewModal
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const ImagePreviewModal = lazy(()=> import("./ImagePreviewModal"))
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
import React, { memo, useId } from "react";
|
||||
import MoonLoader from "react-spinners/MoonLoader";
|
||||
|
||||
const InteractiveButton = memo(
|
||||
({
|
||||
loading = false,
|
||||
disabled,
|
||||
children,
|
||||
type = "button",
|
||||
className,
|
||||
loaderclasses,
|
||||
onClick,
|
||||
color = "#ffffff",
|
||||
}) => {
|
||||
const override = {
|
||||
borderColor: "#ffffff",
|
||||
};
|
||||
const id = useId();
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
className={`flex items-center justify-center gap-5 ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<>
|
||||
<MoonLoader
|
||||
color={color}
|
||||
loading={loading}
|
||||
cssOverride={override}
|
||||
size={20}
|
||||
className={loaderclasses}
|
||||
// aria-label="Loading Spinner"
|
||||
data-testid={id}
|
||||
/>
|
||||
|
||||
<span>{children}</span>
|
||||
</>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default InteractiveButton;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as InteractiveButton } from "./InteractiveButton";
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
import React, { memo, Suspense } from "react";
|
||||
import { SkeletonLoader } from "Components/Skeleton";
|
||||
|
||||
const LazyLoad = ({
|
||||
children,
|
||||
counts = [1],
|
||||
count = 1,
|
||||
circle = false,
|
||||
}) => {
|
||||
const childrenArray = React.Children.toArray(children).filter(Boolean);
|
||||
const className = childrenArray.filter(Boolean)[0]?.props?.className
|
||||
? childrenArray[0]?.props?.className
|
||||
: "";
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<SkeletonLoader
|
||||
counts={counts}
|
||||
count={count}
|
||||
className={className}
|
||||
circle={circle}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LazyLoad);
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from "react";
|
||||
|
||||
export const LazyLoad = lazy(()=> import("./LazyLoad"))
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
import {LoadingIndicator} from "Components/LoadingIndicator";
|
||||
|
||||
const Loader = ({ style }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const Loader = lazy(()=> import("./Loader"))
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const containerVariant = {
|
||||
start: {
|
||||
transition: { staggerChildren: 0.2 }
|
||||
},
|
||||
end: {
|
||||
transition: { staggerChildren: 0.2 }
|
||||
}
|
||||
};
|
||||
|
||||
const dotsVariants = {
|
||||
start: {
|
||||
y: "0%"
|
||||
},
|
||||
end: {
|
||||
y: "100%"
|
||||
}
|
||||
};
|
||||
|
||||
const loadingTransition = {
|
||||
duration: 0.4,
|
||||
yoyo: Infinity,
|
||||
ease: "easeIn"
|
||||
};
|
||||
|
||||
export default function Indicator({ dotsClasses, size, style }) {
|
||||
const dotsStyles = "block w-[9px] h-[9px] bg-slate-900 rounded-md shrink-0 " + dotsClasses;
|
||||
return (
|
||||
<motion.div
|
||||
variants={containerVariant}
|
||||
className={`flex justify-between items-center w-[40px] pb-[10px]`}
|
||||
initial="start"
|
||||
animate="end"
|
||||
style={{ ...style }}
|
||||
>
|
||||
<motion.span
|
||||
className={dotsStyles}
|
||||
variants={dotsVariants}
|
||||
transition={loadingTransition}
|
||||
/>
|
||||
<motion.span
|
||||
className={dotsStyles}
|
||||
variants={dotsVariants}
|
||||
transition={loadingTransition}
|
||||
/>
|
||||
<motion.span
|
||||
className={dotsStyles}
|
||||
variants={dotsVariants}
|
||||
transition={loadingTransition}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const LoadingIndicator = lazy(()=> import("./LoadingIndicator"))
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
const MKDForm = ({ onSubmit, children, className }) => {
|
||||
return (
|
||||
<>
|
||||
<form className={`w-full max-w-lg ${className}`} onSubmit={onSubmit}>
|
||||
{children}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MKDForm;
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
import { lazy } from "react";
|
||||
export const MKDForm = lazy(() => import("./MKDForm"));
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
|
||||
import React from "react";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import { PiUsersThreeFill } from "react-icons/pi";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import { MdDashboard } from "react-icons/md";
|
||||
import { GlobalContext } from "Context/Global";
|
||||
import { AuthContext, tokenExpireError } from "Context/Auth";
|
||||
let sdk = new MkdSDK();
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{
|
||||
to: "/member/dashboard",
|
||||
text: "Dashboard",
|
||||
icon: <MdDashboard className="text-xl text-[#A8A8A8]" />,
|
||||
value: "admin",
|
||||
},
|
||||
|
||||
|
||||
|
||||
{
|
||||
to: "/member/profile",
|
||||
text: "Profile",
|
||||
icon: <PiUsersThreeFill className="text-xl text-[#A8A8A8]" />,
|
||||
value: "profile",
|
||||
},
|
||||
];
|
||||
|
||||
export const MemberHeader = () => {
|
||||
const {
|
||||
state: { isOpen, path },
|
||||
dispatch: gobalDispatch,
|
||||
} = React.useContext(GlobalContext);
|
||||
const { state: authState, dispatch } = React.useContext(AuthContext);
|
||||
const [openDropdown, setOpenDropdown] = React.useState(false);
|
||||
const [isHovering, setIsHovering] = React.useState(false);
|
||||
|
||||
// const handleMouseOver = () => {
|
||||
// setIsHovering(true);
|
||||
// };
|
||||
|
||||
// const handleMouseOut = () => {
|
||||
// setIsHovering(false);
|
||||
// };
|
||||
let toggleOpen = (open) => {
|
||||
gobalDispatch({
|
||||
type: "OPEN_SIDEBAR",
|
||||
payload: { isOpen: open },
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const result = await sdk.getProfile();
|
||||
dispatch({
|
||||
type: "UPDATE_PROFILE",
|
||||
payload: result,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error", error);
|
||||
tokenExpireError(
|
||||
dispatch,
|
||||
error.response.data.message
|
||||
? error.response.data.message
|
||||
: error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
// sidebar-holder
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`z-50 flex max-h-screen flex-1 flex-col border border-[#E0E0E0] bg-white py-4 text-[#A8A8A8] transition-all ${
|
||||
isOpen
|
||||
? "fixed h-screen w-[15rem] min-w-[15rem] max-w-[15rem] md:relative"
|
||||
: "relative min-h-screen w-[4.2rem] min-w-[4.2rem] max-w-[4.2rem] bg-black text-white"
|
||||
} `}
|
||||
>
|
||||
<div
|
||||
className={`text-[#393939] ${
|
||||
isOpen ? "flex w-full" : "flex items-center justify-center"
|
||||
} `}
|
||||
>
|
||||
<div></div>
|
||||
{isOpen && (
|
||||
<div className="text-2xl font-bold">
|
||||
<Link to="/">
|
||||
<h4 className="flex cursor-pointer items-center px-4 pb-4 font-sans font-bold">
|
||||
Baas Brand{" "}
|
||||
</h4>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-fit w-auto flex-1">
|
||||
<div className="sidebar-list w-auto">
|
||||
<ul className="flex flex-wrap px-2 text-sm">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<li className="block w-full list-none" key={item.value}>
|
||||
<NavLink
|
||||
to={item.to}
|
||||
className={`${ path == item.value ? "active-nav" : "" } `}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{item.icon}
|
||||
{isOpen && <span>{item.text}</span>}
|
||||
</div>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="mr-3 cursor-pointer rounded-lg border border-[#E0E0E0] bg-white p-2 text-2xl text-gray-400">
|
||||
<span onClick={() => toggleOpen(!isOpen)}>
|
||||
<svg
|
||||
className={`transition-transform ${
|
||||
!isOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
xmlns="http:www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12ZM10.4142 11L11.7071 9.70711C12.0976 9.31658 12.0976 8.68342 11.7071 8.29289C11.3166 7.90237 10.6834 7.90237 10.2929 8.29289L7.82322 10.7626C7.13981 11.446 7.13981 12.554 7.82322 13.2374L10.2929 15.7071C10.6834 16.0976 11.3166 16.0976 11.7071 15.7071C12.0976 15.3166 12.0976 14.6834 11.7071 14.2929L10.4142 13H16C16.5523 13 17 12.5523 17 12C17 11.4477 16.5523 11 16 11H10.4142Z"
|
||||
fill="#A8A8A8"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemberHeader;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const MemberHeader = lazy(()=> import("./MemberHeader"))
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
|
||||
|
||||
import React, { Suspense, memo } from "react";
|
||||
|
||||
import {MemberHeader} from "Components/MemberHeader";
|
||||
import {TopHeader} from "Components/TopHeader";
|
||||
import { Spinner } from "Assets/svgs";
|
||||
const navigation = []
|
||||
const MemberWrapper = ({ children }) => {
|
||||
return (
|
||||
<div id="member_wrapper" className={`flex w-full max-w-full flex-col bg-white`}>
|
||||
<div className={`flex min-h-screen w-full max-w-full `}>
|
||||
<MemberHeader
|
||||
|
||||
/>
|
||||
<div className={`mb-20 w-full overflow-hidden`}>
|
||||
<TopHeader />
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
className={`flex h-screen w-full items-center justify-center`}
|
||||
>
|
||||
<Spinner size={100} color="#2CC9D5" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="w-full overflow-y-auto overflow-x-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MemberWrapper);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
import {lazy} from 'react'
|
||||
export const MemberWrapper = lazy( ()=> import('./MemberWrapper'))
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
|
||||
import React, { useId, useState } from "react";
|
||||
import { StringCaser } from "Utils/utils";
|
||||
|
||||
let timeout = null;
|
||||
|
||||
const MkdDebounceInput = ({
|
||||
type = "text",
|
||||
label,
|
||||
className,
|
||||
placeholder = "Search",
|
||||
options = [],
|
||||
disabled = false,
|
||||
setValue,
|
||||
value,
|
||||
onReady,
|
||||
timer = 1000,
|
||||
showIcon = true,
|
||||
}) => {
|
||||
const inputId = useId();
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
function handleInput(e) {
|
||||
const inputName = e.target.value;
|
||||
setValue(inputName);
|
||||
setInputValue(inputName);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
// Make the API call here using the `name` state variable
|
||||
if (inputName.length) {
|
||||
onReady(inputName);
|
||||
}
|
||||
}, timer); // 500 milliseconds = half a second
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form>
|
||||
<label
|
||||
className="mb-2 block cursor-pointer text-sm font-bold text-gray-700"
|
||||
htmlFor={inputId}
|
||||
>
|
||||
{StringCaser(label, { casetype: "capitalize", separator: "space" })}
|
||||
</label>
|
||||
{type === "dropdown" || type === "select" ? (
|
||||
<select
|
||||
type={type}
|
||||
id={inputId}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => handleInput(e)}
|
||||
value={value || inputValue}
|
||||
className={`focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none ${className}`}
|
||||
>
|
||||
<option></option>
|
||||
{options.map((option, key) => (
|
||||
<option value={option} key={key + 1}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<div className="relative">
|
||||
{showIcon && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg
|
||||
className="h-4 w-4 text-gray-500 dark:text-gray-400"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type={type}
|
||||
id={inputId}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => handleInput(e)}
|
||||
value={value || inputValue}
|
||||
className={`block w-full rounded-lg border border-blue-600 bg-white p-4 pl-10 text-sm text-black placeholder-black focus:border-blue-500 focus:ring-blue-500 dark:text-gray-400 dark:placeholder-gray-400 ${className}`}
|
||||
/>
|
||||
</div>
|
||||
// <input
|
||||
// type={type}
|
||||
// id={inputId}
|
||||
// disabled={disabled}
|
||||
// placeholder={placeholder}
|
||||
// onChange={(e)=> handleInput(e)}
|
||||
// value={value||inputValue}
|
||||
// className={`focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none ${className}`}
|
||||
// />
|
||||
)}
|
||||
{/* <p className="text-field-error italic text-red-500">
|
||||
{errors[name]?.message}
|
||||
</p> */}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// <input type="search" id="search" className="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Search" required />
|
||||
|
||||
export default MkdDebounceInput;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import { lazy } from 'react'
|
||||
|
||||
export const MkdDebounceInput = lazy(()=> import("./MkdDebounceInput"))
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import * as XLSX from "xlsx";
|
||||
import Papa from "papaparse";
|
||||
import { Spinner } from "Assets/svgs";
|
||||
|
||||
const acceptType = (fileType) => {
|
||||
switch (fileType) {
|
||||
case "excel":
|
||||
return ".xlsx,.xls";
|
||||
case "csv":
|
||||
return ".csv";
|
||||
default:
|
||||
return ".xlsx,.xls";
|
||||
}
|
||||
};
|
||||
|
||||
const MkdFileTable = ({
|
||||
title = "File Table",
|
||||
className = "",
|
||||
fileType = "excel",
|
||||
}) => {
|
||||
const inputRef = useRef(null);
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
const handleExcelFile = (e) => {
|
||||
try {
|
||||
setData(() => []);
|
||||
setDataLoading(true);
|
||||
const reader = new FileReader();
|
||||
reader.readAsBinaryString(e.target.files[0]);
|
||||
reader.onload = (e) => {
|
||||
const data = e.target.result;
|
||||
const workbook = XLSX.read(data, { type: "binary" });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const parsedData = XLSX.utils.sheet_to_json(sheet);
|
||||
setData(parsedData);
|
||||
setDataLoading(false);
|
||||
inputRef.current.value = "";
|
||||
};
|
||||
} catch (error) {
|
||||
setDataLoading(false);
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleCsvFile = (e) => {
|
||||
try {
|
||||
setData(() => []);
|
||||
setDataLoading(true);
|
||||
const file = e.target.files[0];
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
complete: (results) => {
|
||||
setData(results.data);
|
||||
inputRef.current.value = "";
|
||||
setDataLoading(false);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
inputRef.current.value = "";
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInput = (e) => {
|
||||
switch (fileType) {
|
||||
case "excel":
|
||||
return handleExcelFile(e);
|
||||
case "csv":
|
||||
return handleCsvFile(e);
|
||||
default:
|
||||
return handleExcelFile(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`w-full p-5 ${className}`}>
|
||||
<div className="flex flex-col gap-[2.5rem]">
|
||||
<div className="flex flex-col justify-start py-4">
|
||||
<label htmlFor="input" className="mb-2 font-bold">
|
||||
Select <span className="uppercase">{fileType}</span> File.
|
||||
</label>
|
||||
<input
|
||||
disabled={dataLoading}
|
||||
className="w-[20%] cursor-pointer rounded bg-blue-500 p-4 text-white"
|
||||
type="file"
|
||||
accept={acceptType(fileType)}
|
||||
ref={inputRef}
|
||||
onChange={handleFileInput}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative min-h-fit w-full ">
|
||||
<div
|
||||
className={`absolute left-0 top-[-15px] m-auto flex gap-5 bg-white text-black`}
|
||||
>
|
||||
{/* <TableCellsIcon className="h-6 w-6" /> */}
|
||||
{title ? title : "Data Table"}
|
||||
</div>
|
||||
<div className="relative mt-5 min-h-fit w-full max-w-full overflow-auto">
|
||||
{dataLoading ? (
|
||||
<div className="flex h-fit max-h-fit min-h-fit w-full justify-center overflow-hidden">
|
||||
<Spinner size={100} color="#0EA5E9" />
|
||||
</div>
|
||||
) : data.length ? (
|
||||
<table className="min-h-[6.25rem] w-full border border-gray-600">
|
||||
<thead className="min-h-[50px]">
|
||||
<tr className="w-fit ">
|
||||
{data.length
|
||||
? Object.keys(data[0]).map((item, index) => (
|
||||
<th
|
||||
key={`${item}_${index}`}
|
||||
scope="col"
|
||||
className="border border-gray-600 px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{item}
|
||||
</th>
|
||||
))
|
||||
: null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="w-fit ">
|
||||
{Object.values(row).map((value, valueKey) => (
|
||||
<td
|
||||
key={valueKey}
|
||||
// scope="col"
|
||||
className="border border-gray-600 px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>
|
||||
{value}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MkdFileTable;
|
||||
@@ -0,0 +1 @@
|
||||
export {default as MkdFileTable} from "./MkdFileTable"
|
||||
@@ -0,0 +1,457 @@
|
||||
|
||||
import React, { useId, useRef, useState } from "react";
|
||||
import * as XLSX from "xlsx";
|
||||
import Papa from "papaparse";
|
||||
|
||||
const acceptType = (fileType) => {
|
||||
switch (fileType) {
|
||||
case "excel":
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
case ".xlsx":
|
||||
case ".xls":
|
||||
case ".xlsx,.xls":
|
||||
case "xlsx,xls":
|
||||
case ".xls,.xlsx":
|
||||
case "xls,xlsx":
|
||||
return ".xlsx,.xls";
|
||||
case "csv":
|
||||
case ".csv":
|
||||
return ".csv";
|
||||
case ".png":
|
||||
case "png":
|
||||
return ".png";
|
||||
case ".jpg":
|
||||
case "jpg":
|
||||
return ".jpg";
|
||||
case ".jpeg":
|
||||
case "jpeg":
|
||||
return ".jpeg";
|
||||
case "image":
|
||||
case ".png,.jpg,.jpeg":
|
||||
case ".png,.jpeg,.jpg":
|
||||
case ".jpg,.png,.jpeg":
|
||||
case ".jpg,.jpeg,.png":
|
||||
case ".jpeg,.png,.jpg":
|
||||
case ".jpeg,.jpg,.png":
|
||||
return ".png,.jpg,.jpeg";
|
||||
case "doc":
|
||||
case "docx":
|
||||
case "document":
|
||||
case ".doc":
|
||||
case ".docx":
|
||||
case ".docx,.doc":
|
||||
case ".doc,.docx":
|
||||
case "docx,doc":
|
||||
case "doc,docx":
|
||||
return ".docx,.doc";
|
||||
case "pdf":
|
||||
case ".pdf":
|
||||
return ".pdf";
|
||||
default:
|
||||
return "*";
|
||||
}
|
||||
};
|
||||
const transformFileType = (fileType) => {
|
||||
switch (fileType) {
|
||||
case "excel":
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
case ".xlsx":
|
||||
case ".xls":
|
||||
case ".xlsx,.xls":
|
||||
case "xlsx,xls":
|
||||
case ".xls,.xlsx":
|
||||
case "xls,xlsx":
|
||||
return "an excel";
|
||||
case "csv":
|
||||
case ".csv":
|
||||
return "a csv";
|
||||
case ".png":
|
||||
case "png":
|
||||
return "a png";
|
||||
case ".jpg":
|
||||
case "jpg":
|
||||
return "a jpg";
|
||||
case ".jpeg":
|
||||
case "jpeg":
|
||||
return "a jpeg";
|
||||
case "image":
|
||||
case ".png,.jpg,.jpeg":
|
||||
case ".png,.jpeg,.jpg":
|
||||
case ".jpg,.png,.jpeg":
|
||||
case ".jpg,.jpeg,.png":
|
||||
case ".jpeg,.png,.jpg":
|
||||
case ".jpeg,.jpg,.png":
|
||||
return "an image";
|
||||
case "doc":
|
||||
case "docx":
|
||||
case "document":
|
||||
case ".doc":
|
||||
case ".docx":
|
||||
case ".docx,.doc":
|
||||
case ".doc,.docx":
|
||||
case "docx,doc":
|
||||
case "doc,docx":
|
||||
return "a word document";
|
||||
case "pdf":
|
||||
case ".pdf":
|
||||
return "a pdf";
|
||||
default:
|
||||
return "any";
|
||||
}
|
||||
};
|
||||
const testFileTypeToRender = (fileType) => {
|
||||
switch (fileType) {
|
||||
case "excel":
|
||||
case "xls":
|
||||
case "xlsx":
|
||||
case ".xlsx":
|
||||
case ".xls":
|
||||
case ".xlsx,.xls":
|
||||
case "xlsx,xls":
|
||||
case ".xls,.xlsx":
|
||||
case "xls,xlsx":
|
||||
return "excel";
|
||||
case "csv":
|
||||
case ".csv":
|
||||
return "csv";
|
||||
case ".png":
|
||||
case "png":
|
||||
return "image";
|
||||
case ".jpg":
|
||||
case "jpg":
|
||||
return "image";
|
||||
case ".jpeg":
|
||||
case "jpeg":
|
||||
return "image";
|
||||
case "image":
|
||||
case ".png,.jpg,.jpeg":
|
||||
case ".png,.jpeg,.jpg":
|
||||
case ".jpg,.png,.jpeg":
|
||||
case ".jpg,.jpeg,.png":
|
||||
case ".jpeg,.png,.jpg":
|
||||
case ".jpeg,.jpg,.png":
|
||||
return "image";
|
||||
case "doc":
|
||||
case "docx":
|
||||
case "document":
|
||||
case ".doc":
|
||||
case ".docx":
|
||||
case ".docx,.doc":
|
||||
case ".doc,.docx":
|
||||
case "docx,doc":
|
||||
case "doc,docx":
|
||||
return "document";
|
||||
case "pdf":
|
||||
case ".pdf":
|
||||
return "pdf";
|
||||
// default:
|
||||
// return "any";
|
||||
}
|
||||
};
|
||||
const MkdFileUpload = ({
|
||||
fileType,
|
||||
name = "fileData",
|
||||
multiple = false,
|
||||
onAddSuccess,
|
||||
}) => {
|
||||
const fileInputId = useId();
|
||||
const inputRef = useRef(null);
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
const [data, setData] = useState([]);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [uploadedFile, setUploadedFile] = useState(null);
|
||||
const [fileObj, setFileObj] = React.useState({});
|
||||
|
||||
const onAddFile = (field, target, multiple = false) => {
|
||||
let tempFileObj = fileObj;
|
||||
// console.log(target);
|
||||
if (multiple) {
|
||||
if (tempFileObj[field]) {
|
||||
tempFileObj[field] = [
|
||||
...tempFileObj[field],
|
||||
{
|
||||
file: target.files[0],
|
||||
tempFile: {
|
||||
url: URL.createObjectURL(target.files[0]),
|
||||
name: target.files[0].name,
|
||||
type: target.files[0].type,
|
||||
},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
tempFileObj[field] = [
|
||||
{
|
||||
file: target.files[0],
|
||||
tempFile: {
|
||||
url: URL.createObjectURL(target.files[0]),
|
||||
name: target.files[0].name,
|
||||
type: target.files[0].type,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
} else {
|
||||
tempFileObj[field] = {
|
||||
file: target.files[0],
|
||||
tempFile: {
|
||||
url: URL.createObjectURL(target.files[0]),
|
||||
name: target.files[0].name,
|
||||
type: target.files[0].type,
|
||||
},
|
||||
};
|
||||
}
|
||||
setFileObj({ ...tempFileObj });
|
||||
onAddSuccess({ ...tempFileObj });
|
||||
};
|
||||
const updateAssets = (field, data, multiple = false) => {
|
||||
let tempFileObj = fileObj;
|
||||
if (multiple) {
|
||||
if (tempFileObj[field]) {
|
||||
tempFileObj[field] = [
|
||||
...tempFileObj[field],
|
||||
...data.map((item) => ({ file: null, tempFile: item })),
|
||||
];
|
||||
} else {
|
||||
tempFileObj[field] = [
|
||||
...data.map((item) => ({ file: null, tempFile: item })),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
tempFileObj[field] = {
|
||||
file: null,
|
||||
tempURL: data,
|
||||
};
|
||||
}
|
||||
setFileObj({ ...tempFileObj });
|
||||
};
|
||||
|
||||
const removeItem = (index) => {
|
||||
let tempFileObj = fileObj;
|
||||
|
||||
if (multiple) {
|
||||
let tempFiles = tempFileObj[name];
|
||||
tempFiles.splice(index, 1);
|
||||
tempFileObj[name] = [...tempFiles];
|
||||
} else {
|
||||
tempFileObj[name] = null;
|
||||
}
|
||||
|
||||
setFileObj({ ...tempFileObj });
|
||||
};
|
||||
|
||||
const handleExcelFile = (e, dnd = false) => {
|
||||
try {
|
||||
setData(() => []);
|
||||
setDataLoading(true);
|
||||
const reader = new FileReader();
|
||||
reader.readAsBinaryString(e.target.files[0]);
|
||||
reader.onload = (e) => {
|
||||
const data = e.target.result;
|
||||
const workbook = XLSX.read(data, { type: "binary" });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
const parsedData = XLSX.utils.sheet_to_json(sheet);
|
||||
setData(parsedData);
|
||||
setDataLoading(false);
|
||||
inputRef.current.value = "";
|
||||
};
|
||||
} catch (error) {
|
||||
setDataLoading(false);
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleCsvFile = (e, dnd = false) => {
|
||||
try {
|
||||
setData(() => []);
|
||||
setDataLoading(true);
|
||||
const file = e.target.files[0];
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
complete: (results) => {
|
||||
setData(results.data);
|
||||
inputRef.current.value = "";
|
||||
setDataLoading(false);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
inputRef.current.value = "";
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
const handleImageFile = (e, dnd = false) => {
|
||||
try {
|
||||
// setData(() => []);
|
||||
// setDataLoading(true);
|
||||
// const file = e.target.files[0];
|
||||
onAddFile(name, dnd ? e.dataTransfer : { files: e }, multiple);
|
||||
inputRef.current.value = "";
|
||||
// setDataLoading(false);
|
||||
} catch (error) {
|
||||
inputRef.current.value = "";
|
||||
// setDataLoading(false);
|
||||
}
|
||||
};
|
||||
const handleDocumentFile = (e, dnd = falsee) => {
|
||||
try {
|
||||
setData(() => []);
|
||||
setDataLoading(true);
|
||||
const file = e.target.files[0];
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
complete: (results) => {
|
||||
setData(results.data);
|
||||
inputRef.current.value = "";
|
||||
setDataLoading(false);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
inputRef.current.value = "";
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
const handlePdfFile = (e, dnd = false) => {
|
||||
try {
|
||||
setData(() => []);
|
||||
setDataLoading(true);
|
||||
const file = e.target.files[0];
|
||||
Papa.parse(file, {
|
||||
header: true,
|
||||
complete: (results) => {
|
||||
setData(results.data);
|
||||
inputRef.current.value = "";
|
||||
setDataLoading(false);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
inputRef.current.value = "";
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInput = (e, dnd = false) => {
|
||||
// console.log(e.target.files);
|
||||
// console.log(inputRef.current.files);
|
||||
onAddFile(name, dnd ? e.dataTransfer : e.target, multiple);
|
||||
inputRef.current.value = "";
|
||||
// return;
|
||||
// if (givenType === "image") {
|
||||
// } else if (givenType === "excel") {
|
||||
// return handleExcelFile(dnd ? e : e.target.files, dnd);
|
||||
// } else if (givenType === "csv") {
|
||||
// return handleCsvFile(dnd ? e : e.target.files, dnd);
|
||||
// } else if (givenType === "document") {
|
||||
// return handleDocumentFile(dnd ? e : e.target.files, dnd);
|
||||
// } else if (givenType === "pdf") {
|
||||
// return handlePdfFile(dnd ? e : e.target.files, dnd);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
|
||||
const droppedFiles = e.dataTransfer.files;
|
||||
console.log(e.dataTransfer.files);
|
||||
if (droppedFiles.length > 0) {
|
||||
const file = droppedFiles[0];
|
||||
setUploadedFile(file);
|
||||
handleFileInput(e, true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = () => {
|
||||
// Implement your file upload logic here using FormData or other methods.
|
||||
if (uploadedFile) {
|
||||
console.log("Uploading file:", uploadedFile);
|
||||
// Reset uploadedFile state after successful upload.
|
||||
setUploadedFile(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
id={fileInputId}
|
||||
disabled={dataLoading}
|
||||
className="hidden w-[20%] cursor-pointer rounded bg-blue-500 p-4 text-white"
|
||||
type="file"
|
||||
accept={acceptType(fileType)}
|
||||
ref={inputRef}
|
||||
onChange={handleFileInput}
|
||||
/>
|
||||
<div
|
||||
// className={`file-uploader ${dragging ? 'dragging' : ''}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`min-h-[9.375rem] w-full cursor-pointer rounded-md border-[.3125rem] border-dashed md:w-[31.25rem] md:min-w-[31.25rem] md:max-w-[31.25rem] ${
|
||||
dragging ? "border-green-500" : "border-blue-500"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full max-h-full min-h-full w-full min-w-full max-w-full flex-col items-center justify-center py-4">
|
||||
<div className="flex flex-col items-center ">
|
||||
<div className="mb-2 font-bold">
|
||||
Select/Drag and Drop {transformFileType(fileType)} File.
|
||||
</div>
|
||||
<div
|
||||
className="flex h-[50px] w-[100px] cursor-pointer items-center justify-center rounded-md border bg-blue-500 text-white"
|
||||
onClick={() => inputRef.current.click()}
|
||||
>
|
||||
Select File
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[100px] w-full overflow-auto">
|
||||
{multiple && fileObj[name] && fileObj[name].length ? (
|
||||
<>
|
||||
{testFileTypeToRender(fileType) === "image"
|
||||
? fileObj[name].map((photo, photoKey) => (
|
||||
<img
|
||||
key={photoKey}
|
||||
src={photo["tempFile"]["url"]}
|
||||
className="h-full"
|
||||
title={photo["tempFile"]["name"]}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</>
|
||||
) : !multiple && fileObj[name] ? (
|
||||
<>
|
||||
{testFileTypeToRender(fileType) === "image" ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={fileObj[name]["tempFile"]["url"]}
|
||||
className="h-full"
|
||||
title={fileObj[name]["tempFile"]["name"]}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// <input type="search" id="search" class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Search" required />
|
||||
|
||||
export default MkdFileUpload;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { lazy } from 'react'
|
||||
export const MkdFileUpload = lazy(()=> import("./MkdFileUpload"))
|
||||
@@ -0,0 +1,170 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { EyeIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
// import { capitalize } from "Utils/utils";
|
||||
// import { colors } from "Utils/config";
|
||||
// import MkdSDK from "Utils/MkdSDK";
|
||||
// import { Spinner } from "Assets/svgs";
|
||||
// import { AuthContext, tokenExpireError } from "Context/Auth";
|
||||
// import { GlobalContext, showToast } from "Context/Global";
|
||||
|
||||
const MkdGridCard = ({
|
||||
columns,
|
||||
row,
|
||||
actions,
|
||||
tableRole,
|
||||
table,
|
||||
actionId,
|
||||
setDeleteId,
|
||||
getData,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`group relative h-fit min-h-[9.375rem] truncate rounded-[.625rem] bg-white p-5 shadow-md`}
|
||||
>
|
||||
{columns.map((cell, colKey) => {
|
||||
if (
|
||||
cell?.accessor.indexOf("image") > -1 ||
|
||||
cell?.accessor.indexOf("photo") > -1
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key={colKey}
|
||||
className="flex items-center justify-start gap-2"
|
||||
>
|
||||
<div className="capitalize">{cell?.header}:</div>
|
||||
<div className="whitespace-nowrap px-6 py-4 text-[1.5rem]">
|
||||
<img
|
||||
src={row[cell?.accessor]}
|
||||
className="h-[100px] w-[150px]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (
|
||||
cell?.accessor.indexOf("pdf") > -1 ||
|
||||
cell?.accessor.indexOf("doc") > -1 ||
|
||||
cell?.accessor.indexOf("file") > -1 ||
|
||||
cell?.accessor.indexOf("video") > -1
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key={colKey}
|
||||
className="flex items-center justify-start gap-2"
|
||||
>
|
||||
<div className="capitalize">{cell?.header}:</div>
|
||||
<div className="whitespace-nowrap px-6 py-4 text-[1.5rem]">
|
||||
<a
|
||||
className="text-blue-500"
|
||||
target="_blank"
|
||||
href={row[cell?.accessor]}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{" "}
|
||||
View
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (cell?.accessor === "") {
|
||||
if (
|
||||
[actions?.view, actions?.edit, actions?.delete].includes(true)
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
key={colKey}
|
||||
className={`absolute right-0 top-[-100%] m-auto flex gap-2 whitespace-nowrap bg-white px-6 py-4 transition-all group-hover:top-0`}
|
||||
>
|
||||
{actions?.edit && (
|
||||
<button
|
||||
className="text-xs"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/${tableRole}/edit-${table}/` + row[actionId],
|
||||
{
|
||||
state: row,
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<PencilIcon
|
||||
title={`Edit ${table} details`}
|
||||
className={`h-[1rem] w-[1rem] cursor-pointer `}
|
||||
pathClasses={` text-black`}
|
||||
stroke={"#29282990"}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{actions?.view && (
|
||||
<button
|
||||
className="px-1 text-xs text-blue-500"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/${tableRole}/view-${table}/` + row[actionId],
|
||||
{
|
||||
state: row,
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<EyeIcon
|
||||
title={`View ${table} details`}
|
||||
className={`h-[1rem] w-[1rem] cursor-pointer `}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{actions?.delete && (
|
||||
<button
|
||||
className="px-1 text-xs text-red-500"
|
||||
onClick={() => setDeleteId(row[actionId])}
|
||||
>
|
||||
{" "}
|
||||
<TrashIcon
|
||||
title={`Delete ${table}`}
|
||||
className={`h-[1rem] w-[1rem] cursor-pointer `}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (cell?.mappingExist) {
|
||||
return (
|
||||
<div
|
||||
key={colKey}
|
||||
className="flex items-center justify-start gap-2"
|
||||
>
|
||||
<div className="capitalize">{cell?.header}:</div>
|
||||
<div className="whitespace-nowrap px-6 py-4 text-sm">
|
||||
{cell?.mappings[row[cell?.accessor]]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={colKey} className="flex items-center justify-start gap-2">
|
||||
<div className="capitalize">{cell?.header}:</div>
|
||||
<div className="whitespace-nowrap px-6 py-4 text-sm">
|
||||
{row[cell?.accessor]}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MkdGridCard;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from "react";
|
||||
import ModalPrompt from "Components/Modal/ModalPrompt";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { capitalize } from "Utils/utils";
|
||||
import { Spinner } from "Assets/svgs";
|
||||
import { colors } from "Utils/config";
|
||||
import MkdGridCard from "./MkdGridCard";
|
||||
|
||||
const MkdGridCards = ({
|
||||
table,
|
||||
onSort,
|
||||
getData,
|
||||
loading,
|
||||
columns,
|
||||
actions,
|
||||
tableRole,
|
||||
deleteItem,
|
||||
deleteLoading,
|
||||
actionId = "id",
|
||||
showDeleteModal,
|
||||
currentTableData,
|
||||
setShowDeleteModal,
|
||||
}) => {
|
||||
const [deleteId, setIdToDelete] = React.useState(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const setDeleteId = async (id) => {
|
||||
setShowDeleteModal(true);
|
||||
setIdToDelete(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${loading ? "" : ""} border-b border-gray-200 shadow`}>
|
||||
{loading ? (
|
||||
<div
|
||||
className={`flex max-h-fit min-h-fit w-full min-w-full max-w-full items-center justify-center py-5`}
|
||||
>
|
||||
<Spinner size={50} color={colors.primary} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="min-w-full">
|
||||
<div className="grid grid-cols-3 gap-5 bg-white">
|
||||
{currentTableData.map((row, i) => {
|
||||
return (
|
||||
<MkdGridCard
|
||||
key={i}
|
||||
actionId={actionId}
|
||||
actions={actions}
|
||||
columns={columns}
|
||||
getData={getData}
|
||||
row={row}
|
||||
setDeleteId={setDeleteId}
|
||||
table={table}
|
||||
tableRole={tableRole}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDeleteModal ? (
|
||||
<ModalPrompt
|
||||
actionHandler={() => {
|
||||
deleteItem(deleteId);
|
||||
}}
|
||||
closeModalFunction={() => {
|
||||
setIdToDelete(null);
|
||||
setShowDeleteModal(false);
|
||||
}}
|
||||
title={`Delete ${capitalize(table)}`}
|
||||
message={`You are about to delete ${capitalize(
|
||||
table
|
||||
)} ${deleteId}, note that this action is irreversible`}
|
||||
acceptText={`DELETE`}
|
||||
rejectText={`CANCEL`}
|
||||
loading={deleteLoading}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MkdGridCards;
|
||||
@@ -0,0 +1,325 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import MkdSDK from "Utils/MkdSDK";
|
||||
import { getNonNullValue } from "Utils/utils";
|
||||
import {PaginationBar} from "Components/PaginationBar";
|
||||
import { AuthContext, tokenExpireError } from "Context/Auth";
|
||||
import { GlobalContext } from "Context/Global";
|
||||
import { MkdGridCards } from "Components/MkdGridView";
|
||||
import { MkdInput } from "Components/MkdInput";
|
||||
|
||||
let sdk = new MkdSDK();
|
||||
// const getSchemaStructure = (schema) => {
|
||||
// return;
|
||||
// };
|
||||
const getType = (type) => {
|
||||
switch (type) {
|
||||
case "varchar":
|
||||
return "text";
|
||||
case "text":
|
||||
return "textarea";
|
||||
case "mediumtext":
|
||||
return "textarea";
|
||||
case "longtext":
|
||||
return "textarea";
|
||||
case "tinyint":
|
||||
return "number";
|
||||
case "int":
|
||||
return "number";
|
||||
case "bigint":
|
||||
return "number";
|
||||
case "float":
|
||||
return "number";
|
||||
case "double":
|
||||
return "number";
|
||||
case "image":
|
||||
return "image";
|
||||
case "file":
|
||||
return "file";
|
||||
case "date":
|
||||
return "date";
|
||||
case "datetime":
|
||||
return "datetime";
|
||||
default:
|
||||
return "text";
|
||||
}
|
||||
};
|
||||
|
||||
const MkdGridView = ({
|
||||
columns = [],
|
||||
actions = { view: true, edit: true, delete: true },
|
||||
actionId = "id",
|
||||
tableRole = "admin",
|
||||
table = "user",
|
||||
tableSchema = [],
|
||||
hasFilter = true,
|
||||
schemaFields = [],
|
||||
}) => {
|
||||
const { dispatch } = React.useContext(AuthContext);
|
||||
const { dispatch: globalDispatch } = React.useContext(GlobalContext);
|
||||
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [currentTableData, setCurrentTableData] = React.useState([]);
|
||||
const [pageSize, setPageSize] = React.useState(10);
|
||||
const [pageCount, setPageCount] = React.useState(0);
|
||||
const [dataTotal, setDataTotal] = React.useState(0);
|
||||
const [currentPage, setPage] = React.useState(0);
|
||||
const [canPreviousPage, setCanPreviousPage] = React.useState(false);
|
||||
const [canNextPage, setCanNextPage] = React.useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = React.useState(false);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const schema = yup.object({});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
function onSort(columnIndex) {
|
||||
console.log(columns[columnIndex]);
|
||||
if (columns[columnIndex].isSorted) {
|
||||
columns[columnIndex].isSortedDesc = !columns[columnIndex].isSortedDesc;
|
||||
} else {
|
||||
columns.map((i) => (i.isSorted = false));
|
||||
columns.map((i) => (i.isSortedDesc = false));
|
||||
columns[columnIndex].isSorted = true;
|
||||
}
|
||||
|
||||
(async function () {
|
||||
await getData(0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function updatePageSize(limit) {
|
||||
(async function () {
|
||||
setPageSize(limit);
|
||||
await getData(0, limit);
|
||||
})();
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
(async function () {
|
||||
await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize);
|
||||
})();
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
(async function () {
|
||||
await getData(
|
||||
currentPage + 1 <= pageCount ? currentPage + 1 : 0,
|
||||
pageSize
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
async function getData(pageNum, limitNum, currentTableData) {
|
||||
try {
|
||||
// sdk.setTable("nanny_profile");
|
||||
// let sortField = columns.filter(col => col.isSorted);
|
||||
setLoading(true);
|
||||
sdk.setTable(table);
|
||||
const result = await sdk.callRestAPI(
|
||||
{
|
||||
payload: {
|
||||
...currentTableData,
|
||||
},
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
},
|
||||
"PAGINATE"
|
||||
);
|
||||
if (result) {
|
||||
setLoading(false);
|
||||
}
|
||||
const { list, total, limit, num_pages, page } = result;
|
||||
|
||||
setCurrentTableData(list);
|
||||
setPageSize(limit);
|
||||
setPageCount(num_pages);
|
||||
setPage(page);
|
||||
setDataTotal(total);
|
||||
setCanPreviousPage(page > 1);
|
||||
setCanNextPage(page + 1 <= num_pages);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
console.log("ERROR", error);
|
||||
tokenExpireError(dispatch, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteItem = async (id) => {
|
||||
try {
|
||||
setDeleteLoading(true);
|
||||
sdk.setTable(table);
|
||||
const result = await sdk.callRestAPI({ id }, "DELETE");
|
||||
if (!result?.error) {
|
||||
setCurrentTableData((list) =>
|
||||
list.filter((x) => Number(x.user_id) !== Number(id))
|
||||
);
|
||||
setDeleteLoading(false);
|
||||
setShowDeleteModal(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setDeleteLoading(false);
|
||||
setShowDeleteModal(false);
|
||||
tokenExpireError(dispatch, err?.message);
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const exportTable = async (id) => {
|
||||
try {
|
||||
sdk.setTable(table);
|
||||
const result = await sdk.exportCSV();
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = async () => {
|
||||
reset();
|
||||
await getData(0, pageSize);
|
||||
};
|
||||
|
||||
const onSubmit = (_data) => {
|
||||
let filter = {};
|
||||
for (const field of schemaFields) {
|
||||
const [key] = field.split(":");
|
||||
filter[key] = getNonNullValue(_data[key]);
|
||||
}
|
||||
getData(1, pageSize, filter);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// globalDispatch({
|
||||
// type: "SETPATH",
|
||||
// payload: {
|
||||
// path: "nanny_profile",
|
||||
// },
|
||||
// });
|
||||
|
||||
(async function () {
|
||||
await getData(1, pageSize);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{hasFilter ? (
|
||||
<form
|
||||
className="mb-10 rounded bg-white p-5 shadow"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<h4 className="text-2xl font-medium capitalize">{table} Search</h4>
|
||||
<div className="filter-form-holder mt-10 flex w-full flex-wrap gap-5">
|
||||
{tableSchema && tableSchema.length
|
||||
? tableSchema
|
||||
.map((item, key) => {
|
||||
if (item.is_filter) {
|
||||
if (Object.keys(item.mapping).length) {
|
||||
return (
|
||||
<div key={key + 1} className="w-[25%_!important] ">
|
||||
<MkdInput
|
||||
className={``}
|
||||
errors={errors}
|
||||
label={item.name}
|
||||
name={item.name}
|
||||
placeholder={item.name}
|
||||
type={`mapping`}
|
||||
mapping={item.mapping}
|
||||
options={[...Object.keys(item.mapping)]}
|
||||
register={register}
|
||||
page={``}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={key + 1} className="w-[25%_!important] ">
|
||||
<MkdInput
|
||||
className={``}
|
||||
errors={errors}
|
||||
label={item.name}
|
||||
name={item.name}
|
||||
placeholder={item.name}
|
||||
type={getType(item.type)}
|
||||
register={register}
|
||||
page={``}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
: null}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className=" focus:shadow-outline ml-2 inline rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
}}
|
||||
type="button"
|
||||
className=" focus:shadow-outline ml-2 inline rounded bg-red-500 px-4 py-2 font-bold text-white hover:bg-red-700 focus:outline-none"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
<div className="overflow-x-auto rounded bg-white p-5 shadow">
|
||||
<div className="mb-3 flex w-full justify-between text-center ">
|
||||
<h4 className="text-2xl font-medium capitalize">{table} Profile</h4>
|
||||
<div className="flex">
|
||||
{/* <AddButton link={"/admin/add-nanny_profile"} />
|
||||
<ExportButton onClick={exportTable} className="mx-1" /> */}
|
||||
</div>
|
||||
</div>
|
||||
<MkdGridCards
|
||||
table={table}
|
||||
columns={columns}
|
||||
actions={actions}
|
||||
loading={loading}
|
||||
actionId={actionId}
|
||||
tableRole={tableRole}
|
||||
deleteLoading={deleteLoading}
|
||||
showDeleteModal={showDeleteModal}
|
||||
currentTableData={currentTableData}
|
||||
onSort={onSort}
|
||||
getData={getData}
|
||||
deleteItem={deleteItem}
|
||||
setShowDeleteModal={setShowDeleteModal}
|
||||
setCurrentTableData={setCurrentTableData}
|
||||
/>
|
||||
</div>
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
pageCount={pageCount}
|
||||
pageSize={pageSize}
|
||||
canPreviousPage={canPreviousPage}
|
||||
canNextPage={canNextPage}
|
||||
updatePageSize={updatePageSize}
|
||||
previousPage={previousPage}
|
||||
nextPage={nextPage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MkdGridView;
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
import {lazy} from "react"
|
||||
export const MkdGridView = lazy(()=> import("./MkdGridView"))
|
||||
export const MkdGridCards = lazy(()=> import("./MkdGridCards"))
|
||||
export const MkdGridCard = lazy(()=> import("./MkdGridCard"))
|
||||
@@ -0,0 +1,120 @@
|
||||
|
||||
import React from "react";
|
||||
import { StringCaser } from "Utils/utils";
|
||||
|
||||
const MkdInput = ({
|
||||
type = "text",
|
||||
page,
|
||||
cols = "30",
|
||||
rows = "50",
|
||||
name,
|
||||
label,
|
||||
errors,
|
||||
register,
|
||||
className,
|
||||
placeholder,
|
||||
options = [],
|
||||
mapping = null,
|
||||
disabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`mb-4 ${page === "list" ? "w-full pl-2 pr-2 md:w-1/2" : ""}`}
|
||||
>
|
||||
<label
|
||||
className="mb-2 block cursor-pointer text-sm font-bold text-gray-700"
|
||||
htmlFor={name}
|
||||
>
|
||||
{StringCaser(label, { casetype: "capitalize", separator: "space" })}
|
||||
</label>
|
||||
{type === "textarea" ? (
|
||||
<textarea
|
||||
className={`focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none ${className} ${
|
||||
errors[name]?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
disabled={disabled}
|
||||
id={name}
|
||||
cols={cols}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
{...register(name)}
|
||||
></textarea>
|
||||
) : type === "radio" || type === "checkbox" || type === "color" ? (
|
||||
<input
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
id={name}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
{...register(name)}
|
||||
className={`focus:shadow-outline cursor-pointer appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none ${className} ${
|
||||
errors[name]?.message ? "border-red-500" : ""
|
||||
} ${type === "color" ? "min-h-[3.125rem] min-w-[6.25rem]" : ""}`}
|
||||
/>
|
||||
) : type === "dropdown" || type === "select" ? (
|
||||
<select
|
||||
type={type}
|
||||
id={name}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
{...register(name)}
|
||||
className={`focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none ${className} ${
|
||||
errors[name]?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<option></option>
|
||||
{options.map((option, key) => (
|
||||
<option value={option} key={key + 1}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : type === "mapping" ? (
|
||||
<>
|
||||
{mapping ? (
|
||||
<select
|
||||
type={type}
|
||||
id={name}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
{...register(name)}
|
||||
className={`focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none ${className} ${
|
||||
errors[name]?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
>
|
||||
<option></option>
|
||||
{options.map((option, key) => (
|
||||
<option value={option} key={key + 1}>
|
||||
{mapping[option]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
`Please Pass the mapping e.g {key:value}`
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
type={type}
|
||||
id={name}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
{...register(name)}
|
||||
className={`focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none ${className} ${
|
||||
errors[name]?.message ? "border-red-500" : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<p className="text-field-error italic text-red-500">
|
||||
{errors[name]?.message}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MkdInput;
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
export { default as MkdInput } from "./MkdInput";
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import MkdJsonQuizOptions from "../MkdJsonQuizOptions";
|
||||
import { QuestionTypes } from "../MkdJsonQuiz";
|
||||
|
||||
const MultipleChoice = ({ updateQuestions, currentQuestion }) => {
|
||||
// console.log("currentQuestion >>", currentQuestion);
|
||||
return (
|
||||
<>
|
||||
{currentQuestion &&
|
||||
currentQuestion?.type === QuestionTypes.multiple_choice ? (
|
||||
<MkdJsonQuizOptions
|
||||
updateQuestions={updateQuestions}
|
||||
currentQuestion={currentQuestion}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultipleChoice;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useId, useState } from "react";
|
||||
import { QuestionTypes } from "../MkdJsonQuiz";
|
||||
|
||||
let timeout = null;
|
||||
|
||||
const ShortLongAnswer = ({ updateQuestions, currentQuestion }) => {
|
||||
// console.log("currentQuestion >>", currentQuestion);
|
||||
const inputId = useId();
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const getValue = () => {
|
||||
return inputValue || currentQuestion?.answer;
|
||||
};
|
||||
|
||||
function handleInput(e) {
|
||||
const inputValue = e.target.value;
|
||||
// setValue(inputValue);
|
||||
setInputValue(inputValue);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
const tempCurrentQuestion = { ...currentQuestion };
|
||||
tempCurrentQuestion["answer"] = inputValue;
|
||||
updateQuestions(tempCurrentQuestion);
|
||||
setInputValue("");
|
||||
}, 1000); // 500 milliseconds = half a second
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentQuestion &&
|
||||
[QuestionTypes.short_answer, QuestionTypes.long_answer].includes(
|
||||
currentQuestion?.type
|
||||
) ? (
|
||||
<div className="relative flex w-full grow items-center md:w-[60%] md:min-w-[60%]">
|
||||
{/* <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"></div> */}
|
||||
{currentQuestion?.type === QuestionTypes.short_answer ? (
|
||||
<input
|
||||
type={"text"}
|
||||
id={inputId}
|
||||
// disabled={disabled}
|
||||
placeholder={"Type your answer here"}
|
||||
onChange={(e) => handleInput(e)}
|
||||
value={getValue()}
|
||||
className="block w-full rounded-lg border border-blue-600 bg-white p-4 pl-3 text-sm text-black placeholder-black focus:border-blue-500 focus:ring-blue-500 dark:text-gray-400 dark:placeholder-gray-400"
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
id={inputId}
|
||||
rows={5}
|
||||
// disabled={disabled}
|
||||
placeholder={"Type your answer here"}
|
||||
onChange={(e) => handleInput(e)}
|
||||
value={getValue()}
|
||||
className="block w-full resize-none rounded-lg border border-blue-600 bg-white p-4 pl-3 text-sm text-black placeholder-black focus:border-blue-500 focus:ring-blue-500 dark:text-gray-400 dark:placeholder-gray-400"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShortLongAnswer;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import MkdJsonQuizOptions from "../MkdJsonQuizOptions";
|
||||
import { QuestionTypes } from "../MkdJsonQuiz";
|
||||
|
||||
const SingleChoice = ({ updateQuestions, currentQuestion }) => {
|
||||
// console.log("currentQuestion >>", currentQuestion);
|
||||
return (
|
||||
<>
|
||||
{currentQuestion &&
|
||||
currentQuestion?.type === QuestionTypes.single_choice ? (
|
||||
<MkdJsonQuizOptions
|
||||
updateQuestions={updateQuestions}
|
||||
currentQuestion={currentQuestion}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleChoice;
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import { QuestionTypes } from "../MkdJsonQuiz";
|
||||
|
||||
const TrueOrFalse = ({ updateQuestions, currentQuestion }) => {
|
||||
function handleClick(value) {
|
||||
const tempCurrentQuestion = { ...currentQuestion };
|
||||
if (
|
||||
tempCurrentQuestion["answer"] &&
|
||||
tempCurrentQuestion["answer"] === value
|
||||
) {
|
||||
tempCurrentQuestion["answer"] = null;
|
||||
} else {
|
||||
tempCurrentQuestion["answer"] = value;
|
||||
}
|
||||
|
||||
updateQuestions(tempCurrentQuestion);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentQuestion &&
|
||||
[QuestionTypes.true_or_false].includes(currentQuestion?.type) ? (
|
||||
<div className="relative flex w-full grow items-center justify-between gap-5 md:w-[60%] md:min-w-[60%]">
|
||||
{/* <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"></div> */}
|
||||
|
||||
<button
|
||||
onClick={() => handleClick("true")}
|
||||
className={`block w-1/3 rounded-lg border border-blue-600 p-4 pl-3 text-lg font-medium shadow-md transition-all ${
|
||||
currentQuestion["answer"] === "true"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
True
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleClick("false")}
|
||||
className={`block w-1/3 rounded-lg border border-blue-600 p-4 pl-3 text-lg font-medium shadow-md transition-all ${
|
||||
currentQuestion["answer"] === "false"
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white "
|
||||
}`}
|
||||
>
|
||||
False
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrueOrFalse;
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
import {lazy} from "react";
|
||||
|
||||
export const MultipleChoice = lazy(()=> import("./MultipleChoice"))
|
||||
export const ShortLongAnswer = lazy(()=> import("./ShortLongAnswer"))
|
||||
export const SingleChoice = lazy(()=> import("./SingleChoice"))
|
||||
export const TrueOrFalse = lazy(()=> import("./TrueOrFalse"))
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { RingProgress, Text } from "@mantine/core";
|
||||
|
||||
function CircularProgressBar({ percentage = 0 }) {
|
||||
return (
|
||||
<RingProgress
|
||||
size={170}
|
||||
thickness={16}
|
||||
roundCaps
|
||||
label={
|
||||
<Text color="blue" weight={700} align="center" size="xl">
|
||||
{percentage}%
|
||||
</Text>
|
||||
}
|
||||
sections={[{ value: percentage, color: "blue" }]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default CircularProgressBar;
|
||||
@@ -0,0 +1,205 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import MkdJsonQuizQuestions from "./MkdJsonQuizQuestions";
|
||||
import MkdJsonQuizResult from "./MkdJsonQuizResult";
|
||||
import { GlobalContext, showToast } from "Context/Global";
|
||||
|
||||
export const QuestionTypes = {
|
||||
single_choice: "single_choice",
|
||||
multiple_choice: "multiple_choice",
|
||||
short_answer: "short_answer",
|
||||
long_answer: "long_answer",
|
||||
true_or_false: "true_or_false",
|
||||
};
|
||||
|
||||
const Screens = {
|
||||
Questions: "Questions",
|
||||
Result: "Result",
|
||||
};
|
||||
const MkdJsonQuiz = ({ className, onSubmit, onContinue, jsonQuestions }) => {
|
||||
const { dispatch: globalDipatch } = useContext(GlobalContext);
|
||||
|
||||
const [totalScore, setTotalScore] = useState(0);
|
||||
const [questions, setQuestions] = useState(jsonQuestions);
|
||||
const [questionNumber, setQuestionNumber] = useState(0);
|
||||
const [currentQuestion, setCurrentQuestion] = useState(
|
||||
questions[questionNumber]
|
||||
);
|
||||
|
||||
const [currentScreen, setCurrentScreen] = useState(Screens.Questions);
|
||||
|
||||
const updateQuestions = (tempCurrentQuestion) => {
|
||||
// console.log("tempCurrentQuestion >>", tempCurrentQuestion);
|
||||
const tempQuestions = [...questions]; // No need to use JSON.parse/stringify
|
||||
tempQuestions.splice(questionNumber, 1, tempCurrentQuestion);
|
||||
setQuestions(() => [...tempQuestions]);
|
||||
setCurrentQuestion(() => tempCurrentQuestion);
|
||||
};
|
||||
|
||||
const onContinueCLick = () => {
|
||||
// const noAnswer = questions.find((question) => !question?.answer);
|
||||
// return questionNumber + 1 === questions?.length || !currentQuestion?.answer;
|
||||
if (!onContinue) {
|
||||
setQuestionNumber(0);
|
||||
setCurrentScreen(Screens.Questions);
|
||||
return;
|
||||
}
|
||||
onContinue();
|
||||
};
|
||||
|
||||
const isDisable = () => {
|
||||
// const noAnswer = questions.find((question) => !question?.answer);
|
||||
if (questionNumber + 1 === questions?.length) {
|
||||
return true;
|
||||
} else {
|
||||
switch (currentQuestion?.type) {
|
||||
case QuestionTypes.single_choice:
|
||||
case QuestionTypes.short_answer:
|
||||
case QuestionTypes.long_answer:
|
||||
case QuestionTypes.true_or_false:
|
||||
return !currentQuestion?.answer;
|
||||
case QuestionTypes.multiple_choice:
|
||||
return !(
|
||||
currentQuestion?.answers && currentQuestion?.answers?.length
|
||||
);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isSumitDisabled = () => {
|
||||
const answers = questions.map((question) => {
|
||||
switch (question?.type) {
|
||||
case QuestionTypes.single_choice:
|
||||
case QuestionTypes.short_answer:
|
||||
case QuestionTypes.long_answer:
|
||||
case QuestionTypes.true_or_false:
|
||||
return !!question?.answer;
|
||||
case QuestionTypes.multiple_choice:
|
||||
return !!(question?.answers && question?.answers?.length);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return answers.includes(false);
|
||||
};
|
||||
|
||||
const onSubmitQuestion = () => {
|
||||
// setQuestionNumber((prev) => prev - 1);
|
||||
if (!onSubmit) {
|
||||
showToast(globalDipatch, "Unable to Submit Quiz!", 5000, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// const multipleChoiceCount = questions.reduce((prev, current) => {
|
||||
// if (current?.type === "multiple_choice") {
|
||||
// return prev + 1;
|
||||
// } else {
|
||||
// return prev;
|
||||
// }
|
||||
// }, 0);
|
||||
|
||||
const eachScore =
|
||||
parseFloat(100) /
|
||||
parseFloat(questions && questions?.length ? questions?.length : 1);
|
||||
|
||||
const totalScore =
|
||||
questions && questions?.length
|
||||
? questions?.reduce((prev, question) => {
|
||||
switch (question?.type) {
|
||||
case QuestionTypes.single_choice:
|
||||
if (question?.answer?.is_answer) {
|
||||
return prev + eachScore;
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
case QuestionTypes.short_answer:
|
||||
case QuestionTypes.long_answer:
|
||||
case QuestionTypes.true_or_false:
|
||||
if (question?.answer === question?.correct_answer) {
|
||||
return prev + eachScore;
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
case QuestionTypes.multiple_choice:
|
||||
const correctAnswers = question?.options?.filter(
|
||||
(option) => option?.is_answer
|
||||
);
|
||||
const selectedAnswers = question?.answers?.length;
|
||||
const correctSelectedAnswers = question?.answers?.filter(
|
||||
(answer) => answer?.is_answer
|
||||
);
|
||||
if (
|
||||
correctAnswers?.length === selectedAnswers &&
|
||||
correctAnswers?.length === correctSelectedAnswers?.length
|
||||
) {
|
||||
return prev + eachScore;
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
default:
|
||||
return prev;
|
||||
}
|
||||
}, 0)
|
||||
: null;
|
||||
|
||||
setTotalScore(Number(totalScore.toFixed(2)));
|
||||
|
||||
onSubmit({
|
||||
questions,
|
||||
// correct_answers: correctAnswers?.length,
|
||||
// incorrect_answers: questions?.length - correctAnswers?.length,
|
||||
total_score: Number(totalScore.toFixed(2)),
|
||||
score_per_question: eachScore,
|
||||
});
|
||||
|
||||
setCurrentScreen(Screens.Result);
|
||||
};
|
||||
const onPrev = () => {
|
||||
setQuestionNumber((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const onNext = () => {
|
||||
setQuestionNumber((prev) => prev + 1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentQuestion(questions[questionNumber]);
|
||||
}, [questionNumber]);
|
||||
|
||||
// console.log(questionNumber);
|
||||
return (
|
||||
<div
|
||||
className={`flex h-fit max-h-fit min-h-[31.25rem] w-full min-w-full max-w-full flex-col items-center rounded-md p-5 pb-10 shadow-md ${className}`}
|
||||
>
|
||||
{currentScreen === Screens.Questions ? (
|
||||
<MkdJsonQuizQuestions
|
||||
Screens={Screens}
|
||||
currentQuestion={currentQuestion}
|
||||
isDisable={isDisable}
|
||||
isSumitDisabled={isSumitDisabled}
|
||||
onNext={onNext}
|
||||
onPrev={onPrev}
|
||||
questionLength={questions?.length}
|
||||
questionNumber={questionNumber}
|
||||
updateQuestions={updateQuestions}
|
||||
onSubmit={onSubmitQuestion}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{currentScreen === Screens.Result ? (
|
||||
<MkdJsonQuizResult
|
||||
totalScore={totalScore}
|
||||
onContinue={onContinueCLick}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MkdJsonQuiz.defaultProps = {
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default MkdJsonQuiz;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user