Compare commits

..

4 Commits

Author SHA1 Message Date
Ayobami b0d86465f2 feat: complete day 20 react task 2025-07-23 23:25:19 +01:00
Ayobami 743187b216 feat: complete day 17 2025-07-22 19:21:32 +01:00
Ayobami 29e6eb82c7 feat: complete day 19 2025-07-22 17:49:50 +01:00
Ayobami dc35cfcb3f feat: complete day 16 2025-07-21 22:23:26 +01:00
293 changed files with 43917 additions and 132 deletions
+24
View File
@@ -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?
+29
View File
@@ -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_]' }],
},
},
])
+13
View File
@@ -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>
+33
View File
@@ -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"
}
}
+1
View File
@@ -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

+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+317
View File
@@ -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;
+1
View File
@@ -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

+7
View File
@@ -0,0 +1,7 @@
@import "tailwindcss";
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
}
+10
View File
@@ -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>,
)
+8
View File
@@ -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
View File
@@ -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;
+32
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
+285 -4
View File
@@ -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;
}
}
+69
View File
@@ -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
View File
@@ -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;
+97
View File
@@ -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>
+60
View File
@@ -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') %>
+10
View File
@@ -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') %>
-6
View File
@@ -1,6 +0,0 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}
+8
View File
@@ -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') %>
-5
View File
@@ -1,5 +0,0 @@
extends layout
block content
h1= title
p Welcome to #{title}
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/stylesheets/style.css" />
</head>
<body>
<%- body %>
</body>
</html>
-7
View File
@@ -1,7 +0,0 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
+2
View File
@@ -0,0 +1,2 @@
</body>
</html>
+9
View File
@@ -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>
+8
View File
@@ -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') %>
+78
View File
@@ -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
View File
@@ -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;
+36
View File
@@ -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
View File
@@ -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;
+32
View File
@@ -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
View File
@@ -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",
+52
View File
@@ -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;
+27
View File
@@ -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?
+5
View File
@@ -0,0 +1,5 @@
{
"tabWidth": 2,
"useTabs": false
}
+5 -5
View File
@@ -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.
+26
View File
@@ -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>
+17
View File
@@ -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/*"]
}
}
}
+179
View File
@@ -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"
}
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
View File
+34
View File
@@ -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;
+3
View File
@@ -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

+18
View File
@@ -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

+24
View File
@@ -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>
);
};
+14
View File
@@ -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>
);
};
+13
View File
@@ -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>
)
}
+21
View File
@@ -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}
/>
);
};
+9
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
export {default as AddButton } from "./AddButton";
+11
View File
@@ -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">&times;</span></button></li>
);
};
export default AddTags;
+5
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
import { lazy } from "react";
export const BackButton = lazy(()=> import("./BackButton"))
+160
View File
@@ -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%;
}
+3
View File
@@ -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"))
+509
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
import { lazy } from 'react'
export const Chat = lazy(()=> import("./Chat"))
+233
View File
@@ -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;
+80
View File
@@ -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
});
}
+97
View File
@@ -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)}
>
&lt;
</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)}
>
&gt;
</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;
+50
View File
@@ -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;
+8
View File
@@ -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 )
+5
View File
@@ -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"))
+47
View File
@@ -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;
+2
View File
@@ -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