Anul 3 Semestrul 1
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "react"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,50 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
settings: { react: { version: '18.3' } },
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true,
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -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>JyrosXD</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
+10654
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "jyros",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint src",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mynaui/icons-react": "^0.3.2",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-icons": "^1.3.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"axios": "^1.7.9",
|
||||
"chart.js": "^4.4.7",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"recharts": "^2.15.0",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-react": "^1.1.7",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.14",
|
||||
"globals": "^15.11.0",
|
||||
"husky": "^8.0.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.11.0",
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { AppProvider } from './context/AppContext';
|
||||
import BacklogPage from './pages/BacklogPage';
|
||||
import TicketView from './ticketPopup/TicketView';
|
||||
import TicketCreate from './ticketPopup/TicketCreate';
|
||||
import Board from './components/Board/Board';
|
||||
import Availability from './components/AvailabiltyPage/Availability';
|
||||
import LoginPage from './assets/user/LoginPage';
|
||||
import PrivateRoute from '@/assets/user/PrivatRoute'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AppProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/" element={<Navigate to="/board" />} />
|
||||
<Route
|
||||
path="/board"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Board />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/backlog"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<BacklogPage />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/availability"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Availability />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AppProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 116 B |
@@ -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 |
+46
@@ -0,0 +1,46 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import { SignUpForm } from '@/assets/user/SignUpPage'
|
||||
import '@/assets/user/index.css'
|
||||
import { AppContext } from '@/context/AppContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function LoginPage() {
|
||||
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const {LogIn, SignIn} = useContext(AppContext) as any;
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const navigate = useNavigate(); // Initialize navigate hook
|
||||
|
||||
const handleSubmit = async (username: string, password: string) => {
|
||||
if (isLogin) {
|
||||
try {
|
||||
await LogIn(username, password);
|
||||
navigate('/board'); // Redirect to board page
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
}
|
||||
}else{
|
||||
await SignIn(username, password);
|
||||
setIsLogin(true);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAuthMode = () => {
|
||||
setIsLogin(!isLogin)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className=" items-center justify-center bg-gray-100">
|
||||
<div className="bg-white w-full max-w-md">
|
||||
<h1 className="text-2xl font-bold mb-6 text-center text-gray-800">
|
||||
{isLogin ? 'Log In' : 'Sign Up'}
|
||||
</h1>
|
||||
<SignUpForm
|
||||
isLogin={isLogin}
|
||||
onSubmit={handleSubmit}
|
||||
onToggle={toggleAuthMode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { AppContext } from '@/context/AppContext';
|
||||
|
||||
function PrivateRoute({ children }) {
|
||||
const { isAuthenticated } = useContext(AppContext);
|
||||
|
||||
return isAuthenticated ? children : <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
export default PrivateRoute;
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { useContext, useState } from 'react'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { AppContext } from '@/context/AppContext'
|
||||
|
||||
interface SignUpProps {
|
||||
isLogin: boolean
|
||||
onSubmit: (username: string, password: string, confirmPassword?: string) => void
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
export function SignUpForm({ isLogin, onSubmit, onToggle }: SignUpProps) {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const {SignIn, LogIn} = useContext(AppContext) as any;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (isLogin) {
|
||||
try {
|
||||
await onSubmit(username, password);
|
||||
// LogIn(username, password);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
} else {
|
||||
if (password === confirmPassword) {
|
||||
try {
|
||||
await SignIn(username, password);
|
||||
await onSubmit(username, password);
|
||||
isLogin = true;
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username" className="text-sm font-medium text-gray-700">
|
||||
Username
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
{!isLogin && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword" className="text-sm font-medium text-gray-700">
|
||||
Confirm Password
|
||||
</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Reenter your password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" className="w-full bg-black text-white hover:bg-gray-800">
|
||||
{isLogin ? 'Log In' : 'Sign Up'}
|
||||
</Button>
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="text-sm text-gray-600 hover:underline focus:outline-none"
|
||||
>
|
||||
{isLogin ? "Don't have an account? Sign up" : "Already have an account? Log in"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f3f4f6; /* Tailwind's bg-gray-100 equivalent */
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
+35
@@ -0,0 +1,35 @@
|
||||
import SideBar from "@/components/Shared/SideBar/SideBar"
|
||||
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { MoreVertical } from "lucide-react"
|
||||
import ShiftAvailability from "./ShiftAvailability"
|
||||
|
||||
|
||||
const Availability = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<SidebarProvider>
|
||||
<SideBar />
|
||||
<SidebarInset className="flex-1 overflow-auto">
|
||||
<header className="flex items-center justify-between border-b p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-semibold">Team Name/ Project Name/ Backlog</h1>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</header>
|
||||
<div style={{ "marginLeft": "5rem" }}>
|
||||
<ShiftAvailability/>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Availability
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
'use client'
|
||||
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
import { AppContext } from '@/context/AppContext'
|
||||
import { parse } from 'path'
|
||||
|
||||
|
||||
interface Developer {
|
||||
id: number
|
||||
name: string
|
||||
availableDays: number
|
||||
}
|
||||
|
||||
interface Adjustment {
|
||||
adjustmentPoints: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
export default function ShiftAvailability() {
|
||||
|
||||
const {usersInShift, fetchUsersInShift, fetchUserAvailability, shifts, fetchShifts, fetchShiftAdjustment, addAdjustment, updateAvailability, shiftAdjustment, fetchShiftAdjustmentList, setAdjustments,adjustments} = useContext(AppContext) as any;
|
||||
|
||||
const [currentShift, setCurrentShift] = useState<number>(1);
|
||||
const [developers, setDevelopers] = useState<Developer[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsersInShift(currentShift);
|
||||
fetchShifts();
|
||||
fetchShiftAdjustment(currentShift);
|
||||
const fetchTest = async () => {
|
||||
let test = await fetchShiftAdjustmentList(currentShift);
|
||||
}
|
||||
fetchTest();
|
||||
|
||||
}, [fetchUsersInShift, fetchShifts, currentShift]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAvailability = async () => {
|
||||
let newDevelopers: Developer[] = [];
|
||||
for (let i = 0; i < usersInShift.length; i++) {
|
||||
const user = usersInShift[i];
|
||||
const availability = await fetchUserAvailability(user.userId, currentShift);
|
||||
newDevelopers.push({
|
||||
id: user.userId,
|
||||
name: user.username,
|
||||
availableDays: availability.availabilityPoints
|
||||
});
|
||||
}
|
||||
setDevelopers(newDevelopers);
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
sleep(1000);
|
||||
// setTotalDays(developers.reduce((sum, dev) => sum + dev.availableDays, 0));
|
||||
};
|
||||
|
||||
if (usersInShift.length > 0) {
|
||||
fetchAvailability();
|
||||
}
|
||||
}, [usersInShift, fetchUserAvailability]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTotalDays(developers.reduce((sum, dev) => sum + dev.availableDays, 0));
|
||||
}, [developers])
|
||||
|
||||
// const [adjustments, setAdjustments] = useState<Adjustment[]>([
|
||||
// { days: 1, reason: 'Team Planning Meeting' }
|
||||
// ])
|
||||
|
||||
const [newAdjustment, setNewAdjustment] = useState<Adjustment>({
|
||||
adjustmentPoints: 0,
|
||||
reason: ''
|
||||
})
|
||||
|
||||
// let totalDays = developers.reduce((sum, dev) => sum + dev.availableDays, 0)
|
||||
const [totalDays, setTotalDays] = useState(0);
|
||||
|
||||
|
||||
const handleUpdateAvailability = () => {
|
||||
developers.forEach(developer => {
|
||||
let newAvailability = { userId : developer.id,sprintId: currentShift, availabilityPoints: developer.availableDays }
|
||||
updateAvailability(developer.id, currentShift, newAvailability);
|
||||
})
|
||||
}
|
||||
|
||||
const handleAddAdjustment = () => {
|
||||
if (newAdjustment.adjustmentPoints && newAdjustment.reason) {
|
||||
setAdjustments([...adjustments, newAdjustment])
|
||||
setNewAdjustment({ adjustmentPoints: 0, reason: '' })
|
||||
let adjustmentToAdd = {
|
||||
AdjustmentPoints: newAdjustment.adjustmentPoints,
|
||||
reason: newAdjustment.reason
|
||||
}
|
||||
addAdjustment(currentShift, adjustmentToAdd);
|
||||
setTotalDays(totalDays - newAdjustment.adjustmentPoints);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveAdjustment = (index: number) => {
|
||||
setAdjustments(adjustments.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const onShiftChange = (e: any) => {
|
||||
setDevelopers([]);
|
||||
setAdjustments([]);
|
||||
setCurrentShift(e);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 pl-0 pr-12 space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-semibold text-center">Shift Availability</h1>
|
||||
<div className="flex justify-center">
|
||||
<Select
|
||||
value={currentShift.toString()}
|
||||
onValueChange={onShiftChange}
|
||||
>
|
||||
<SelectTrigger className="w-[250px]">
|
||||
<SelectValue placeholder="Select shift" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{shifts.map((shift: any) => (
|
||||
<SelectItem key={shift.sprintId} value={shift.sprintId.toString()}>
|
||||
{shift.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center gap-6 text-base text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="14" height="14" rx="2" stroke="currentColor" strokeWidth="2"/>
|
||||
</svg>
|
||||
<span>{totalDays} Availablilty Points</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 3V13M3 8H13" stroke="currentColor" strokeWidth="2"/>
|
||||
</svg>
|
||||
<span>{shiftAdjustment} Adjustments</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="2"/>
|
||||
</svg>
|
||||
<span>{totalDays - shiftAdjustment} Total Days</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{developers.map((developer, index) => (
|
||||
<div key={index} className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-base text-muted-foreground">Developer Name</label>
|
||||
<Input
|
||||
value={developer.name}
|
||||
onChange={(e) => {
|
||||
const newDevelopers = [...developers]
|
||||
newDevelopers[index].name = e.target.value
|
||||
setDevelopers(newDevelopers)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-base text-muted-foreground">Availabilty Points</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={developer.availableDays !== undefined ? developer.availableDays : 0}
|
||||
onChange={(e) => {
|
||||
const newDevelopers = [...developers]
|
||||
newDevelopers[index].availableDays = parseInt(e.target.value)
|
||||
setDevelopers(newDevelopers)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button onClick={handleUpdateAvailability}>Update Availability</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Adjustments</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-base text-muted-foreground">Days</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newAdjustment.adjustmentPoints || ''}
|
||||
onChange={(e) => setNewAdjustment({
|
||||
...newAdjustment,
|
||||
adjustmentPoints: parseInt(e.target.value)
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-base text-muted-foreground">Reason</label>
|
||||
<Textarea
|
||||
value={newAdjustment.reason}
|
||||
onChange={(e) => setNewAdjustment({
|
||||
...newAdjustment,
|
||||
reason: e.target.value
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleAddAdjustment}>Add Adjustment</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{adjustments.map((adjustment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-4 bg-muted rounded-lg"
|
||||
>
|
||||
<span className="text-base">
|
||||
{adjustment.adjustmentPoints} {adjustment.adjustmentPoints === 1 ? 'day' : 'days'} - {adjustment.reason}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveAdjustment(index)}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'
|
||||
import { MoreVertical, Utensils, UtensilsCrossed } from 'lucide-react'
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
SidebarProvider,
|
||||
SidebarInset,
|
||||
} from "@/components/ui/sidebar"
|
||||
import TicketCreate from '../../ticketPopup/TicketCreate'
|
||||
import TeamMates from '@/components/TeamMates/TeamMates'
|
||||
import SideBar from '@/components/Shared/SideBar/SideBar'
|
||||
import TicketView from '@/ticketPopup/TicketView'
|
||||
|
||||
import { AppContext } from '@/context/AppContext'
|
||||
|
||||
const ForkIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2" />
|
||||
<path d="M7 2v20" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
content: string
|
||||
state: string,
|
||||
priority?: number,
|
||||
intId: number
|
||||
}
|
||||
|
||||
interface Column {
|
||||
id: string
|
||||
title: string
|
||||
tasks: Task[]
|
||||
}
|
||||
|
||||
interface ColumnState {
|
||||
[key: string]: Column
|
||||
}
|
||||
|
||||
const Tasks = [
|
||||
]
|
||||
|
||||
const initcolumns: ColumnState = {
|
||||
'1': { id: '1', title: 'To Do UwU', tasks: [] },
|
||||
'2': { id: '2', title: 'Cooking UwU', tasks: [] },
|
||||
'3': { id: '3', title: 'In Plating UwU', tasks: [] },
|
||||
'4': { id: '4', title: 'Bon appetit UwU', tasks: [] },
|
||||
}
|
||||
|
||||
const status_to_state: { [key: string]: string } = {
|
||||
"To Do": "1",
|
||||
"Cooking": "2",
|
||||
"In Plating": "3",
|
||||
"Bon appétit": "4"
|
||||
}
|
||||
|
||||
const getPriorityIcon = (priority: number) => {
|
||||
switch (priority) {
|
||||
case 3:
|
||||
return <UtensilsCrossed className="h-4 w-4" />;
|
||||
case 2:
|
||||
return <Utensils className="h-4 w-4" />;
|
||||
case 1:
|
||||
return <ForkIcon />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Board() {
|
||||
const [columns, setColumns] = useState<ColumnState>(initcolumns);
|
||||
const [taskId, setTaskId] = useState<number>(0);
|
||||
const [viewOpen, setViewOpen] = useState<boolean>(false);
|
||||
const [storyTickets, setStoryTickets] = useState<any>([]);
|
||||
const context = useContext(AppContext);
|
||||
const { tickets, fetchTickets, updateTicketStatus } = context as any;
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
await fetchTickets();
|
||||
};
|
||||
asyncFunc();
|
||||
}, [fetchTickets]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
if (!tickets || tickets.length === 0) {
|
||||
console.error('No tickets available');
|
||||
return;
|
||||
}
|
||||
|
||||
const stories = JSON.parse(JSON.stringify(tickets));
|
||||
if (!stories) {
|
||||
console.error('Failed to parse stories');
|
||||
return;
|
||||
}
|
||||
|
||||
setStoryTickets(stories);
|
||||
Tasks.splice(0, Tasks.length);
|
||||
stories.forEach((story: any) => {
|
||||
Tasks.push({
|
||||
id: `task-${story.storyId}`,
|
||||
content: story.title,
|
||||
state: status_to_state[story.status as string],
|
||||
priority: story.priority,
|
||||
intId: story.storyId,
|
||||
});
|
||||
});
|
||||
|
||||
const columnsCopy = { ...initcolumns };
|
||||
Object.values(columnsCopy).forEach((column) => {
|
||||
column.tasks = [];
|
||||
});
|
||||
Tasks.forEach((task) => {
|
||||
columnsCopy[task.state].tasks.push(task);
|
||||
});
|
||||
setColumns(columnsCopy);
|
||||
};
|
||||
|
||||
asyncFunc();
|
||||
}, [tickets]);
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
const { source, destination } = result;
|
||||
if (!destination) return;
|
||||
|
||||
const sourceColumn = columns[source.droppableId];
|
||||
const destColumn = columns[destination.droppableId];
|
||||
const sourceTasks = [...sourceColumn.tasks];
|
||||
const destTasks = [...destColumn.tasks];
|
||||
|
||||
if (source.droppableId === destination.droppableId) {
|
||||
const [reorderedItem] = sourceTasks.splice(source.index, 1);
|
||||
sourceTasks.splice(destination.index, 0, reorderedItem);
|
||||
setColumns({
|
||||
...columns,
|
||||
[source.droppableId]: {
|
||||
...sourceColumn,
|
||||
tasks: sourceTasks,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const [movedItem] = sourceTasks.splice(source.index, 1);
|
||||
movedItem.state = destination.droppableId; // Update the state of the moved task
|
||||
const story = storyTickets.find((story: any) => story.storyId === movedItem.intId);
|
||||
story.status = Object.keys(status_to_state).find(key => status_to_state[key] === movedItem.state);
|
||||
updateTicketStatus(movedItem.intId, story.status);
|
||||
destTasks.splice(destination.index, 0, movedItem);
|
||||
setColumns({
|
||||
...columns,
|
||||
[source.droppableId]: {
|
||||
...sourceColumn,
|
||||
tasks: sourceTasks,
|
||||
},
|
||||
[destination.droppableId]: {
|
||||
...destColumn,
|
||||
tasks: destTasks,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex h-screen">
|
||||
<SideBar />
|
||||
<SidebarInset className="flex-1 overflow-auto">
|
||||
<header className="flex items-center justify-between border-b p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-semibold">Team Name/ Project Name/ Board</h1>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</header>
|
||||
<main className="p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<TeamMates teamId={1} />
|
||||
<TicketCreate />
|
||||
{
|
||||
viewOpen ? (<TicketView id={taskId} handleClose={() => setViewOpen(false)} />) : ""
|
||||
}
|
||||
</div>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{Object.values(columns).map((column) => (
|
||||
<Droppable key={column.id} droppableId={column.id}>
|
||||
{(provided) => (
|
||||
<Card {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<CardHeader>
|
||||
<CardTitle>{column.title} ({column.tasks.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{column.tasks.map((task, index) => (
|
||||
<Draggable key={task.id} draggableId={task.id} index={index}>
|
||||
{(provided) => (
|
||||
<Card
|
||||
onClick={() => {
|
||||
setTaskId(task.intId);
|
||||
setViewOpen(true)
|
||||
}}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className="p-4"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">{task.content}</h3>
|
||||
<span className="inline-block bg-green-100 text-green-800 text-xs px-2 py-1 rounded">Parent</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getPriorityIcon(task.priority as number)}
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarFallback>U</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Droppable>
|
||||
))}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
.filter-menu{
|
||||
background-color: white;
|
||||
border: 1px solid #d6d4d4;
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
padding: 0%;
|
||||
padding-left: 10px;
|
||||
text-align:start;
|
||||
font-weight: 400; /*Optional: change font, font weight or font-size*/
|
||||
}
|
||||
|
||||
.filter-item{
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
display: flex;
|
||||
align-items: center; /* Centers text and icon vertically */
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
margin-left: 100px; /* Adjust spacing between text and icon */
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
/* margin-bottom: 5px; */
|
||||
margin-right: 10px;
|
||||
background-color: white;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
/* display: block; */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.custom-checkbox[data-state="checked"] {
|
||||
background-color: black; /* Background color when checked */
|
||||
border-color: black; /* Border color when checked */
|
||||
}
|
||||
|
||||
/* Add a tick mark with CSS */
|
||||
.custom-checkbox[data-state="checked"] .indicator::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border-right: 2px solid white;
|
||||
border-bottom: 2px solid white;
|
||||
top: 3px;
|
||||
left: 6px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import "./FilterDropdown.css";
|
||||
import { ChevronDown, ChevronUp } from "@mynaui/icons-react"; // import ChevronUp
|
||||
import { useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
const FilterDropdown = () => {
|
||||
// State for individual filter checkboxes
|
||||
const [selectedFilters, setSelectedFilters] = useState<{ [key: string]: boolean }>({
|
||||
Epic: false,
|
||||
Done: false,
|
||||
"In Plating": false,
|
||||
Cooking: false,
|
||||
"To Do": false,
|
||||
});
|
||||
|
||||
// State to track if the dropdown is open
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// List of filters
|
||||
const filterList = ["Epic", "Done", "In Plating", "Cooking", "To Do"];
|
||||
|
||||
// Handle individual filter change
|
||||
const handleFilterChange = (filter: string, isChecked: boolean) => {
|
||||
setSelectedFilters((prevSelected) => ({
|
||||
...prevSelected,
|
||||
[filter]: isChecked,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu onOpenChange={(open) => setIsOpen(open)}> {/* Track dropdown open state */}
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" style={{paddingRight: "5px"}}>
|
||||
Filter by {isOpen ? <ChevronUp style={{marginLeft: "60px"}}/> : <ChevronDown style={{marginLeft: "60px"}}/>} {/* Conditionally render chevron */}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
{/* Render filter checkboxes */}
|
||||
{filterList.map((filter) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={filter}
|
||||
checked={selectedFilters[filter] || false}
|
||||
onCheckedChange={(isChecked) => handleFilterChange(filter, isChecked)}
|
||||
>
|
||||
{filter}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterDropdown;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
.search-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 10rem;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 1rem; /* Adjust based on preference */
|
||||
color: #5f5c5c; /* Optional: change icon color */
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding-left: 3rem; /* Adjust based on icon width */
|
||||
border-radius: 100px;
|
||||
border: 1px solid #1d1c1c; /* Optional: change border color */
|
||||
/* font: ""; */
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// SearchBar.tsx
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Search } from "@mynaui/icons-react"
|
||||
import "./SearchBar.css"
|
||||
|
||||
interface SearchBarProps {
|
||||
searchQuery: string;
|
||||
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const SearchBar = ({ searchQuery, setSearchQuery }: SearchBarProps) => {
|
||||
return (
|
||||
<div className="search-bar">
|
||||
<Search className="search-icon" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search task"
|
||||
className="search-input"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)} // Handle input change
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default SearchBar;
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { Archive, ClipboardList, Users } from 'lucide-react'
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { useNavigate, useLocation } from 'react-router-dom' // Import useLocation
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuButton,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
const SideBar = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation(); // Get current route
|
||||
|
||||
return (
|
||||
<Sidebar className="w-64 border-r">
|
||||
<SidebarHeader className="border-b p-4 pl-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarFallback className="bg-primary text-primary-foreground text-lg">TN</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="font-semibold text-xl">Team Name</span>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="pl-6">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate("/backlog")}
|
||||
className={`justify-start text-base ${
|
||||
location.pathname === "/backlog" ? "bg-gray-300 dark:bg-gray-700" : ""
|
||||
}`}
|
||||
>
|
||||
<ClipboardList className="h-5 w-5 mr-3" />
|
||||
<span>Backlog</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate("/board")}
|
||||
className={`justify-start text-base ${
|
||||
location.pathname === "/board" ? "bg-gray-300 dark:bg-gray-700" : ""
|
||||
}`}
|
||||
>
|
||||
<Archive className="h-5 w-5 mr-3" />
|
||||
<span>Board</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate("/availability")}
|
||||
className={`justify-start text-base ${
|
||||
location.pathname === "/availability" ? "bg-gray-300 dark:bg-gray-700" : ""
|
||||
}`}
|
||||
>
|
||||
<Users className="h-5 w-5 mr-3" />
|
||||
<span>Availability</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
export default SideBar
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
.user-card {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2.5vw;
|
||||
height: 2.5vw;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.user-logo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.user-logo-hover-text {
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
font-size: 0.75em;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.user-card:hover .user-logo-hover-text {
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { useEffect } from "react";
|
||||
import "./UserCard.css";
|
||||
const UserCard = ({ hoverName }: { hoverName?: string }) => {
|
||||
const id=`user-${hoverName}`;
|
||||
useEffect(() => {
|
||||
const hoverElement = document.getElementById(id);
|
||||
const hoverText = document.getElementById(`${id}-hover-text`);
|
||||
|
||||
if (hoverElement && hoverText && hoverName) {
|
||||
const handleMouseEnter = () => {
|
||||
hoverText.style.display = 'block';
|
||||
};
|
||||
const handleMouseLeave = () => {
|
||||
hoverText.style.display = 'none';
|
||||
};
|
||||
|
||||
hoverElement.addEventListener('mouseenter', handleMouseEnter);
|
||||
hoverElement.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
// Cleanup listeners on unmount
|
||||
return () => {
|
||||
hoverElement.removeEventListener('mouseenter', handleMouseEnter);
|
||||
hoverElement.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}
|
||||
}, [id]);
|
||||
if(hoverName === undefined){
|
||||
return (<div className="user-card">
|
||||
<img className="user-logo" id={`user-${hoverName}`} src="src/assets/solid_red.png">
|
||||
|
||||
</img>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (<div className="user-card">
|
||||
<img className="user-logo" id={`user-${hoverName}`} src="src/assets/user_logo.png">
|
||||
|
||||
</img>
|
||||
<p className="user-logo-hover-text" id={`user-${hoverName}-hover-text`}>{hoverName}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default UserCard;
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
/* Custom checkbox styling */
|
||||
.custom-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
/* margin-bottom: 5px; */
|
||||
margin-right: 10px;
|
||||
background-color: white;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
/* display: block; */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.custom-checkbox[data-state="checked"] {
|
||||
background-color: black; /* Background color when checked */
|
||||
border-color: black; /* Border color when checked */
|
||||
}
|
||||
|
||||
/* Add a tick mark with CSS */
|
||||
.custom-checkbox[data-state="checked"] .indicator::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border-right: 2px solid white;
|
||||
border-bottom: 2px solid white;
|
||||
top: 3px;
|
||||
left: 6px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.task-table {
|
||||
width: 70vw;
|
||||
/* height: 100vh; */
|
||||
display: table;
|
||||
border-collapse: collapse;
|
||||
border: #e1dfdf 1px solid;
|
||||
table-layout: fixed; /* Ensures consistent column width */
|
||||
|
||||
}
|
||||
|
||||
.task-table th,
|
||||
.task-table td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* Ensures ellipsis for overflow text */
|
||||
white-space: nowrap; /* Prevents wrapping of text */
|
||||
}
|
||||
|
||||
.arrow{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.status-bubble {
|
||||
width: 50%;
|
||||
height: 30px;
|
||||
padding: 5px;
|
||||
border-radius: 50px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.status-cell{
|
||||
justify-content: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.status-to-do {
|
||||
background-color: #a7f3d0; /* Light green */
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-cooking {
|
||||
background-color: #d9f99d; /* Light lime */
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-in-plating {
|
||||
background-color: #fdba74; /* Light orange */
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-bon-appetit {
|
||||
background-color: #fca5a5; /* Light red */
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-default {
|
||||
background-color: #9ca3af; /* Light gray */
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.search-dropdown-div{
|
||||
display: flex;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.dropdown-div{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
/* General Button Styling */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.modal-content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 400px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Modal Header */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Modal Body */
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-body label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.modal-body input,
|
||||
.modal-body select,
|
||||
.modal-body textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.modal-body textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
/* Modal Footer */
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #ccc;
|
||||
color: #000;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
input, select, textarea{
|
||||
background-color: rgb(228, 224, 224); /* Set background to white */
|
||||
color: black; /* Set text color to black */
|
||||
padding: 0.5rem; /* Add padding for better appearance */
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
border: 1px solid #ccc; /* Optional: Add a visible border */
|
||||
}
|
||||
|
||||
.select-task {
|
||||
background-color: transparent;
|
||||
/*height: fit-content;*/
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.select-task:focus {
|
||||
outline: none;
|
||||
}
|
||||
+572
@@ -0,0 +1,572 @@
|
||||
'use client'
|
||||
|
||||
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { ArrowUpDown, BaggageClaim, ChevronDown, ChevronDownCircle, ChevronUp, Icon, PlusCircleIcon } from 'lucide-react'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Utensils, UtensilsCrossed } from 'lucide-react'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import TeamMates from '../TeamMates/TeamMates'
|
||||
import TicketCreate from '@/ticketPopup/TicketCreate'
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import SearchBar from '@/components/SearchBar/SearchBar'
|
||||
import './TaskTable.css'
|
||||
import { Badge } from '../ui/badge'
|
||||
import UserCard from '../Shared/UserCard/UserCard'
|
||||
import { AppContext } from '@/context/AppContext'
|
||||
import { Password } from '@mynaui/icons-react'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
|
||||
const ForkIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2" />
|
||||
<path d="M7 2v20" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
|
||||
// const tasksData = [
|
||||
// { task: 'Task 1', title: 'Title 1', priority: 1, status: 'Cooking', shift: 'Shift 1' },
|
||||
// { task: 'Task 2', title: 'Title 2', priority: 2, status: 'To Do', shift: 'Shift 2' },
|
||||
// { task: 'Task 3', title: 'Title 3', priority: 3, status: 'Cooking', shift: 'Shift 1' },
|
||||
// { task: 'Task 4', title: 'Title 4', priority: 1, status: 'Bon appétit', shift: 'Shift 2' },
|
||||
// { task: 'Task 5', title: 'Title 5', priority: 2, status: 'Bon appétit', shift: 'Shift 1' },
|
||||
// { task: 'Task 6', title: 'Title 6', priority: 3, status: 'In Plating', shift: 'Shift 2' },
|
||||
// { task: 'Task 7', title: 'Title 7', priority: 1, status: 'In Plating', shift: 'Shift 1' },
|
||||
// ]
|
||||
|
||||
// const shiftsData = [
|
||||
// { task: 1, title: 'Title 1', startDate: '2022-10-01', endDate: '2022-10-03', description: 'Description 1' },
|
||||
// { task: 2, title: 'Title 2', startDate: '2022-10-04', endDate: '2022-10-06', description: 'Description 2' },
|
||||
// ]
|
||||
|
||||
|
||||
const filterList = ['Bon appétit', 'In Plating', 'Cooking', 'To Do']
|
||||
|
||||
|
||||
|
||||
const FilterableTaskTable = () => {
|
||||
const [tasksLocal, setTasks] = useState([]);
|
||||
const [shiftsLocal, setShifts] = useState([]);
|
||||
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedFilters, setSelectedFilters] = useState<{ [key: string]: boolean }>({})
|
||||
// const [selectedFilters, setSelectedFilters] = useState({});
|
||||
const [selectedTasks, setSelectedTasks] = useState(new Set());
|
||||
// const [sortConfig, setSortConfig] = useState(null);
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [sortConfig, setSortConfig] = useState<{ key: keyof typeof tickets[0]; direction: 'asc' | 'desc' } | null>(null)
|
||||
// const [selectedTasks, setSelectedTasks] = useState<Set<string>>(new Set()) // Track selected tasks
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
||||
|
||||
const [localUsers, setLocalUsers] = useState([]);
|
||||
|
||||
|
||||
const [viewOpen, setViewOpen] = useState<boolean>(false);
|
||||
|
||||
const { tickets, fetchTickets, shifts, fetchShifts, addTicket, addShift, fetchUser, users, fetchUsers, availabilities, fetchSprintAvailability, updateTicketStatus } = useContext(AppContext) as any;
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
await fetchTickets();
|
||||
};
|
||||
asyncFunc();
|
||||
}, [fetchTickets]);
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
if (!tickets || tickets.length === 0) {
|
||||
console.error('No tickets available');
|
||||
return;
|
||||
}
|
||||
|
||||
const newTasks = tickets.map((ticket: any) => ({
|
||||
taskId: ticket.storyId,
|
||||
title: ticket.title,
|
||||
priority: ticket.priority,
|
||||
status: ticket.status,
|
||||
shiftId: ticket.sprintId,
|
||||
createdBy: ticket.createdBy,
|
||||
}));
|
||||
|
||||
setTasks(newTasks);
|
||||
}
|
||||
asyncFunc();
|
||||
}, [tickets])
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
await fetchShifts();
|
||||
};
|
||||
asyncFunc();
|
||||
}, [fetchShifts]);
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
if (!shifts || shifts.length === 0) {
|
||||
console.error('No shifts available');
|
||||
return;
|
||||
}
|
||||
|
||||
const newShifts = shifts.map((shift: any) => ({
|
||||
shiftId: shift.sprintId,
|
||||
name: shift.name,
|
||||
startDate: shift.startDate,
|
||||
endDate: shift.endDate,
|
||||
goal: shift.goal,
|
||||
}));
|
||||
setShifts(newShifts);
|
||||
}
|
||||
asyncFunc();
|
||||
}, [shifts]);
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
await fetchSprintAvailability();
|
||||
}
|
||||
asyncFunc();
|
||||
}, [fetchSprintAvailability]);
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
await fetchUsers();
|
||||
console.log(users);
|
||||
console.log("users----------------------------------------------------------", users);
|
||||
};
|
||||
asyncFunc();
|
||||
}, [fetchUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
const asyncFunc = async () => {
|
||||
if (!users || users.length === 0) {
|
||||
console.error('No users available');
|
||||
return;
|
||||
}
|
||||
|
||||
const newUsers = users.map((user: any) => ({
|
||||
id: user.userId,
|
||||
name: user.username,
|
||||
Password: user.password,
|
||||
}));
|
||||
setLocalUsers(newUsers);
|
||||
}
|
||||
asyncFunc();
|
||||
}, [users])
|
||||
|
||||
|
||||
const handleFilterChange = (filter: string, isChecked: boolean) => {
|
||||
setSelectedFilters((prev) => ({ ...prev, [filter]: isChecked }))
|
||||
}
|
||||
|
||||
const handleCreateShift = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const target = event.target as typeof event.target & {
|
||||
title: { value: string };
|
||||
startDate: { value: string };
|
||||
endDate: { value: string };
|
||||
description: { value: string };
|
||||
};
|
||||
event.preventDefault();
|
||||
const newShift = {
|
||||
name: target.title.value,
|
||||
startDate: target.startDate.value,
|
||||
endDate: target.endDate.value,
|
||||
goal: target.description.value,
|
||||
sprintId: 0,
|
||||
status: "Active",
|
||||
teamId: 1
|
||||
};
|
||||
console.log("newShift", newShift)
|
||||
addShift(newShift);
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
|
||||
const statusOrder: { [key: string]: number } = {
|
||||
"To Do": 1,
|
||||
"Cooking": 2,
|
||||
"In Plating": 3,
|
||||
"Bon appétit": 4,
|
||||
}
|
||||
|
||||
const handleSort = (key: keyof typeof tasksLocal[0]) => {
|
||||
setSortConfig((prev) => ({
|
||||
key,
|
||||
direction: prev?.key === key && prev.direction === "asc" ? "desc" : "asc",
|
||||
}));
|
||||
};
|
||||
|
||||
// Filter tasks based on selected filters
|
||||
const filteredTasks = tasksLocal.filter((task) => {
|
||||
if (Object.values(selectedFilters).every((v) => !v)) return true
|
||||
return selectedFilters[task.status]
|
||||
})
|
||||
|
||||
// Further filter tasks based on the search query
|
||||
const searchFilteredTasks = filteredTasks.filter((task) => {
|
||||
const normalizedQuery = searchQuery.replace(/\s+/g, '').toLowerCase();
|
||||
const normalizedTitle = task.title.replace(/\s+/g, '').toLowerCase();
|
||||
return normalizedTitle.includes(normalizedQuery);
|
||||
});
|
||||
|
||||
// Sort tasks based on sortConfig
|
||||
const sortedTasks = [...searchFilteredTasks].sort((a, b) => {
|
||||
// console.log("searchFilteredTasks", searchFilteredTasks);
|
||||
if (!sortConfig) return 0;
|
||||
const { key, direction } = sortConfig;
|
||||
|
||||
// Custom comparison for 'status'
|
||||
if (key === 'status') {
|
||||
const orderA = statusOrder[a.status] || 0;
|
||||
const orderB = statusOrder[b.status] || 0;
|
||||
return direction === 'asc' ? orderA - orderB : orderB - orderA;
|
||||
}
|
||||
|
||||
// Default comparison for other fields
|
||||
if (a[key] < b[key]) return direction === 'asc' ? -1 : 1;
|
||||
if (a[key] > b[key]) return direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Handle selecting or deselecting all tasks
|
||||
const handleSelectAll = (isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
setSelectedTasks(new Set(sortedTasks.map((task) => task.taskId))) // Select all tasks
|
||||
} else {
|
||||
setSelectedTasks(new Set()) // Deselect all tasks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle individual task selection
|
||||
const handleTaskSelect = (task: string, isChecked: boolean) => {
|
||||
setSelectedTasks((prev) => {
|
||||
const updated = new Set(prev)
|
||||
if (isChecked) {
|
||||
updated.add(task)
|
||||
} else {
|
||||
updated.delete(task)
|
||||
}
|
||||
return updated
|
||||
})
|
||||
}
|
||||
const groupedTasks = shiftsLocal.map((shift) => {
|
||||
|
||||
//console.log("sortedTasks", sortedTasks);
|
||||
const tasksForShift = sortedTasks.filter(
|
||||
(task) =>
|
||||
task.shiftId === shift.shiftId
|
||||
|
||||
|
||||
);
|
||||
|
||||
|
||||
//console.log("tasksForShift", tasksForShift);
|
||||
//console.log("shiftsLocal", shiftsLocal);
|
||||
|
||||
// Count tasks based on their status
|
||||
const statusCounts = tasksForShift.reduce(
|
||||
(counts, task) => {
|
||||
if (task.status === 'To Do') counts.toDo++;
|
||||
if (task.status === 'Cooking') counts.cooking++;
|
||||
if (task.status === 'In Plating') counts.inPlating++;
|
||||
if (task.status === 'Bon appétit') counts.bonAppetit++;
|
||||
return counts;
|
||||
},
|
||||
{ toDo: 0, cooking: 0, inPlating: 0, bonAppetit: 0 }
|
||||
);
|
||||
|
||||
return {
|
||||
shiftId: shift.shiftId,
|
||||
shiftName: shift.name,
|
||||
tasks: tasksForShift,
|
||||
statusCounts, // Add the status counts here
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Function to get the appropriate class based on status
|
||||
const getStatusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'To Do':
|
||||
return 'status-bubble status-to-do';
|
||||
case 'Cooking':
|
||||
return 'status-bubble status-cooking';
|
||||
case 'In Plating':
|
||||
return 'status-bubble status-in-plating';
|
||||
case 'Bon appétit':
|
||||
return 'status-bubble status-bon-appetit';
|
||||
default:
|
||||
return 'status-bubble status-default';
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateTask = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const target = event.target as typeof event.target & {
|
||||
title: { value: string };
|
||||
shift: { value: string };
|
||||
priority: { value: string };
|
||||
status: { value: string };
|
||||
};
|
||||
const newTask = {
|
||||
storyId: tasksLocal.length + 1,
|
||||
title: target.title.value,
|
||||
sprintId: parseInt(target.shift.value),
|
||||
priority: parseInt(target.priority.value),
|
||||
status: target.status.value,
|
||||
};
|
||||
addTicket(newTask);
|
||||
setIsTaskModalOpen(false);
|
||||
}
|
||||
|
||||
const handleStatusChange = (taskId: number, newStatus: string) => {
|
||||
console.log("taskId --------------", taskId);
|
||||
updateTicketStatus(taskId, newStatus);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 mb-20">
|
||||
{/* Search Bar */}
|
||||
<div className="search-dropdown-div">
|
||||
<SearchBar searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
|
||||
|
||||
<div className='dropdown-div'>
|
||||
<DropdownMenu onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="w-[150px] justify-between">
|
||||
Filter by
|
||||
{isOpen ? <ChevronUp className="ml-2 h-4 w-4" /> : <ChevronDown className="ml-2 h-4 w-4" />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
{filterList.map((filter) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={filter}
|
||||
checked={selectedFilters[filter] || false}
|
||||
onCheckedChange={(isChecked) => handleFilterChange(filter, isChecked)}
|
||||
>
|
||||
{filter}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="bg-white-500 text-gray flex items-center justify-between">
|
||||
<TicketCreate />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{groupedTasks.map((group) => (
|
||||
<div key={group.shiftId} className="task-group">
|
||||
<div className="flex items-center justify-between gap-4 p-2 border border-gray-300 rounded-md bg-white shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-s font-bold text-gray-400">{group.shiftName}</span>
|
||||
<span className="text-s font-bold text-gray-400">
|
||||
Availability: {availabilities.find((a: any) => a.sprintId === group.shiftId).availability}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 ml-auto">
|
||||
<span className="text-xs font-bold text-gray-400">To do:
|
||||
<Badge variant="outline" className="status-to-do mr-2">
|
||||
{group.statusCounts.toDo}
|
||||
</Badge></span>
|
||||
<span className="text-xs font-bold text-gray-400">Cooking:
|
||||
<Badge variant="outline" className="status-cooking mr-2">
|
||||
{group.statusCounts.cooking}
|
||||
</Badge></span>
|
||||
<span className="text-xs font-bold text-gray-400">In plating:
|
||||
<Badge variant="outline" className="status-in-plating mr-2">
|
||||
{group.statusCounts.inPlating}
|
||||
</Badge></span>
|
||||
<span className="text-xs font-bold text-gray-400">Bon appétit:
|
||||
<Badge variant="outline" className="status-bon-appetit mr-2">
|
||||
{group.statusCounts.bonAppetit}
|
||||
</Badge></span>
|
||||
</div>
|
||||
</div>
|
||||
<Table className="task-table">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-100">
|
||||
<TableHead className="w-[100px] pl-2">
|
||||
{/* <Checkbox
|
||||
checked={sortedTasks.length > 0 && sortedTasks.every((task) => selectedTasks.has(task.taskId))}
|
||||
onCheckedChange={handleSelectAll}
|
||||
className="custom-checkbox"
|
||||
color='grey'
|
||||
/> */}
|
||||
Task
|
||||
</TableHead>
|
||||
<TableHead onClick={() => handleSort('title')} className="cursor-pointer w-[150px]">
|
||||
Title <ArrowUpDown className='inline h-4 w-4' />
|
||||
</TableHead>
|
||||
<TableHead onClick={() => handleSort('priority')} className="cursor-pointer w-[100px]">
|
||||
Priority <ArrowUpDown className='inline h-4 w-4' />
|
||||
</TableHead>
|
||||
<TableHead onClick={() => handleSort('status')} className="cursor-pointer w-[200px] text-center">
|
||||
Status <ArrowUpDown className='inline h-4 w-4' />
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{group.tasks.map((task) => (
|
||||
<TableRow key={task.taskId}>
|
||||
<TableCell className="font-medium truncate max-w-[100px] text-gray-500" >
|
||||
<Checkbox
|
||||
checked={selectedTasks.has(task.taskId)}
|
||||
onCheckedChange={(isChecked) => handleTaskSelect(task.taskId, isChecked as boolean)}
|
||||
className="mr-2 custom-checkbox"
|
||||
/>
|
||||
Task {task.taskId}
|
||||
</TableCell>
|
||||
<TableCell className='truncate max-w-[150px] font-bold'>{task.title} </TableCell>
|
||||
<TableCell className='truncate max-w-[100px]'>
|
||||
{task.priority === 1 ? <ForkIcon /> : task.priority === 2 ? <Utensils /> : <UtensilsCrossed />}
|
||||
</TableCell>
|
||||
<TableCell className='status-cell'>
|
||||
<Badge variant="outline" className="bg-green-300 mr-4 w-[100px] status-div h-[30px]">
|
||||
Parent
|
||||
</Badge>
|
||||
|
||||
<div className={`${getStatusClass(task.status)}`}>
|
||||
<Select value={task.status} onValueChange={(value: string) => handleStatusChange(task.taskId, value)}>
|
||||
<SelectTrigger className={`border-0 select-task focus:ring-0 focus:box-shadow-0`}>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="To Do">To Do</SelectItem>
|
||||
<SelectItem value="Cooking">Cooking</SelectItem>
|
||||
<SelectItem value="In Plating">In Plating</SelectItem>
|
||||
<SelectItem value="Bon appétit">Bon appétit</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{/*<DropdownMenu onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className={`status-div`}>
|
||||
{task.status}
|
||||
{<ChevronDownCircle />}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
{filterList.map((status) => (
|
||||
<DropdownMenuItem
|
||||
key={status}
|
||||
checked={selectedFilters[filter] || false}
|
||||
onCheckedChange={(isChecked) => handleFilterChange(filter, isChecked)}
|
||||
>
|
||||
{filter}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>*/}
|
||||
</div>
|
||||
|
||||
{/* <img className="user-logo" src="src/assets/user_logo.png"></img> */}
|
||||
<UserCard hoverName={users.find((user) => user.userId == task.createdBy).username} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div>
|
||||
{/* Modal for Shift Creation */}
|
||||
<Button onClick={() => setIsModalOpen(true)} className="bg-white-500 hover:bg-gray-300 text-gray">
|
||||
<PlusCircleIcon size={18} /> Create Shift
|
||||
</Button>
|
||||
{isModalOpen && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<h2 className="text-xl font-bold mb-4">Create New Shift</h2>
|
||||
<form onSubmit={handleCreateShift} className="space-y-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="title" className="block font-medium mb-1">Shift Name</label>
|
||||
<input
|
||||
id="title"
|
||||
name="title"
|
||||
className="input-field w-full p-2 border border-gray-300 rounded-md"
|
||||
placeholder="Enter shift name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="startDate" className="block font-medium mb-1">Start Date</label>
|
||||
<input
|
||||
id="startDate"
|
||||
name="startDate"
|
||||
type="date"
|
||||
className="input-field w-full p-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="endDate" className="block font-medium mb-1">End Date</label>
|
||||
<input
|
||||
id="endDate"
|
||||
name="endDate"
|
||||
type="date"
|
||||
className="input-field w-full p-2 border border-gray-300 rounded-md"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="description" className="block font-medium mb-1">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
className="input-field w-full p-2 border border-gray-300 rounded-md"
|
||||
// rows="4"
|
||||
placeholder="Enter shift description"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="form-actions flex items-center justify-end space-x-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" className="bg-blue-500 hover:bg-blue-600 text-white">
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilterableTaskTable
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
.flex-container {
|
||||
display: flex; /* Flexbox for alignment */
|
||||
flex-wrap: wrap; /* Allow cards to wrap to the next line */
|
||||
gap: 1em; /* Even spacing between cards */
|
||||
justify-content: flex-start; /* Align items to the left (or adjust as needed) */
|
||||
align-items: center; /* Ensure items align properly */
|
||||
flex-direction: row; /* Align items horizontally */
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import UserCard from "../Shared/UserCard/UserCard";
|
||||
import './TeamMates.css';
|
||||
import { AppContext } from "@/context/AppContext";
|
||||
|
||||
const TeamMates = ({ teamId }: { teamId: number }) => {
|
||||
|
||||
const {teamMates, fetchTeamMates} = useContext(AppContext) as any;
|
||||
|
||||
useEffect(() => {
|
||||
// fetch(`http://localhost:5073/Test/GetUsersInTeam/${teamId}`)
|
||||
// .then((response) => response.json())
|
||||
// .then((data: { username: string }[]) => {
|
||||
// const usernames = data.map(user => user.username);
|
||||
// setTeamMates(usernames);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error("Error fetching teammates:", error);
|
||||
// });
|
||||
async function fetchTeamMates1(teamId: number) {
|
||||
fetchTeamMates(teamId);
|
||||
}
|
||||
fetchTeamMates1(teamId);
|
||||
}, [teamId]);
|
||||
|
||||
return (
|
||||
<div className="flex-container teammates-component">
|
||||
<span>Team Mates: </span>
|
||||
{
|
||||
teamMates.map((teammate, index) => (
|
||||
<UserCard key={index} hoverName={teammate.username} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamMates;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon } from "@radix-ui/react-icons"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<DotFilledIcon className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
+760
@@ -0,0 +1,760 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { ViewVerticalIcon } from "@radix-ui/react-icons"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar:state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
type SidebarContext = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContext | null>(null)
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
},
|
||||
[setOpenProp, open]
|
||||
)
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo<SidebarContext>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarProvider.displayName = "SidebarProvider"
|
||||
|
||||
const Sidebar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="group peer hidden md:block text-sidebar-foreground"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Sidebar.displayName = "Sidebar"
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-7 w-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ViewVerticalIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
SidebarTrigger.displayName = "SidebarTrigger"
|
||||
|
||||
const SidebarRail = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button">
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
|
||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarRail.displayName = "SidebarRail"
|
||||
|
||||
const SidebarInset = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"main">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex min-h-svh flex-1 flex-col bg-background",
|
||||
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarInset.displayName = "SidebarInset"
|
||||
|
||||
const SidebarInput = React.forwardRef<
|
||||
React.ElementRef<typeof Input>,
|
||||
React.ComponentProps<typeof Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarInput.displayName = "SidebarInput"
|
||||
|
||||
const SidebarHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarHeader.displayName = "SidebarHeader"
|
||||
|
||||
const SidebarFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarFooter.displayName = "SidebarFooter"
|
||||
|
||||
const SidebarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof Separator>,
|
||||
React.ComponentProps<typeof Separator>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Separator
|
||||
ref={ref}
|
||||
data-sidebar="separator"
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarSeparator.displayName = "SidebarSeparator"
|
||||
|
||||
const SidebarContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarContent.displayName = "SidebarContent"
|
||||
|
||||
const SidebarGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarGroup.displayName = "SidebarGroup"
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel"
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction"
|
||||
|
||||
const SidebarGroupContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent"
|
||||
|
||||
const SidebarMenu = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarMenu.displayName = "SidebarMenu"
|
||||
|
||||
const SidebarMenuItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton"
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction"
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge"
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 flex-1 max-w-[--skeleton-width]"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
|
||||
|
||||
const SidebarMenuSub = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub"
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ ...props }, ref) => <li ref={ref} {...props} />)
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
import { createContext, useState, useEffect, Dispatch, SetStateAction, ReactNode, useCallback } from 'react';
|
||||
|
||||
import api, { getTicketEstimation, deleteTicket, getTest, getTickets, getTicket, putTicketStatus, postTicket, getUsers, getUser, avalabilityUser, getShifts, postShift, getUsersInShift, postAdjustment, putTeamMemberAvailability, getShiftAdjustment, getShiftAdjustmentList, getAvailabilityPerSprint, logIn, getTeamMates, signIn } from '../lib/api';
|
||||
|
||||
interface AppContextType {
|
||||
data: any;
|
||||
setData: Dispatch<SetStateAction<any>>;
|
||||
tickets: any[];
|
||||
ticket: any;
|
||||
storyPoints: number;
|
||||
fetchTickets: () => Promise<void>;
|
||||
addTicket: (ticket: any) => Promise<void>;
|
||||
updateTicketStatus: (ticketId: number, status: string) => Promise<void>;
|
||||
removeTicket: (ticketId: number) => Promise<void>;
|
||||
getEstimation: (title: string, description: string) => Promise<void>;
|
||||
fetchTicket: (ticketId: number) => Promise<void>;
|
||||
users: any[];
|
||||
user: any;
|
||||
fetchUsers: () => Promise<void>;
|
||||
fetchUser: (userId: number) => Promise<void>;
|
||||
userAvailability: any;
|
||||
fetchUserAvailability: (userId: number, sprintId: number) => Promise<void>;
|
||||
shifts: any[];
|
||||
fetchShifts: () => Promise<void>;
|
||||
addShift: (shift: any) => Promise<void>;
|
||||
usersInShift: any[];
|
||||
fetchUsersInShift: (sprintId: number) => Promise<void>;
|
||||
addAdjustment: (sprintId: number, adjustment: any) => void;
|
||||
updateAvailability: (userId: number, sprintId: number, availability: any) => Promise<void>;
|
||||
shiftAdjustment: any;
|
||||
fetchShiftAdjustment: (sprintId: number) => Promise<void>;
|
||||
fetchShiftAdjustmentList: (sprintId: number) => Promise<any>;
|
||||
adjustments: any[];
|
||||
setAdjustments: Dispatch<SetStateAction<any[]>>;
|
||||
teamMates: any[];
|
||||
setTeamMates: Dispatch<SetStateAction<any[]>>;
|
||||
fetchTeamMates: (teamID: number) => Promise<any>;
|
||||
LogIn:(userName: string, Password: string) => Promise<any>;
|
||||
isAuthenticated: boolean;
|
||||
SignIn: (username: string, password: string) => Promise<void>;
|
||||
fetchSprintAvailability: (sprintId: number) => Promise<void>;
|
||||
availabilities: any[];
|
||||
}
|
||||
|
||||
|
||||
export const AppContext = createContext<AppContextType | undefined>(undefined);
|
||||
|
||||
export const AppProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
//------------------- 1. Tiket API -------------------
|
||||
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [tickets, setTickets] = useState<any[]>([]);
|
||||
const [ticket, setTicket] = useState<any>(null);
|
||||
const [storyPoints, setStoryPoints] = useState<number>(-1);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const result = await getTest();
|
||||
setData(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const addTicket = async (ticket: any) => {
|
||||
try {
|
||||
await postTicket(ticket);
|
||||
await fetchTickets();
|
||||
} catch (error) {
|
||||
console.error('Error adding ticket:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTickets = useCallback(async () => {
|
||||
try {
|
||||
const result = await getTickets();
|
||||
setTickets(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching tickets:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTickets();
|
||||
}, [fetchTickets]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTickets();
|
||||
}, []);
|
||||
|
||||
const fetchTicket = useCallback(async (ticketId: number) => {
|
||||
try {
|
||||
const result = await getTicket(ticketId);
|
||||
setTicket(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching ticket:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updateTicketStatus = async (ticketId: number, status: string) => {
|
||||
try {
|
||||
await putTicketStatus(ticketId, status);
|
||||
await fetchTicket(ticketId);
|
||||
await fetchTickets();
|
||||
} catch (error) {
|
||||
console.error('Error updating ticket:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const removeTicket = async (ticketId: number) => {
|
||||
try {
|
||||
await deleteTicket(ticketId);
|
||||
await fetchTickets();
|
||||
} catch (error) {
|
||||
console.error('Error deleting ticket:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const getEstimation = async (title: string, description: string) => {
|
||||
try {
|
||||
const response = await getTicketEstimation(title, description);
|
||||
const data = await response.data;
|
||||
setStoryPoints(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching ticket estimation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------- 2. User Api -------------------
|
||||
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [currentUser, setCurrentUser] = useState<any>(null);
|
||||
const [teamMates, setTeamMates] = useState<any[]>([]);
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
try {
|
||||
const result = await getUsers();
|
||||
setUsers(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const fetchUser = useCallback(async (userId: number) => {
|
||||
try {
|
||||
const result = await getUser(userId);
|
||||
setUser(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const LogIn = async (userName: string, Password: string) => {
|
||||
try {
|
||||
const result = await logIn(userName, Password)
|
||||
setCurrentUser(result.data);
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fetchTeamMates = useCallback(async (teamID:number) =>{
|
||||
try{
|
||||
const result = await getTeamMates(teamID);
|
||||
setTeamMates(result.data);
|
||||
|
||||
}catch{
|
||||
console.error('Error');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const SignIn = async (userName: string, Password: string) => {
|
||||
try {
|
||||
const result = await signIn(userName, Password)
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ------------------- 3. User Availability Api -------------------
|
||||
const [userAvailability, setUserAvailability] = useState<any>(null);
|
||||
const [shifts, setShifts] = useState<any[]>([]);
|
||||
const [usersInShift, setUsersInShift] = useState<any[]>([]);
|
||||
const [shiftAdjustment, setShiftAdjustment] = useState<any[]>([]);
|
||||
const [adjustments, setAdjustments] = useState<any[]>([]);
|
||||
|
||||
const fetchUserAvailability = useCallback(async (userId: number, sprintId: number) => {
|
||||
try {
|
||||
const result = await avalabilityUser(userId, sprintId);
|
||||
setUserAvailability(result.data);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user availability:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchShifts = useCallback(async () => {
|
||||
try {
|
||||
const result = await getShifts();
|
||||
setShifts(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching shifts:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchUsersInShift = useCallback(async (sprintId: number) => {
|
||||
try {
|
||||
setUsersInShift([]);
|
||||
const result = await getUsersInShift(sprintId);
|
||||
setUsersInShift(result.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching users in shift:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addAdjustment = (sprintId: number, adjustment: any) => {
|
||||
try {
|
||||
postAdjustment(sprintId, adjustment);
|
||||
} catch (error) {
|
||||
console.error('Error adding adjustment:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const updateAvailability = async (userId: number, sprintId: number, availability: any) => {
|
||||
try {
|
||||
await putTeamMemberAvailability(userId, sprintId, availability);
|
||||
await fetchUserAvailability(userId, sprintId);
|
||||
} catch (error) {
|
||||
console.error('Error updating availability:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchShiftAdjustment = useCallback(async (sprintId: number) => {
|
||||
try {
|
||||
const result = await getShiftAdjustment(sprintId);
|
||||
setShiftAdjustment(result.data);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching shift adjustment:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchShiftAdjustmentList = useCallback(async (sprintId: number) => {
|
||||
try {
|
||||
const result = await getShiftAdjustmentList(sprintId);
|
||||
setAdjustments(result.data);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching shift adjustment:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ------------------- 4. Backlog -------------------
|
||||
const [availabilities, setAvailabilities] = useState<any>([])
|
||||
|
||||
const addShift = async (shift: any) => {
|
||||
try {
|
||||
await postShift(shift);
|
||||
await fetchShifts();
|
||||
} catch (error) {
|
||||
console.error('Error adding shift:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSprintAvailability = useCallback(async () => {
|
||||
try {
|
||||
const result = await getAvailabilityPerSprint();
|
||||
setAvailabilities(result.data);
|
||||
console.log("Availabilities", result.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching ticket:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSprintAvailability();
|
||||
}, [fetchSprintAvailability]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSprintAvailability();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
data,
|
||||
setData,
|
||||
tickets,
|
||||
fetchTickets,
|
||||
updateTicketStatus,
|
||||
removeTicket,
|
||||
getEstimation,
|
||||
fetchTicket,
|
||||
ticket,
|
||||
storyPoints,
|
||||
addTicket,
|
||||
users,
|
||||
fetchUsers,
|
||||
user,
|
||||
fetchUser,
|
||||
userAvailability,
|
||||
fetchUserAvailability,
|
||||
shifts,
|
||||
fetchShifts,
|
||||
addShift,
|
||||
usersInShift,
|
||||
fetchUsersInShift,
|
||||
addAdjustment,
|
||||
updateAvailability,
|
||||
shiftAdjustment,
|
||||
fetchShiftAdjustment,
|
||||
fetchShiftAdjustmentList,
|
||||
adjustments,
|
||||
setAdjustments,
|
||||
fetchTeamMates,
|
||||
teamMates,
|
||||
setTeamMates,
|
||||
isAuthenticated,
|
||||
LogIn,
|
||||
SignIn,
|
||||
availabilities,
|
||||
fetchSprintAvailability,
|
||||
}}>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #f1f1f1;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
|
||||
<App />
|
||||
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
import TaskTable from "@/components/TaskTable/TaskTable"
|
||||
import SideBar from "@/components/Shared/SideBar/SideBar"
|
||||
import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { MoreVertical } from "lucide-react"
|
||||
|
||||
|
||||
const BacklogPage = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<SidebarProvider>
|
||||
<SideBar />
|
||||
<SidebarInset className="flex-1 overflow-auto">
|
||||
<header className="flex items-center justify-between border-b p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-semibold">Team Name/ Project Name/ Backlog</h1>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</header>
|
||||
<div style={{ "marginLeft": "5rem" }}>
|
||||
<TaskTable />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default BacklogPage
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
.knife-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9998;
|
||||
}
|
||||
|
||||
.knife {
|
||||
font-size: 120px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.knife-left {
|
||||
left: -100px; /* Starts off-screen on the left */
|
||||
animation: throwLeft 4s ease-in-out infinite; /* Increased duration for smoother effect */
|
||||
}
|
||||
|
||||
.knife-right {
|
||||
right: -100px; /* Starts off-screen on the right */
|
||||
animation: throwRight 4s ease-in-out infinite; /* Increased duration for smoother effect */
|
||||
}
|
||||
|
||||
@keyframes throwLeft {
|
||||
0% {
|
||||
left: -100px; /* Starts off-screen */
|
||||
transform: translateY(-50%) rotate(0deg); /* Tail to the left, blade to the right */
|
||||
}
|
||||
45%, 55% {
|
||||
left: 47%; /* Centered position */
|
||||
transform: translateY(-50%) scale(1.1) rotate(100deg); /* Pause in the center with rotation reset */
|
||||
}
|
||||
100% {
|
||||
left: 100%; /* Ends off-screen on the right */
|
||||
transform: translateY(-50%) rotate(300deg); /* Tail to the right, blade to the left */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes throwRight {
|
||||
0% {
|
||||
right: -100px; /* Starts off-screen */
|
||||
transform: translateY(-50%) rotate(-90deg); /* Tail to the right, blade to the left */
|
||||
}
|
||||
45%, 55% {
|
||||
right: 47%; /* Centered position */
|
||||
transform: translateY(-50%) scale(1.1) rotate(-15deg); /* Pause in the center with rotation reset */
|
||||
}
|
||||
100% {
|
||||
right: 100%; /* Ends off-screen on the left */
|
||||
transform: translateY(-50%) rotate(125deg); /* Tail to the left, blade to the right */
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import './CrossingKnives.css';
|
||||
|
||||
const CrossingKnives = () => {
|
||||
return (
|
||||
<div className="knife-container">
|
||||
<div className="knife knife-left">🔪</div>
|
||||
<div className="knife knife-right">🔪</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CrossingKnives;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
.estimation-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.bot-icon {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1.2rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
animation: blink 0.7s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30%;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 480px) {
|
||||
.estimation-popup {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-container button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Bot } from 'lucide-react';
|
||||
import './EstimationPopup.css'
|
||||
import { AppContext } from "@/context/AppContext";
|
||||
|
||||
const EstimationPopup = ({ title, description, handleExit, setStoryPlates }: {
|
||||
title: string,
|
||||
description: string,
|
||||
handleExit: () => void,
|
||||
setStoryPlates: (storyPlates: number) => void
|
||||
}) => {
|
||||
|
||||
const [displayedText, setDisplayedText] = useState("Thinking...");
|
||||
const [isTyping, setIsTyping] = useState<boolean>(true);
|
||||
const { getEstimation, storyPoints } = useContext(AppContext) as any;
|
||||
const [estimation, setEstimation] = useState<number>(0);
|
||||
|
||||
const handleAccept = () => {
|
||||
if (storyPoints > 0) {
|
||||
setStoryPlates(storyPoints);
|
||||
handleExit();
|
||||
}
|
||||
else {
|
||||
handleExit();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getEstimation(title, description);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const asyncfunc = async () => {
|
||||
setDisplayedText("Thinking...");
|
||||
let displayMessage = "";
|
||||
if (title === "") {
|
||||
displayMessage = "Unfortunately, story point estimation could not be performed on a ticket with no title.";
|
||||
}
|
||||
else {
|
||||
if (storyPoints > 13) {
|
||||
if (description === "") {
|
||||
displayMessage = `Ticket with title "${title}" and no description is estimated to be too large. Consider breaking it down into smaller tickets.`;
|
||||
}
|
||||
else {
|
||||
displayMessage = `Ticket with title "${title}" and description "${description}" is estimated to be too large. Consider breaking it down into smaller tickets.`;
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
if (description === "") {
|
||||
displayMessage = `Ticket with title "${title}" and no description is estimated to be worth ${storyPoints} story points.`;
|
||||
}
|
||||
|
||||
else {
|
||||
displayMessage = `Ticket with title "${title}" and description "${description}" is estimated to be worth ${storyPoints} story points.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setEstimation(storyPoints);
|
||||
// console.log("displayMessage", displayMessage);
|
||||
// console.log("estimation", estimation);
|
||||
// console.log("storyPoints", storyPoints);
|
||||
// console.log("storyPoints !== estimation", storyPoints !== estimation);
|
||||
|
||||
// necessary condition in order to prevent duplicated triggers of useEffect to try to print
|
||||
// their respective messages
|
||||
// estimation doesn't actually store anything besides 0 and 1, storyPoints is initialised
|
||||
// in AppContext with -1 and is set to -1 when no input is provided for the model
|
||||
// I don't fully understand why it works, and I think that some things I do are not required,
|
||||
// but I shall not be the one to figure this out
|
||||
if (estimation > 0 && storyPoints !== estimation || title === "" && storyPoints === -1) {
|
||||
console.log("displayMessage", displayMessage);
|
||||
console.log("estimation", estimation);
|
||||
console.log("storyPoints", storyPoints);
|
||||
let index = 0;
|
||||
const typingInterval = setInterval(() => {
|
||||
if (index < displayMessage.length + 1) {
|
||||
setDisplayedText(displayMessage.slice(0, index));
|
||||
index++;
|
||||
} else {
|
||||
clearInterval(typingInterval);
|
||||
setIsTyping(false);
|
||||
}
|
||||
}, 30);
|
||||
|
||||
return () => clearInterval(typingInterval);
|
||||
}
|
||||
}
|
||||
|
||||
asyncfunc();
|
||||
}, [title, description, storyPoints]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("initialization")
|
||||
setEstimation(1);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="estimation-popup">
|
||||
<div className="bot-icon">
|
||||
<Bot size={48} />
|
||||
</div>
|
||||
<div className="message-container">
|
||||
<p className="message">{displayedText}</p>
|
||||
{isTyping && <span className="typing-indicator">▋</span>}
|
||||
</div>
|
||||
<div className="button-container">
|
||||
{
|
||||
storyPoints > 0 && storyPoints <= 13 &&
|
||||
<Button onClick={handleAccept} disabled={isTyping}>Accept</Button>
|
||||
}
|
||||
<Button variant="outline" onClick={handleExit}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EstimationPopup;
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import {PointElement, LineElement} from 'chart.js';
|
||||
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
const performanceMetrics = [
|
||||
{ name: "Accuracy", value: 0.9044, color: "rgba(255, 99, 132, 0.2)", borderColor: "rgba(255, 99, 132, 1)" },
|
||||
{ name: "Macro F1", value: 0.9075, color: "rgba(54, 162, 235, 0.2)", borderColor: "rgba(54, 162, 235, 1)" },
|
||||
{ name: "Weighted F1", value: 0.9047, color: "rgba(255, 206, 86, 0.2)", borderColor: "rgba(255, 206, 86, 1)" },
|
||||
{ name: "Precision", value: 0.9141, color: "rgba(75, 192, 192, 0.2)", borderColor: "rgba(75, 192, 192, 1)" },
|
||||
{ name: "Recall", value: 0.9024, color: "rgba(153, 102, 255, 0.2)", borderColor: "rgba(153, 102, 255, 1)" },
|
||||
];
|
||||
|
||||
const projectData = [
|
||||
{ repository: "Apache", projects: ["mesos", "usergrid"], color: "rgba(255, 99, 132, 0.2)", border: "rgba(255, 99, 132, 1)" },
|
||||
{ repository: "Appcelerator", projects: ["Appcelerator studio", "Aptana studio", "Titanium"], color: "rgba(54, 162, 235, 0.2)", border: "rgba(54, 162, 235, 1)" },
|
||||
{ repository: "Duraspace", projects: ["Duracloud"], color: "rgba(255, 206, 86, 0.2)", border: "rgba(255, 206, 86, 1)" },
|
||||
{ repository: "Atlassian", projects: ["Bamboo", "Clover"], color: "rgba(75, 192, 192, 0.2)", border: "rgba(75, 192, 192, 1)" },
|
||||
{ repository: "Moodle", projects: ["Moodle"], color: "rgba(153, 102, 255, 0.2)", border: "rgba(153, 102, 255, 1)" },
|
||||
{ repository: "Lsstcorp", projects: ["Data management"], color: "rgba(255, 159, 64, 0.2)", border: "rgba(255, 159, 64, 1)" },
|
||||
{ repository: "Mulesoft", projects: ["Mule", "Mule studio"], color: "rgba(199, 199, 199, 0.2)", border: "rgba(199, 199, 199, 1)" },
|
||||
{ repository: "Spring", projects: ["Spring XD"], color: "rgba(83, 102, 255, 0.2)", border: "rgba(83, 102, 255, 1)" },
|
||||
{ repository: "Talendforge", projects: ["Talend Data Quality", "Talend ESB"], color: "rgba(255, 99, 255, 0.2)", border: "rgba(255, 99, 255, 1)" },
|
||||
];
|
||||
|
||||
|
||||
const ModelDescription = () => {
|
||||
const datasetChartData = {
|
||||
labels: projectData.map(repo => repo.repository),
|
||||
datasets: [{
|
||||
label: 'Number of Projects',
|
||||
data: projectData.map(repo => repo.projects.length),
|
||||
backgroundColor: projectData.map(repo => repo.color),
|
||||
borderColor: projectData.map(repo => repo.border),
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const repo = projectData[context.dataIndex];
|
||||
return [
|
||||
`Projects: ${context.raw}`,
|
||||
...repo.projects.map(project => `- ${project}`)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>DistilBERT Model Overview</CardTitle>
|
||||
<CardDescription>
|
||||
Story Point estimation is done using a DistilBERT base. DistilBERT is a compact and efficient version of BERT, retaining 97% of BERT's language understanding capabilities while being 40% smaller and 60% faster.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Dataset Information</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
It consists of 16 csv files from 16 projects collected from 9 open source repositories. The following table shows the list of projects and the repositories where the project was collected from.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 mt-6">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Repository Distribution</h4>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="h-64">
|
||||
<Bar data={datasetChartData} options={chartOptions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">Model Architecture</h4>
|
||||
<div className="bg-gray-100 p-4 rounded-lg">
|
||||
<img
|
||||
src="https://www.researchgate.net/publication/358239462/figure/fig2/AS:1120931644747777@1644262338087/The-DistilBERT-model-architecture-and-components.png"
|
||||
alt="DistilBERT Architecture"
|
||||
className="w-full h-64 object-contain"
|
||||
/>
|
||||
<p className="text-xs text-center mt-2 text-gray-500">
|
||||
DistilBERT architecture visualization showing the transformer layers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||
|
||||
const MetricsAndMatrix = () => {
|
||||
const accuracyImprovement = [0.35, 0.42, 0.51, 0.6, 0.63, 0.74, 0.8, 0.84, 0.841, 0.87, 0.88, 0.88, 0.89, 0.898, 0.9];
|
||||
|
||||
const metricsData = {
|
||||
labels: performanceMetrics.map((metric) => metric.name),
|
||||
datasets: [
|
||||
{
|
||||
label: "Performance Metrics",
|
||||
data: performanceMetrics.map((metric) => metric.value),
|
||||
backgroundColor: performanceMetrics.map((metric) => metric.color),
|
||||
borderColor: performanceMetrics.map((metric) => metric.borderColor),
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const metricsOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => `${context.dataset.label}: ${(context.raw * 100).toFixed(2)}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 1,
|
||||
ticks: {
|
||||
callback: (value) => `${(value * 100).toFixed(0)}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const lrImprovementData = {
|
||||
labels: Array.from({ length: accuracyImprovement.length }, (_, i) => `Epoch ${i + 1}`),
|
||||
datasets: [
|
||||
{
|
||||
label: "Accuracy Improvement",
|
||||
data: accuracyImprovement,
|
||||
fill: false,
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
tension: 0.1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const lrImprovementOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => `Accuracy: ${(context.raw * 100).toFixed(2)}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 1,
|
||||
ticks: {
|
||||
callback: (value) => `${(value * 100).toFixed(0)}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Model Performance Analysis</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Learning Rate Improvement</h3>
|
||||
<div className="h-80">
|
||||
<Line data={lrImprovementData} options={lrImprovementOptions} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-medium">Performance Metrics</h3>
|
||||
<div className="h-80">
|
||||
<Bar data={metricsData} options={metricsOptions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-gray-600">
|
||||
<p>* Hover over the charts to see detailed metrics and confusion matrix values.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const EstimatorDetailsPopup = () => {
|
||||
return (
|
||||
<div className="space-y-6 p-4">
|
||||
<ModelDescription />
|
||||
<MetricsAndMatrix />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EstimatorDetailsPopup;
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
'use client'
|
||||
|
||||
import { SetStateAction, useContext, useEffect, useState } from "react"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { ArrowLeft, ChevronDown, HelpCircle, MoreVertical, Plus, Zap } from 'lucide-react'
|
||||
import { Utensils, UtensilsCrossed } from 'lucide-react'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import CrossingKnives from "./AnimationKnives/CrossingKnives"
|
||||
import EstimationPopup from "./EstimationPopup"
|
||||
import DetailsPopup from "./EstimatorDetailsPopup"
|
||||
import { DialogTitle } from "@radix-ui/react-dialog"
|
||||
import { send } from "process"
|
||||
|
||||
import { AppContext } from "@/context/AppContext"
|
||||
import EstimatorDetailsPopup from "./EstimatorDetailsPopup"
|
||||
|
||||
const ForkIcon = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2" />
|
||||
<path d="M7 2v20" />
|
||||
</svg>
|
||||
)
|
||||
// #TODO - this is the low priority ^
|
||||
|
||||
export default function Component() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [title, setTitle] = useState("")
|
||||
const [description, setDescription] = useState("")
|
||||
const [comments, setComments] = useState("")
|
||||
const [showAnimation, setShowAnimation] = useState(false)
|
||||
const [assignee, setAssignee] = useState("Unassigned")
|
||||
const [reporter, setReporter] = useState("Unassigned")
|
||||
const [reporterId, setReporterId] = useState(0)
|
||||
const [team, setTeam] = useState("Unassigned")
|
||||
const [shift, setShift] = useState<{ sprintId: number; name: string } | null>(null);
|
||||
const [storyPlates, setStoryPlates] = useState(0)
|
||||
const [parent, setParent] = useState("None")
|
||||
const [parentId, setParentId] = useState(0)
|
||||
const [priority, setPriority] = useState("Select priority")
|
||||
const [status, setStatus] = useState("To Do")
|
||||
|
||||
const [openEstimateDialog, setOpenEstimateDialog] = useState(false);
|
||||
const [openHelpDialog, setOpenHelpDialog] = useState(false);
|
||||
|
||||
|
||||
const { tickets, fetchTickets, addTicket, shifts, fetchShifts } = useContext(AppContext) as any
|
||||
|
||||
|
||||
const handlePriorityChange = (value: string) => setPriority(value)
|
||||
const handleStatusChange = (value: string) => setStatus(value)
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "Cooking":
|
||||
return "bg-[#F7FFA6] text-black"
|
||||
case "In Plating":
|
||||
return "bg-[#FFB992] text-black"
|
||||
case "Bon appétit":
|
||||
return "bg-[#FF8E8E] text-black"
|
||||
case "To Do":
|
||||
default:
|
||||
return "bg-[#C1FFB8] text-black"
|
||||
}
|
||||
}
|
||||
|
||||
const getPriorityIcon = (value: string) => {
|
||||
switch (value) {
|
||||
case "High":
|
||||
return <UtensilsCrossed className="h-4 w-4" />
|
||||
case "Medium":
|
||||
return <Utensils className="h-4 w-4" />
|
||||
case "Low":
|
||||
return <ForkIcon />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const priorityToNumber = (priority: string) => {
|
||||
switch (priority) {
|
||||
case "High":
|
||||
return 3
|
||||
case "Medium":
|
||||
return 2
|
||||
case "Low":
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async () => {
|
||||
await fetchShifts();
|
||||
}
|
||||
fetch();
|
||||
console.log("Test-------------------------------", shifts);
|
||||
}, [])
|
||||
|
||||
|
||||
const [teamName, setTeamName] = useState("Team AB")
|
||||
const [projectName, setProjectName] = useState("Project X")
|
||||
const [featureName, setFeatureName] = useState("Feature Y")
|
||||
const [epicName, setEpicName] = useState("Epic Z")
|
||||
const [ticketName, setTicketName] = useState("Ticket #123")
|
||||
|
||||
const handleInputChange = (setter: (value: SetStateAction<string>) => void) => (e: { target: { value: any } }) => setter(e.target.value)
|
||||
const handleNumberChange = (setter: (value: SetStateAction<number>) => void) => (e: { target: { value: any } }) => {
|
||||
const value = Number(e.target.value)
|
||||
setter(value > 5 ? 5 : value)
|
||||
}
|
||||
|
||||
const handleTeamChange = (e: { target: { value: SetStateAction<string> } }) => {
|
||||
setTeam(e.target.value)
|
||||
}
|
||||
|
||||
const handleParentChange = (e: { target: { value: SetStateAction<string> } }) => {
|
||||
setParent(e.target.value)
|
||||
}
|
||||
|
||||
const sendTicketCreationRequest = () => {
|
||||
const data = {
|
||||
title: title,
|
||||
description: description,
|
||||
parentId: parentId,
|
||||
sprintId: shift?.sprintId,
|
||||
createdBy: reporterId,
|
||||
priority: priorityToNumber(priority),
|
||||
status: status,
|
||||
storyPoints: storyPlates,
|
||||
};
|
||||
addTicket(data);
|
||||
}
|
||||
|
||||
const handleTicketCreation = () => {
|
||||
sendTicketCreationRequest()
|
||||
setShowAnimation(true)
|
||||
setTimeout(() => {
|
||||
setShowAnimation(false)
|
||||
setOpen(false)
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handleExitEstimate = () => {
|
||||
setOpenEstimateDialog(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`p-4 ${showAnimation ? "opacity-10" : ""}`}>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Create Ticket
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl p-0 [&>button]:hidden">
|
||||
<DialogHeader className="p-4 bg-[#F8F9FA] border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => setOpen(false)} className="h-8 w-8 bg-[#212529] text-white rounded-md hover:bg-[#343A40]">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<span>{teamName} / {projectName} / {featureName} / {epicName} / {ticketName}</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 bg-[#212529] text-white rounded-md hover:bg-[#343A40]">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="p-6">
|
||||
<Input
|
||||
className="text-xl font-semibold border-0 px-0 mb-4"
|
||||
placeholder="Input title of ticket"
|
||||
value={title}
|
||||
onChange={handleInputChange(setTitle)}
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex gap-2">
|
||||
<Dialog open={openEstimateDialog} onOpenChange={setOpenEstimateDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" size="sm" className="bg-[#F1F3F5] hover:bg-[#E9ECEF] text-gray-700">
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
<Zap className="mr-2 h-3 w-3" />
|
||||
<DialogTitle>
|
||||
Estimate with AI
|
||||
</DialogTitle>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="p-4">
|
||||
<EstimationPopup title={title} description={description} handleExit={handleExitEstimate} setStoryPlates={setStoryPlates} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={openHelpDialog} onOpenChange={setOpenHelpDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" size="sm" className="bg-[#F1F3F5] hover:bg-[#E9ECEF] text-gray-700">
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="p-4 w-full h-full max-w-none max-h-none overflow-auto">
|
||||
<div className="w-full h-full">
|
||||
<EstimatorDetailsPopup />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
<Button variant="secondary" size="sm" className="bg-[#F1F3F5] hover:bg-[#E9ECEF] text-gray-700">
|
||||
Planning Poker
|
||||
</Button>
|
||||
</div>
|
||||
<Select value={status} onValueChange={handleStatusChange}>
|
||||
<SelectTrigger className={`w-[200px] ${getStatusColor(status)} border-0`}>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="To Do">To Do</SelectItem>
|
||||
<SelectItem value="Cooking">Cooking</SelectItem>
|
||||
<SelectItem value="In Plating">In Plating</SelectItem>
|
||||
<SelectItem value="Bon appétit">Bon appétit</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr,300px] gap-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="description" className="text-base font-semibold">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
className="mt-2 min-h-[200px]"
|
||||
placeholder="Add description here"
|
||||
value={description}
|
||||
onChange={handleInputChange(setDescription)}
|
||||
/>
|
||||
</div>
|
||||
{/* <div>
|
||||
<Label htmlFor="comments" className="text-base font-semibold">Comments</Label>
|
||||
<Label htmlFor="comments" className="block text-sm text-gray-500 mt-1">Your message</Label>
|
||||
<Textarea
|
||||
id="comments"
|
||||
className="mt-1"
|
||||
placeholder="Type your message here"
|
||||
value={comments}
|
||||
onChange={handleInputChange(setComments)}
|
||||
/>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="bg-[#F8F9FA] rounded-lg p-4">
|
||||
<h3 className="font-semibold mb-4">Details</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-sm">Assignee</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarFallback>UN</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm text-gray-600">{assignee}</span>
|
||||
<Button variant="ghost" size="sm" className="ml-auto h-6 w-6 p-0">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-sm">Reporter</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarFallback>UN</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm text-gray-600">{reporter}</span>
|
||||
<Button variant="ghost" size="sm" className="ml-auto h-6 w-6 p-0">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-sm">Team</Label>
|
||||
<Input
|
||||
value={team}
|
||||
onChange={handleTeamChange}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-sm">Shift</Label>
|
||||
<Select
|
||||
value={shift?.name}
|
||||
onValueChange={(selectedName) => {
|
||||
const selectedShift = shifts.find((s: { name: string }) => s.name === selectedName);
|
||||
if (selectedShift) setShift(selectedShift);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full h-8">
|
||||
<SelectValue placeholder="Select Shift" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{shifts?.map((shiftItem: { sprintId: number; name: string }) => (
|
||||
<SelectItem key={shiftItem.sprintId} value={shiftItem.name}>
|
||||
{shiftItem.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-sm">Story Plates</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={storyPlates}
|
||||
onChange={handleNumberChange(setStoryPlates)}
|
||||
max={5}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-sm">Parent</Label>
|
||||
<Input
|
||||
value={parent}
|
||||
onChange={handleParentChange}
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label htmlFor="priority-select" className="text-sm">Priority</Label>
|
||||
<Select value={priority} onValueChange={handlePriorityChange}>
|
||||
<SelectTrigger id="priority-select" className="w-full h-8">
|
||||
<SelectValue>
|
||||
{priority !== "Select priority" && getPriorityIcon(priority)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Select priority" disabled>Select priority</SelectItem>
|
||||
<SelectItem value="High">
|
||||
<div className="flex items-center">
|
||||
<UtensilsCrossed className="mr-2 h-4 w-4" />
|
||||
<span>High</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="Medium">
|
||||
<div className="flex items-center">
|
||||
<Utensils className="mr-2 h-4 w-4" />
|
||||
<span>Medium</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="Low">
|
||||
<div className="flex items-center">
|
||||
<ForkIcon />
|
||||
<span className="ml-2">Low</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
<Button variant="outline" onClick={handleCancel}>Cancel</Button>
|
||||
<Button onClick={handleTicketCreation}>Create Ticket</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
{showAnimation && <CrossingKnives />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
+673
@@ -0,0 +1,673 @@
|
||||
import { useContext, useEffect, useState } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { ChevronLeft, ChevronUp, Zap, Trash } from "lucide-react"
|
||||
|
||||
import { AppContext } from "@/context/AppContext"
|
||||
import { Context } from "vm"
|
||||
|
||||
export default function TicketView({ id, handleClose }: { id: number, handleClose: () => void }) {
|
||||
const [ticketTitle, setTicketTitle] = useState("BLA BLA BLA BLA BLA BLA bla bla bla bal bal abl")
|
||||
const [ticketDescription, setTicketDescription] = useState("This is an example of a ticket description. This is an example of a ticket description. This is an example of a ticket description.")
|
||||
const [comment, setComment] = useState("")
|
||||
const [assignee, setAssignee] = useState("Ion Ionescu")
|
||||
const [reporter, setReporter] = useState("Mihai Eminescu")
|
||||
const [team, setTeam] = useState("NotAtlassian")
|
||||
const [shift, setShift] = useState(0)
|
||||
const [storyPlates, setStoryPlates] = useState(5)
|
||||
const [parent, setParent] = useState("blzbalbal bnlab")
|
||||
const [priority, setPriority] = useState("Select priority")
|
||||
const [status, setStatus] = useState("To Do")
|
||||
|
||||
const [teamName, setTeamName] = useState("Team A")
|
||||
const [projectName, setProjectName] = useState("Project X")
|
||||
const [featureName, setFeatureName] = useState("Feature Y")
|
||||
const [epicName, setEpicName] = useState("Epic Z")
|
||||
const [ticketName, setTicketName] = useState("Ticket #123")
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isDeleteDialogOpen2, setIsDeleteDialogOpen2] = useState(false);
|
||||
const [isDeleteDialogOpen3, setIsDeleteDialogOpen3] = useState(false);
|
||||
|
||||
const context = useContext(AppContext);
|
||||
const { removeTicket, fetchTicket, ticket } = context as Context;
|
||||
|
||||
const [dialogPosition2, setDialogPosition2] = useState({ top: 0, left: 0 });
|
||||
const [dialogPosition3, setDialogPosition3] = useState({ top: 0, left: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
setDialogPosition2(generateRandomPosition());
|
||||
setDialogPosition3(generateRandomPosition());
|
||||
}, []);
|
||||
|
||||
const generateRandomPosition = () => {
|
||||
const top = Math.floor(Math.random() * (window.innerHeight - 200));
|
||||
const left = Math.floor(Math.random() * (window.innerWidth - 300));
|
||||
return { top, left };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!ticket || ticket.id !== id)
|
||||
await fetchTicket(id);
|
||||
};
|
||||
fetchData();
|
||||
}, [id, fetchTicket])
|
||||
|
||||
useEffect(() => {
|
||||
if (ticket) {
|
||||
setTicketTitle(ticket.title);
|
||||
setTicketDescription(ticket.description);
|
||||
setStoryPlates(ticket.storyPoints);
|
||||
setStatus(ticket.status);
|
||||
setShift(ticket.sprintId)
|
||||
} else {
|
||||
console.log("Ticket not found");
|
||||
}
|
||||
}, [ticket]);
|
||||
|
||||
const handleDeleteTicket = () => {
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
// Perform the delete action here
|
||||
|
||||
setIsDeleteDialogOpen(false);
|
||||
handleDeleteTicket2();
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
setIsDeleteDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteTicket2 = () => {
|
||||
setIsDeleteDialogOpen2(true);
|
||||
};
|
||||
|
||||
const confirmDelete2 = () => {
|
||||
// Perform the delete action here
|
||||
|
||||
setIsDeleteDialogOpen2(false);
|
||||
handleDeleteTicket3();
|
||||
|
||||
};
|
||||
|
||||
const cancelDelete2 = () => {
|
||||
setIsDeleteDialogOpen2(false);
|
||||
};
|
||||
|
||||
const handleDeleteTicket3 = () => {
|
||||
setIsDeleteDialogOpen3(true);
|
||||
};
|
||||
|
||||
const confirmDelete3 = () => {
|
||||
// Perform the delete action here
|
||||
console.log("Ticket deleted");
|
||||
removeTicket(id);
|
||||
setIsDeleteDialogOpen3(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const cancelDelete3 = () => {
|
||||
setIsDeleteDialogOpen3(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA NIGGA
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const handleEstimateAI = () => {
|
||||
console.log("Estimate with AI clicked")
|
||||
}
|
||||
|
||||
const handlePlanningPoker = () => {
|
||||
console.log("Planning Poker clicked")
|
||||
}
|
||||
|
||||
const statusColorClass = (status: string) => {
|
||||
switch (status) {
|
||||
case "To Do":
|
||||
return "bg-yellow-200 text-yellow-800";
|
||||
case "Cooking":
|
||||
return "bg-blue-200 text-blue-800";
|
||||
case "In Plating":
|
||||
return "bg-green-200 text-green-800";
|
||||
case "Bon appétit":
|
||||
return "bg-red-200 text-red-800";
|
||||
default:
|
||||
return "bg-gray-200 text-gray-800";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog defaultOpen onOpenChange={(open) => { if (!open) handleClose() }}>
|
||||
<DialogContent className="max-w-4xl p-0 [&>button]:hidden">
|
||||
<DialogHeader className="p-4 bg-[#F8F9FA]">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 bg-[#212529] text-white rounded-md hover:bg-[#343A40]" onClick={handleClose}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<span>{teamName} / {projectName} / {featureName} / {epicName} / {ticketName}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto h-8 w-8 bg-[#212529] text-white rounded-md hover:bg-[#343A40]"
|
||||
onClick={handleDeleteTicket}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-4 pt-2">
|
||||
<DialogTitle className="text-xl font-semibold leading-relaxed">
|
||||
Title of Ticket: {ticketTitle}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="gap-1 bg-[#F1F3F5] text-gray-700 hover:bg-[#E9ECEF]"
|
||||
onClick={handleEstimateAI}
|
||||
>
|
||||
<Zap className="h-3 w-3" />
|
||||
Estimate with AI
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-[#F1F3F5] text-gray-700 hover:bg-[#E9ECEF]"
|
||||
onClick={handlePlanningPoker}
|
||||
>
|
||||
Planning Poker
|
||||
</Button>
|
||||
<div className="ml-auto">
|
||||
<Badge variant="outline" className={statusColorClass(status)}>
|
||||
{status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="grid md:grid-cols-[1fr,300px]">
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">Description</h3>
|
||||
<div className="mt-2 rounded-lg bg-[#F8F9FA] p-4">
|
||||
<p className="text-gray-600">{ticketDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">Comments</h3>
|
||||
<div className="mt-2">
|
||||
<Label htmlFor="comment" className="text-base">Your message</Label>
|
||||
<Textarea
|
||||
id="comment"
|
||||
className="mt-1"
|
||||
placeholder="Type your message here"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-l border-gray-200">
|
||||
<div className="p-4">
|
||||
<h3 className="text-xl font-semibold">Details</h3>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-base">Assignee</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage alt={assignee} />
|
||||
<AvatarFallback>{assignee.slice(0, 2).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{assignee}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-base">Reporter</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage alt={reporter} />
|
||||
<AvatarFallback>{reporter.slice(0, 2).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{reporter}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-base">Team</Label>
|
||||
<span>{team}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-base">Shift</Label>
|
||||
<span className="text-gray-600">{shift}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-base">Story Plates</Label>
|
||||
<span className="text-gray-600">{storyPlates}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-base">Parent</Label>
|
||||
<span>{parent}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[100px,1fr] items-center gap-2">
|
||||
<Label className="text-base">Priority</Label>
|
||||
<span>{priority}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
{isDeleteDialogOpen && (
|
||||
<Dialog defaultOpen onOpenChange={(open) => { if (!open) cancelDelete() }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirm Delete</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={cancelDelete}>Cancel</Button>
|
||||
<Button variant="destructive" onClick={confirmDelete}>Delete</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{isDeleteDialogOpen2 && (
|
||||
<Dialog defaultOpen onOpenChange={(open) => { if (!open) cancelDelete2() }}>
|
||||
<DialogContent style={{ position: 'absolute', top: dialogPosition2.top, left: dialogPosition2.left }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you really sure ?</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={confirmDelete2}>Delete</Button>
|
||||
<Button variant="destructive" onClick={cancelDelete2}>Cancel</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{isDeleteDialogOpen3 && (
|
||||
<Dialog defaultOpen onOpenChange={(open) => { if (!open) cancelDelete3() }}>
|
||||
<DialogContent style={{ position: 'absolute', top: dialogPosition3.top, left: dialogPosition3.left }}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you really, really, reeeeaaally sure ?</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="default" onClick={confirmDelete3}>Delete</Button>
|
||||
<Button variant="default" onClick={cancelDelete3}>Cancel</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,66 @@
|
||||
export default {
|
||||
darkMode: ["class"],
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [import("tailwindcss-animate")],
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import path from "path"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user