Anul 3 Semestrul 1

This commit is contained in:
2025-02-06 20:33:26 +02:00
parent 0b130ee18c
commit 184f3bd92e
313 changed files with 348499 additions and 0 deletions
@@ -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>
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

@@ -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>
)
}
@@ -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;
@@ -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

@@ -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
@@ -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>
)
}
@@ -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>
);
}
@@ -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);
}
@@ -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;
@@ -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: ""; */
}
@@ -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;
@@ -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
@@ -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;
}
@@ -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;
@@ -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;
}
@@ -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
@@ -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 */
}
@@ -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;
@@ -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 }
@@ -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 }
@@ -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 }
@@ -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 }
@@ -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,
}
@@ -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,
}
@@ -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 }
@@ -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 }
@@ -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 }
@@ -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,
}
@@ -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 }
@@ -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,
}
@@ -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,
}
@@ -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 }
@@ -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,
}
@@ -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 }
@@ -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 }
@@ -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
@@ -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 */
}
}
@@ -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;
@@ -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%;
}
}
@@ -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;
@@ -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;
@@ -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 />}
</>
)
}
@@ -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