Compare commits
4 Commits
cbbb0ed4c4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b0d86465f2 | |||
| 743187b216 | |||
| 29e6eb82c7 | |||
| dc35cfcb3f |
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "day16",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"fabric": "^6.7.0",
|
||||
"fabricjs-react": "^1.2.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Canvas, Textbox, Image } from "fabric";
|
||||
|
||||
function App() {
|
||||
const canvasRef = useRef(null);
|
||||
const fabricRef = useRef(null);
|
||||
const [activeMenu, setActiveMenu] = useState("Background");
|
||||
const [selectedObject, setSelectedObject] = useState(null);
|
||||
const [fontSize, setFontSize] = useState(60);
|
||||
const [images, setImages] = useState([]);
|
||||
const [isImagesLoading, setIsImagesLoading] = useState(false);
|
||||
|
||||
const colors = [
|
||||
"#F4A261",
|
||||
"#E76F51",
|
||||
"#2A9D8F",
|
||||
"#264653",
|
||||
"#A8DADC",
|
||||
"#457B9D",
|
||||
"#E63946",
|
||||
"#F7B801",
|
||||
"#A259F7",
|
||||
"#6D6875",
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = new Canvas(canvasRef.current, {
|
||||
width: 900,
|
||||
height: 400,
|
||||
backgroundColor: "#F4A261",
|
||||
});
|
||||
fabricRef.current = canvas;
|
||||
|
||||
// Object selection logic
|
||||
function updateSelectedObject() {
|
||||
setSelectedObject(canvas.getActiveObject());
|
||||
const obj = canvas.getActiveObject();
|
||||
if (obj && obj.type === "text") {
|
||||
setFontSize(obj.fontSize);
|
||||
}
|
||||
}
|
||||
canvas.on("selection:created", updateSelectedObject);
|
||||
canvas.on("selection:updated", updateSelectedObject);
|
||||
canvas.on("selection:cleared", updateSelectedObject);
|
||||
|
||||
// Allow editing text on double click
|
||||
canvas.on("mouse:dblclick", (e) => {
|
||||
if (e.target && e.target.type === "text") {
|
||||
e.target.enterEditing();
|
||||
e.target.selectAll();
|
||||
}
|
||||
});
|
||||
|
||||
// Force initial render so canvas is visible
|
||||
canvas.requestRenderAll();
|
||||
|
||||
return () => {
|
||||
canvas.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (images.length === 0) {
|
||||
setIsImagesLoading(true);
|
||||
fetch("https://picsum.photos/v2/list")
|
||||
.then((res) => res.json())
|
||||
.then((data) => setImages(data))
|
||||
.catch((err) => console.error("Failed to fetch images", err))
|
||||
.finally(() => setIsImagesLoading(false));
|
||||
}
|
||||
}, [images]);
|
||||
|
||||
// Add text to canvas
|
||||
const addText = (type) => {
|
||||
let text = "";
|
||||
let size = 60;
|
||||
if (type === "h1") {
|
||||
text = "Add a heading";
|
||||
size = 60;
|
||||
} else if (type === "h6") {
|
||||
text = "Add a subheading";
|
||||
size = 32;
|
||||
} else {
|
||||
text = "Add a little bit of body text";
|
||||
size = 20;
|
||||
}
|
||||
const canvas = fabricRef.current;
|
||||
const textbox = new Textbox(text, {
|
||||
left: canvas.width / 2,
|
||||
top: canvas.height / 2,
|
||||
fontSize: size,
|
||||
fontWeight: type === "h1" ? "bold" : "normal",
|
||||
editable: true,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
fill: "#222",
|
||||
});
|
||||
canvas.add(textbox).setActiveObject(textbox);
|
||||
canvas.renderAll();
|
||||
};
|
||||
|
||||
// Add image to canvas
|
||||
const addImageToCanvas = async (url) => {
|
||||
const img = await Image.fromURL(url, { crossOrigin: "anonymous" });
|
||||
const canvas = fabricRef.current;
|
||||
img.scaleToWidth(200);
|
||||
img.set({
|
||||
left: canvas.width / 2,
|
||||
top: canvas.height / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
canvas.add(img);
|
||||
canvas.setActiveObject(img);
|
||||
canvas.requestRenderAll();
|
||||
};
|
||||
|
||||
// Set background color
|
||||
const setBackgroundColor = (color) => {
|
||||
if (fabricRef.current) {
|
||||
fabricRef.current.backgroundColor = color;
|
||||
fabricRef.current.requestRenderAll();
|
||||
}
|
||||
};
|
||||
|
||||
// Download canvas
|
||||
const downloadCanvas = () => {
|
||||
if (fabricRef.current) {
|
||||
const dataURL = fabricRef.current.toDataURL({
|
||||
format: "png",
|
||||
quality: 1,
|
||||
});
|
||||
const link = document.createElement("a");
|
||||
link.href = dataURL;
|
||||
link.download = "canvas.png";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
};
|
||||
|
||||
// Change font size of selected text
|
||||
const handleFontSizeChange = (e) => {
|
||||
const size = parseInt(e.target.value, 10);
|
||||
setFontSize(size);
|
||||
if (selectedObject && selectedObject.type === "text") {
|
||||
selectedObject.set({ fontSize: size });
|
||||
fabricRef.current.renderAll();
|
||||
}
|
||||
};
|
||||
|
||||
// Delete selected object (text or image)
|
||||
const deleteSelectedObject = () => {
|
||||
if (selectedObject) {
|
||||
fabricRef.current.remove(selectedObject);
|
||||
setSelectedObject(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='h-screen w-screen flex flex-col bg-gray-100'>
|
||||
{/* Navbar */}
|
||||
<nav className='flex items-center justify-between h-12 bg-black px-4'>
|
||||
<div className='text-white text-2xl font-bold flex items-center gap-2'>
|
||||
<span role='img' aria-label='logo'>
|
||||
🧩
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
{selectedObject && (
|
||||
<>
|
||||
{selectedObject.type === "text" && (
|
||||
<select
|
||||
className='border rounded px-2 py-1 text-sm'
|
||||
value={fontSize}
|
||||
onChange={handleFontSizeChange}
|
||||
>
|
||||
{[12, 16, 20, 24, 32, 40, 48, 60, 72, 96].map((size) => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
className='ml-2 text-xl text-gray-700 hover:text-red-600'
|
||||
onClick={deleteSelectedObject}
|
||||
title='Delete selected object'
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className='bg-white text-black px-4 py-1 rounded shadow hover:bg-gray-200'
|
||||
onClick={downloadCanvas}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div className='flex flex-1 overflow-hidden'>
|
||||
{/* Sidebar */}
|
||||
<aside className='w-56 bg-white border-r flex flex-col p-4 gap-6'>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<button
|
||||
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
|
||||
activeMenu === "Elements" ? "text-blue-600" : ""
|
||||
}`}
|
||||
onClick={() => setActiveMenu("Elements")}
|
||||
>
|
||||
Elements
|
||||
</button>
|
||||
<button
|
||||
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
|
||||
activeMenu === "Images" ? "text-blue-600" : ""
|
||||
}`}
|
||||
onClick={() => setActiveMenu("Images")}
|
||||
>
|
||||
Images
|
||||
</button>
|
||||
<button
|
||||
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
|
||||
activeMenu === "Text" ? "text-blue-600" : ""
|
||||
}`}
|
||||
onClick={() => setActiveMenu("Text")}
|
||||
>
|
||||
Text
|
||||
</button>
|
||||
<button
|
||||
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
|
||||
activeMenu === "Background" ? "text-blue-600" : ""
|
||||
}`}
|
||||
onClick={() => setActiveMenu("Background")}
|
||||
>
|
||||
Background
|
||||
</button>
|
||||
</div>
|
||||
{/* Images menu */}
|
||||
{activeMenu === "Images" && (
|
||||
<>
|
||||
{isImagesLoading ? (
|
||||
<div className='w-full grid place-items-center'>
|
||||
<div className='rounded-full border-2 border-black size-20 border-t-transparent animate-spin' />
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col gap-2 mt-6 overflow-y-auto'>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
{images.map((image) => (
|
||||
<button
|
||||
key={image.id}
|
||||
onClick={() => addImageToCanvas(image.download_url)}
|
||||
className='hover:opacity-80'
|
||||
>
|
||||
<img
|
||||
src={image.download_url}
|
||||
alt={image.author}
|
||||
className='w-full h-auto rounded'
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Text menu */}
|
||||
{activeMenu === "Text" && (
|
||||
<div className='flex flex-col gap-2 mt-6'>
|
||||
<button
|
||||
className='bg-gray-100 rounded px-3 py-2 text-left font-bold text-lg hover:bg-gray-200'
|
||||
onClick={() => addText("h1")}
|
||||
>
|
||||
Add a heading
|
||||
</button>
|
||||
<button
|
||||
className='bg-gray-100 rounded px-3 py-2 text-left font-semibold hover:bg-gray-200'
|
||||
onClick={() => addText("h6")}
|
||||
>
|
||||
Add a subheading
|
||||
</button>
|
||||
<button
|
||||
className='bg-gray-100 rounded px-3 py-2 text-left text-sm hover:bg-gray-200'
|
||||
onClick={() => addText("p")}
|
||||
>
|
||||
Add a little bit of body text
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Background color swatches */}
|
||||
{activeMenu === "Background" && (
|
||||
<div>
|
||||
<div className='mb-2 text-sm font-semibold'>Default colors</div>
|
||||
<div className='grid grid-cols-5 gap-2'>
|
||||
{colors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
className='w-7 h-7 rounded shadow border-2 border-white hover:border-black focus:outline-none'
|
||||
style={{ background: color }}
|
||||
aria-label={color}
|
||||
onClick={() => setBackgroundColor(color)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
{/* Canvas Area */}
|
||||
<main className='flex-1 flex items-center justify-center bg-gray-50'>
|
||||
<canvas ref={canvasRef} width={900} height={400} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
});
|
||||
+17
-16
@@ -1,11 +1,12 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
var apiRouter = require("./routes/api");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
@@ -13,18 +14,18 @@ var cors = require("cors");
|
||||
var app = express();
|
||||
app.set("db", db);
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "ejs");
|
||||
app.use(cors());
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
app.use("/api", apiRouter);
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
next(createError(404));
|
||||
@@ -34,11 +35,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const booking = sequelize.define(
|
||||
"booking",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
email: DataTypes.STRING,
|
||||
company: DataTypes.STRING,
|
||||
phone: DataTypes.STRING,
|
||||
notes: DataTypes.TEXT,
|
||||
date: DataTypes.STRING,
|
||||
time: DataTypes.STRING,
|
||||
timezone: DataTypes.STRING,
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "booking",
|
||||
createdAt: "created_at",
|
||||
updatedAt: false,
|
||||
}
|
||||
);
|
||||
return booking;
|
||||
};
|
||||
+48
-34
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,49 +8,63 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_17",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
console.log("Database & tables created!");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
module.exports = db;
|
||||
|
||||
+5
-1
@@ -3,17 +3,21 @@
|
||||
"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",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~2.6.9",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"jade": "~1.11.0",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-input-validator": "^4.5.1",
|
||||
"sequelize": "^6.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,289 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Inter", "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
background: #eaeaea;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
.calendar-container {
|
||||
max-width: 1100px;
|
||||
margin: 40px auto;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.07);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
background: #04316a;
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
padding: 28px 32px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.calendar-content {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
|
||||
.calendar-labels {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.calendar-label-main {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.calendar-label-duration {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.calendar-label-duration span {
|
||||
font-weight: 400;
|
||||
}
|
||||
.calendar-label-timezone {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.timezone-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 1rem;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 32px 40px;
|
||||
min-width: 340px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 18px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.modal-format-switch {
|
||||
margin-bottom: 18px;
|
||||
font-size: 0.98rem;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.timezone-groups {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 32px 48px;
|
||||
justify-content: center;
|
||||
}
|
||||
.timezone-group {
|
||||
min-width: 180px;
|
||||
}
|
||||
.timezone-group-title {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #04316a;
|
||||
}
|
||||
.timezone-option {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.97rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Calendar Table */
|
||||
.calendar-table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.calendar-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
.calendar-table th {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #04316a;
|
||||
border-bottom: 2px solid #eaeaea;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.calendar-day-label {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.calendar-date-label {
|
||||
font-size: 0.98rem;
|
||||
color: #888;
|
||||
font-weight: 400;
|
||||
}
|
||||
.calendar-slot-btn {
|
||||
background: #f5f8fa;
|
||||
border: 1px solid #dbe6f3;
|
||||
border-radius: 5px;
|
||||
color: #04316a;
|
||||
font-size: 1rem;
|
||||
padding: 7px 0;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border 0.15s;
|
||||
font-weight: 500;
|
||||
}
|
||||
.calendar-slot-btn:hover {
|
||||
background: #e6f0ff;
|
||||
border-color: #04316a;
|
||||
}
|
||||
.calendar-table td {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.calendar-week-nav {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 18px;
|
||||
margin-top: 12px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.calendar-week-nav a {
|
||||
color: #04316a;
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Booking Form */
|
||||
.booking-form {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.form-error {
|
||||
color: red;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: #222;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
border: 1px solid #dbe6f3;
|
||||
border-radius: 5px;
|
||||
padding: 8px 10px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background: #f5f8fa;
|
||||
resize: none;
|
||||
}
|
||||
.form-group textarea {
|
||||
min-height: 70px;
|
||||
}
|
||||
.form-submit-btn {
|
||||
background: #04316a;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 600;
|
||||
padding: 10px 0;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.form-submit-btn:hover {
|
||||
background: #0050b3;
|
||||
}
|
||||
|
||||
/* Success Message */
|
||||
.success-message {
|
||||
text-align: center;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 500;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media (max-width: 900px) {
|
||||
.calendar-container {
|
||||
max-width: 98vw;
|
||||
margin: 16px auto;
|
||||
}
|
||||
.calendar-content {
|
||||
padding: 18px 6vw 0 6vw;
|
||||
}
|
||||
.modal-content {
|
||||
padding: 18px 8vw;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
min-width: 80px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.calendar-header {
|
||||
font-size: 1.2rem;
|
||||
padding: 18px 10px;
|
||||
}
|
||||
.calendar-content {
|
||||
padding: 10px 2vw 0 2vw;
|
||||
}
|
||||
.modal-content {
|
||||
min-width: 90vw;
|
||||
padding: 10px 2vw;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
min-width: 60px;
|
||||
font-size: 0.93rem;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.booking-form {
|
||||
max-width: 98vw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const db = require("../models");
|
||||
const {
|
||||
validateInput,
|
||||
handleValidationErrorForAPI,
|
||||
} = require("../services/ValidationService");
|
||||
|
||||
// Validation rules for booking
|
||||
const bookingValidation = {
|
||||
name: "required|string",
|
||||
email: "required|email",
|
||||
company: "required|string",
|
||||
phone: "required|string",
|
||||
notes: "required|string",
|
||||
date: "required|string",
|
||||
time: "required|string",
|
||||
timezone: "required|string",
|
||||
};
|
||||
|
||||
// POST /api/bookings - Create a new booking
|
||||
router.post(
|
||||
"/bookings",
|
||||
validateInput(bookingValidation, {
|
||||
"name.required": "Name is required",
|
||||
"email.required": "Email is required",
|
||||
"email.email": "Invalid email address",
|
||||
"company.required": "Company is required",
|
||||
"phone.required": "Phone is required",
|
||||
"notes.required": "Notes are required",
|
||||
"date.required": "Date is required",
|
||||
"time.required": "Time is required",
|
||||
"timezone.required": "Timezone is required",
|
||||
}),
|
||||
handleValidationErrorForAPI,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { name, email, company, phone, notes, date, time, timezone } =
|
||||
req.body;
|
||||
const booking = await db.booking.create({
|
||||
name,
|
||||
email,
|
||||
company,
|
||||
phone,
|
||||
notes,
|
||||
date,
|
||||
time,
|
||||
timezone,
|
||||
});
|
||||
res.status(201).json({ success: true, booking });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GET /api/bookings - List all bookings
|
||||
router.get("/bookings", async (req, res) => {
|
||||
try {
|
||||
const bookings = await db.booking.findAll({
|
||||
order: [["created_at", "DESC"]],
|
||||
});
|
||||
res.json({ bookings });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+134
-4
@@ -1,9 +1,139 @@
|
||||
var express = require('express');
|
||||
var express = require("express");
|
||||
var router = express.Router();
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
// Helper: Group timezones by region for the timezone selection screen
|
||||
function getTimezoneGroups() {
|
||||
const regions = {
|
||||
"USA/CANADA": [
|
||||
"America/Los_Angeles",
|
||||
"America/Denver",
|
||||
"America/New_York",
|
||||
"America/Halifax",
|
||||
],
|
||||
EUROPE: [
|
||||
"Europe/Berlin",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Dublin",
|
||||
"Europe/Samara",
|
||||
],
|
||||
ASIA: ["Asia/Hong_Kong", "Asia/Jakarta", "Asia/Kabul", "Asia/Kathmandu"],
|
||||
"SOUTH AMERICA": [
|
||||
"America/Bogota",
|
||||
"America/Campo_Grande",
|
||||
"America/Caracas",
|
||||
"America/Lima",
|
||||
],
|
||||
};
|
||||
const groups = {};
|
||||
for (const region in regions) {
|
||||
groups[region] = regions[region].map((tz) => ({
|
||||
name: tz,
|
||||
label: tz.replace(/_/g, " ").replace("America/", ""),
|
||||
time_am: moment().tz(tz).format("h:mma"),
|
||||
time_24: moment().tz(tz).format("HH:mm"),
|
||||
}));
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// Helper: All timezones for dropdown
|
||||
function getAllTimezones() {
|
||||
return moment.tz.names().map((tz) => ({
|
||||
value: tz,
|
||||
label: tz,
|
||||
}));
|
||||
}
|
||||
|
||||
// Helper: Generate week days and slots
|
||||
function getWeekDaysAndSlots(selectedTz, weekOffset = 0) {
|
||||
const weekDays = [];
|
||||
const today = moment()
|
||||
.tz(selectedTz)
|
||||
.add(weekOffset * 7, "days");
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = today.clone().add(i, "days");
|
||||
weekDays.push({
|
||||
label: day.format("dddd"),
|
||||
date: day.format("MMMM D"),
|
||||
dateISO: day.format("YYYY-MM-DD"),
|
||||
slots: [
|
||||
"9:00am",
|
||||
"9:15am",
|
||||
"9:30am",
|
||||
"9:45am",
|
||||
"10:00am",
|
||||
"10:15am",
|
||||
"10:30am",
|
||||
"10:45am",
|
||||
"11:00am",
|
||||
"11:15am",
|
||||
"11:30am",
|
||||
"11:45am",
|
||||
"12:00pm",
|
||||
"12:15pm",
|
||||
"12:30pm",
|
||||
"12:45pm",
|
||||
"1:00pm",
|
||||
"1:15pm",
|
||||
],
|
||||
});
|
||||
}
|
||||
return { weekDays, maxSlots: 17 };
|
||||
}
|
||||
|
||||
// Timezone selection screen
|
||||
router.get("/timezone", function (req, res) {
|
||||
res.render("timezone", {
|
||||
timezones: getAllTimezones(),
|
||||
timezoneGroups: getTimezoneGroups(),
|
||||
});
|
||||
});
|
||||
|
||||
// Calendar slot selection screen
|
||||
router.get("/calendar", function (req, res) {
|
||||
const selectedTimezone = req.query.tz || "America/New_York";
|
||||
const weekOffset = parseInt(req.query.week) || 0;
|
||||
const { weekDays, maxSlots } = getWeekDaysAndSlots(
|
||||
selectedTimezone,
|
||||
weekOffset
|
||||
);
|
||||
res.render("calendar", {
|
||||
selectedTimezone,
|
||||
weekDays,
|
||||
maxSlots,
|
||||
showPrevWeek: weekOffset > 0,
|
||||
prevWeek: weekOffset - 1,
|
||||
nextWeek: weekOffset + 1,
|
||||
});
|
||||
});
|
||||
|
||||
// Booking form screen
|
||||
router.get("/book", function (req, res) {
|
||||
const selectedTimezone = req.query.tz || "America/New_York";
|
||||
const selectedDate = req.query.date || "";
|
||||
const selectedTime = req.query.time || "";
|
||||
res.render("booking-form", {
|
||||
selectedTimezone,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
});
|
||||
});
|
||||
|
||||
// Booking POST (simulate success)
|
||||
router.post("/book", function (req, res) {
|
||||
// Here you would save booking info to DB
|
||||
res.redirect("/success");
|
||||
});
|
||||
|
||||
// Success screen
|
||||
router.get("/success", function (req, res) {
|
||||
res.render("success");
|
||||
});
|
||||
|
||||
// Home page redirect to timezone selection
|
||||
router.get("/", function (req, res) {
|
||||
res.redirect("/timezone");
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone: <%= selectedTimezone %>
|
||||
</div>
|
||||
<% if (selectedDate && selectedTime) { %>
|
||||
<div class="calendar-label-selected">
|
||||
<strong>Selected:</strong> <%= selectedDate %> at <%= selectedTime %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<form class="booking-form" id="bookingForm">
|
||||
<input type="hidden" name="date" value="<%= selectedDate %>" />
|
||||
<input type="hidden" name="time" value="<%= selectedTime %>" />
|
||||
<input type="hidden" name="tz" value="<%= selectedTimezone %>" />
|
||||
<div class="form-group">
|
||||
<label for="fullName">Full Name</label>
|
||||
<input type="text" id="fullName" name="name" required />
|
||||
<div class="form-error" id="error-name"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required />
|
||||
<div class="form-error" id="error-email"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="company">Company</label>
|
||||
<input type="text" id="company" name="company" required />
|
||||
<div class="form-error" id="error-company"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone">Phone</label>
|
||||
<input type="tel" id="phone" name="phone" required />
|
||||
<div class="form-error" id="error-phone"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes">Your Notes</label>
|
||||
<textarea id="notes" name="notes" required></textarea>
|
||||
<div class="form-error" id="error-notes"></div>
|
||||
</div>
|
||||
<button type="submit" class="form-submit-btn">Done</button>
|
||||
<div class="form-error" id="error-date"></div>
|
||||
<div class="form-error" id="error-time"></div>
|
||||
<div class="form-error" id="error-timezone"></div>
|
||||
<div class="form-error" id="error-general"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
<script>
|
||||
document.getElementById("bookingForm").onsubmit = async function (e) {
|
||||
e.preventDefault();
|
||||
// Clear previous errors
|
||||
document
|
||||
.querySelectorAll(".form-error")
|
||||
.forEach((el) => (el.textContent = ""));
|
||||
const form = e.target;
|
||||
const data = {
|
||||
name: form.fullName.value,
|
||||
email: form.email.value,
|
||||
company: form.company.value,
|
||||
phone: form.phone.value,
|
||||
notes: form.notes.value,
|
||||
date: form.date.value,
|
||||
time: form.time.value,
|
||||
timezone: form.tz.value,
|
||||
};
|
||||
try {
|
||||
const res = await fetch("/api/bookings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
window.location.href = "/success";
|
||||
} else if (result.error) {
|
||||
if (typeof result.error === "object") {
|
||||
for (const key in result.error) {
|
||||
const el = document.getElementById("error-" + key);
|
||||
if (el) el.textContent = result.error[key];
|
||||
}
|
||||
} else {
|
||||
document.getElementById("error-general").textContent = result.error;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById("error-general").textContent =
|
||||
"An error occurred. Please try again.";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone: <%= selectedTimezone %> <a href="/timezone">(Change)</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-table-wrapper">
|
||||
<table class="calendar-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<% weekDays.forEach(function(day) { %>
|
||||
<th>
|
||||
<div class="calendar-day-label"><%= day.label %></div>
|
||||
<div class="calendar-date-label"><%= day.date %></div>
|
||||
</th>
|
||||
<% }) %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (let i = 0; i < maxSlots; i++) { %>
|
||||
<tr>
|
||||
<% weekDays.forEach(function(day) { %>
|
||||
<td>
|
||||
<% if (day.slots[i]) { %>
|
||||
<form action="/book" method="get">
|
||||
<input type="hidden" name="date" value="<%= day.dateISO %>" />
|
||||
<input type="hidden" name="time" value="<%= day.slots[i] %>" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="tz"
|
||||
value="<%= selectedTimezone %>"
|
||||
/>
|
||||
<button class="calendar-slot-btn"><%= day.slots[i] %></button>
|
||||
</form>
|
||||
<% } %>
|
||||
</td>
|
||||
<% }) %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="calendar-week-nav">
|
||||
<% if (showPrevWeek) { %>
|
||||
<a href="/calendar?tz=<%= selectedTimezone %>&week=<%= prevWeek %>"
|
||||
>Previous Week</a
|
||||
>
|
||||
<% } %>
|
||||
<a href="/calendar?tz=<%= selectedTimezone %>&week=<%= nextWeek %>"
|
||||
>Next Week</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
@@ -0,0 +1,10 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Error</div>
|
||||
<div class="calendar-content">
|
||||
<h1><%= message %></h1>
|
||||
<h2><%= error.status %></h2>
|
||||
<pre><%= error.stack %></pre>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
@@ -1,6 +0,0 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
||||
@@ -0,0 +1,8 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header"><%= title %></div>
|
||||
<div class="calendar-content">
|
||||
<p>Welcome to <%= title %></p>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
@@ -1,5 +0,0 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= title
|
||||
p Welcome to #{title}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<%- body %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +0,0 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
body
|
||||
block content
|
||||
@@ -0,0 +1,2 @@
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Calendar</title>
|
||||
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content success-message">
|
||||
Thanks for filling in the form. You will be emailed next steps.
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
@@ -0,0 +1,78 @@
|
||||
<%- include('partials/header') %>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone:
|
||||
<button id="select-timezone-btn" class="timezone-btn">
|
||||
Please Select
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal Overlay -->
|
||||
<div id="timezone-modal" class="modal-overlay" style="display: none">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">TIME ZONE</div>
|
||||
<div class="modal-format-switch">
|
||||
<label
|
||||
><input type="radio" name="format" value="ampm" checked />
|
||||
am/pm</label
|
||||
>
|
||||
<label><input type="radio" name="format" value="24hr" /> 24hr</label>
|
||||
</div>
|
||||
<div class="timezone-groups">
|
||||
<% for (const group in timezoneGroups) { %>
|
||||
<div class="timezone-group">
|
||||
<div class="timezone-group-title"><%= group %></div>
|
||||
<% timezoneGroups[group].forEach(function(tz) { %>
|
||||
<label class="timezone-option">
|
||||
<input type="radio" name="timezone" value="<%= tz.name %>" />
|
||||
<span
|
||||
class="tz-time"
|
||||
data-am="<%= tz.time_am %>"
|
||||
data-24="<%= tz.time_24 %>"
|
||||
>
|
||||
<%= tz.label %>
|
||||
<span class="tz-time-value"><%= tz.time_am %></span>
|
||||
</span>
|
||||
</label>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
<script>
|
||||
// Modal logic
|
||||
const btn = document.getElementById("select-timezone-btn");
|
||||
const modal = document.getElementById("timezone-modal");
|
||||
btn.onclick = () => {
|
||||
modal.style.display = "flex";
|
||||
};
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) modal.style.display = "none";
|
||||
};
|
||||
document.querySelectorAll('input[name="timezone"]').forEach((el) => {
|
||||
el.onclick = () => {
|
||||
window.location.href = "/calendar?tz=" + encodeURIComponent(el.value);
|
||||
};
|
||||
});
|
||||
|
||||
// Time format toggle logic
|
||||
document.querySelectorAll('input[name="format"]').forEach((el) => {
|
||||
el.onchange = function () {
|
||||
const is24 = this.value === "24hr";
|
||||
document.querySelectorAll(".tz-time").forEach((span) => {
|
||||
const am = span.getAttribute("data-am");
|
||||
const t24 = span.getAttribute("data-24");
|
||||
span.querySelector(".tz-time-value").textContent = is24 ? t24 : am;
|
||||
});
|
||||
};
|
||||
});
|
||||
</script>
|
||||
+17
-15
@@ -1,11 +1,12 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
var scheduleRouter = require("./routes/schedule");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
@@ -13,17 +14,18 @@ var cors = require("cors");
|
||||
var app = express();
|
||||
app.set("db", db);
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "jade");
|
||||
app.use(cors());
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
app.use("/api/v1", scheduleRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const availability = sequelize.define(
|
||||
"availability",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
day_of_week: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
start_time: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: false,
|
||||
},
|
||||
end_time: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: false,
|
||||
},
|
||||
created_at: DataTypes.DATE,
|
||||
updated_at: DataTypes.DATE,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "availability",
|
||||
}
|
||||
);
|
||||
return availability;
|
||||
};
|
||||
+48
-34
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,49 +8,63 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_19",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
console.log("Database & tables created!");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
module.exports = db;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const schedule = sequelize.define(
|
||||
"schedule",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
start_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
end_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
created_at: DataTypes.DATE,
|
||||
updated_at: DataTypes.DATE,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "schedule",
|
||||
}
|
||||
);
|
||||
return schedule;
|
||||
};
|
||||
+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,52 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const db = require("../models");
|
||||
const { Op } = require("sequelize");
|
||||
|
||||
// POST /api/v1/schedule - Save an appointment
|
||||
router.post("/schedule", async (req, res) => {
|
||||
try {
|
||||
const { user_id, start_time, end_time } = req.body;
|
||||
if (!start_time || !end_time) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "start_time and end_time are required",
|
||||
});
|
||||
}
|
||||
const appointment = await db.schedule.create({
|
||||
user_id,
|
||||
start_time,
|
||||
end_time,
|
||||
});
|
||||
res.json({ success: true, data: appointment });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/v1/booked - Return available times excluding scheduled
|
||||
router.get("/booked", async (req, res) => {
|
||||
try {
|
||||
// For demo: get all availability, then exclude times that overlap with schedule
|
||||
const availabilities = await db.availability.findAll();
|
||||
const schedules = await db.schedule.findAll();
|
||||
|
||||
// This is a simple exclusion, not a full calendar logic
|
||||
// For each availability, exclude if any schedule overlaps
|
||||
const availableTimes = availabilities.filter((avail) => {
|
||||
const availStart = avail.start_time;
|
||||
const availEnd = avail.end_time;
|
||||
const overlap = schedules.some((sch) => {
|
||||
return sch.start_time < availEnd && sch.end_time > availStart;
|
||||
});
|
||||
return !overlap;
|
||||
});
|
||||
res.json({ success: true, data: availableTimes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -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
|
||||
}
|
||||
+5
-5
@@ -1,22 +1,23 @@
|
||||
# Day 20
|
||||
|
||||
Read:
|
||||
|
||||
- https://www.notion.so/How-to-Use-Baas-00f549dda3a84dc48b352c79222f1a3a
|
||||
- https://www.notion.so/Create-Manage-Projects-With-Wireframe-Tool-df67b882f0c14735a0192d69dc3ff777
|
||||
|
||||
- Request for Wireframe tool url from Project Manager.
|
||||
|
||||
1. login to Wireframe tool. Create SOW and Wireframe called <name-inventory>.
|
||||
1. login to Wireframe tool. Create SOW and Wireframe called <name-inventory>.
|
||||
|
||||
2. Navigate to Wireframe side-menu click, Edit > Setting, create a project (<name-inventory>) from here according to specifications of wireframe document provided (inventory-app.pdf).
|
||||
2. Navigate to Wireframe side-menu click, Edit > Setting, create a project (<name-inventory>) from here according to specifications of wireframe document provided (inventory-app.pdf).
|
||||
|
||||
3. Create Models. Switch to Models tab or Web/React tab (Manage Models).
|
||||
|
||||
4. Create Roles and set Permissions. (Web/React Tab > Manage Permissions).
|
||||
4. Create Roles and set Permissions. (Web/React Tab > Manage Permissions).
|
||||
|
||||
5. Create React portal and marketing pages and then export React. (Web/React Tab).
|
||||
|
||||
6. Create Custom APIs and commit. (API tab). API code would be commited to http://23.29.118.76:3000/mkdlabs/<name-inventory_backend>.git
|
||||
6. Create Custom APIs and commit. (API tab). API code would be commited to http://23.29.118.76:3000/mkdlabs/<name-inventory_backend>.git
|
||||
|
||||
7. Switch to Deployment Tab. Initialize deployment and create repositories.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user