first commit
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Node modules
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Mac / Linux / Windows system files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary
|
||||||
|
*.tmp
|
||||||
11
README.md
Normal file
11
README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
# CityCards Partner Web Make
|
||||||
|
|
||||||
|
This is a code bundle for CityCards Partner Web Make. The original project is available at https://www.figma.com/design/J1K6yloEjg1b802fwMUmV1/CityCards-Partner-Web-Make.
|
||||||
|
|
||||||
|
## Running the code
|
||||||
|
|
||||||
|
Run `npm i` to install the dependencies.
|
||||||
|
|
||||||
|
Run `npm run dev` to start the development server.
|
||||||
|
|
||||||
15
index.html
Normal file
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>CityCards Partner Web Make</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
4248
package-lock.json
generated
Normal file
4248
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
64
package.json
Normal file
64
package.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name": "CityCards Partner Web Make",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.3",
|
||||||
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
|
"@radix-ui/react-collapsible": "^1.1.3",
|
||||||
|
"@radix-ui/react-context-menu": "^2.2.6",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.6",
|
||||||
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
|
"@radix-ui/react-menubar": "^1.1.6",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||||
|
"@radix-ui/react-popover": "^1.1.6",
|
||||||
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
|
"@radix-ui/react-radio-group": "^1.2.3",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
|
"@radix-ui/react-separator": "^1.1.2",
|
||||||
|
"@radix-ui/react-slider": "^1.2.3",
|
||||||
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-switch": "^1.1.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.2",
|
||||||
|
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
|
"@tailwindcss/postcss": "^4.1.13",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "*",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"lucide-react": "^0.487.0",
|
||||||
|
"motion": "*",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-day-picker": "^8.10.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.55.0",
|
||||||
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"recharts": "^2.15.2",
|
||||||
|
"sonner": "^2.0.3",
|
||||||
|
"tailwind-merge": "*",
|
||||||
|
"tailwindcss": "^4.1.13",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"@types/react": "^19.1.13",
|
||||||
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
|
"vite": "6.3.5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
postcss.config.mjs
Normal file
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
}
|
||||||
|
}
|
||||||
458
src/App.tsx
Normal file
458
src/App.tsx
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
import { Button } from "./components/ui/button";
|
||||||
|
import { Input } from "./components/ui/input";
|
||||||
|
import { Label } from "./components/ui/label";
|
||||||
|
import { Checkbox } from "./components/ui/checkbox";
|
||||||
|
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./components/ui/input-otp";
|
||||||
|
import { Mail, Lock, Eye, EyeOff, Search, Users, Calendar, TrendingUp, Clock, User } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import FlightBookingRafiki from "./imports/FlightBookingRafiki";
|
||||||
|
import Sidebar from "./components/Sidebar";
|
||||||
|
import Header from "./components/Header";
|
||||||
|
import Dashboard from "./components/Dashboard";
|
||||||
|
import RedemptionsPage from "./components/RedemptionsPage";
|
||||||
|
import StaffManagementPage from "./components/StaffManagementPage";
|
||||||
|
import SupportPage from "./components/SupportPage";
|
||||||
|
import BookingManagementPage from "./components/BookingManagementPage";
|
||||||
|
import RecurringBlockPage from "./components/RecurringBlockPage";
|
||||||
|
import NotificationsPage from "./components/NotificationsPage";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
||||||
|
const [showOTPVerification, setShowOTPVerification] = useState(false);
|
||||||
|
const [showResetPassword, setShowResetPassword] = useState(false);
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [otpValue, setOtpValue] = useState("");
|
||||||
|
const [newPassword, setNewPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [showPasswordPolicies, setShowPasswordPolicies] = useState(false);
|
||||||
|
const [activeNavItem, setActiveNavItem] = useState("dashboard");
|
||||||
|
|
||||||
|
// Password validation functions
|
||||||
|
const validatePassword = (password) => {
|
||||||
|
return {
|
||||||
|
hasMinLength: password.length >= 8,
|
||||||
|
hasUppercase: /[A-Z]/.test(password),
|
||||||
|
hasLowercase: /[a-z]/.test(password),
|
||||||
|
hasNumber: /[0-9]/.test(password),
|
||||||
|
hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/.test(password)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordValidation = validatePassword(newPassword);
|
||||||
|
|
||||||
|
// If user is logged in, show dashboard
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Fixed Sidebar */}
|
||||||
|
<Sidebar
|
||||||
|
activeItem={activeNavItem}
|
||||||
|
onItemSelect={setActiveNavItem}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main Content with left margin for sidebar */}
|
||||||
|
<div className="ml-[280px]">
|
||||||
|
{/* Header with notifications and profile */}
|
||||||
|
<Header onNavigateToNotifications={() => setActiveNavItem("notifications")} />
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
{activeNavItem === "dashboard" && <Dashboard />}
|
||||||
|
{(activeNavItem === "booking-table" || activeNavItem === "booking-calendar") && (
|
||||||
|
<BookingManagementPage
|
||||||
|
activeView={activeNavItem}
|
||||||
|
onNavigateToRecurringBlock={() => setActiveNavItem("recurring-block")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeNavItem === "recurring-block" && (
|
||||||
|
<RecurringBlockPage onNavigateBack={() => setActiveNavItem("booking-calendar")} />
|
||||||
|
)}
|
||||||
|
{activeNavItem === "redemptions" && <RedemptionsPage />}
|
||||||
|
{activeNavItem === "staff" && <StaffManagementPage />}
|
||||||
|
{activeNavItem === "support" && <SupportPage />}
|
||||||
|
{activeNavItem === "notifications" && <NotificationsPage />}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex">
|
||||||
|
{/* Left Panel - Welcome Section */}
|
||||||
|
<div className="flex-1 bg-gray-50 flex flex-col items-center justify-center p-12">
|
||||||
|
<div className="max-w-md text-center">
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="w-full h-64 mb-8 flex items-center justify-center">
|
||||||
|
<FlightBookingRafiki />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-gray-600 text-lg">Welcome to</p>
|
||||||
|
<h1 className="text-4xl text-gray-900 font-medium">CityCards</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel - Login Section */}
|
||||||
|
<div className="flex-1 bg-white flex items-center justify-center p-12">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{!showForgotPassword && !showOTPVerification && !showResetPassword ? (
|
||||||
|
<motion.div
|
||||||
|
key="signin"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-3xl text-gray-900 font-bold">Sign In to Access Your Account</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="space-y-6">
|
||||||
|
{/* Email Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="text-base font-medium text-gray-900">Email</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter your email address"
|
||||||
|
className="pl-10 bg-white border border-gray-200 rounded-lg h-12 font-normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password" className="text-base font-medium text-gray-900">Password</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
className="pl-10 pr-10 bg-white border border-gray-200 rounded-lg h-12 font-normal"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Remember Me and Forgot Password */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="remember"
|
||||||
|
checked={rememberMe}
|
||||||
|
onCheckedChange={setRememberMe}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="remember" className="text-sm font-normal text-gray-600">
|
||||||
|
Remember me
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowForgotPassword(true)}
|
||||||
|
className="text-sm font-normal text-blue-600 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
Forgot Password?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sign In Button */}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
}}
|
||||||
|
className="w-full h-12 bg-gray-900 hover:bg-gray-800 text-white rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
) : showForgotPassword && !showOTPVerification && !showResetPassword ? (
|
||||||
|
<motion.div
|
||||||
|
key="forgot"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">Forgot Password</h1>
|
||||||
|
<p className="text-base font-normal text-gray-600 leading-relaxed">
|
||||||
|
Forgot your password? Don't worry — just enter your email and we'll help you reset it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="space-y-6">
|
||||||
|
{/* Email Field */}
|
||||||
|
<div className="space-y-2 text-left">
|
||||||
|
<Label htmlFor="forgot-email" className="text-base font-medium text-gray-900">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="forgot-email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter your email address"
|
||||||
|
className="pl-10 bg-gray-50 border border-gray-200 rounded-lg h-12 font-normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Continue Button */}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowOTPVerification(true);
|
||||||
|
}}
|
||||||
|
className="w-full h-12 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium mt-8"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Back to Sign In */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowForgotPassword(false)}
|
||||||
|
className="text-sm font-normal text-blue-600 hover:text-blue-700 mt-6"
|
||||||
|
>
|
||||||
|
Back to Sign In
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
) : showOTPVerification && !showResetPassword ? (
|
||||||
|
<motion.div
|
||||||
|
key="otp"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">Verify OTP</h1>
|
||||||
|
<p className="text-base font-normal text-gray-600 leading-relaxed">
|
||||||
|
We've sent an OTP to your registered email. Please enter it below, or use the reset link included in the email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="space-y-6">
|
||||||
|
{/* OTP Input */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<InputOTP
|
||||||
|
maxLength={4}
|
||||||
|
value={otpValue}
|
||||||
|
onChange={(value) => setOtpValue(value)}
|
||||||
|
className="gap-4"
|
||||||
|
>
|
||||||
|
<InputOTPGroup className="gap-4">
|
||||||
|
<InputOTPSlot
|
||||||
|
index={0}
|
||||||
|
className="w-12 h-12 text-lg font-medium border-2 border-gray-200 rounded-lg bg-white focus:border-gray-400 focus:ring-0"
|
||||||
|
/>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={1}
|
||||||
|
className="w-12 h-12 text-lg font-medium border-2 border-gray-200 rounded-lg bg-white focus:border-gray-400 focus:ring-0"
|
||||||
|
/>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={2}
|
||||||
|
className="w-12 h-12 text-lg font-medium border-2 border-gray-200 rounded-lg bg-white focus:border-gray-400 focus:ring-0"
|
||||||
|
/>
|
||||||
|
<InputOTPSlot
|
||||||
|
index={3}
|
||||||
|
className="w-12 h-12 text-lg font-medium border-2 border-gray-200 rounded-lg bg-white focus:border-gray-400 focus:ring-0"
|
||||||
|
/>
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Continue Button */}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowResetPassword(true);
|
||||||
|
}}
|
||||||
|
className="w-full h-12 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium mt-8"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Back to Forgot Password */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowOTPVerification(false)}
|
||||||
|
className="text-sm font-normal text-blue-600 hover:text-blue-700 mt-6"
|
||||||
|
>
|
||||||
|
Back to Forgot Password
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="reset"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">Reset Password</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="space-y-6">
|
||||||
|
{/* New Password Field */}
|
||||||
|
<div className="space-y-2 text-left">
|
||||||
|
<Label htmlFor="new-password" className="text-base font-medium text-gray-900">
|
||||||
|
New password
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="new-password"
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
placeholder="Enter your new password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
onFocus={() => setShowPasswordPolicies(true)}
|
||||||
|
className="pl-10 pr-10 bg-gray-50 border border-gray-200 rounded-lg h-12 font-normal"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password Policies */}
|
||||||
|
{showPasswordPolicies && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="text-left bg-gray-50 p-4 rounded-lg space-y-2"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className={`flex items-center text-sm ${passwordValidation.hasMinLength ? 'text-green-600' : 'text-gray-600'}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasMinLength ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
||||||
|
Contains at least 8 characters
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-center text-sm ${passwordValidation.hasUppercase ? 'text-green-600' : 'text-gray-600'}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasUppercase ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
||||||
|
At least one uppercase letter (A-Z)
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-center text-sm ${passwordValidation.hasLowercase ? 'text-green-600' : 'text-gray-600'}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasLowercase ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
||||||
|
At least one lowercase letter (a-z)
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-center text-sm ${passwordValidation.hasNumber ? 'text-green-600' : 'text-gray-600'}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasNumber ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
||||||
|
At least one number (0-9)
|
||||||
|
</div>
|
||||||
|
<div className={`flex items-center text-sm ${passwordValidation.hasSpecialChar ? 'text-green-600' : 'text-gray-600'}`}>
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full mr-2 ${passwordValidation.hasSpecialChar ? 'bg-green-600' : 'bg-gray-400'}`}></div>
|
||||||
|
At least one special character (e.g., !, @, #, $, %, ^, &, *)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Confirm Password Field */}
|
||||||
|
<div className="space-y-2 text-left">
|
||||||
|
<Label htmlFor="confirm-password" className="text-base font-medium text-gray-900">
|
||||||
|
Confirm Password
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="confirm-password"
|
||||||
|
type={showConfirmPassword ? "text" : "password"}
|
||||||
|
placeholder="Confirm your new password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="pl-10 pr-10 bg-gray-50 border border-gray-200 rounded-lg h-12 font-normal"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? (
|
||||||
|
<EyeOff className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{confirmPassword && newPassword !== confirmPassword && (
|
||||||
|
<p className="text-sm text-red-600">Passwords do not match</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Continue Button */}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!Object.values(passwordValidation).every(Boolean) || newPassword !== confirmPassword}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (Object.values(passwordValidation).every(Boolean) && newPassword === confirmPassword) {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full h-12 bg-gray-600 hover:bg-gray-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg font-medium mt-8"
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Back to OTP */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowResetPassword(false)}
|
||||||
|
className="text-sm font-normal text-blue-600 hover:text-blue-700 mt-6"
|
||||||
|
>
|
||||||
|
Back to OTP Verification
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
src/Attributions.md
Normal file
3
src/Attributions.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
|
||||||
|
|
||||||
|
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
|
||||||
BIN
src/assets/fc17f89f308e91a12011ae1dc4ea7b32cda951d8.png
Normal file
BIN
src/assets/fc17f89f308e91a12011ae1dc4ea7b32cda951d8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/fe850ec33f7da20cadeecc91695cda7ad837415e.png
Normal file
BIN
src/assets/fe850ec33f7da20cadeecc91695cda7ad837415e.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
272
src/components/BookingManagementPage.tsx
Normal file
272
src/components/BookingManagementPage.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Search, Download, Filter, MoreHorizontal, Eye, Trash2 } from "lucide-react";
|
||||||
|
import CalendarView from "./CalendarView";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "./ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "./ui/table";
|
||||||
|
|
||||||
|
interface BookingManagementPageProps {
|
||||||
|
activeView: string;
|
||||||
|
onNavigateToRecurringBlock?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BookingManagementPage({ activeView, onNavigateToRecurringBlock }: BookingManagementPageProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
// Sample booking data
|
||||||
|
const bookings = [
|
||||||
|
{
|
||||||
|
id: "A8529479",
|
||||||
|
fullName: "Ruby Williamson",
|
||||||
|
email: "ruby-williamson@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529479",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Three Pins Adventure",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529478",
|
||||||
|
fullName: "Joshua Thompson",
|
||||||
|
email: "joshua.thompson@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529478",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Digital Dreams",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529477",
|
||||||
|
fullName: "Sophia Turner",
|
||||||
|
email: "sophia.turner@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529477",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Crystal Dreams",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529476",
|
||||||
|
fullName: "Billy Johnson",
|
||||||
|
email: "billy.johnson@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529476",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Welcoming Hands",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529475",
|
||||||
|
fullName: "Chris Bernard",
|
||||||
|
email: "chris.bernard@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529475",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Sunrise SMS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529474",
|
||||||
|
fullName: "Zoe Adams",
|
||||||
|
email: "zoe.adams@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529474",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Creative Sandbox",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529473",
|
||||||
|
fullName: "Jane Hartley",
|
||||||
|
email: "jane.hartley@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529473",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Bustling Trends",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529472",
|
||||||
|
fullName: "Michael Lee",
|
||||||
|
email: "michael.lee@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529472",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Gateway Teamwork",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529471",
|
||||||
|
fullName: "Anita Carter",
|
||||||
|
email: "anita.carter@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529471",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Alliance Hype",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529470",
|
||||||
|
fullName: "Laura Johnson",
|
||||||
|
email: "laura.johnson@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529470",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "RASM Solutions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "A8529469",
|
||||||
|
fullName: "Sunita Wilson",
|
||||||
|
email: "sunita.wilson@outlook.com",
|
||||||
|
cardType: "Executive",
|
||||||
|
bookingId: "A8529469",
|
||||||
|
date: "06/10/25 16:30",
|
||||||
|
attendantName: "Creative Dashboard Spas",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Filter bookings based on search query
|
||||||
|
const filteredBookings = bookings.filter(booking =>
|
||||||
|
booking.fullName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
booking.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
booking.attendantName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeView === "booking-calendar") {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||||
|
|
||||||
|
>
|
||||||
|
<CalendarView onNavigateToRecurringBlock={onNavigateToRecurringBlock} />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm text-gray-500">Booking Management {">"} Table View</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-1">Booking Management</h1>
|
||||||
|
<p className="text-base text-gray-600">View and Manage all bookings</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Bar with Buttons */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search bookings..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 bg-white border border-gray-200 rounded-lg h-10 w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button className="flex items-center gap-2 bg-gray-900 hover:bg-gray-800 text-white">
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
Export data
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="flex items-center gap-2">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-gray-50">
|
||||||
|
<TableHead className="text-center font-medium text-gray-900">Full Name</TableHead>
|
||||||
|
<TableHead className="text-center font-medium text-gray-900">Email</TableHead>
|
||||||
|
<TableHead className="text-center font-medium text-gray-900">Card Type</TableHead>
|
||||||
|
<TableHead className="text-center font-medium text-gray-900">Booking ID</TableHead>
|
||||||
|
<TableHead className="text-center font-medium text-gray-900">Date</TableHead>
|
||||||
|
<TableHead className="text-center font-medium text-gray-900">Attendant Name</TableHead>
|
||||||
|
<TableHead className="text-center font-medium text-gray-900">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredBookings.map((booking, index) => (
|
||||||
|
<TableRow key={booking.id} className="hover:bg-gray-50">
|
||||||
|
<TableCell className="text-center text-gray-900">{booking.fullName}</TableCell>
|
||||||
|
<TableCell className="text-center text-gray-600">{booking.email}</TableCell>
|
||||||
|
<TableCell className="text-center text-gray-900">{booking.cardType}</TableCell>
|
||||||
|
<TableCell className="text-center text-gray-900">{booking.bookingId}</TableCell>
|
||||||
|
<TableCell className="text-center text-gray-600">{booking.date}</TableCell>
|
||||||
|
<TableCell className="text-center text-gray-900">{booking.attendantName}</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem className="flex items-center gap-2">
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
View Details
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem className="flex items-center gap-2 text-red-600">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="mt-6 flex items-center justify-between">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Showing 1 to {filteredBookings.length} of {bookings.length} entries
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" disabled>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" className="bg-gray-900 text-white">
|
||||||
|
1
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" disabled>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
369
src/components/CalendarView.tsx
Normal file
369
src/components/CalendarView.tsx
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { ChevronLeft, ChevronRight, Plus } from "lucide-react";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import SlotDetailPanel from "./SlotDetailPanel";
|
||||||
|
|
||||||
|
interface AttractionBlock {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
color: string;
|
||||||
|
timeSlot?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CalendarViewProps {
|
||||||
|
onNavigateToRecurringBlock?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CalendarView({ onNavigateToRecurringBlock }: CalendarViewProps) {
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date(2024, 6)); // July 2024
|
||||||
|
const [selectedView, setSelectedView] = useState("Month");
|
||||||
|
const [hoveredDate, setHoveredDate] = useState<number | null>(null);
|
||||||
|
const [selectedSlot, setSelectedSlot] = useState<any>(null);
|
||||||
|
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||||
|
|
||||||
|
// Sample attraction data
|
||||||
|
const attractions: AttractionBlock[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Ice Dreamland Adventure",
|
||||||
|
startDate: new Date(2024, 6, 6), // July 6
|
||||||
|
endDate: new Date(2024, 6, 8), // July 8
|
||||||
|
color: "bg-blue-500",
|
||||||
|
timeSlot: "2:00pm - 4:00pm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Bright Adventure Tour",
|
||||||
|
startDate: new Date(2024, 6, 10), // July 10
|
||||||
|
endDate: new Date(2024, 6, 11), // July 11
|
||||||
|
color: "bg-teal-500",
|
||||||
|
timeSlot: "10:00am - 12:00pm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "Selle Zelle Experience",
|
||||||
|
startDate: new Date(2024, 6, 11), // July 11
|
||||||
|
endDate: new Date(2024, 6, 11), // July 11
|
||||||
|
color: "bg-purple-500",
|
||||||
|
timeSlot: "3:00pm - 5:00pm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
name: "Ocean Wonders Quest",
|
||||||
|
startDate: new Date(2024, 6, 18), // July 18
|
||||||
|
endDate: new Date(2024, 6, 20), // July 20
|
||||||
|
color: "bg-emerald-500",
|
||||||
|
timeSlot: "1:00pm - 3:00pm"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const monthNames = [
|
||||||
|
"January", "February", "March", "April", "May", "June",
|
||||||
|
"July", "August", "September", "October", "November", "December"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||||
|
|
||||||
|
const getDaysInMonth = (date: Date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const daysInMonth = lastDay.getDate();
|
||||||
|
|
||||||
|
// Get the day of the week for the first day (0 = Sunday, 1 = Monday, etc.)
|
||||||
|
// Convert to Monday = 0, Tuesday = 1, etc.
|
||||||
|
let startingDayOfWeek = (firstDay.getDay() + 6) % 7;
|
||||||
|
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
// Add empty cells for days before the first day of the month
|
||||||
|
for (let i = 0; i < startingDayOfWeek; i++) {
|
||||||
|
days.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all days of the month
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
days.push(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateMonth = (direction: "prev" | "next") => {
|
||||||
|
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + (direction === "next" ? 1 : -1)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToday = (day: number | null) => {
|
||||||
|
if (!day) return false;
|
||||||
|
const today = new Date();
|
||||||
|
return (
|
||||||
|
day === today.getDate() &&
|
||||||
|
currentDate.getMonth() === today.getMonth() &&
|
||||||
|
currentDate.getFullYear() === today.getFullYear()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPastDate = (day: number | null) => {
|
||||||
|
if (!day) return false;
|
||||||
|
const today = new Date();
|
||||||
|
const cellDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
|
||||||
|
return cellDate < today && !isToday(day);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAttractionsForDate = (day: number | null) => {
|
||||||
|
if (!day) return [];
|
||||||
|
|
||||||
|
const cellDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
|
||||||
|
|
||||||
|
return attractions.filter(attraction => {
|
||||||
|
const startDate = new Date(attraction.startDate);
|
||||||
|
const endDate = new Date(attraction.endDate);
|
||||||
|
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
cellDate.setHours(12, 0, 0, 0);
|
||||||
|
|
||||||
|
return cellDate >= startDate && cellDate <= endDate;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBlockPosition = (attraction: AttractionBlock, day: number | null) => {
|
||||||
|
if (!day) return { isStart: false, isEnd: false, isContinuation: false };
|
||||||
|
|
||||||
|
const cellDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day);
|
||||||
|
const startDate = new Date(attraction.startDate);
|
||||||
|
const endDate = new Date(attraction.endDate);
|
||||||
|
|
||||||
|
cellDate.setHours(0, 0, 0, 0);
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
endDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const isStart = cellDate.getTime() === startDate.getTime();
|
||||||
|
const isEnd = cellDate.getTime() === endDate.getTime();
|
||||||
|
const isContinuation = cellDate > startDate && cellDate < endDate;
|
||||||
|
|
||||||
|
return { isStart, isEnd, isContinuation };
|
||||||
|
};
|
||||||
|
|
||||||
|
const days = getDaysInMonth(currentDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-sm text-gray-500">Booking Management {">"} Calendar View</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white border-b border-gray-200 rounded-t-lg p-6 mb-0">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Calendar view</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Bar */}
|
||||||
|
<div className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
|
||||||
|
{/* Month Navigation */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigateMonth("prev")}
|
||||||
|
className="h-8 w-8 p-0 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 min-w-[140px] text-center">
|
||||||
|
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigateMonth("next")}
|
||||||
|
className="h-8 w-8 p-0 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Tabs */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{["Day", "Week", "Month"].map((view) => (
|
||||||
|
<Button
|
||||||
|
key={view}
|
||||||
|
variant={selectedView === view ? "default" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSelectedView(view)}
|
||||||
|
className={`px-4 py-2 ${
|
||||||
|
selectedView === view
|
||||||
|
? "bg-gray-900 text-white hover:bg-gray-800"
|
||||||
|
: "text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{view}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Recurring Block Button */}
|
||||||
|
<Button
|
||||||
|
className="bg-gray-900 hover:bg-gray-800 text-white px-4 py-2"
|
||||||
|
onClick={onNavigateToRecurringBlock}
|
||||||
|
>
|
||||||
|
Add Recurring Block
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar Grid */}
|
||||||
|
<div className="bg-white rounded-b-lg border border-gray-200 border-t-0">
|
||||||
|
{/* Week Headers */}
|
||||||
|
<div className="grid grid-cols-7 border-b border-gray-200">
|
||||||
|
{dayNames.map((day) => (
|
||||||
|
<div
|
||||||
|
key={day}
|
||||||
|
className="p-3 text-center font-medium text-gray-600 border-r border-gray-200 last:border-r-0 bg-gray-50"
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar Days */}
|
||||||
|
<div className="grid grid-cols-7 auto-rows-max">
|
||||||
|
{days.map((day, index) => {
|
||||||
|
const dayAttractions = getAttractionsForDate(day);
|
||||||
|
const cellDate = day ? new Date(currentDate.getFullYear(), currentDate.getMonth(), day) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
className={`
|
||||||
|
border-r border-b border-gray-200 last:border-r-0 p-2 h-32 relative
|
||||||
|
${day ? "hover:bg-gray-50 cursor-pointer" : "bg-gray-25"}
|
||||||
|
${isToday(day) ? "bg-blue-50" : ""}
|
||||||
|
${isPastDate(day) ? "opacity-60" : ""}
|
||||||
|
`}
|
||||||
|
onMouseEnter={() => day && setHoveredDate(day)}
|
||||||
|
onMouseLeave={() => setHoveredDate(null)}
|
||||||
|
whileHover={day ? { scale: 1.01 } : undefined}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
onClick={() => {
|
||||||
|
if (day && dayAttractions.length === 0) {
|
||||||
|
const slotData = {
|
||||||
|
attractionName: "New Attraction",
|
||||||
|
date: new Date(currentDate.getFullYear(), currentDate.getMonth(), day).toISOString().split('T')[0],
|
||||||
|
timeSlots: [],
|
||||||
|
capacity: 0
|
||||||
|
};
|
||||||
|
setSelectedSlot(slotData);
|
||||||
|
setIsPanelOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Date Number */}
|
||||||
|
{day && (
|
||||||
|
<div className={`
|
||||||
|
text-sm font-medium mb-1
|
||||||
|
${isToday(day) ? "text-blue-600 font-semibold" : "text-gray-900"}
|
||||||
|
${isPastDate(day) ? "text-gray-400" : ""}
|
||||||
|
`}>
|
||||||
|
{day}
|
||||||
|
{isToday(day) && (
|
||||||
|
<div className="absolute top-2 left-2 w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center">
|
||||||
|
<span className="text-white text-xs font-medium">{day}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Attraction Blocks */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{dayAttractions.map((attraction, attractionIndex) => {
|
||||||
|
const position = getBlockPosition(attraction, day);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={`${attraction.id}-${day}`}
|
||||||
|
className={`
|
||||||
|
${attraction.color} text-white text-xs p-1 rounded cursor-pointer
|
||||||
|
shadow-sm hover:shadow-md transition-shadow
|
||||||
|
${position.isStart ? "rounded-l" : position.isContinuation ? "rounded-none" : ""}
|
||||||
|
${position.isEnd ? "rounded-r" : position.isContinuation ? "rounded-none" : ""}
|
||||||
|
`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
transition={{ duration: 0.1 }}
|
||||||
|
onClick={() => {
|
||||||
|
const slotData = {
|
||||||
|
attractionName: attraction.name,
|
||||||
|
date: new Date(currentDate.getFullYear(), currentDate.getMonth(), day!).toISOString().split('T')[0],
|
||||||
|
timeSlots: ["10:30AM - 1:30PM", "3:30PM - 5:30PM"],
|
||||||
|
capacity: 25
|
||||||
|
};
|
||||||
|
setSelectedSlot(slotData);
|
||||||
|
setIsPanelOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="font-medium truncate">
|
||||||
|
{position.isStart && attraction.name}
|
||||||
|
</div>
|
||||||
|
{position.isStart && attraction.timeSlot && (
|
||||||
|
<div className="text-xs opacity-90 truncate">
|
||||||
|
{attraction.timeSlot}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty State - Add Button */}
|
||||||
|
{day && dayAttractions.length === 0 && hoveredDate === day && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="absolute inset-0 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 text-gray-400 hover:text-gray-600 hover:bg-gray-200"
|
||||||
|
onClick={() => {
|
||||||
|
const slotData = {
|
||||||
|
attractionName: "New Attraction",
|
||||||
|
date: new Date(currentDate.getFullYear(), currentDate.getMonth(), day).toISOString().split('T')[0],
|
||||||
|
timeSlots: [],
|
||||||
|
capacity: 0
|
||||||
|
};
|
||||||
|
setSelectedSlot(slotData);
|
||||||
|
setIsPanelOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Today Indicator */}
|
||||||
|
{isToday(day) && (
|
||||||
|
<div className="absolute bottom-1 right-1 w-2 h-2 bg-blue-600 rounded-full"></div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slot Detail Panel */}
|
||||||
|
<SlotDetailPanel
|
||||||
|
isOpen={isPanelOpen}
|
||||||
|
onClose={() => setIsPanelOpen(false)}
|
||||||
|
slotData={selectedSlot}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
180
src/components/CustomerDetailView.tsx
Normal file
180
src/components/CustomerDetailView.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { ArrowLeft, Eye } from "lucide-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
|
||||||
|
|
||||||
|
interface CustomerDetailViewProps {
|
||||||
|
customerData: {
|
||||||
|
bookingId: string;
|
||||||
|
passId: string;
|
||||||
|
customerName: string;
|
||||||
|
cardType: string;
|
||||||
|
result: string;
|
||||||
|
staff: string;
|
||||||
|
};
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CustomerDetailView({ customerData, onBack }: CustomerDetailViewProps) {
|
||||||
|
// Mock scan history data
|
||||||
|
const scanHistoryData = [
|
||||||
|
{
|
||||||
|
staff: "Jason R.",
|
||||||
|
scanOutcome: "Success",
|
||||||
|
timestamp: "2024/9/15, 14:30",
|
||||||
|
deviceLocation: "1241414141",
|
||||||
|
reasonForFailure: "-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
staff: "Kendall T.",
|
||||||
|
scanOutcome: "Failed",
|
||||||
|
timestamp: "2024/9/15, 14:30",
|
||||||
|
deviceLocation: "2141414174",
|
||||||
|
reasonForFailure: "Invalid Pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
staff: "Morgan A.",
|
||||||
|
scanOutcome: "Success",
|
||||||
|
timestamp: "2024/9/15, 14:30",
|
||||||
|
deviceLocation: "2147417417",
|
||||||
|
reasonForFailure: "-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
staff: "Taylor S.",
|
||||||
|
scanOutcome: "Failed",
|
||||||
|
timestamp: "2024/9/15, 14:30",
|
||||||
|
deviceLocation: "2141476324",
|
||||||
|
reasonForFailure: "Pass Expired"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const getResultBadge = (result: string) => {
|
||||||
|
return result === "Success"
|
||||||
|
? <Badge className="bg-green-100 text-green-800 hover:bg-green-100">Success</Badge>
|
||||||
|
: <Badge className="bg-red-100 text-red-800 hover:bg-red-100">Failed</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||||
|
className="min-h-screen bg-gray-50 p-6"
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header with Back Button */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4 p-0"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Detail View</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Attraction Details */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 mb-4">Attraction Details</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-1">City</p>
|
||||||
|
<p className="text-gray-900">Dubai</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-1">Attraction Name</p>
|
||||||
|
<p className="text-gray-900">The Enchanted Garden</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pass Summary */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 mb-4">Pass Summary</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-1">Card Type</p>
|
||||||
|
<p className="text-gray-900">{customerData.cardType}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-1">Validity</p>
|
||||||
|
<p className="text-gray-900">Valid</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Customer Details */}
|
||||||
|
<div className="bg-white rounded-lg p-6 shadow-sm border border-gray-200">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 mb-4">Customer Details</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-1">Customer Name</p>
|
||||||
|
<p className="text-gray-900">{customerData.customerName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-1">Phone</p>
|
||||||
|
<p className="text-gray-900">(+971) 050 421 4456</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 mb-1">Email</p>
|
||||||
|
<p className="text-gray-900">miranda21@gmail.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scan History */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">Scan History</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="text-blue-600 hover:text-blue-700 text-sm"
|
||||||
|
>
|
||||||
|
View Full History
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-b border-gray-200 bg-gray-50">
|
||||||
|
<TableHead className="font-medium text-gray-900 py-4 text-center">Staff</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center">Scan Outcome</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center">Timestamp</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center">Device ID or location</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center">Reason for Failure</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{scanHistoryData.map((item, index) => (
|
||||||
|
<motion.tr
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="border-b border-gray-100 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<TableCell className="text-gray-900 py-4 text-center">{item.staff}</TableCell>
|
||||||
|
<TableCell className="text-center">{getResultBadge(item.scanOutcome)}</TableCell>
|
||||||
|
<TableCell className="text-gray-600 text-center">{item.timestamp}</TableCell>
|
||||||
|
<TableCell className="text-gray-600 text-center">{item.deviceLocation}</TableCell>
|
||||||
|
<TableCell className="text-gray-600 text-center">{item.reasonForFailure}</TableCell>
|
||||||
|
</motion.tr>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
631
src/components/Dashboard.tsx
Normal file
631
src/components/Dashboard.tsx
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
import { Search, UserPlus, Grid3X3, Users, Clock, TrendingUp, Calendar, ChevronDown, CreditCard } from "lucide-react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import RedemptionModal from "./RedemptionModal";
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const [isRedemptionModalOpen, setIsRedemptionModalOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gray-50 p-6"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Page Title Section */}
|
||||||
|
<motion.div
|
||||||
|
className="mb-8"
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-1">Dashboard</h1>
|
||||||
|
<p className="text-gray-600">Welcome back, Kassandra!</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Quick Links Section */}
|
||||||
|
<motion.div
|
||||||
|
className="mb-8"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 mb-4">Quick Links</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -4, scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-lg transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<motion.div
|
||||||
|
className="bg-blue-50 p-3 rounded-lg"
|
||||||
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1, rotate: -5 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<Search className="h-6 w-6 text-blue-600" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<h3 className="font-medium text-gray-900 group-hover:text-blue-600 transition-colors">View Redemption Logs</h3>
|
||||||
|
<p className="text-sm text-gray-600">Check recent activity</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.3, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -4, scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-lg transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<motion.div
|
||||||
|
className="bg-green-50 p-3 rounded-lg"
|
||||||
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1, rotate: -5 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<UserPlus className="h-6 w-6 text-green-600" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<h3 className="font-medium text-gray-900 group-hover:text-green-600 transition-colors">Add Staff</h3>
|
||||||
|
<p className="text-sm text-gray-600">Manage team members</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.4, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -4, scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={() => setIsRedemptionModalOpen(true)}
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-lg transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<motion.div
|
||||||
|
className="bg-purple-50 p-3 rounded-lg"
|
||||||
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1, rotate: -5 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<Grid3X3 className="h-6 w-6 text-purple-600" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<h3 className="font-medium text-gray-900 group-hover:text-purple-600 transition-colors">Redemption</h3>
|
||||||
|
<p className="text-sm text-gray-600">Process new redemptions</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Summary Metrics Section */}
|
||||||
|
<motion.div
|
||||||
|
className="mb-8"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.5, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center justify-between mb-4"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.6, duration: 0.4 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">Summary</h2>
|
||||||
|
<motion.span
|
||||||
|
className="text-sm text-gray-500"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
29 • 10 • 2024
|
||||||
|
</motion.span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<motion.h3
|
||||||
|
className="text-base font-medium text-gray-900 mb-4"
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.7, duration: 0.4 }}
|
||||||
|
>
|
||||||
|
Total Redemptions
|
||||||
|
</motion.h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-all group cursor-pointer"
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.8, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -2, scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium text-gray-900">Daily</h3>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1, rotate: 10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<CreditCard className="h-8 w-8 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-end gap-2 mb-2"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<span className="text-2xl font-bold text-gray-900">150</span>
|
||||||
|
<span className="text-sm text-gray-600">redemptions</span>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.05 }}
|
||||||
|
>
|
||||||
|
<span className="text-xs text-green-600">+2.3%</span>
|
||||||
|
<span className="text-xs text-gray-500">since yesterday</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-all group cursor-pointer"
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.9, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -2, scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium text-gray-900">Weekly</h3>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1, rotate: 10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<CreditCard className="h-8 w-8 text-gray-400 group-hover:text-green-500 transition-colors" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-end gap-2 mb-2"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<span className="text-2xl font-bold text-gray-900">150</span>
|
||||||
|
<span className="text-sm text-gray-600">redemptions</span>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.05 }}
|
||||||
|
>
|
||||||
|
<span className="text-xs text-green-600">+8.5%</span>
|
||||||
|
<span className="text-xs text-gray-500">since yesterday</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-all group cursor-pointer"
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 1.0, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -2, scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium text-gray-900">Monthly</h3>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1, rotate: 10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<CreditCard className="h-8 w-8 text-gray-400 group-hover:text-purple-500 transition-colors" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-end gap-2 mb-2"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<span className="text-2xl font-bold text-gray-900">150</span>
|
||||||
|
<span className="text-sm text-gray-600">redemptions</span>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.05 }}
|
||||||
|
>
|
||||||
|
<span className="text-xs text-green-600">+3.3%</span>
|
||||||
|
<span className="text-xs text-gray-500">since yesterday</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Staff & Time Analytics */}
|
||||||
|
<motion.div
|
||||||
|
className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 1.1, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-all group cursor-pointer"
|
||||||
|
initial={{ opacity: 0, x: -20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 1.2, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -2, scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium text-gray-900">Total Staff</h3>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.15, rotate: 15 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<Users className="h-8 w-8 text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-end gap-2 mb-2"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<span className="text-3xl font-bold text-gray-900">150</span>
|
||||||
|
<span className="text-gray-600">employees</span>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.05 }}
|
||||||
|
>
|
||||||
|
<span className="text-sm text-green-600">+2.3%</span>
|
||||||
|
<span className="text-sm text-gray-500">since last month</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-all group cursor-pointer"
|
||||||
|
initial={{ opacity: 0, x: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 1.3, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -2, scale: 1.02 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-medium text-gray-900">Peak Time Slots</h3>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.1, rotate: 15 }}
|
||||||
|
animate={{
|
||||||
|
rotate: [0, 5, -5, 0],
|
||||||
|
scale: [1, 1.05, 1]
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 3,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
whileHover: { duration: 0.3 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Clock className="h-12 w-12 text-gray-400 group-hover:text-orange-500 transition-colors" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
className="mb-2"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<p className="text-gray-900">Most redemptions occur between</p>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-2 text-lg font-medium"
|
||||||
|
whileHover={{ x: 6 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
className="text-gray-900"
|
||||||
|
whileHover={{ scale: 1.1, color: "#f59e0b" }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
11AM
|
||||||
|
</motion.span>
|
||||||
|
<span className="text-gray-500">and</span>
|
||||||
|
<motion.span
|
||||||
|
className="text-gray-900"
|
||||||
|
whileHover={{ scale: 1.1, color: "#f59e0b" }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
1PM
|
||||||
|
</motion.span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Graphs Section */}
|
||||||
|
<motion.div
|
||||||
|
className="mb-8"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 1.4, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<motion.h2
|
||||||
|
className="text-lg font-medium text-gray-900 mb-4"
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 1.5, duration: 0.4 }}
|
||||||
|
>
|
||||||
|
Graphs
|
||||||
|
</motion.h2>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Daily Redemptions Chart */}
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-all group"
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 1.6, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -2, scale: 1.01 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center justify-between mb-4"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<h3 className="font-medium text-gray-900">Daily Redemptions</h3>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<span className="text-lg font-bold text-green-600">+15%</span>
|
||||||
|
<span className="text-sm text-gray-500">Last 7 Days</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="h-40 relative">
|
||||||
|
<svg className="w-full h-full" viewBox="0 0 300 120">
|
||||||
|
{/* Grid lines */}
|
||||||
|
<defs>
|
||||||
|
<pattern id="grid" width="30" height="24" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 30 0 L 0 0 0 24" fill="none" stroke="#f3f4f6" strokeWidth="1"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||||
|
|
||||||
|
{/* Line chart */}
|
||||||
|
<motion.path
|
||||||
|
d="M 20 80 Q 50 60 80 70 T 140 50 T 200 60 T 260 40"
|
||||||
|
fill="none"
|
||||||
|
stroke="#ec4899"
|
||||||
|
strokeWidth="2"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{ duration: 2, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Data points */}
|
||||||
|
{[{x: 20, y: 80}, {x: 50, y: 60}, {x: 80, y: 70}, {x: 110, y: 50}, {x: 140, y: 50}, {x: 170, y: 60}, {x: 200, y: 60}, {x: 230, y: 40}, {x: 260, y: 40}].map((point, i) => (
|
||||||
|
<motion.circle
|
||||||
|
key={i}
|
||||||
|
cx={point.x}
|
||||||
|
cy={point.y}
|
||||||
|
r="3"
|
||||||
|
fill="#ec4899"
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ delay: i * 0.1 + 1, duration: 0.3 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Days labels */}
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 mt-2">
|
||||||
|
<span>Mon</span>
|
||||||
|
<span>Tue</span>
|
||||||
|
<span>Wed</span>
|
||||||
|
<span>Thu</span>
|
||||||
|
<span>Fri</span>
|
||||||
|
<span>Sat</span>
|
||||||
|
<span>Sun</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Time-slot Usage Chart */}
|
||||||
|
<motion.div
|
||||||
|
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-all group"
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, delay: 1.7, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -2, scale: 1.01 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center justify-between mb-4"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<h3 className="font-medium text-gray-900">Time-slot Usage</h3>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<span className="text-lg font-bold text-green-600">+8%</span>
|
||||||
|
<span className="text-sm text-gray-500">Last 30 Days</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="h-40 relative">
|
||||||
|
<div className="flex items-end justify-between h-32 gap-2">
|
||||||
|
{[20, 35, 45, 55, 70, 85, 60, 40].map((height, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="bg-pink-200 rounded-t flex-1 hover:bg-pink-300 transition-colors cursor-pointer"
|
||||||
|
style={{ height: `${height}%` }}
|
||||||
|
initial={{ height: 0, scaleY: 0 }}
|
||||||
|
animate={{ height: `${height}%`, scaleY: 1 }}
|
||||||
|
whileHover={{ scaleY: 1.05, scaleX: 1.1 }}
|
||||||
|
transition={{
|
||||||
|
delay: i * 0.1 + 1.8,
|
||||||
|
duration: 0.6,
|
||||||
|
whileHover: { duration: 0.2 }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time labels */}
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 mt-2">
|
||||||
|
<span>9 AM</span>
|
||||||
|
<span>10 AM</span>
|
||||||
|
<span>11 AM</span>
|
||||||
|
<span>12 PM</span>
|
||||||
|
<span>1 PM</span>
|
||||||
|
<span>2 PM</span>
|
||||||
|
<span>3 PM</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Upcoming Bookings */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 2.5, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<motion.h2
|
||||||
|
className="text-lg font-medium text-gray-900 mb-4"
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 2.6, duration: 0.4 }}
|
||||||
|
>
|
||||||
|
Upcoming Bookings
|
||||||
|
</motion.h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ delay: 2.7, duration: 0.5, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -4, scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
className="bg-gray-100 rounded-lg p-6 hover:bg-gray-200 transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-3 mb-2"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.2, rotate: 10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<Calendar className="h-5 w-5 text-gray-600 group-hover:text-blue-600 transition-colors" />
|
||||||
|
</motion.div>
|
||||||
|
<span className="font-medium text-gray-900">July 19, 2024</span>
|
||||||
|
</motion.div>
|
||||||
|
<motion.p
|
||||||
|
className="text-gray-600"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.05 }}
|
||||||
|
>
|
||||||
|
10:30AM - 3:30PM
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ delay: 2.8, duration: 0.5, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -4, scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
className="bg-gray-100 rounded-lg p-6 hover:bg-gray-200 transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-3 mb-2"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.2, rotate: 10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<Calendar className="h-5 w-5 text-gray-600 group-hover:text-green-600 transition-colors" />
|
||||||
|
</motion.div>
|
||||||
|
<span className="font-medium text-gray-900">July 20, 2024</span>
|
||||||
|
</motion.div>
|
||||||
|
<motion.p
|
||||||
|
className="text-gray-600"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.05 }}
|
||||||
|
>
|
||||||
|
10:30AM - 3:30PM
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
transition={{ delay: 2.9, duration: 0.5, ease: "easeOut" }}
|
||||||
|
whileHover={{ y: -4, scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
className="bg-gray-100 rounded-lg p-6 hover:bg-gray-200 transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-3 mb-2"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.2, rotate: 10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<Calendar className="h-5 w-5 text-gray-600 group-hover:text-purple-600 transition-colors" />
|
||||||
|
</motion.div>
|
||||||
|
<span className="font-medium text-gray-900">July 21, 2024</span>
|
||||||
|
</motion.div>
|
||||||
|
<motion.p
|
||||||
|
className="text-gray-600"
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.05 }}
|
||||||
|
>
|
||||||
|
10:30AM - 3:30PM
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Redemption Modal */}
|
||||||
|
<RedemptionModal
|
||||||
|
isOpen={isRedemptionModalOpen}
|
||||||
|
onClose={() => setIsRedemptionModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
204
src/components/Header.tsx
Normal file
204
src/components/Header.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { Bell, User, Settings, LogOut, ChevronDown } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
onNavigateToNotifications?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header({ onNavigateToNotifications }: HeaderProps) {
|
||||||
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
|
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||||
|
|
||||||
|
const notifications = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "New booking received",
|
||||||
|
message: "Forest Adventure Park - July 19, 10:30 AM",
|
||||||
|
time: "2 minutes ago",
|
||||||
|
unread: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Staff schedule updated",
|
||||||
|
message: "John Smith's shift has been modified",
|
||||||
|
time: "1 hour ago",
|
||||||
|
unread: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Redemption processed",
|
||||||
|
message: "Customer ID: CU12345 - $25.00",
|
||||||
|
time: "3 hours ago",
|
||||||
|
unread: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const unreadCount = notifications.filter(n => n.unread).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white border-b border-gray-200 px-6 py-4">
|
||||||
|
<div className="max-w-7xl mx-auto flex items-center justify-end gap-4">
|
||||||
|
{/* Notifications */}
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowNotifications(!showNotifications)}
|
||||||
|
className="relative p-2 hover:bg-gray-100 rounded-lg"
|
||||||
|
>
|
||||||
|
<Bell className="h-5 w-5 text-gray-600" />
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<Badge
|
||||||
|
variant="destructive"
|
||||||
|
className="absolute -top-1 -right-1 h-5 w-5 rounded-full p-0 flex items-center justify-center text-xs"
|
||||||
|
>
|
||||||
|
{unreadCount}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Notifications Dropdown */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showNotifications && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="absolute right-0 top-full mt-2 w-80 bg-white border border-gray-200 rounded-lg shadow-lg z-50"
|
||||||
|
>
|
||||||
|
<div className="p-4 border-b border-gray-100">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium text-gray-900">Notifications</h3>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<span className="text-sm text-blue-600 hover:text-blue-700 cursor-pointer">
|
||||||
|
Mark all as read
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-96 overflow-y-auto">
|
||||||
|
{notifications.map((notification) => (
|
||||||
|
<div
|
||||||
|
key={notification.id}
|
||||||
|
className={`p-4 border-b border-gray-50 hover:bg-gray-50 cursor-pointer ${
|
||||||
|
notification.unread ? "bg-blue-50" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className={`w-2 h-2 rounded-full mt-2 ${
|
||||||
|
notification.unread ? "bg-blue-600" : "bg-transparent"
|
||||||
|
}`}></div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium text-gray-900 text-sm">
|
||||||
|
{notification.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
{notification.message}
|
||||||
|
</p>
|
||||||
|
<span className="text-xs text-gray-500 mt-1">
|
||||||
|
{notification.time}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="p-4 text-center border-t border-gray-100">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-blue-600 hover:text-blue-700"
|
||||||
|
onClick={() => {
|
||||||
|
setShowNotifications(false);
|
||||||
|
onNavigateToNotifications?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View all notifications
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Notifications Backdrop */}
|
||||||
|
{showNotifications && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40"
|
||||||
|
onClick={() => setShowNotifications(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Menu */}
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
||||||
|
className="flex items-center gap-2 hover:bg-gray-100 rounded-lg px-3 py-2"
|
||||||
|
>
|
||||||
|
<Avatar className="h-8 w-8">
|
||||||
|
<AvatarImage src="/api/placeholder/32/32" alt="Admin" />
|
||||||
|
<AvatarFallback>KA</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="text-left hidden sm:block">
|
||||||
|
<div className="font-medium text-gray-900 text-sm">Kassandra</div>
|
||||||
|
<div className="text-xs text-gray-500">Admin</div>
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="h-4 w-4 text-gray-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Profile Dropdown */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showProfileMenu && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="absolute right-0 top-full mt-2 w-48 bg-white border border-gray-200 rounded-lg shadow-lg z-50"
|
||||||
|
>
|
||||||
|
<div className="p-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start gap-2 px-3 py-2 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
<span>Profile</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start gap-2 px-3 py-2 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
<span>Settings</span>
|
||||||
|
</Button>
|
||||||
|
<div className="border-t border-gray-100 my-1"></div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start gap-2 px-3 py-2 hover:bg-gray-100 text-red-600 hover:text-red-700"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
<span>Sign out</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Profile Backdrop */}
|
||||||
|
{showProfileMenu && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40"
|
||||||
|
onClick={() => setShowProfileMenu(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
330
src/components/NotificationsPage.tsx
Normal file
330
src/components/NotificationsPage.tsx
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
import { Search, X } from "lucide-react";
|
||||||
|
import { useState, forwardRef } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import Notifications from "../imports/Notifications";
|
||||||
|
import svgPaths from "../imports/svg-xewg6e9vz";
|
||||||
|
|
||||||
|
// Enhanced Notification Component with interactive features
|
||||||
|
const NotificationCard = forwardRef<HTMLDivElement, {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
date: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
isRead?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
index?: number;
|
||||||
|
}>(({
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
date,
|
||||||
|
icon,
|
||||||
|
isRead = false,
|
||||||
|
onClose,
|
||||||
|
index = 0
|
||||||
|
}, ref) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.4,
|
||||||
|
delay: index * 0.1,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
whileHover={{
|
||||||
|
y: -2,
|
||||||
|
scale: 1.01,
|
||||||
|
transition: { duration: 0.2 }
|
||||||
|
}}
|
||||||
|
className={`relative bg-white h-[124px] overflow-clip rounded-[12px] shadow-[0px_20px_24px_-4px_rgba(0,0,0,0.08),0px_8px_8px_-4px_rgba(0,0,0,0.03)] w-full mb-4 cursor-pointer group`}
|
||||||
|
style={{ opacity: isRead ? 0.42 : 1 }}
|
||||||
|
>
|
||||||
|
{/* Notification Details */}
|
||||||
|
<div className="absolute flex gap-3.5 items-start justify-start left-3 top-3">
|
||||||
|
<motion.div
|
||||||
|
className="bg-[#f6f6f6] rounded-[12px] shrink-0 size-[100px] flex items-center justify-center overflow-hidden"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 1 }}
|
||||||
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col gap-2 items-start justify-start leading-[0] shrink-0 text-black w-[449px]"
|
||||||
|
initial={{ opacity: 0.8 }}
|
||||||
|
whileHover={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="font-semibold text-[16px] w-full"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<p className="leading-[23.556px]">{title}</p>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="font-normal text-[14px] w-full"
|
||||||
|
whileHover={{ x: 2 }}
|
||||||
|
transition={{ duration: 0.2, delay: 0.05 }}
|
||||||
|
>
|
||||||
|
<p className="leading-[20px]">{message}</p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Close Button */}
|
||||||
|
<motion.button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute right-3 top-3 size-5 hover:bg-gray-100 rounded transition-colors"
|
||||||
|
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.svg
|
||||||
|
className="block size-full"
|
||||||
|
fill="none"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
whileHover={{ rotate: 90 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<path d={svgPaths.p1f279900} fill="#656D76" />
|
||||||
|
</motion.svg>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute flex flex-col font-normal h-[15px] justify-center leading-[0] right-3 text-[16px] text-[rgba(0,0,0,0.42)] top-[104.5px] translate-y-[-50%] w-[65px]"
|
||||||
|
initial={{ opacity: 0.6 }}
|
||||||
|
whileHover={{ opacity: 1, scale: 1.05 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<p className="leading-[23.556px] text-right">{date}</p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
NotificationCard.displayName = "NotificationCard";
|
||||||
|
|
||||||
|
export default function NotificationsPage() {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [notifications, setNotifications] = useState([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Account Update Notification",
|
||||||
|
message: "Your account has been successfully updated. Please check your email for confirmation.",
|
||||||
|
date: "10/01/23",
|
||||||
|
unread: true,
|
||||||
|
icon: (
|
||||||
|
<svg className="block size-[60px]" fill="none" preserveAspectRatio="none" viewBox="0 0 60 60">
|
||||||
|
<path d={svgPaths.p343fd100} fill="black" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "New Message Alert",
|
||||||
|
message: "We have received your request and are processing it. Expect an update shortly.",
|
||||||
|
date: "10/01/23",
|
||||||
|
unread: true,
|
||||||
|
icon: (
|
||||||
|
<svg className="block size-[60px]" fill="none" preserveAspectRatio="none" viewBox="0 0 60 60">
|
||||||
|
<path d={svgPaths.paa2b800} fill="black" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "System Maintenance Notice",
|
||||||
|
message: "A new message has arrived in your inbox. Don't forget to check it out!",
|
||||||
|
date: "10/01/23",
|
||||||
|
unread: false,
|
||||||
|
icon: (
|
||||||
|
<svg className="block size-[60px]" fill="none" preserveAspectRatio="none" viewBox="0 0 60 60">
|
||||||
|
<path d={svgPaths.p2aacbc00} fill="black" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Event Registration Confirmation",
|
||||||
|
message: "Scheduled maintenance will occur this weekend. We appreciate your understanding.",
|
||||||
|
date: "10/01/23",
|
||||||
|
unread: false,
|
||||||
|
icon: (
|
||||||
|
<svg className="block size-[60px]" fill="none" preserveAspectRatio="none" viewBox="0 0 60 60">
|
||||||
|
<g>
|
||||||
|
<path d={svgPaths.p1d319800} fill="black" />
|
||||||
|
<g>
|
||||||
|
<path d={svgPaths.p3a49e400} stroke="black" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
<path d="M17.5 10V5M42.5 10V5" stroke="black" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
<path d="M17.5 27.5H42.5" stroke="black" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
<path d="M17.5 37.5H35" stroke="black" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleCloseNotification = (id: number) => {
|
||||||
|
setNotifications(prev => prev.filter(notification => notification.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkAllAsRead = () => {
|
||||||
|
setNotifications(prev =>
|
||||||
|
prev.map(notification => ({ ...notification, unread: false }))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredNotifications = notifications.filter(notification =>
|
||||||
|
notification.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
notification.message.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="min-h-screen bg-gray-50 p-6"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<motion.div
|
||||||
|
className="mb-8"
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Notifications</h1>
|
||||||
|
<p className="text-gray-600">Stay informed with the latest notifications</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Search Bar and Mark as Read */}
|
||||||
|
<motion.div
|
||||||
|
className="mb-8 flex items-center gap-4"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<div>
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search Subject, body type, etc."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 bg-white border border-gray-200 rounded-lg h-12 font-normal w-full focus:ring-2 focus:ring-blue-200 transition-all duration-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AnimatePresence>
|
||||||
|
{notifications.some(n => n.unread) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.8, x: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.8, x: 20 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleMarkAllAsRead}
|
||||||
|
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 whitespace-nowrap transition-all duration-200 hover:scale-105"
|
||||||
|
>
|
||||||
|
Mark all as read
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Notifications List */}
|
||||||
|
<motion.div
|
||||||
|
className="space-y-0"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{filteredNotifications.map((notification, index) => (
|
||||||
|
<NotificationCard
|
||||||
|
key={notification.id}
|
||||||
|
title={notification.title}
|
||||||
|
message={notification.message}
|
||||||
|
date={notification.date}
|
||||||
|
icon={notification.icon}
|
||||||
|
isRead={!notification.unread}
|
||||||
|
onClose={() => handleCloseNotification(notification.id)}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{filteredNotifications.length === 0 && (
|
||||||
|
<motion.div
|
||||||
|
className="text-center py-12"
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: -20, scale: 0.9 }}
|
||||||
|
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||||
|
whileHover={{ scale: 1.1, rotate: 10 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.1, 1],
|
||||||
|
rotate: [0, 5, -5, 0]
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search className="h-8 w-8 text-gray-400" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.h3
|
||||||
|
className="text-lg font-medium text-gray-900 mb-2"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.3, duration: 0.4 }}
|
||||||
|
>
|
||||||
|
{searchQuery ? "No matching notifications" : "No notifications yet"}
|
||||||
|
</motion.h3>
|
||||||
|
<motion.p
|
||||||
|
className="text-gray-600"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.4, duration: 0.4 }}
|
||||||
|
>
|
||||||
|
{searchQuery
|
||||||
|
? "Try adjusting your search terms to find what you're looking for."
|
||||||
|
: "You're all caught up! Check back later for new updates."
|
||||||
|
}
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
439
src/components/RecurringBlockPage.tsx
Normal file
439
src/components/RecurringBlockPage.tsx
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { ChevronLeft, ChevronDown, ChevronUp, Check } from "lucide-react";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Card, CardContent } from "./ui/card";
|
||||||
|
import { Switch } from "./ui/switch";
|
||||||
|
import { Checkbox } from "./ui/checkbox";
|
||||||
|
import exampleImage from 'figma:asset/fc17f89f308e91a12011ae1dc4ea7b32cda951d8.png';
|
||||||
|
|
||||||
|
interface RecurringBlockPageProps {
|
||||||
|
onNavigateBack?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RecurringBlockPage({ onNavigateBack }: RecurringBlockPageProps) {
|
||||||
|
const [showDaySelector, setShowDaySelector] = useState(false);
|
||||||
|
const [selectedDays, setSelectedDays] = useState<string[]>(["Monday", "Friday"]);
|
||||||
|
const [expandedDays, setExpandedDays] = useState<string[]>(["Monday"]);
|
||||||
|
const [showAttractionSelector, setShowAttractionSelector] = useState(false);
|
||||||
|
const [selectedAttractions, setSelectedAttractions] = useState<string[]>(["The Enchanted Forest Adventure Park", "Ocean World Marine Experience"]);
|
||||||
|
|
||||||
|
const daysOfWeek = [
|
||||||
|
"Monday",
|
||||||
|
"Tuesday",
|
||||||
|
"Wednesday",
|
||||||
|
"Thursday",
|
||||||
|
"Friday",
|
||||||
|
"Saturday",
|
||||||
|
"Sunday"
|
||||||
|
];
|
||||||
|
|
||||||
|
const availableAttractions = [
|
||||||
|
"The Enchanted Forest Adventure Park",
|
||||||
|
"Ocean World Marine Experience",
|
||||||
|
"Sky High Observation Deck",
|
||||||
|
"Heritage Museum & Cultural Center",
|
||||||
|
"Adventure Sports Complex",
|
||||||
|
"Botanical Gardens & Nature Walk",
|
||||||
|
"City Zoo & Wildlife Safari",
|
||||||
|
"Art Gallery & Creative Studios"
|
||||||
|
];
|
||||||
|
|
||||||
|
const timeSlots = [
|
||||||
|
{ id: "10-1", time: "10:00AM - 1:00PM", capacity: 35 },
|
||||||
|
{ id: "1-3", time: "1:00PM - 3:00PM", capacity: 25 },
|
||||||
|
{ id: "3-5", time: "3:00PM - 5:00PM", capacity: null },
|
||||||
|
{ id: "5-7", time: "5:00PM - 7:00PM", capacity: null },
|
||||||
|
{ id: "7-9", time: "7:00PM - 9:00PM", capacity: null }
|
||||||
|
];
|
||||||
|
|
||||||
|
const toggleDay = (day: string) => {
|
||||||
|
setSelectedDays(prev =>
|
||||||
|
prev.includes(day)
|
||||||
|
? prev.filter(d => d !== day)
|
||||||
|
: [...prev, day]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDayExpansion = (day: string) => {
|
||||||
|
setExpandedDays(prev =>
|
||||||
|
prev.includes(day)
|
||||||
|
? prev.filter(d => d !== day)
|
||||||
|
: [...prev, day]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAttraction = (attraction: string) => {
|
||||||
|
setSelectedAttractions(prev =>
|
||||||
|
prev.includes(attraction)
|
||||||
|
? prev.filter(a => a !== attraction)
|
||||||
|
: [...prev, attraction]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedDaysText = () => {
|
||||||
|
if (selectedDays.length === 0) return "Select Days";
|
||||||
|
if (selectedDays.length === 1) return selectedDays[0];
|
||||||
|
if (selectedDays.length === 2) return `Every ${selectedDays.join(" & ")}`;
|
||||||
|
return `Every ${selectedDays.slice(0, -1).join(", ")} & ${selectedDays[selectedDays.length - 1]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedAttractionsText = () => {
|
||||||
|
if (selectedAttractions.length === 0) return "Select Attractions";
|
||||||
|
if (selectedAttractions.length === 1) return selectedAttractions[0];
|
||||||
|
return `${selectedAttractions.length} attractions selected`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="max-w-7xl mx-auto px-6 py-8">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 mb-2"
|
||||||
|
onClick={onNavigateBack}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Back</span>
|
||||||
|
</button>
|
||||||
|
<p className="text-sm text-gray-500">Booking Management {">"} Calendar View {">"} Add Recurring Block</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Recurring Block</h1>
|
||||||
|
<p className="text-gray-600">Automatically block bookings on recurring days and hours.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Day Selection Display */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowDaySelector(!showDaySelector)}
|
||||||
|
className="w-64 h-12 justify-between text-left bg-white border border-gray-200 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<span className="text-gray-900">{getSelectedDaysText()}</span>
|
||||||
|
<ChevronDown className={`h-4 w-4 text-gray-500 transition-transform ${showDaySelector ? 'rotate-180' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Day Selector Dropdown */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showDaySelector && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="absolute top-full left-0 mt-2 w-64 bg-white border border-gray-200 rounded-lg shadow-lg z-50"
|
||||||
|
>
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
|
{daysOfWeek.map((day) => (
|
||||||
|
<button
|
||||||
|
key={day}
|
||||||
|
onClick={() => toggleDay(day)}
|
||||||
|
className={`w-full text-left p-3 rounded-lg transition-colors ${
|
||||||
|
selectedDays.includes(day)
|
||||||
|
? "bg-gray-100 text-gray-900"
|
||||||
|
: "hover:bg-gray-50 text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Backdrop */}
|
||||||
|
{showDaySelector && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40"
|
||||||
|
onClick={() => setShowDaySelector(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Attractions Selection */}
|
||||||
|
{selectedDays.length > 0 && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 mb-1">Select Attractions</h2>
|
||||||
|
<p className="text-sm text-gray-600">Choose which attractions to apply the recurring block to</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowAttractionSelector(!showAttractionSelector)}
|
||||||
|
className="w-full max-w-md h-12 justify-between text-left bg-white border border-gray-200 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<span className="text-gray-900 truncate pr-2">{getSelectedAttractionsText()}</span>
|
||||||
|
<ChevronDown className={`h-4 w-4 text-gray-500 transition-transform flex-shrink-0 ${showAttractionSelector ? 'rotate-180' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Attractions Selector Dropdown */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showAttractionSelector && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="absolute top-full left-0 mt-2 w-full max-w-md bg-white border border-gray-200 rounded-lg shadow-lg z-50 max-h-80 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="flex items-center justify-between p-3 border-b border-gray-100">
|
||||||
|
<span className="font-medium text-gray-900">Select Attractions</span>
|
||||||
|
<span className="text-sm text-gray-500">{selectedAttractions.length} selected</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1 mt-2">
|
||||||
|
{availableAttractions.map((attraction) => (
|
||||||
|
<button
|
||||||
|
key={attraction}
|
||||||
|
onClick={() => toggleAttraction(attraction)}
|
||||||
|
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center w-4 h-4 border border-gray-300 rounded bg-white">
|
||||||
|
{selectedAttractions.includes(attraction) && (
|
||||||
|
<Check className="h-3 w-3 text-blue-600" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-gray-700 flex-1">{attraction}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Backdrop */}
|
||||||
|
{showAttractionSelector && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40"
|
||||||
|
onClick={() => setShowAttractionSelector(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected Attractions Display */}
|
||||||
|
{selectedAttractions.length > 0 && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedAttractions.map((attraction) => (
|
||||||
|
<div
|
||||||
|
key={attraction}
|
||||||
|
className="inline-flex items-center gap-2 bg-blue-50 text-blue-700 px-3 py-1.5 rounded-full text-sm"
|
||||||
|
>
|
||||||
|
<span className="truncate max-w-48">{attraction}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleAttraction(attraction)}
|
||||||
|
className="hover:bg-blue-100 rounded-full p-0.5 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-3 w-3 rotate-45" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Selected Days Containers */}
|
||||||
|
<div className="space-y-4 mb-8">
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedDays.map((day) => (
|
||||||
|
<motion.div
|
||||||
|
key={day}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<Card className="border border-gray-200 bg-white">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleDayExpansion(day)}
|
||||||
|
className="w-full flex items-center justify-between p-6 hover:bg-gray-50 transition-colors group"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-medium text-gray-900">{day}</span>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{expandedDays.includes(day) && (
|
||||||
|
<span className="text-sm text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
|
||||||
|
2 slots selected
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{expandedDays.includes(day) ? (
|
||||||
|
<ChevronUp className="h-5 w-5 text-gray-500 group-hover:text-gray-700 transition-colors" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-5 w-5 text-gray-500 group-hover:text-gray-700 transition-colors" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{expandedDays.includes(day) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-6 pb-6 border-t border-gray-100">
|
||||||
|
<div className="pt-6 space-y-8">
|
||||||
|
{/* Time Slots Available */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-base font-medium text-gray-900">Available Time Slots</h3>
|
||||||
|
<span className="text-sm text-gray-500">Select slots to configure</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{timeSlots.map((slot, index) => (
|
||||||
|
<Button
|
||||||
|
key={slot.id}
|
||||||
|
variant={index < 2 ? "default" : "outline"}
|
||||||
|
className={`h-11 justify-start text-sm font-medium transition-all ${
|
||||||
|
index < 2
|
||||||
|
? "bg-blue-600 hover:bg-blue-700 text-white shadow-sm"
|
||||||
|
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50 border-gray-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${
|
||||||
|
index < 2 ? "bg-white" : "bg-gray-400"
|
||||||
|
}`}></div>
|
||||||
|
{slot.time}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected Slots Configuration */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-medium text-gray-900 mb-4">Selected Slots Configuration</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-blue-600 rounded-full"></div>
|
||||||
|
<span className="font-medium text-blue-900">10:00AM - 1:30PM</span>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" className="text-blue-600 hover:text-blue-700 hover:bg-blue-100 h-8 px-2">
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<label className="text-sm font-medium text-blue-900">Capacity:</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" className="h-8 w-8 p-0 border-blue-300 text-blue-600 hover:bg-blue-100">-</Button>
|
||||||
|
<div className="w-16 h-10 bg-white border border-blue-300 rounded-md flex items-center justify-center">
|
||||||
|
<span className="font-medium text-blue-900">35</span>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" className="h-8 w-8 p-0 border-blue-300 text-blue-600 hover:bg-blue-100">+</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 bg-green-600 rounded-full"></div>
|
||||||
|
<span className="font-medium text-green-900">1:00PM - 3:30PM</span>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" className="text-green-600 hover:text-green-700 hover:bg-green-100 h-8 px-2">
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<label className="text-sm font-medium text-green-900">Capacity:</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" className="h-8 w-8 p-0 border-green-300 text-green-600 hover:bg-green-100">-</Button>
|
||||||
|
<div className="w-16 h-10 bg-white border border-green-300 rounded-md flex items-center justify-center">
|
||||||
|
<span className="font-medium text-green-900">25</span>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" className="h-8 w-8 p-0 border-green-300 text-green-600 hover:bg-green-100">+</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Repeat Toggle */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900">Repeat Every {day}</span>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">This configuration will repeat weekly</p>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview Section */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 mb-4">Preview</h2>
|
||||||
|
<div className="text-sm text-gray-600 mb-4">Every Monday</div>
|
||||||
|
|
||||||
|
{/* Preview Card */}
|
||||||
|
<Card className="w-80 bg-gray-800 text-white">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="mb-3">
|
||||||
|
<h3 className="font-medium text-white">The Enchanted Forest Adventure Park</h3>
|
||||||
|
<div className="text-xs text-gray-300 mt-1">2 time slots</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||||
|
10:00AM - 1:30PM
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs">👥</span>
|
||||||
|
<span>35</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||||
|
1:00PM - 3:30PM
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs">👥</span>
|
||||||
|
<span>25</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Button */}
|
||||||
|
<div>
|
||||||
|
<Button className="w-full h-12 bg-gray-900 hover:bg-gray-800 text-white rounded-lg">
|
||||||
|
Create Recurring Block
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
240
src/components/RedemptionModal.tsx
Normal file
240
src/components/RedemptionModal.tsx
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
|
||||||
|
interface RedemptionModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TicketDetails {
|
||||||
|
customer: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
cityCard: {
|
||||||
|
cardType: string;
|
||||||
|
validity: string;
|
||||||
|
};
|
||||||
|
partner: {
|
||||||
|
attraction: string;
|
||||||
|
venue: string;
|
||||||
|
bookingDate: string;
|
||||||
|
ticketId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RedemptionModal({ isOpen, onClose }: RedemptionModalProps) {
|
||||||
|
const [ticketNumber, setTicketNumber] = useState("");
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Mock ticket details - in real app this would come from API
|
||||||
|
const mockTicketDetails: TicketDetails = {
|
||||||
|
customer: {
|
||||||
|
name: "Jenny Johnson",
|
||||||
|
email: "jenny@hawaii.com",
|
||||||
|
phone: "(+61) 001 864 644"
|
||||||
|
},
|
||||||
|
cityCard: {
|
||||||
|
cardType: "Diamond Plan",
|
||||||
|
validity: "valid until December 31, 2026"
|
||||||
|
},
|
||||||
|
partner: {
|
||||||
|
attraction: "The Enchanted Garden",
|
||||||
|
venue: "Downtown",
|
||||||
|
bookingDate: "November 15, 2024",
|
||||||
|
ticketId: "GD004E - 1UJHM"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!ticketNumber.trim()) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
setIsLoading(false);
|
||||||
|
setShowDetails(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApprove = () => {
|
||||||
|
// Handle approval logic
|
||||||
|
console.log("Redemption approved for ticket:", ticketNumber);
|
||||||
|
onClose();
|
||||||
|
resetModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisapprove = () => {
|
||||||
|
// Handle disapproval logic
|
||||||
|
console.log("Redemption disapproved for ticket:", ticketNumber);
|
||||||
|
onClose();
|
||||||
|
resetModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetModal = () => {
|
||||||
|
setTicketNumber("");
|
||||||
|
setShowDetails(false);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
resetModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="max-w-lg w-full mx-4 p-0 gap-0 bg-white overflow-hidden">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{!showDetails ? (
|
||||||
|
// Ticket Number Input View
|
||||||
|
<motion.div
|
||||||
|
key="input"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="p-6"
|
||||||
|
>
|
||||||
|
<DialogHeader className="text-center mb-6">
|
||||||
|
<DialogTitle className="text-xl font-semibold text-gray-900">
|
||||||
|
Create Redemption
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-600">
|
||||||
|
Enter the ticket number to process a new redemption
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ticket-number" className="text-base font-medium text-gray-900">
|
||||||
|
Ticket Number
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="ticket-number"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter ticket number"
|
||||||
|
value={ticketNumber}
|
||||||
|
onChange={(e) => setTicketNumber(e.target.value)}
|
||||||
|
className="h-12 bg-gray-50 border border-gray-200 rounded-lg font-normal"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!ticketNumber.trim() || isLoading}
|
||||||
|
className="w-full h-12 bg-gray-600 hover:bg-gray-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
{isLoading ? "Searching..." : "Submit"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
// Ticket Details View
|
||||||
|
<motion.div
|
||||||
|
key="details"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="p-6"
|
||||||
|
>
|
||||||
|
<DialogHeader className="text-center mb-6">
|
||||||
|
<DialogTitle className="text-xl font-semibold text-gray-900">
|
||||||
|
Create Redemption
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-600">
|
||||||
|
Review the ticket details and approve or disapprove the redemption
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Customer Details */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-medium text-gray-900 mb-3">Customer Details</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">Name</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.customer.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">Email</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.customer.email}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">Phone</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.customer.phone}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* City Card Details */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-medium text-gray-900 mb-3">City Card Details</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">Card Type</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.cityCard.cardType}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">Validity</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.cityCard.validity}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Partner Details */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-medium text-gray-900 mb-3">Partner Details</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">Attraction booked</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.partner.attraction}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">Venue</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.partner.venue}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">Booking date</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.partner.bookingDate}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-gray-600">TicketID</span>
|
||||||
|
<span className="text-sm text-gray-900">{mockTicketDetails.partner.ticketId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="space-y-3 pt-4">
|
||||||
|
<Button
|
||||||
|
onClick={handleApprove}
|
||||||
|
className="w-full h-12 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDisapprove}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full h-12 border-red-300 text-red-600 hover:bg-red-50 hover:border-red-400 rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
Disapprove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
427
src/components/RedemptionsPage.tsx
Normal file
427
src/components/RedemptionsPage.tsx
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
import { Search, Download, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
|
||||||
|
import { useState } from "react";
|
||||||
|
import CustomerDetailView from "./CustomerDetailView";
|
||||||
|
|
||||||
|
export default function RedemptionsPage() {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||||
|
const [showDetailView, setShowDetailView] = useState(false);
|
||||||
|
|
||||||
|
// Mock data for redemption logs
|
||||||
|
const redemptionData = [
|
||||||
|
{
|
||||||
|
bookingId: "WA234456",
|
||||||
|
passId: "PRB7654",
|
||||||
|
customerName: "Lisa Hast",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Jordan T.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234567",
|
||||||
|
passId: "PRB7655",
|
||||||
|
customerName: "Maya Thompson",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "Taylor A.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234568",
|
||||||
|
passId: "PRB7656",
|
||||||
|
customerName: "Sophie Turner",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Morgan S.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234569",
|
||||||
|
passId: "PRB7657",
|
||||||
|
customerName: "Ella Johnson",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "Casey J.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234570",
|
||||||
|
passId: "PRB7658",
|
||||||
|
customerName: "Chloe Bennett",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Riley K.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234571",
|
||||||
|
passId: "PRB7659",
|
||||||
|
customerName: "Zoe Adams",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Jamie L.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234572",
|
||||||
|
passId: "PRB7660",
|
||||||
|
customerName: "Ava Martinez",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "Alex H.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234573",
|
||||||
|
passId: "PRB7661",
|
||||||
|
customerName: "Isabella Lee",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Dylan O.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234574",
|
||||||
|
passId: "PRB7662",
|
||||||
|
customerName: "Emma Watson",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Sam R.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234575",
|
||||||
|
passId: "PRB7663",
|
||||||
|
customerName: "Grace Miller",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "Charlie M.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234576",
|
||||||
|
passId: "PRB7664",
|
||||||
|
customerName: "Olivia Brown",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Parker N.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234577",
|
||||||
|
passId: "PRB7665",
|
||||||
|
customerName: "Amelia Davis",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Avery P.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234578",
|
||||||
|
passId: "PRB7666",
|
||||||
|
customerName: "Harper Wilson",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "River Q.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234579",
|
||||||
|
passId: "PRB7667",
|
||||||
|
customerName: "Evelyn Moore",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Quinn S.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234580",
|
||||||
|
passId: "PRB7668",
|
||||||
|
customerName: "Abigail Taylor",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Sage T.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234581",
|
||||||
|
passId: "PRB7669",
|
||||||
|
customerName: "Emily Anderson",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "Rowan U.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234582",
|
||||||
|
passId: "PRB7670",
|
||||||
|
customerName: "Elizabeth Jackson",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Kai V.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234583",
|
||||||
|
passId: "PRB7671",
|
||||||
|
customerName: "Sofia White",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Drew W.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234584",
|
||||||
|
passId: "PRB7672",
|
||||||
|
customerName: "Avery Harris",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "Blake X.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234585",
|
||||||
|
passId: "PRB7673",
|
||||||
|
customerName: "Ella Martin",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Casey Y.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234586",
|
||||||
|
passId: "PRB7674",
|
||||||
|
customerName: "Scarlett Thompson",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Jordan Z.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234587",
|
||||||
|
passId: "PRB7675",
|
||||||
|
customerName: "Victoria Garcia",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "Taylor A.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234588",
|
||||||
|
passId: "PRB7676",
|
||||||
|
customerName: "Madison Martinez",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Morgan B.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234589",
|
||||||
|
passId: "PRB7677",
|
||||||
|
customerName: "Luna Rodriguez",
|
||||||
|
cardType: "Selective",
|
||||||
|
result: "Success",
|
||||||
|
staff: "Riley C.",
|
||||||
|
action: "View"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bookingId: "WA234590",
|
||||||
|
passId: "PRB7678",
|
||||||
|
customerName: "Grace Lopez",
|
||||||
|
cardType: "Unlimited",
|
||||||
|
result: "Failed",
|
||||||
|
staff: "Cameron D.",
|
||||||
|
action: "View"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const itemsPerPage = 12;
|
||||||
|
const totalPages = Math.ceil(redemptionData.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentData = redemptionData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const getResultBadge = (result: string) => {
|
||||||
|
return result === "Success"
|
||||||
|
? <Badge className="bg-green-100 text-green-800 hover:bg-green-100">Success</Badge>
|
||||||
|
: <Badge className="bg-red-100 text-red-800 hover:bg-red-100">Failed</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCardTypeBadge = (cardType: string) => {
|
||||||
|
return cardType === "Unlimited"
|
||||||
|
? <Badge variant="secondary" className="bg-blue-100 text-blue-800 hover:bg-blue-100">Unlimited</Badge>
|
||||||
|
: <Badge variant="secondary" className="bg-purple-100 text-purple-800 hover:bg-purple-100">Selective</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewCustomer = (customer: any) => {
|
||||||
|
setSelectedCustomer(customer);
|
||||||
|
setShowDetailView(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackToList = () => {
|
||||||
|
setShowDetailView(false);
|
||||||
|
setSelectedCustomer(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If showing detail view, render the CustomerDetailView component
|
||||||
|
if (showDetailView && selectedCustomer) {
|
||||||
|
return (
|
||||||
|
<CustomerDetailView
|
||||||
|
customerData={selectedCustomer}
|
||||||
|
onBack={handleBackToList}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||||
|
className="min-h-screen bg-gray-50 p-6"
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto h-full flex flex-col">
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Redemption Logs</h1>
|
||||||
|
<p className="text-base text-gray-600 mt-2">Track and monitor all card redemption activities and results</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Bar with Buttons */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by booking ID, customer name, or staff..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 bg-white border border-gray-200 rounded-lg h-10 font-normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 bg-white border-gray-200 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-2 bg-gray-800 hover:bg-gray-900 text-white"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
Export Logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Section */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden flex-1 flex flex-col">
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<div className="overflow-y-auto" style={{ height: 'calc(100vh - 280px)' }}>
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="sticky top-0 z-10">
|
||||||
|
<TableRow className="border-b border-gray-200 bg-gray-50">
|
||||||
|
<TableHead className="font-medium text-gray-900 py-4 text-left w-[120px]">Booking ID</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-left w-[100px]">Pass ID</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-left w-[160px]">Customer Name</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[120px]">Card Type</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[100px]">Result</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-left w-[100px]">Staff</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[100px]">Action</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{currentData.map((item, index) => (
|
||||||
|
<motion.tr
|
||||||
|
key={item.bookingId}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="border-b border-gray-100 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<TableCell className="font-medium text-gray-900 py-4 text-left w-[120px]">{item.bookingId}</TableCell>
|
||||||
|
<TableCell className="text-gray-600 text-left w-[100px]">{item.passId}</TableCell>
|
||||||
|
<TableCell className="text-gray-900 text-left w-[160px]">{item.customerName}</TableCell>
|
||||||
|
<TableCell className="text-center w-[120px]">{getCardTypeBadge(item.cardType)}</TableCell>
|
||||||
|
<TableCell className="text-center w-[100px]">{getResultBadge(item.result)}</TableCell>
|
||||||
|
<TableCell className="text-gray-600 text-left w-[100px]">{item.staff}</TableCell>
|
||||||
|
<TableCell className="text-center w-[100px]">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewCustomer(item)}
|
||||||
|
className="flex items-center gap-1 text-blue-600 hover:text-blue-700 hover:bg-blue-50 px-2 py-1"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</motion.tr>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 bg-white border-t border-gray-200">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Showing {startIndex + 1} to {Math.min(endIndex, redemptionData.length)} of {redemptionData.length} entries
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => (
|
||||||
|
<Button
|
||||||
|
key={i + 1}
|
||||||
|
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
|
className={`w-8 h-8 p-0 ${
|
||||||
|
currentPage === i + 1
|
||||||
|
? 'bg-gray-900 text-white hover:bg-gray-800'
|
||||||
|
: 'bg-white text-gray-600 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
225
src/components/Sidebar.tsx
Normal file
225
src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { Home, Calendar, Users, CreditCard, HelpCircle, LogOut, ChevronDown, Settings, CalendarDays, Table2 } from "lucide-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
activeItem: string;
|
||||||
|
onItemSelect: (item: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Sidebar({ activeItem, onItemSelect }: SidebarProps) {
|
||||||
|
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||||
|
const [isBookingExpanded, setIsBookingExpanded] = useState(activeItem === 'booking-table' || activeItem === 'booking-calendar');
|
||||||
|
|
||||||
|
const navigationItems = [
|
||||||
|
{ id: 'dashboard', label: 'Dashboard', icon: Home },
|
||||||
|
{
|
||||||
|
id: 'booking-management',
|
||||||
|
label: 'Booking Management',
|
||||||
|
icon: Calendar,
|
||||||
|
subItems: [
|
||||||
|
{ id: 'booking-calendar', label: 'Calendar View', icon: CalendarDays },
|
||||||
|
{ id: 'booking-table', label: 'Table View', icon: Table2 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ id: 'staff', label: 'Staff Management', icon: Users },
|
||||||
|
{ id: 'redemptions', label: 'Redemptions', icon: CreditCard },
|
||||||
|
{ id: 'support', label: 'Support', icon: HelpCircle },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed left-0 top-0 w-[280px] h-full bg-white border-r border-gray-200 flex flex-col z-50">
|
||||||
|
{/* Logo Section */}
|
||||||
|
<div className="p-6 border-b border-gray-100">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 bg-gradient-to-br from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
|
||||||
|
<span className="text-white font-bold text-sm">CC</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-bold text-gray-900">CityCards</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Items */}
|
||||||
|
<nav className="flex-1 p-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{navigationItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const hasActiveSubItem = item.subItems && item.subItems.some(sub => activeItem === sub.id);
|
||||||
|
const isDirectlyActive = activeItem === item.id;
|
||||||
|
const isExpanded = item.id === 'booking-management' ? isBookingExpanded : false;
|
||||||
|
const hasSubItems = item.subItems && item.subItems.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.id} className={`${
|
||||||
|
hasActiveSubItem || isDirectlyActive
|
||||||
|
? 'bg-slate-800 rounded-lg shadow-sm mx-2'
|
||||||
|
: isExpanded && hasSubItems
|
||||||
|
? 'bg-gray-100 rounded-lg mx-2'
|
||||||
|
: ''
|
||||||
|
}`}>
|
||||||
|
<motion.button
|
||||||
|
onClick={() => {
|
||||||
|
if (hasSubItems) {
|
||||||
|
if (item.id === 'booking-management') {
|
||||||
|
setIsBookingExpanded(!isBookingExpanded);
|
||||||
|
// Default to table view when expanding
|
||||||
|
if (!isBookingExpanded) {
|
||||||
|
onItemSelect('booking-table');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onItemSelect(item.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center gap-3 px-3 py-3 text-left transition-all duration-200 ${
|
||||||
|
hasActiveSubItem || isDirectlyActive
|
||||||
|
? 'text-white'
|
||||||
|
: isExpanded && hasSubItems
|
||||||
|
? 'text-gray-700'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 rounded-lg mx-2'
|
||||||
|
}`}
|
||||||
|
whileHover={{ x: (hasActiveSubItem || isDirectlyActive) ? 0 : 4 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Icon className={`h-5 w-5 ${
|
||||||
|
hasActiveSubItem
|
||||||
|
? 'text-white'
|
||||||
|
: isDirectlyActive
|
||||||
|
? 'text-white'
|
||||||
|
: isExpanded && hasSubItems
|
||||||
|
? 'text-gray-600'
|
||||||
|
: 'text-gray-500'
|
||||||
|
}`} />
|
||||||
|
<span className="font-medium">{item.label}</span>
|
||||||
|
|
||||||
|
{/* Expand/Collapse arrow for sub-items */}
|
||||||
|
{hasSubItems && (
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: isExpanded ? 90 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
<ChevronDown className={`h-4 w-4 ${
|
||||||
|
hasActiveSubItem
|
||||||
|
? 'text-white'
|
||||||
|
: isDirectlyActive
|
||||||
|
? 'text-white'
|
||||||
|
: isExpanded && hasSubItems
|
||||||
|
? 'text-gray-600'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`} />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Sub-items */}
|
||||||
|
{hasSubItems && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
height: isExpanded ? 'auto' : 0,
|
||||||
|
opacity: isExpanded ? 1 : 0
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="px-4 pt-1 pb-3 space-y-2">
|
||||||
|
{item.subItems.map((subItem) => {
|
||||||
|
const SubIcon = subItem.icon;
|
||||||
|
const isSubActive = activeItem === subItem.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={subItem.id}
|
||||||
|
onClick={() => onItemSelect(subItem.id)}
|
||||||
|
className={`w-full flex items-center px-3 py-2 text-left transition-all duration-200 ${
|
||||||
|
isSubActive
|
||||||
|
? 'bg-white text-gray-900 rounded-lg shadow-sm'
|
||||||
|
: hasActiveSubItem
|
||||||
|
? 'text-gray-300 hover:text-white hover:bg-slate-700/50 rounded-lg'
|
||||||
|
: 'text-gray-600 hover:text-gray-800 hover:bg-gray-200/50 rounded-lg'
|
||||||
|
}`}
|
||||||
|
whileHover={{ x: isSubActive ? 0 : 2 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full flex-shrink-0 mr-3">
|
||||||
|
{isSubActive && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
className="w-full h-full bg-blue-500 rounded-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">{subItem.label}</span>
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Profile Section */}
|
||||||
|
<div className="p-4 border-t border-gray-100">
|
||||||
|
<motion.button
|
||||||
|
onClick={() => setIsProfileOpen(!isProfileOpen)}
|
||||||
|
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
<AvatarImage src="/api/placeholder/40/40" alt="Profile" />
|
||||||
|
<AvatarFallback className="bg-gradient-to-br from-blue-600 to-purple-600 text-white">
|
||||||
|
KA
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
<p className="font-medium text-gray-900">Kassandra</p>
|
||||||
|
<p className="text-sm text-gray-500">Admin</p>
|
||||||
|
</div>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: isProfileOpen ? 180 : 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4 text-gray-400" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Profile Dropdown */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
height: isProfileOpen ? 'auto' : 0,
|
||||||
|
opacity: isProfileOpen ? 1 : 0
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="pt-2 space-y-1">
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2 text-left text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 text-gray-500" />
|
||||||
|
<span className="text-sm">Account Settings</span>
|
||||||
|
</motion.button>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ x: 4 }}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2 text-left text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4 text-red-500" />
|
||||||
|
<span className="text-sm">Sign Out</span>
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
419
src/components/SlotDetailPanel.tsx
Normal file
419
src/components/SlotDetailPanel.tsx
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { X, Edit, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Textarea } from "./ui/textarea";
|
||||||
|
|
||||||
|
interface SlotDetailPanelProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
slotData: {
|
||||||
|
attractionName: string;
|
||||||
|
date: string;
|
||||||
|
timeSlots: string[];
|
||||||
|
capacity: number;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SlotDetailPanel({ isOpen, onClose, slotData }: SlotDetailPanelProps) {
|
||||||
|
const [editingAttraction, setEditingAttraction] = useState(false);
|
||||||
|
const [editingDate, setEditingDate] = useState(false);
|
||||||
|
const [editingCapacity, setEditingCapacity] = useState(false);
|
||||||
|
const [attractionName, setAttractionName] = useState("");
|
||||||
|
const [selectedDate, setSelectedDate] = useState("");
|
||||||
|
const [capacity, setCapacity] = useState(0);
|
||||||
|
const [selectedTimeSlots, setSelectedTimeSlots] = useState<string[]>([]);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [isBlockMode, setIsBlockMode] = useState(false);
|
||||||
|
const [notes, setNotes] = useState("");
|
||||||
|
|
||||||
|
// Initialize form data when slot data changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (slotData) {
|
||||||
|
setAttractionName(slotData.attractionName);
|
||||||
|
setSelectedDate(slotData.date);
|
||||||
|
setCapacity(slotData.capacity);
|
||||||
|
setSelectedTimeSlots(slotData.timeSlots);
|
||||||
|
setHasChanges(false);
|
||||||
|
setIsBlockMode(false);
|
||||||
|
setNotes("");
|
||||||
|
}
|
||||||
|
}, [slotData]);
|
||||||
|
|
||||||
|
// Track changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (slotData) {
|
||||||
|
const hasFormChanges =
|
||||||
|
attractionName !== slotData.attractionName ||
|
||||||
|
selectedDate !== slotData.date ||
|
||||||
|
capacity !== slotData.capacity ||
|
||||||
|
JSON.stringify(selectedTimeSlots) !== JSON.stringify(slotData.timeSlots);
|
||||||
|
setHasChanges(hasFormChanges);
|
||||||
|
}
|
||||||
|
}, [attractionName, selectedDate, capacity, selectedTimeSlots, slotData]);
|
||||||
|
|
||||||
|
const handleSaveChanges = () => {
|
||||||
|
// Here you would save the changes to your backend
|
||||||
|
console.log("Saving changes:", {
|
||||||
|
attractionName,
|
||||||
|
selectedDate,
|
||||||
|
capacity,
|
||||||
|
selectedTimeSlots
|
||||||
|
});
|
||||||
|
setHasChanges(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (slotData) {
|
||||||
|
setAttractionName(slotData.attractionName);
|
||||||
|
setSelectedDate(slotData.date);
|
||||||
|
setCapacity(slotData.capacity);
|
||||||
|
setSelectedTimeSlots(slotData.timeSlots);
|
||||||
|
}
|
||||||
|
setHasChanges(false);
|
||||||
|
setEditingAttraction(false);
|
||||||
|
setEditingDate(false);
|
||||||
|
setEditingCapacity(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlockTimeSlot = () => {
|
||||||
|
setIsBlockMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmBlock = () => {
|
||||||
|
// Handle actual blocking logic here
|
||||||
|
console.log("Confirmed blocking time slot");
|
||||||
|
setIsBlockMode(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelBlock = () => {
|
||||||
|
setIsBlockMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableTimeSlots = [
|
||||||
|
"10:30AM - 1:30PM",
|
||||||
|
"1:30PM - 3:30PM",
|
||||||
|
"3:30PM - 5:30PM",
|
||||||
|
"5:30PM - 7:30PM"
|
||||||
|
];
|
||||||
|
|
||||||
|
const toggleTimeSlot = (timeSlot: string) => {
|
||||||
|
setSelectedTimeSlots(prev =>
|
||||||
|
prev.includes(timeSlot)
|
||||||
|
? prev.filter(slot => slot !== timeSlot)
|
||||||
|
: [...prev, timeSlot]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!slotData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="fixed inset-0 bg-black/20 z-40"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Panel */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: "100%" }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: "100%" }}
|
||||||
|
transition={{ type: "spring", damping: 30, stiffness: 300 }}
|
||||||
|
className="fixed top-0 right-0 h-full w-96 bg-white shadow-2xl z-50 flex flex-col"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">
|
||||||
|
{isBlockMode ? "Block Time Slot" : "Selected Slot"}
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{!isBlockMode && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBlockTimeSlot}
|
||||||
|
className="text-red-600 border-red-200 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Block Time Slot
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={isBlockMode ? handleCancelBlock : onClose}
|
||||||
|
className="p-1 h-8 w-8"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||||
|
{isBlockMode ? (
|
||||||
|
// Block Mode Content
|
||||||
|
<>
|
||||||
|
{/* Name of attraction */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium text-gray-900">Name of attraction</Label>
|
||||||
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="text-gray-900">{attractionName}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="p-1 h-6 w-6 text-gray-500 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="p-1 h-6 w-6 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timing Slot */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium text-gray-900">Timing Slot</Label>
|
||||||
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="text-gray-900">10:00AM - 9:00PM</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Total Capacity */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium text-gray-900">Total Capacity</Label>
|
||||||
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="text-gray-900">{capacity}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium text-gray-900">Notes</Label>
|
||||||
|
<Textarea
|
||||||
|
value={notes}
|
||||||
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
|
placeholder="..."
|
||||||
|
className="bg-gray-50 border border-gray-200 rounded-lg"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Edit Mode Content
|
||||||
|
<>
|
||||||
|
{/* Attraction Name */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium text-gray-900">Attraction Name</Label>
|
||||||
|
{editingAttraction ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={attractionName}
|
||||||
|
onChange={(e) => setAttractionName(e.target.value)}
|
||||||
|
onBlur={() => setEditingAttraction(false)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") setEditingAttraction(false);
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setAttractionName(slotData.attractionName);
|
||||||
|
setEditingAttraction(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="text-gray-900">{attractionName}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEditingAttraction(true)}
|
||||||
|
className="p-1 h-6 w-6"
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time Slots Available */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-sm font-medium text-gray-900">Time Slots Available</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{availableTimeSlots.map((timeSlot) => (
|
||||||
|
<Button
|
||||||
|
key={timeSlot}
|
||||||
|
variant={selectedTimeSlots.includes(timeSlot) ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleTimeSlot(timeSlot)}
|
||||||
|
className={`text-xs h-8 ${
|
||||||
|
selectedTimeSlots.includes(timeSlot)
|
||||||
|
? "bg-gray-900 text-white hover:bg-gray-800"
|
||||||
|
: "text-gray-600 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{timeSlot}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs h-8 text-gray-600 hover:text-gray-900 bg-gray-600 text-white hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3 mr-1" />
|
||||||
|
Add Timing
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Capacity Used by Online Bookings */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-sm font-medium text-gray-900">Capacity Used by Online Bookings</Label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{editingCapacity ? (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={capacity}
|
||||||
|
onChange={(e) => setCapacity(parseInt(e.target.value) || 0)}
|
||||||
|
onBlur={() => setEditingCapacity(false)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") setEditingCapacity(false);
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setCapacity(slotData.capacity);
|
||||||
|
setEditingCapacity(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
className="w-20"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-2xl font-medium text-gray-900 bg-gray-50 px-3 py-2 rounded-lg min-w-[60px] text-center">
|
||||||
|
{capacity}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEditingCapacity(true)}
|
||||||
|
className="bg-gray-600 text-white hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
Edit Capacity
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium text-gray-900">Date</Label>
|
||||||
|
{editingDate ? (
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={(e) => setSelectedDate(e.target.value)}
|
||||||
|
onBlur={() => setEditingDate(false)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") setEditingDate(false);
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setSelectedDate(slotData.date);
|
||||||
|
setEditingDate(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||||
|
<span className="text-gray-900">{selectedDate}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setEditingDate(true)}
|
||||||
|
className="p-1 h-6 w-6"
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer with buttons */}
|
||||||
|
{(hasChanges && !isBlockMode) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 100, opacity: 0 }}
|
||||||
|
className="p-6 border-t border-gray-200 bg-white"
|
||||||
|
>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveChanges}
|
||||||
|
className="flex-1 bg-gray-900 hover:bg-gray-800 text-white"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Block Mode Footer */}
|
||||||
|
{isBlockMode && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 100, opacity: 0 }}
|
||||||
|
className="p-6 border-t border-gray-200 bg-white"
|
||||||
|
>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirmBlock}
|
||||||
|
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
>
|
||||||
|
Block
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancelBlock}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
636
src/components/StaffManagementPage.tsx
Normal file
636
src/components/StaffManagementPage.tsx
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
import { Search, UserPlus, Filter, Edit, ChevronLeft, ChevronRight, X } from "lucide-react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
import { Switch } from "./ui/switch";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function StaffManagementPage() {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [isEditPanelOpen, setIsEditPanelOpen] = useState(false);
|
||||||
|
const [isAddPanelOpen, setIsAddPanelOpen] = useState(false);
|
||||||
|
const [editingStaff, setEditingStaff] = useState(null);
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
fullName: "",
|
||||||
|
phone: "",
|
||||||
|
role: "",
|
||||||
|
staff: ""
|
||||||
|
});
|
||||||
|
const [addForm, setAddForm] = useState({
|
||||||
|
fullName: "",
|
||||||
|
phone: "",
|
||||||
|
role: "",
|
||||||
|
staff: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock data for staff management
|
||||||
|
const [staffData, setStaffData] = useState([
|
||||||
|
{
|
||||||
|
idNo: "WA23456",
|
||||||
|
fullName: "Lily Hart",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Scanner",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Nancy B.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23457",
|
||||||
|
fullName: "Maya Thompson",
|
||||||
|
phone: "(+971) 050 4332 564",
|
||||||
|
role: "Manager",
|
||||||
|
status: "Inactive",
|
||||||
|
staff: "Taylor A.",
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23458",
|
||||||
|
fullName: "Sophie Turner",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Scanner",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Morgan S.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23459",
|
||||||
|
fullName: "Ella Johnson",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Manager",
|
||||||
|
status: "Inactive",
|
||||||
|
staff: "Casey J.",
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23460",
|
||||||
|
fullName: "Chloe Bennett",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Scanner",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Riley K.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23461",
|
||||||
|
fullName: "Zoe Adams",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Manager",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Jamie L.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23462",
|
||||||
|
fullName: "Ava Martinez",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Scanner",
|
||||||
|
status: "Inactive",
|
||||||
|
staff: "Alex H.",
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23463",
|
||||||
|
fullName: "Isabella Lee",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Manager",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Dylan O.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23464",
|
||||||
|
fullName: "Emma Watson",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Scanner",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Sam R.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23465",
|
||||||
|
fullName: "Grace Miller",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Manager",
|
||||||
|
status: "Inactive",
|
||||||
|
staff: "Charlie M.",
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23466",
|
||||||
|
fullName: "Olivia Brown",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Scanner",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Parker N.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23467",
|
||||||
|
fullName: "Amelia Davis",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Manager",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Avery P.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23468",
|
||||||
|
fullName: "Harper Wilson",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Scanner",
|
||||||
|
status: "Inactive",
|
||||||
|
staff: "River Q.",
|
||||||
|
isActive: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23469",
|
||||||
|
fullName: "Evelyn Moore",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Manager",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Quinn S.",
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
idNo: "WA23470",
|
||||||
|
fullName: "Abigail Taylor",
|
||||||
|
phone: "(+971) 050 4245 564",
|
||||||
|
role: "Scanner",
|
||||||
|
status: "Active",
|
||||||
|
staff: "Sage T.",
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const itemsPerPage = 12;
|
||||||
|
const totalPages = Math.ceil(staffData.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const currentData = staffData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
return status === "Active"
|
||||||
|
? <Badge className="bg-green-100 text-green-800 hover:bg-green-100">Active</Badge>
|
||||||
|
: <Badge className="bg-red-100 text-red-800 hover:bg-red-100">Inactive</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoleBadge = (role: string) => {
|
||||||
|
return role === "Manager"
|
||||||
|
? <Badge variant="secondary" className="bg-blue-100 text-blue-800 hover:bg-blue-100">Manager</Badge>
|
||||||
|
: <Badge variant="secondary" className="bg-purple-100 text-purple-800 hover:bg-purple-100">Scanner</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleStatus = (idNo: string) => {
|
||||||
|
setStaffData(prevData =>
|
||||||
|
prevData.map(staff =>
|
||||||
|
staff.idNo === idNo
|
||||||
|
? {
|
||||||
|
...staff,
|
||||||
|
isActive: !staff.isActive,
|
||||||
|
status: !staff.isActive ? "Active" : "Inactive"
|
||||||
|
}
|
||||||
|
: staff
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditStaff = (idNo: string) => {
|
||||||
|
const staffMember = staffData.find(staff => staff.idNo === idNo);
|
||||||
|
if (staffMember) {
|
||||||
|
setEditingStaff(staffMember);
|
||||||
|
setEditForm({
|
||||||
|
fullName: staffMember.fullName,
|
||||||
|
phone: staffMember.phone,
|
||||||
|
role: staffMember.role,
|
||||||
|
staff: staffMember.staff
|
||||||
|
});
|
||||||
|
setIsEditPanelOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseEditPanel = () => {
|
||||||
|
setIsEditPanelOpen(false);
|
||||||
|
setEditingStaff(null);
|
||||||
|
setEditForm({
|
||||||
|
fullName: "",
|
||||||
|
phone: "",
|
||||||
|
role: "",
|
||||||
|
staff: ""
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateProfile = () => {
|
||||||
|
if (editingStaff) {
|
||||||
|
setStaffData(prevData =>
|
||||||
|
prevData.map(staff =>
|
||||||
|
staff.idNo === editingStaff.idNo
|
||||||
|
? {
|
||||||
|
...staff,
|
||||||
|
fullName: editForm.fullName,
|
||||||
|
phone: editForm.phone,
|
||||||
|
role: editForm.role,
|
||||||
|
staff: editForm.staff
|
||||||
|
}
|
||||||
|
: staff
|
||||||
|
)
|
||||||
|
);
|
||||||
|
handleCloseEditPanel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddStaff = () => {
|
||||||
|
setAddForm({
|
||||||
|
fullName: "",
|
||||||
|
phone: "",
|
||||||
|
role: "",
|
||||||
|
staff: ""
|
||||||
|
});
|
||||||
|
setIsAddPanelOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseAddPanel = () => {
|
||||||
|
setIsAddPanelOpen(false);
|
||||||
|
setAddForm({
|
||||||
|
fullName: "",
|
||||||
|
phone: "",
|
||||||
|
role: "",
|
||||||
|
staff: ""
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateNewIdNo = () => {
|
||||||
|
const lastId = Math.max(...staffData.map(staff => parseInt(staff.idNo.substring(2))));
|
||||||
|
return `WA${lastId + 1}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateStaff = () => {
|
||||||
|
const newStaff = {
|
||||||
|
idNo: generateNewIdNo(),
|
||||||
|
fullName: addForm.fullName,
|
||||||
|
phone: addForm.phone,
|
||||||
|
role: addForm.role,
|
||||||
|
status: "Active",
|
||||||
|
staff: addForm.staff,
|
||||||
|
isActive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
setStaffData(prevData => [...prevData, newStaff]);
|
||||||
|
handleCloseAddPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||||
|
className="min-h-screen bg-gray-50 p-6"
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto h-full flex">
|
||||||
|
{/* Edit Panel */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isEditPanelOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: -400, opacity: 0 }}
|
||||||
|
animate={{ x: 0, opacity: 1 }}
|
||||||
|
exit={{ x: -400, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
className="w-80 bg-white rounded-lg shadow-lg border border-gray-200 mr-6 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<div className="p-6 h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">Edit Details</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCloseEditPanel}
|
||||||
|
className="h-8 w-8 p-0 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 flex-1">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-name" className="text-sm font-medium text-gray-900">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-name"
|
||||||
|
type="text"
|
||||||
|
value={editForm.fullName}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, fullName: e.target.value }))}
|
||||||
|
className="bg-gray-50 border-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-phone" className="text-sm font-medium text-gray-900">
|
||||||
|
Phone
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-phone"
|
||||||
|
type="text"
|
||||||
|
value={editForm.phone}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, phone: e.target.value }))}
|
||||||
|
className="bg-gray-50 border-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-role" className="text-sm font-medium text-gray-900">
|
||||||
|
Role
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={editForm.role}
|
||||||
|
onValueChange={(value) => setEditForm(prev => ({ ...prev, role: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-gray-50 border-gray-200">
|
||||||
|
<SelectValue placeholder="Select role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Manager">Manager</SelectItem>
|
||||||
|
<SelectItem value="Scanner">Scanner</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-staff" className="text-sm font-medium text-gray-900">
|
||||||
|
Staff
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-staff"
|
||||||
|
type="text"
|
||||||
|
value={editForm.staff}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, staff: e.target.value }))}
|
||||||
|
className="bg-gray-50 border-gray-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleUpdateProfile}
|
||||||
|
className="w-full mt-6 bg-gray-800 hover:bg-gray-900 text-white"
|
||||||
|
>
|
||||||
|
Update Profile
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Add Staff Panel */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isAddPanelOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: -400, opacity: 0 }}
|
||||||
|
animate={{ x: 0, opacity: 1 }}
|
||||||
|
exit={{ x: -400, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
className="w-80 bg-white rounded-lg shadow-lg border border-gray-200 mr-6 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<div className="p-6 h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900">Add New Staff</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCloseAddPanel}
|
||||||
|
className="h-8 w-8 p-0 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 flex-1">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="add-name" className="text-sm font-medium text-gray-900">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="add-name"
|
||||||
|
type="text"
|
||||||
|
value={addForm.fullName}
|
||||||
|
onChange={(e) => setAddForm(prev => ({ ...prev, fullName: e.target.value }))}
|
||||||
|
className="bg-gray-50 border-gray-200"
|
||||||
|
placeholder="Enter full name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="add-phone" className="text-sm font-medium text-gray-900">
|
||||||
|
Phone
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="add-phone"
|
||||||
|
type="text"
|
||||||
|
value={addForm.phone}
|
||||||
|
onChange={(e) => setAddForm(prev => ({ ...prev, phone: e.target.value }))}
|
||||||
|
className="bg-gray-50 border-gray-200"
|
||||||
|
placeholder="Enter phone number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="add-role" className="text-sm font-medium text-gray-900">
|
||||||
|
Role
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={addForm.role}
|
||||||
|
onValueChange={(value) => setAddForm(prev => ({ ...prev, role: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-gray-50 border-gray-200">
|
||||||
|
<SelectValue placeholder="Select role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Manager">Manager</SelectItem>
|
||||||
|
<SelectItem value="Scanner">Scanner</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="add-staff" className="text-sm font-medium text-gray-900">
|
||||||
|
Staff
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="add-staff"
|
||||||
|
type="text"
|
||||||
|
value={addForm.staff}
|
||||||
|
onChange={(e) => setAddForm(prev => ({ ...prev, staff: e.target.value }))}
|
||||||
|
className="bg-gray-50 border-gray-200"
|
||||||
|
placeholder="Enter staff initials"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateStaff}
|
||||||
|
disabled={!addForm.fullName || !addForm.phone || !addForm.role || !addForm.staff}
|
||||||
|
className="w-full mt-6 bg-gray-800 hover:bg-gray-900 text-white disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Create Staff
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">Staff Management</h1>
|
||||||
|
<p className="text-base text-gray-600 mt-2">Manage staff members, roles, and access permissions</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Bar with Buttons */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by ID, name, phone, or role..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 bg-white border border-gray-200 rounded-lg h-10 font-normal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 bg-white border-gray-200 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddStaff}
|
||||||
|
className="flex items-center gap-2 bg-gray-800 hover:bg-gray-900 text-white"
|
||||||
|
>
|
||||||
|
<UserPlus className="h-4 w-4" />
|
||||||
|
Add Staff
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table Section */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden flex-1 flex flex-col">
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<div className="overflow-y-auto" style={{ height: 'calc(100vh - 280px)' }}>
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="sticky top-0 z-10">
|
||||||
|
<TableRow className="border-b border-gray-200 bg-gray-50">
|
||||||
|
<TableHead className="font-medium text-gray-900 py-4 text-center w-[100px]">ID No.</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[160px]">Full Name</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[140px]">Phone</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[100px]">Role</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[100px]">Status</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[100px]">Staff</TableHead>
|
||||||
|
<TableHead className="font-medium text-gray-900 text-center w-[140px]">Action</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{currentData.map((item, index) => (
|
||||||
|
<motion.tr
|
||||||
|
key={item.idNo}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="border-b border-gray-100 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<TableCell className="font-medium text-gray-900 py-4 text-center w-[100px]">{item.idNo}</TableCell>
|
||||||
|
<TableCell className="text-gray-900 text-center w-[160px]">{item.fullName}</TableCell>
|
||||||
|
<TableCell className="text-gray-600 text-center w-[140px]">{item.phone}</TableCell>
|
||||||
|
<TableCell className="text-center w-[100px]">{getRoleBadge(item.role)}</TableCell>
|
||||||
|
<TableCell className="text-center w-[100px]">{getStatusBadge(item.status)}</TableCell>
|
||||||
|
<TableCell className="text-gray-600 text-center w-[100px]">{item.staff}</TableCell>
|
||||||
|
<TableCell className="text-center w-[140px]">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleEditStaff(item.idNo)}
|
||||||
|
className="flex items-center gap-1 text-blue-600 hover:text-blue-700 hover:bg-blue-50 px-2 py-1"
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={item.isActive}
|
||||||
|
onCheckedChange={() => handleToggleStatus(item.idNo)}
|
||||||
|
className="data-[state=checked]:bg-green-600"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-600 min-w-[45px]">
|
||||||
|
{item.isActive ? "Active" : "Inactive"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</motion.tr>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 bg-white border-t border-gray-200">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Showing {startIndex + 1} to {Math.min(endIndex, staffData.length)} of {staffData.length} entries
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => (
|
||||||
|
<Button
|
||||||
|
key={i + 1}
|
||||||
|
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
|
className={`w-8 h-8 p-0 ${
|
||||||
|
currentPage === i + 1
|
||||||
|
? 'bg-gray-900 text-white hover:bg-gray-800'
|
||||||
|
: 'bg-white text-gray-600 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
170
src/components/SupportPage.tsx
Normal file
170
src/components/SupportPage.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Upload, Mail, Phone } from "lucide-react";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Input } from "./ui/input";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Textarea } from "./ui/textarea";
|
||||||
|
|
||||||
|
export default function SupportPage() {
|
||||||
|
const [subject, setSubject] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
setSelectedFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Handle form submission logic here
|
||||||
|
console.log("Support ticket submitted:", { subject, description, file: selectedFile });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">Support</h1>
|
||||||
|
<p className="text-base text-gray-600">
|
||||||
|
Need help? We're here for you. Raise a ticket and our support team will get back to you shortly
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Support Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Subject Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="subject" className="text-base font-medium text-gray-900">
|
||||||
|
Subject
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="subject"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter subject"
|
||||||
|
value={subject}
|
||||||
|
onChange={(e) => setSubject(e.target.value)}
|
||||||
|
className="w-full h-12 bg-white border border-gray-200 rounded-lg px-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description" className="text-base font-medium text-gray-900">
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
placeholder="Please describe your issue in detail..."
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className="w-full min-h-[120px] bg-white border border-gray-200 rounded-lg px-4 py-3 resize-none"
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-base font-medium text-gray-900">
|
||||||
|
File Upload
|
||||||
|
</Label>
|
||||||
|
<div className="border-2 border-dashed border-gray-200 rounded-lg p-6 bg-white">
|
||||||
|
<div className="text-center">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="file-upload"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.txt"
|
||||||
|
/>
|
||||||
|
<label htmlFor="file-upload" className="cursor-pointer">
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<Upload className="h-8 w-8 text-gray-400" />
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{selectedFile ? (
|
||||||
|
<span className="font-medium">{selectedFile.name}</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="font-medium text-blue-600 hover:text-blue-700">
|
||||||
|
Upload file
|
||||||
|
</span>
|
||||||
|
<span> or drag and drop</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!selectedFile && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
PNG, JPG, GIF, PDF up to 10MB
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Details */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">Contact Details</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Email */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email" className="text-base font-medium text-gray-900">
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value="support@cityads.com"
|
||||||
|
readOnly
|
||||||
|
className="pl-10 h-12 bg-gray-50 border border-gray-200 rounded-lg text-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Phone */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="phone" className="text-base font-medium text-gray-900">
|
||||||
|
Phone
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||||
|
<Input
|
||||||
|
id="phone"
|
||||||
|
type="tel"
|
||||||
|
value="(437) 040 260 6843"
|
||||||
|
readOnly
|
||||||
|
className="pl-10 h-12 bg-gray-50 border border-gray-200 rounded-lg text-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full h-12 bg-gray-900 hover:bg-gray-800 text-white rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
Submit Ticket
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/components/figma/ImageWithFallback.tsx
Normal file
27
src/components/figma/ImageWithFallback.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
const ERROR_IMG_SRC =
|
||||||
|
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
|
||||||
|
|
||||||
|
export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
|
||||||
|
const [didError, setDidError] = useState(false)
|
||||||
|
|
||||||
|
const handleError = () => {
|
||||||
|
setDidError(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { src, alt, style, className, ...rest } = props
|
||||||
|
|
||||||
|
return didError ? (
|
||||||
|
<div
|
||||||
|
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center w-full h-full">
|
||||||
|
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
|
||||||
|
)
|
||||||
|
}
|
||||||
66
src/components/ui/accordion.tsx
Normal file
66
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion@1.2.3";
|
||||||
|
import { ChevronDownIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||||
157
src/components/ui/alert-dialog.tsx
Normal file
157
src/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog@1.1.6";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import { buttonVariants } from "./button";
|
||||||
|
|
||||||
|
function AlertDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogAction({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogCancel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
};
|
||||||
66
src/components/ui/alert.tsx
Normal file
66
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription };
|
||||||
11
src/components/ui/aspect-ratio.tsx
Normal file
11
src/components/ui/aspect-ratio.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio@1.1.2";
|
||||||
|
|
||||||
|
function AspectRatio({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||||
|
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AspectRatio };
|
||||||
53
src/components/ui/avatar.tsx
Normal file
53
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as AvatarPrimitive from "@radix-ui/react-avatar@1.1.3";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn("aspect-square size-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
109
src/components/ui/breadcrumb.tsx
Normal file
109
src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
data-slot="breadcrumb-list"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-item"
|
||||||
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbLink({
|
||||||
|
asChild,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="breadcrumb-link"
|
||||||
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("text-foreground font-normal", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparator({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
};
|
||||||
58
src/components/ui/button.tsx
Normal file
58
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9 rounded-md",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Button = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
75
src/components/ui/calendar.tsx
Normal file
75
src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react@0.487.0";
|
||||||
|
import { DayPicker } from "react-day-picker@8.10.1";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import { buttonVariants } from "./button";
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayPicker>) {
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn("p-3", className)}
|
||||||
|
classNames={{
|
||||||
|
months: "flex flex-col sm:flex-row gap-2",
|
||||||
|
month: "flex flex-col gap-4",
|
||||||
|
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||||
|
caption_label: "text-sm font-medium",
|
||||||
|
nav: "flex items-center gap-1",
|
||||||
|
nav_button: cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||||
|
),
|
||||||
|
nav_button_previous: "absolute left-1",
|
||||||
|
nav_button_next: "absolute right-1",
|
||||||
|
table: "w-full border-collapse space-x-1",
|
||||||
|
head_row: "flex",
|
||||||
|
head_cell:
|
||||||
|
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||||
|
row: "flex w-full mt-2",
|
||||||
|
cell: cn(
|
||||||
|
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||||
|
props.mode === "range"
|
||||||
|
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||||
|
: "[&:has([aria-selected])]:rounded-md",
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
buttonVariants({ variant: "ghost" }),
|
||||||
|
"size-8 p-0 font-normal aria-selected:opacity-100",
|
||||||
|
),
|
||||||
|
day_range_start:
|
||||||
|
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||||
|
day_range_end:
|
||||||
|
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||||
|
day_selected:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||||
|
day_today: "bg-accent text-accent-foreground",
|
||||||
|
day_outside:
|
||||||
|
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
|
day_disabled: "text-muted-foreground opacity-50",
|
||||||
|
day_range_middle:
|
||||||
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||||
|
day_hidden: "invisible",
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
IconLeft: ({ className, ...props }) => (
|
||||||
|
<ChevronLeft className={cn("size-4", className)} {...props} />
|
||||||
|
),
|
||||||
|
IconRight: ({ className, ...props }) => (
|
||||||
|
<ChevronRight className={cn("size-4", className)} {...props} />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Calendar };
|
||||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<h4
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6 [&:last-child]:pb-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 pb-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
241
src/components/ui/carousel.tsx
Normal file
241
src/components/ui/carousel.tsx
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import useEmblaCarousel, {
|
||||||
|
type UseEmblaCarouselType,
|
||||||
|
} from "embla-carousel-react@8.6.0";
|
||||||
|
import { ArrowLeft, ArrowRight } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
|
type CarouselApi = UseEmblaCarouselType[1];
|
||||||
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
||||||
|
type CarouselOptions = UseCarouselParameters[0];
|
||||||
|
type CarouselPlugin = UseCarouselParameters[1];
|
||||||
|
|
||||||
|
type CarouselProps = {
|
||||||
|
opts?: CarouselOptions;
|
||||||
|
plugins?: CarouselPlugin;
|
||||||
|
orientation?: "horizontal" | "vertical";
|
||||||
|
setApi?: (api: CarouselApi) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CarouselContextProps = {
|
||||||
|
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
||||||
|
api: ReturnType<typeof useEmblaCarousel>[1];
|
||||||
|
scrollPrev: () => void;
|
||||||
|
scrollNext: () => void;
|
||||||
|
canScrollPrev: boolean;
|
||||||
|
canScrollNext: boolean;
|
||||||
|
} & CarouselProps;
|
||||||
|
|
||||||
|
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
||||||
|
|
||||||
|
function useCarousel() {
|
||||||
|
const context = React.useContext(CarouselContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useCarousel must be used within a <Carousel />");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Carousel({
|
||||||
|
orientation = "horizontal",
|
||||||
|
opts,
|
||||||
|
setApi,
|
||||||
|
plugins,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & CarouselProps) {
|
||||||
|
const [carouselRef, api] = useEmblaCarousel(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
axis: orientation === "horizontal" ? "x" : "y",
|
||||||
|
},
|
||||||
|
plugins,
|
||||||
|
);
|
||||||
|
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
||||||
|
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
||||||
|
|
||||||
|
const onSelect = React.useCallback((api: CarouselApi) => {
|
||||||
|
if (!api) return;
|
||||||
|
setCanScrollPrev(api.canScrollPrev());
|
||||||
|
setCanScrollNext(api.canScrollNext());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollPrev = React.useCallback(() => {
|
||||||
|
api?.scrollPrev();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const scrollNext = React.useCallback(() => {
|
||||||
|
api?.scrollNext();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (event.key === "ArrowLeft") {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollPrev();
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollNext();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollPrev, scrollNext],
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api || !setApi) return;
|
||||||
|
setApi(api);
|
||||||
|
}, [api, setApi]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!api) return;
|
||||||
|
onSelect(api);
|
||||||
|
api.on("reInit", onSelect);
|
||||||
|
api.on("select", onSelect);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
api?.off("select", onSelect);
|
||||||
|
};
|
||||||
|
}, [api, onSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CarouselContext.Provider
|
||||||
|
value={{
|
||||||
|
carouselRef,
|
||||||
|
api: api,
|
||||||
|
opts,
|
||||||
|
orientation:
|
||||||
|
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||||
|
scrollPrev,
|
||||||
|
scrollNext,
|
||||||
|
canScrollPrev,
|
||||||
|
canScrollNext,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
className={cn("relative", className)}
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
data-slot="carousel"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</CarouselContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const { carouselRef, orientation } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={carouselRef}
|
||||||
|
className="overflow-hidden"
|
||||||
|
data-slot="carousel-content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex",
|
||||||
|
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const { orientation } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
data-slot="carousel-item"
|
||||||
|
className={cn(
|
||||||
|
"min-w-0 shrink-0 grow-0 basis-full",
|
||||||
|
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CarouselPrevious({
|
||||||
|
className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
|
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-slot="carousel-previous"
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute size-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "top-1/2 -left-12 -translate-y-1/2"
|
||||||
|
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={!canScrollPrev}
|
||||||
|
onClick={scrollPrev}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowLeft />
|
||||||
|
<span className="sr-only">Previous slide</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CarouselNext({
|
||||||
|
className,
|
||||||
|
variant = "outline",
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
|
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-slot="carousel-next"
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={cn(
|
||||||
|
"absolute size-8 rounded-full",
|
||||||
|
orientation === "horizontal"
|
||||||
|
? "top-1/2 -right-12 -translate-y-1/2"
|
||||||
|
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={!canScrollNext}
|
||||||
|
onClick={scrollNext}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ArrowRight />
|
||||||
|
<span className="sr-only">Next slide</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
type CarouselApi,
|
||||||
|
Carousel,
|
||||||
|
CarouselContent,
|
||||||
|
CarouselItem,
|
||||||
|
CarouselPrevious,
|
||||||
|
CarouselNext,
|
||||||
|
};
|
||||||
353
src/components/ui/chart.tsx
Normal file
353
src/components/ui/chart.tsx
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as RechartsPrimitive from "recharts@2.15.2";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
const THEMES = { light: "", dark: ".dark" } as const;
|
||||||
|
|
||||||
|
export type ChartConfig = {
|
||||||
|
[k in string]: {
|
||||||
|
label?: React.ReactNode;
|
||||||
|
icon?: React.ComponentType;
|
||||||
|
} & (
|
||||||
|
| { color?: string; theme?: never }
|
||||||
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChartContextProps = {
|
||||||
|
config: ChartConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||||
|
|
||||||
|
function useChart() {
|
||||||
|
const context = React.useContext(ChartContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useChart must be used within a <ChartContainer />");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartContainer({
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
config,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
config: ChartConfig;
|
||||||
|
children: React.ComponentProps<
|
||||||
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
|
>["children"];
|
||||||
|
}) {
|
||||||
|
const uniqueId = React.useId();
|
||||||
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartContext.Provider value={{ config }}>
|
||||||
|
<div
|
||||||
|
data-slot="chart"
|
||||||
|
data-chart={chartId}
|
||||||
|
className={cn(
|
||||||
|
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChartStyle id={chartId} config={config} />
|
||||||
|
<RechartsPrimitive.ResponsiveContainer>
|
||||||
|
{children}
|
||||||
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</ChartContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
|
const colorConfig = Object.entries(config).filter(
|
||||||
|
([, config]) => config.theme || config.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!colorConfig.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: Object.entries(THEMES)
|
||||||
|
.map(
|
||||||
|
([theme, prefix]) => `
|
||||||
|
${prefix} [data-chart=${id}] {
|
||||||
|
${colorConfig
|
||||||
|
.map(([key, itemConfig]) => {
|
||||||
|
const color =
|
||||||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
|
itemConfig.color;
|
||||||
|
return color ? ` --color-${key}: ${color};` : null;
|
||||||
|
})
|
||||||
|
.join("\n")}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||||
|
|
||||||
|
function ChartTooltipContent({
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
className,
|
||||||
|
indicator = "dot",
|
||||||
|
hideLabel = false,
|
||||||
|
hideIndicator = false,
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
labelClassName,
|
||||||
|
formatter,
|
||||||
|
color,
|
||||||
|
nameKey,
|
||||||
|
labelKey,
|
||||||
|
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
hideLabel?: boolean;
|
||||||
|
hideIndicator?: boolean;
|
||||||
|
indicator?: "line" | "dot" | "dashed";
|
||||||
|
nameKey?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
}) {
|
||||||
|
const { config } = useChart();
|
||||||
|
|
||||||
|
const tooltipLabel = React.useMemo(() => {
|
||||||
|
if (hideLabel || !payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [item] = payload;
|
||||||
|
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
const value =
|
||||||
|
!labelKey && typeof label === "string"
|
||||||
|
? config[label as keyof typeof config]?.label || label
|
||||||
|
: itemConfig?.label;
|
||||||
|
|
||||||
|
if (labelFormatter) {
|
||||||
|
return (
|
||||||
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
|
{labelFormatter(value, payload)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||||
|
}, [
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
payload,
|
||||||
|
hideLabel,
|
||||||
|
labelClassName,
|
||||||
|
config,
|
||||||
|
labelKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!active || !payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!nestLabel ? tooltipLabel : null}
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{payload.map((item, index) => {
|
||||||
|
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
const indicatorColor = color || item.payload.fill || item.color;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.dataKey}
|
||||||
|
className={cn(
|
||||||
|
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||||
|
indicator === "dot" && "items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{itemConfig?.icon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
!hideIndicator && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||||
|
{
|
||||||
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
|
"w-1": indicator === "line",
|
||||||
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
|
indicator === "dashed",
|
||||||
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--color-bg": indicatorColor,
|
||||||
|
"--color-border": indicatorColor,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 justify-between leading-none",
|
||||||
|
nestLabel ? "items-end" : "items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{nestLabel ? tooltipLabel : null}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{itemConfig?.label || item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.value && (
|
||||||
|
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||||
|
{item.value.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartLegend = RechartsPrimitive.Legend;
|
||||||
|
|
||||||
|
function ChartLegendContent({
|
||||||
|
className,
|
||||||
|
hideIcon = false,
|
||||||
|
payload,
|
||||||
|
verticalAlign = "bottom",
|
||||||
|
nameKey,
|
||||||
|
}: React.ComponentProps<"div"> &
|
||||||
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
|
hideIcon?: boolean;
|
||||||
|
nameKey?: string;
|
||||||
|
}) {
|
||||||
|
const { config } = useChart();
|
||||||
|
|
||||||
|
if (!payload?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center gap-4",
|
||||||
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payload.map((item) => {
|
||||||
|
const key = `${nameKey || item.dataKey || "value"}`;
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={cn(
|
||||||
|
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{itemConfig?.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
|
function getPayloadConfigFromPayload(
|
||||||
|
config: ChartConfig,
|
||||||
|
payload: unknown,
|
||||||
|
key: string,
|
||||||
|
) {
|
||||||
|
if (typeof payload !== "object" || payload === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadPayload =
|
||||||
|
"payload" in payload &&
|
||||||
|
typeof payload.payload === "object" &&
|
||||||
|
payload.payload !== null
|
||||||
|
? payload.payload
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let configLabelKey: string = key;
|
||||||
|
|
||||||
|
if (
|
||||||
|
key in payload &&
|
||||||
|
typeof payload[key as keyof typeof payload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||||
|
} else if (
|
||||||
|
payloadPayload &&
|
||||||
|
key in payloadPayload &&
|
||||||
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payloadPayload[
|
||||||
|
key as keyof typeof payloadPayload
|
||||||
|
] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return configLabelKey in config
|
||||||
|
? config[configLabelKey]
|
||||||
|
: config[key as keyof typeof config];
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartStyle,
|
||||||
|
};
|
||||||
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox@1.1.4";
|
||||||
|
import { CheckIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer border bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="flex items-center justify-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox };
|
||||||
33
src/components/ui/collapsible.tsx
Normal file
33
src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible@1.1.3";
|
||||||
|
|
||||||
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||||
177
src/components/ui/command.tsx
Normal file
177
src/components/ui/command.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Command as CommandPrimitive } from "cmdk@1.1.1";
|
||||||
|
import { SearchIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "./dialog";
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent className="overflow-hidden p-0">
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
};
|
||||||
252
src/components/ui/context-menu.tsx
Normal file
252
src/components/ui/context-menu.tsx
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu@2.2.6";
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function ContextMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||||
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||||
|
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioGroup
|
||||||
|
data-slot="context-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubTrigger
|
||||||
|
data-slot="context-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto" />
|
||||||
|
</ContextMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubContent
|
||||||
|
data-slot="context-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Content
|
||||||
|
data-slot="context-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
data-slot="context-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="context-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
data-slot="context-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</ContextMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Label
|
||||||
|
data-slot="context-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
data-slot="context-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="context-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
};
|
||||||
135
src/components/ui/dialog.tsx
Normal file
135
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog@1.1.6";
|
||||||
|
import { XIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
};
|
||||||
132
src/components/ui/drawer.tsx
Normal file
132
src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul@1.1.2";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={cn(
|
||||||
|
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||||
|
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||||
|
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||||
|
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||||
|
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
};
|
||||||
257
src/components/ui/dropdown-menu.tsx
Normal file
257
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu@2.1.6";
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
};
|
||||||
168
src/components/ui/form.tsx
Normal file
168
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
|
||||||
|
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} from "react-hook-form@7.55.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import { Label } from "./label";
|
||||||
|
|
||||||
|
const Form = FormProvider;
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
|
const itemContext = React.useContext(FormItemContext);
|
||||||
|
const { getFieldState } = useFormContext();
|
||||||
|
const formState = useFormState({ name: fieldContext.name });
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const id = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
|
useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
data-slot="form-control"
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-description"
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { error, formMessageId } = useFormField();
|
||||||
|
const body = error ? String(error?.message ?? "") : props.children;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-message"
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-destructive text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
};
|
||||||
44
src/components/ui/hover-card.tsx
Normal file
44
src/components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card@1.1.6";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function HoverCard({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||||
|
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HoverCardTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HoverCardContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||||
|
<HoverCardPrimitive.Content
|
||||||
|
data-slot="hover-card-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</HoverCardPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||||
77
src/components/ui/input-otp.tsx
Normal file
77
src/components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { OTPInput, OTPInputContext } from "input-otp@1.4.2";
|
||||||
|
import { MinusIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function InputOTP({
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof OTPInput> & {
|
||||||
|
containerClassName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<OTPInput
|
||||||
|
data-slot="input-otp"
|
||||||
|
containerClassName={cn(
|
||||||
|
"flex items-center gap-2 has-disabled:opacity-50",
|
||||||
|
containerClassName,
|
||||||
|
)}
|
||||||
|
className={cn("disabled:cursor-not-allowed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="input-otp-group"
|
||||||
|
className={cn("flex items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPSlot({
|
||||||
|
index,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
index: number;
|
||||||
|
}) {
|
||||||
|
const inputOTPContext = React.useContext(OTPInputContext);
|
||||||
|
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="input-otp-slot"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
{hasFakeCaret && (
|
||||||
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||||
|
<MinusIcon />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input };
|
||||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label };
|
||||||
276
src/components/ui/menubar.tsx
Normal file
276
src/components/ui/menubar.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as MenubarPrimitive from "@radix-ui/react-menubar@1.1.6";
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Menubar({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Root
|
||||||
|
data-slot="menubar"
|
||||||
|
className={cn(
|
||||||
|
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
|
||||||
|
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
|
||||||
|
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
|
||||||
|
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Trigger
|
||||||
|
data-slot="menubar-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarContent({
|
||||||
|
className,
|
||||||
|
align = "start",
|
||||||
|
alignOffset = -4,
|
||||||
|
sideOffset = 8,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<MenubarPortal>
|
||||||
|
<MenubarPrimitive.Content
|
||||||
|
data-slot="menubar-content"
|
||||||
|
align={align}
|
||||||
|
alignOffset={alignOffset}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in 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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</MenubarPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
variant?: "default" | "destructive";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Item
|
||||||
|
data-slot="menubar-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.CheckboxItem
|
||||||
|
data-slot="menubar-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<MenubarPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</MenubarPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</MenubarPrimitive.CheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.RadioItem
|
||||||
|
data-slot="menubar-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<MenubarPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</MenubarPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</MenubarPrimitive.RadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Label
|
||||||
|
data-slot="menubar-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.Separator
|
||||||
|
data-slot="menubar-separator"
|
||||||
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="menubar-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
|
||||||
|
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.SubTrigger
|
||||||
|
data-slot="menubar-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||||
|
</MenubarPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenubarSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<MenubarPrimitive.SubContent
|
||||||
|
data-slot="menubar-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Menubar,
|
||||||
|
MenubarPortal,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarTrigger,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarGroup,
|
||||||
|
MenubarSeparator,
|
||||||
|
MenubarLabel,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarShortcut,
|
||||||
|
MenubarCheckboxItem,
|
||||||
|
MenubarRadioGroup,
|
||||||
|
MenubarRadioItem,
|
||||||
|
MenubarSub,
|
||||||
|
MenubarSubTrigger,
|
||||||
|
MenubarSubContent,
|
||||||
|
};
|
||||||
168
src/components/ui/navigation-menu.tsx
Normal file
168
src/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu@1.2.5";
|
||||||
|
import { cva } from "class-variance-authority@0.7.1";
|
||||||
|
import { ChevronDownIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function NavigationMenu({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
viewport = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||||
|
viewport?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
data-slot="navigation-menu"
|
||||||
|
data-viewport={viewport}
|
||||||
|
className={cn(
|
||||||
|
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{viewport && <NavigationMenuViewport />}
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
data-slot="navigation-menu-list"
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Item
|
||||||
|
data-slot="navigation-menu-item"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
||||||
|
);
|
||||||
|
|
||||||
|
function NavigationMenuTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
data-slot="navigation-menu-trigger"
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
data-slot="navigation-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||||
|
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuViewport({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-full left-0 isolate z-50 flex justify-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
data-slot="navigation-menu-viewport"
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuLink({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Link
|
||||||
|
data-slot="navigation-menu-link"
|
||||||
|
className={cn(
|
||||||
|
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuIndicator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
data-slot="navigation-menu-indicator"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
};
|
||||||
127
src/components/ui/pagination.tsx
Normal file
127
src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import { Button, buttonVariants } from "./button";
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
React.ComponentProps<"a">;
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className="hidden sm:block">Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:block">Next</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationPrevious,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationEllipsis,
|
||||||
|
};
|
||||||
48
src/components/ui/popover.tsx
Normal file
48
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover@1.1.6";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
31
src/components/ui/progress.tsx
Normal file
31
src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ProgressPrimitive from "@radix-ui/react-progress@1.1.2";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Progress({
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
data-slot="progress"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
data-slot="progress-indicator"
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Progress };
|
||||||
45
src/components/ui/radio-group.tsx
Normal file
45
src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group@1.2.3";
|
||||||
|
import { CircleIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function RadioGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
className={cn(
|
||||||
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
data-slot="radio-group-indicator"
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem };
|
||||||
56
src/components/ui/resizable.tsx
Normal file
56
src/components/ui/resizable.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { GripVerticalIcon } from "lucide-react@0.487.0";
|
||||||
|
import * as ResizablePrimitive from "react-resizable-panels@2.1.7";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function ResizablePanelGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||||
|
return (
|
||||||
|
<ResizablePrimitive.PanelGroup
|
||||||
|
data-slot="resizable-panel-group"
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizablePanel({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||||
|
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizableHandle({
|
||||||
|
withHandle,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||||
|
withHandle?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ResizablePrimitive.PanelResizeHandle
|
||||||
|
data-slot="resizable-handle"
|
||||||
|
className={cn(
|
||||||
|
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{withHandle && (
|
||||||
|
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||||
|
<GripVerticalIcon className="size-2.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ResizablePrimitive.PanelResizeHandle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||||
58
src/components/ui/scroll-area.tsx
Normal file
58
src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area@1.2.3";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar };
|
||||||
189
src/components/ui/select.tsx
Normal file
189
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select@2.1.6";
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronUpIcon,
|
||||||
|
} from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "popper",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
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)] scroll-my-1",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
};
|
||||||
28
src/components/ui/separator.tsx
Normal file
28
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator@1.1.2";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator-root"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
139
src/components/ui/sheet.tsx
Normal file
139
src/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog@1.1.6";
|
||||||
|
import { XIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
|
side === "right" &&
|
||||||
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
|
side === "left" &&
|
||||||
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
|
side === "top" &&
|
||||||
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
|
side === "bottom" &&
|
||||||
|
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
data-slot="sheet-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
data-slot="sheet-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
};
|
||||||
726
src/components/ui/sidebar.tsx
Normal file
726
src/components/ui/sidebar.tsx
Normal file
@@ -0,0 +1,726 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
||||||
|
import { VariantProps, cva } from "class-variance-authority@0.7.1";
|
||||||
|
import { PanelLeftIcon } from "lucide-react@0.487.0";
|
||||||
|
|
||||||
|
import { useIsMobile } from "./use-mobile";
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import { Button } from "./button";
|
||||||
|
import { Input } from "./input";
|
||||||
|
import { Separator } from "./separator";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
} from "./sheet";
|
||||||
|
import { Skeleton } from "./skeleton";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "./tooltip";
|
||||||
|
|
||||||
|
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 SidebarContextProps = {
|
||||||
|
state: "expanded" | "collapsed";
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
openMobile: boolean;
|
||||||
|
setOpenMobile: (open: boolean) => void;
|
||||||
|
isMobile: boolean;
|
||||||
|
toggleSidebar: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||||
|
|
||||||
|
function useSidebar() {
|
||||||
|
const context = React.useContext(SidebarContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarProvider({
|
||||||
|
defaultOpen = true,
|
||||||
|
open: openProp,
|
||||||
|
onOpenChange: setOpenProp,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
}) {
|
||||||
|
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<SidebarContextProps>(
|
||||||
|
() => ({
|
||||||
|
state,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isMobile,
|
||||||
|
openMobile,
|
||||||
|
setOpenMobile,
|
||||||
|
toggleSidebar,
|
||||||
|
}),
|
||||||
|
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarContext.Provider value={contextValue}>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-wrapper"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--sidebar-width": SIDEBAR_WIDTH,
|
||||||
|
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||||
|
...style,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</SidebarContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar({
|
||||||
|
side = "left",
|
||||||
|
variant = "sidebar",
|
||||||
|
collapsible = "offcanvas",
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
side?: "left" | "right";
|
||||||
|
variant?: "sidebar" | "floating" | "inset";
|
||||||
|
collapsible?: "offcanvas" | "icon" | "none";
|
||||||
|
}) {
|
||||||
|
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||||
|
|
||||||
|
if (collapsible === "none") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar"
|
||||||
|
className={cn(
|
||||||
|
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||||
|
<SheetContent
|
||||||
|
data-sidebar="sidebar"
|
||||||
|
data-slot="sidebar"
|
||||||
|
data-mobile="true"
|
||||||
|
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
side={side}
|
||||||
|
>
|
||||||
|
<SheetHeader className="sr-only">
|
||||||
|
<SheetTitle>Sidebar</SheetTitle>
|
||||||
|
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="group peer text-sidebar-foreground hidden md:block"
|
||||||
|
data-state={state}
|
||||||
|
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||||
|
data-variant={variant}
|
||||||
|
data-side={side}
|
||||||
|
data-slot="sidebar"
|
||||||
|
>
|
||||||
|
{/* This is what handles the sidebar gap on desktop */}
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-gap"
|
||||||
|
className={cn(
|
||||||
|
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 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)+(--spacing(4)))]"
|
||||||
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-container"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 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)+(--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"
|
||||||
|
data-slot="sidebar-inner"
|
||||||
|
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarTrigger({
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Button>) {
|
||||||
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-sidebar="trigger"
|
||||||
|
data-slot="sidebar-trigger"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={cn("size-7", className)}
|
||||||
|
onClick={(event) => {
|
||||||
|
onClick?.(event);
|
||||||
|
toggleSidebar();
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<PanelLeftIcon />
|
||||||
|
<span className="sr-only">Toggle Sidebar</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||||
|
const { toggleSidebar } = useSidebar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
data-sidebar="rail"
|
||||||
|
data-slot="sidebar-rail"
|
||||||
|
aria-label="Toggle Sidebar"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
title="Toggle Sidebar"
|
||||||
|
className={cn(
|
||||||
|
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||||
|
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||||
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||||
|
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||||
|
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||||
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||||
|
return (
|
||||||
|
<main
|
||||||
|
data-slot="sidebar-inset"
|
||||||
|
className={cn(
|
||||||
|
"bg-background relative flex w-full flex-1 flex-col",
|
||||||
|
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Input>) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
data-slot="sidebar-input"
|
||||||
|
data-sidebar="input"
|
||||||
|
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-header"
|
||||||
|
data-sidebar="header"
|
||||||
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-footer"
|
||||||
|
data-sidebar="footer"
|
||||||
|
className={cn("flex flex-col gap-2 p-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Separator>) {
|
||||||
|
return (
|
||||||
|
<Separator
|
||||||
|
data-slot="sidebar-separator"
|
||||||
|
data-sidebar="separator"
|
||||||
|
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-content"
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-group"
|
||||||
|
data-sidebar="group"
|
||||||
|
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroupLabel({
|
||||||
|
className,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "div";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-group-label"
|
||||||
|
data-sidebar="group-label"
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroupAction({
|
||||||
|
className,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-group-action"
|
||||||
|
data-sidebar="group-action"
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 md:after:hidden",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarGroupContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-group-content"
|
||||||
|
data-sidebar="group-content"
|
||||||
|
className={cn("w-full text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="sidebar-menu"
|
||||||
|
data-sidebar="menu"
|
||||||
|
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="sidebar-menu-item"
|
||||||
|
data-sidebar="menu-item"
|
||||||
|
className={cn("group/menu-item relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarMenuButtonVariants = cva(
|
||||||
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function SidebarMenuButton({
|
||||||
|
asChild = false,
|
||||||
|
isActive = false,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
tooltip,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||||
|
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
const { isMobile, state } = useSidebar();
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-menu-button"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuAction({
|
||||||
|
className,
|
||||||
|
asChild = false,
|
||||||
|
showOnHover = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
showOnHover?: boolean;
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-menu-action"
|
||||||
|
data-sidebar="menu-action"
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
|
// Increases the hit area of the button on mobile.
|
||||||
|
"after:absolute after:-inset-2 md:after: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 &&
|
||||||
|
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuBadge({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-menu-badge"
|
||||||
|
data-sidebar="menu-badge"
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuSkeleton({
|
||||||
|
className,
|
||||||
|
showIcon = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
showIcon?: boolean;
|
||||||
|
}) {
|
||||||
|
// Random width between 50 to 90%.
|
||||||
|
const width = React.useMemo(() => {
|
||||||
|
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sidebar-menu-skeleton"
|
||||||
|
data-sidebar="menu-skeleton"
|
||||||
|
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{showIcon && (
|
||||||
|
<Skeleton
|
||||||
|
className="size-4 rounded-md"
|
||||||
|
data-sidebar="menu-skeleton-icon"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Skeleton
|
||||||
|
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||||
|
data-sidebar="menu-skeleton-text"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--skeleton-width": width,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="sidebar-menu-sub"
|
||||||
|
data-sidebar="menu-sub"
|
||||||
|
className={cn(
|
||||||
|
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||||
|
"group-data-[collapsible=icon]:hidden",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuSubItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="sidebar-menu-sub-item"
|
||||||
|
data-sidebar="menu-sub-item"
|
||||||
|
className={cn("group/menu-sub-item relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarMenuSubButton({
|
||||||
|
asChild = false,
|
||||||
|
size = "md",
|
||||||
|
isActive = false,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean;
|
||||||
|
size?: "sm" | "md";
|
||||||
|
isActive?: boolean;
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "a";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="sidebar-menu-sub-button"
|
||||||
|
data-sidebar="menu-sub-button"
|
||||||
|
data-size={size}
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 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",
|
||||||
|
"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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
13
src/components/ui/skeleton.tsx
Normal file
13
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="skeleton"
|
||||||
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton };
|
||||||
63
src/components/ui/slider.tsx
Normal file
63
src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider@1.2.3";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Slider({
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
min = 0,
|
||||||
|
max = 100,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||||
|
const _values = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: Array.isArray(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: [min, max],
|
||||||
|
[value, defaultValue, min, max],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
data-slot="slider"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track
|
||||||
|
data-slot="slider-track"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-4 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Range
|
||||||
|
data-slot="slider-range"
|
||||||
|
className={cn(
|
||||||
|
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
{Array.from({ length: _values.length }, (_, index) => (
|
||||||
|
<SliderPrimitive.Thumb
|
||||||
|
data-slot="slider-thumb"
|
||||||
|
key={index}
|
||||||
|
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Slider };
|
||||||
25
src/components/ui/sonner.tsx
Normal file
25
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes@0.4.6";
|
||||||
|
import { Toaster as Sonner, ToasterProps } from "sonner@2.0.3";
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Toaster };
|
||||||
31
src/components/ui/switch.tsx
Normal file
31
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SwitchPrimitive from "@radix-ui/react-switch@1.1.3";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
className={cn(
|
||||||
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-switch-background focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-card dark:data-[state=unchecked]:bg-card-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch };
|
||||||
116
src/components/ui/table.tsx
Normal file
116
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
};
|
||||||
66
src/components/ui/tabs.tsx
Normal file
66
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs@1.1.3";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Tabs({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
className={cn("flex flex-col gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-xl p-[3px] flex",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=active]:bg-card dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot="tabs-content"
|
||||||
|
className={cn("flex-1 outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"resize-none border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-input-background px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
73
src/components/ui/toggle-group.tsx
Normal file
73
src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group@1.1.2";
|
||||||
|
import { type VariantProps } from "class-variance-authority@0.7.1";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import { toggleVariants } from "./toggle";
|
||||||
|
|
||||||
|
const ToggleGroupContext = React.createContext<
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>({
|
||||||
|
size: "default",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
function ToggleGroup({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Root
|
||||||
|
data-slot="toggle-group"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToggleGroupItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
const context = React.useContext(ToggleGroupContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive.Item
|
||||||
|
data-slot="toggle-group-item"
|
||||||
|
data-variant={context.variant || variant}
|
||||||
|
data-size={context.size || size}
|
||||||
|
className={cn(
|
||||||
|
toggleVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem };
|
||||||
47
src/components/ui/toggle.tsx
Normal file
47
src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TogglePrimitive from "@radix-ui/react-toggle@1.1.2";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-2 min-w-9",
|
||||||
|
sm: "h-8 px-1.5 min-w-8",
|
||||||
|
lg: "h-10 px-2.5 min-w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Toggle({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
data-slot="toggle"
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants };
|
||||||
61
src/components/ui/tooltip.tsx
Normal file
61
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip@1.1.8";
|
||||||
|
|
||||||
|
import { cn } from "./utils";
|
||||||
|
|
||||||
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-primary 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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||||
21
src/components/ui/use-mobile.ts
Normal file
21
src/components/ui/use-mobile.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
6
src/components/ui/utils.ts
Normal file
6
src/components/ui/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
29
src/global.d.ts
vendored
Normal file
29
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
61
src/guidelines/Guidelines.md
Normal file
61
src/guidelines/Guidelines.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
**Add your own guidelines here**
|
||||||
|
<!--
|
||||||
|
|
||||||
|
System Guidelines
|
||||||
|
|
||||||
|
Use this file to provide the AI with rules and guidelines you want it to follow.
|
||||||
|
This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs
|
||||||
|
|
||||||
|
TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need
|
||||||
|
|
||||||
|
# General guidelines
|
||||||
|
|
||||||
|
Any general rules you want the AI to follow.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
* Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
|
||||||
|
* Refactor code as you go to keep code clean
|
||||||
|
* Keep file sizes small and put helper functions and components in their own files.
|
||||||
|
|
||||||
|
--------------
|
||||||
|
|
||||||
|
# Design system guidelines
|
||||||
|
Rules for how the AI should make generations look like your company's design system
|
||||||
|
|
||||||
|
Additionally, if you select a design system to use in the prompt box, you can reference
|
||||||
|
your design system's components, tokens, variables and components.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
* Use a base font-size of 14px
|
||||||
|
* Date formats should always be in the format “Jun 10”
|
||||||
|
* The bottom toolbar should only ever have a maximum of 4 items
|
||||||
|
* Never use the floating action button with the bottom toolbar
|
||||||
|
* Chips should always come in sets of 3 or more
|
||||||
|
* Don't use a dropdown if there are 2 or fewer options
|
||||||
|
|
||||||
|
You can also create sub sections and add more specific details
|
||||||
|
For example:
|
||||||
|
|
||||||
|
|
||||||
|
## Button
|
||||||
|
The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
|
||||||
|
users through the application. It provides visual feedback and clear affordances to enhance user experience.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
|
||||||
|
or initiating processes. They communicate interactivity and should have clear, action-oriented labels.
|
||||||
|
|
||||||
|
### Variants
|
||||||
|
* Primary Button
|
||||||
|
* Purpose : Used for the main action in a section or page
|
||||||
|
* Visual Style : Bold, filled with the primary brand color
|
||||||
|
* Usage : One primary button per section to guide users toward the most important action
|
||||||
|
* Secondary Button
|
||||||
|
* Purpose : Used for alternative or supporting actions
|
||||||
|
* Visual Style : Outlined with the primary color, transparent background
|
||||||
|
* Usage : Can appear alongside a primary button for less important actions
|
||||||
|
* Tertiary Button
|
||||||
|
* Purpose : Used for the least important actions
|
||||||
|
* Visual Style : Text-only with no border, using primary color
|
||||||
|
* Usage : For actions that should be available but not emphasized
|
||||||
|
-->
|
||||||
621
src/imports/Content.tsx
Normal file
621
src/imports/Content.tsx
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
import { motion } from 'motion/react';
|
||||||
|
import { useState, useEffect, Component } from 'react';
|
||||||
|
import svgPaths from "./svg-1dw6tu7s10";
|
||||||
|
|
||||||
|
// Mock data for graphs
|
||||||
|
const barData = [
|
||||||
|
{ name: 'Mon', value: 120 },
|
||||||
|
{ name: 'Tue', value: 200 },
|
||||||
|
{ name: 'Wed', value: 150 },
|
||||||
|
{ name: 'Thu', value: 180 },
|
||||||
|
{ name: 'Fri', value: 250 },
|
||||||
|
{ name: 'Sat', value: 300 },
|
||||||
|
{ name: 'Sun', value: 220 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const lineData = [
|
||||||
|
{ name: 'Jan', value: 1200 },
|
||||||
|
{ name: 'Feb', value: 1900 },
|
||||||
|
{ name: 'Mar', value: 1500 },
|
||||||
|
{ name: 'Apr', value: 2200 },
|
||||||
|
{ name: 'May', value: 2800 },
|
||||||
|
{ name: 'Jun', value: 3200 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const areaData = [
|
||||||
|
{ name: '9AM', value: 30 },
|
||||||
|
{ name: '10AM', value: 45 },
|
||||||
|
{ name: '11AM', value: 80 },
|
||||||
|
{ name: '12PM', value: 120 },
|
||||||
|
{ name: '1PM', value: 110 },
|
||||||
|
{ name: '2PM', value: 65 },
|
||||||
|
{ name: '3PM', value: 40 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Animated Clock Component
|
||||||
|
function AnimatedClock() {
|
||||||
|
const [hasPath, setHasPath] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasPath(!!svgPaths?.p318c7f00);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-12 h-12 flex items-center justify-center">
|
||||||
|
<svg className="w-full h-full text-gray-600" viewBox="0 0 88 88">
|
||||||
|
<g>
|
||||||
|
{hasPath ? (
|
||||||
|
<path d={svgPaths.p318c7f00} fill="currentColor" fillOpacity="0.7" />
|
||||||
|
) : (
|
||||||
|
// Fallback clock icon
|
||||||
|
<>
|
||||||
|
<circle cx="44" cy="44" r="36" stroke="currentColor" strokeWidth="3" fill="none" />
|
||||||
|
<circle cx="44" cy="44" r="2" fill="currentColor" />
|
||||||
|
<line x1="44" y1="44" x2="44" y2="20" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
<line x1="44" y1="44" x2="60" y2="44" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
{/* Only show animated hands if we have the original SVG */}
|
||||||
|
{hasPath && (
|
||||||
|
<>
|
||||||
|
{/* Animated hour hand */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute w-0.5 h-4 bg-gray-600 rounded-full origin-bottom"
|
||||||
|
style={{ top: '24px', left: '50%', marginLeft: '-1px' }}
|
||||||
|
animate={{ rotate: [0, 5, -5, 0] }}
|
||||||
|
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
{/* Animated minute hand */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute w-0.5 h-5 bg-gray-800 rounded-full origin-bottom"
|
||||||
|
style={{ top: '20px', left: '50%', marginLeft: '-1px' }}
|
||||||
|
animate={{ rotate: [0, 15, -10, 0] }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
{/* Center dot */}
|
||||||
|
<div className="absolute w-1.5 h-1.5 bg-gray-800 rounded-full" style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header Component
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-bold text-black">Dashboard</h1>
|
||||||
|
<p className="text-xs text-gray-600">Welcome back Kassandra!</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="bg-white rounded-xl px-3 py-1.5 shadow-sm border">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
className="text-xs bg-transparent border-none outline-none w-32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs text-black">Kassandra</p>
|
||||||
|
<p className="text-xs font-medium text-black">Admin</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 bg-gray-300 rounded-full"></div>
|
||||||
|
<div className="w-8 h-8 bg-gray-400 rounded-full flex items-center justify-center">
|
||||||
|
<div className="w-4 h-4 text-white">🔔</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick Action Card Component
|
||||||
|
function QuickActionCard({ icon, title, color = "bg-white" }) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={`${color} rounded-lg p-3 shadow-sm border hover:shadow-md transition-shadow cursor-pointer h-20`}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 h-full">
|
||||||
|
<div className="w-8 h-8 flex items-center justify-center text-gray-700">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-medium text-gray-800">{title}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metric Card Component
|
||||||
|
function MetricCard({ title, value, subtitle, trend, icon, chart }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg p-3 shadow-sm border h-32">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-xs font-medium text-gray-700">{title}</h3>
|
||||||
|
<div className="w-6 h-6 text-gray-500">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-between h-20">
|
||||||
|
<div>
|
||||||
|
<p className="text-lg font-bold text-black">{value}</p>
|
||||||
|
<p className="text-xs text-gray-500">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
{trend && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs text-green-600">{trend}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple CSS-based Chart Components (no external dependencies)
|
||||||
|
function MiniBarChart({ data, color = "#3B82F6" }) {
|
||||||
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xl mb-1">📊</div>
|
||||||
|
<p className="text-xs">No data</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxValue = Math.max(...data.map(item => item.value));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-end justify-between h-full px-2 pb-2">
|
||||||
|
{data.map((item, index) => {
|
||||||
|
const height = (item.value / maxValue) * 100;
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex flex-col items-center gap-1 flex-1">
|
||||||
|
<div
|
||||||
|
className="w-3 bg-blue-500 rounded-t"
|
||||||
|
style={{
|
||||||
|
height: `${height}%`,
|
||||||
|
backgroundColor: color,
|
||||||
|
minHeight: '2px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500 text-center truncate w-full">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiniLineChart({ data, color = "#10B981" }) {
|
||||||
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xl mb-1">📈</div>
|
||||||
|
<p className="text-xs">No data</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxValue = Math.max(...data.map(item => item.value));
|
||||||
|
const minValue = Math.min(...data.map(item => item.value));
|
||||||
|
const range = maxValue - minValue || 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full px-2 pb-6 pt-2">
|
||||||
|
<svg className="w-full h-full" viewBox="0 0 100 60">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
points={data.map((item, index) => {
|
||||||
|
const x = (index / (data.length - 1)) * 100;
|
||||||
|
const y = 60 - ((item.value - minValue) / range) * 50;
|
||||||
|
return `${x},${y}`;
|
||||||
|
}).join(' ')}
|
||||||
|
/>
|
||||||
|
{data.map((item, index) => {
|
||||||
|
const x = (index / (data.length - 1)) * 100;
|
||||||
|
const y = 60 - ((item.value - minValue) / range) * 50;
|
||||||
|
return (
|
||||||
|
<circle
|
||||||
|
key={index}
|
||||||
|
cx={x}
|
||||||
|
cy={y}
|
||||||
|
r="2"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<span key={index} className="text-xs text-gray-500 text-center">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiniAreaChart({ data, color = "#F59E0B", fillColor = "#FEF3C7" }) {
|
||||||
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xl mb-1">📊</div>
|
||||||
|
<p className="text-xs">No data</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxValue = Math.max(...data.map(item => item.value));
|
||||||
|
const minValue = Math.min(...data.map(item => item.value));
|
||||||
|
const range = maxValue - minValue || 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full px-2 pb-6 pt-2">
|
||||||
|
<svg className="w-full h-full" viewBox="0 0 100 60">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="areaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor={color} stopOpacity="0.3" />
|
||||||
|
<stop offset="100%" stopColor={color} stopOpacity="0.1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
fill="url(#areaGradient)"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
d={`M ${data.map((item, index) => {
|
||||||
|
const x = (index / (data.length - 1)) * 100;
|
||||||
|
const y = 60 - ((item.value - minValue) / range) * 50;
|
||||||
|
return `${x},${y}`;
|
||||||
|
}).join(' L ')} L 100,60 L 0,60 Z`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-2">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<span key={index} className="text-xs text-gray-500 text-center">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graph Card Component
|
||||||
|
function GraphCard({ title, type = "bar", data = barData }) {
|
||||||
|
// Ensure data is valid
|
||||||
|
const validData = data && Array.isArray(data) && data.length > 0 ? data : barData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg p-3 shadow-sm border h-32">
|
||||||
|
<h3 className="text-xs font-medium text-gray-700 mb-2">{title}</h3>
|
||||||
|
<div className="h-20 w-full">
|
||||||
|
{type === "bar" && <MiniBarChart data={validData} />}
|
||||||
|
{type === "line" && <MiniLineChart data={validData} />}
|
||||||
|
{type === "area" && <MiniAreaChart data={validData} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special Time Card with Animated Clock
|
||||||
|
function TimeCard() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg p-3 shadow-sm border h-32">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-xs font-medium text-gray-700">Peak Time</h3>
|
||||||
|
<AnimatedClock />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center h-16">
|
||||||
|
<p className="text-xs text-gray-600 mb-1">Most active between</p>
|
||||||
|
<p className="text-sm font-bold text-black">11AM - 1PM</p>
|
||||||
|
<p className="text-xs text-gray-500">Daily average</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large Chart Components
|
||||||
|
function LargeBarChart({ data, color = "#6366F1" }) {
|
||||||
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-4xl mb-2">📊</div>
|
||||||
|
<p className="text-sm">No data available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxValue = Math.max(...data.map(item => item.value));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{/* Y-axis labels */}
|
||||||
|
<div className="flex justify-between mb-2 px-8">
|
||||||
|
<span className="text-xs text-gray-400">{maxValue}</span>
|
||||||
|
<span className="text-xs text-gray-400">{Math.floor(maxValue / 2)}</span>
|
||||||
|
<span className="text-xs text-gray-400">0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart area */}
|
||||||
|
<div className="flex items-end justify-between flex-1 px-4 pb-8">
|
||||||
|
{data.map((item, index) => {
|
||||||
|
const height = (item.value / maxValue) * 100;
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex flex-col items-center gap-2 flex-1">
|
||||||
|
<div
|
||||||
|
className="w-8 bg-indigo-500 rounded-t transition-all duration-300 hover:opacity-80"
|
||||||
|
style={{
|
||||||
|
height: `${height}%`,
|
||||||
|
backgroundColor: color,
|
||||||
|
minHeight: '4px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500 text-center">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LargeLineChart({ data, color = "#EF4444" }) {
|
||||||
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-4xl mb-2">📈</div>
|
||||||
|
<p className="text-sm">No data available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxValue = Math.max(...data.map(item => item.value));
|
||||||
|
const minValue = Math.min(...data.map(item => item.value));
|
||||||
|
const range = maxValue - minValue || 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{/* Y-axis labels */}
|
||||||
|
<div className="flex flex-col justify-between h-24 px-2 mb-2">
|
||||||
|
<span className="text-xs text-gray-400">{maxValue}</span>
|
||||||
|
<span className="text-xs text-gray-400">{Math.floor((maxValue + minValue) / 2)}</span>
|
||||||
|
<span className="text-xs text-gray-400">{minValue}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart area */}
|
||||||
|
<div className="relative flex-1 px-4 pb-8">
|
||||||
|
<svg className="w-full h-full" viewBox="0 0 100 80">
|
||||||
|
{/* Grid lines */}
|
||||||
|
<defs>
|
||||||
|
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#f3f4f6" strokeWidth="0.5"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100" height="80" fill="url(#grid)" />
|
||||||
|
|
||||||
|
{/* Line */}
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="3"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
points={data.map((item, index) => {
|
||||||
|
const x = (index / (data.length - 1)) * 100;
|
||||||
|
const y = 80 - ((item.value - minValue) / range) * 60;
|
||||||
|
return `${x},${y}`;
|
||||||
|
}).join(' ')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Data points */}
|
||||||
|
{data.map((item, index) => {
|
||||||
|
const x = (index / (data.length - 1)) * 100;
|
||||||
|
const y = 80 - ((item.value - minValue) / range) * 60;
|
||||||
|
return (
|
||||||
|
<circle
|
||||||
|
key={index}
|
||||||
|
cx={x}
|
||||||
|
cy={y}
|
||||||
|
r="4"
|
||||||
|
fill={color}
|
||||||
|
className="drop-shadow-sm"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* X-axis labels */}
|
||||||
|
<div className="absolute bottom-0 left-4 right-4 flex justify-between">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<span key={index} className="text-xs text-gray-500 text-center">
|
||||||
|
{item.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large Graph Card Component
|
||||||
|
function LargeGraphCard({ title, type = "bar", data }) {
|
||||||
|
// Use appropriate data based on type
|
||||||
|
const chartData = data || (type === "line" ? lineData : barData);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg p-4 shadow-sm border h-48 col-span-2">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700">{title}</h3>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button className="text-xs px-2 py-1 bg-gray-100 rounded text-gray-600">Daily</button>
|
||||||
|
<button className="text-xs px-2 py-1 text-gray-500">Weekly</button>
|
||||||
|
<button className="text-xs px-2 py-1 text-gray-500">Monthly</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="h-36 w-full">
|
||||||
|
{type === "bar" && <LargeBarChart data={chartData} />}
|
||||||
|
{type === "line" && <LargeLineChart data={chartData} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Content() {
|
||||||
|
return (
|
||||||
|
<div className="relative size-full" data-name="content">
|
||||||
|
<motion.div
|
||||||
|
className="w-full h-full p-4 space-y-4"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
{/* Quick Actions Row */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-medium text-gray-700 mb-3">Quick Actions</h2>
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
<QuickActionCard
|
||||||
|
icon="🔍"
|
||||||
|
title="View Logs"
|
||||||
|
/>
|
||||||
|
<QuickActionCard
|
||||||
|
icon="👥"
|
||||||
|
title="Add Staff"
|
||||||
|
/>
|
||||||
|
<QuickActionCard
|
||||||
|
icon="🎫"
|
||||||
|
title="Redemption"
|
||||||
|
/>
|
||||||
|
<QuickActionCard
|
||||||
|
icon="📊"
|
||||||
|
title="Analytics"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Grid - First Row */}
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
<MetricCard
|
||||||
|
title="Total Staff"
|
||||||
|
value="150"
|
||||||
|
subtitle="employees"
|
||||||
|
trend="+8.5% since last month"
|
||||||
|
icon="👥"
|
||||||
|
/>
|
||||||
|
<TimeCard />
|
||||||
|
<MetricCard
|
||||||
|
title="Daily Redemptions"
|
||||||
|
value="75"
|
||||||
|
subtitle="today"
|
||||||
|
trend="+12% vs yesterday"
|
||||||
|
icon="🎫"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Revenue"
|
||||||
|
value="$12,450"
|
||||||
|
subtitle="this month"
|
||||||
|
trend="+15.3% vs last month"
|
||||||
|
icon="💰"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Graph Row */}
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
<GraphCard
|
||||||
|
title="Weekly Activity"
|
||||||
|
type="bar"
|
||||||
|
data={barData}
|
||||||
|
/>
|
||||||
|
<GraphCard
|
||||||
|
title="Growth Trend"
|
||||||
|
type="line"
|
||||||
|
data={lineData}
|
||||||
|
/>
|
||||||
|
<GraphCard
|
||||||
|
title="Peak Hours"
|
||||||
|
type="area"
|
||||||
|
data={areaData}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Active Users"
|
||||||
|
value="2,847"
|
||||||
|
subtitle="online now"
|
||||||
|
trend="+5.2% vs last hour"
|
||||||
|
icon="🌐"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Large Charts Row */}
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
<LargeGraphCard
|
||||||
|
title="Redemption Trends"
|
||||||
|
type="bar"
|
||||||
|
/>
|
||||||
|
<LargeGraphCard
|
||||||
|
title="Monthly Growth"
|
||||||
|
type="line"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Metrics Row */}
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
<MetricCard
|
||||||
|
title="Satisfaction"
|
||||||
|
value="4.8/5"
|
||||||
|
subtitle="customer rating"
|
||||||
|
trend="+0.2 vs last month"
|
||||||
|
icon="⭐"
|
||||||
|
/>
|
||||||
|
<GraphCard
|
||||||
|
title="Conversion Rate"
|
||||||
|
type="area"
|
||||||
|
data={[
|
||||||
|
{ name: 'Q1', value: 65 },
|
||||||
|
{ name: 'Q2', value: 78 },
|
||||||
|
{ name: 'Q3', value: 82 },
|
||||||
|
{ name: 'Q4', value: 89 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="Support Tickets"
|
||||||
|
value="23"
|
||||||
|
subtitle="open tickets"
|
||||||
|
trend="-15% vs last week"
|
||||||
|
icon="🎧"
|
||||||
|
/>
|
||||||
|
<MetricCard
|
||||||
|
title="App Downloads"
|
||||||
|
value="8,247"
|
||||||
|
subtitle="this month"
|
||||||
|
trend="+23% vs last month"
|
||||||
|
icon="📱"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
336
src/imports/DashboardMenu-4-997.tsx
Normal file
336
src/imports/DashboardMenu-4-997.tsx
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
import svgPaths from "./svg-iw8mz5qegz";
|
||||||
|
|
||||||
|
function Logo() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-3 items-center justify-start relative shrink-0 w-full" data-name="logo">
|
||||||
|
<div className="h-[69px] relative shrink-0 w-[70px]">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 70 69">
|
||||||
|
<ellipse cx="35" cy="34.5" fill="var(--fill-0, #D9D9D9)" id="Ellipse 166" rx="35" ry="34.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[32px] text-black w-[145px]">
|
||||||
|
<p className="leading-[normal]">CityCards</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Component11() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 size-11" data-name="11">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 44 44">
|
||||||
|
<g id="11">
|
||||||
|
<g id="zcaj">
|
||||||
|
<path clipRule="evenodd" d={svgPaths.p10f2cc40} fill="var(--fill-0, black)" fillRule="evenodd" />
|
||||||
|
<path clipRule="evenodd" d={svgPaths.p17ac4800} fill="var(--fill-0, black)" fillRule="evenodd" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextIcon() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-1 items-center justify-start relative shrink-0" data-name="text+icon">
|
||||||
|
<Component11 />
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[18px] text-black text-nowrap tracking-[0.36px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">Dashboard</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem() {
|
||||||
|
return (
|
||||||
|
<div className="h-[68px] relative rounded-[32px] shrink-0 w-full" data-name="menu item">
|
||||||
|
<div className="flex flex-row items-center relative size-full">
|
||||||
|
<div className="box-border content-stretch flex gap-1 h-[68px] items-center justify-start p-[12px] relative w-full">
|
||||||
|
<TextIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Group() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bottom-[31.82%] left-[28.75%] right-[28.07%] top-1/4" data-name="Group">
|
||||||
|
<div className="absolute inset-[-5.26%]">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 21 21">
|
||||||
|
<g id="Group">
|
||||||
|
<path d={svgPaths.p27056df0} fill="var(--fill-0, white)" id="Vector" stroke="var(--stroke-0, white)" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
<path d={svgPaths.p39fcb880} id="Vector_2" stroke="var(--stroke-0, white)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
<path d={svgPaths.pbd4ab00} id="Vector_3" stroke="var(--stroke-0, black)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Group1() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bottom-[31.82%] contents left-[28.75%] right-[28.07%] top-1/4" data-name="Group">
|
||||||
|
<Group />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MaskGroup() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bottom-[31.82%] contents left-[28.75%] right-[28.07%] top-1/4" data-name="Mask group">
|
||||||
|
<Group1 />
|
||||||
|
<div className="absolute inset-[20.68%_19.18%_27.5%_19.86%]" data-name="Vector">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 27 23">
|
||||||
|
<path d="M0 0H26.8235V22.7999H0V0Z" fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IconParkSolidLog() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 size-11" data-name="icon-park-solid:log">
|
||||||
|
<MaskGroup />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextIcon1() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-1 items-center justify-start relative shrink-0" data-name="text+icon">
|
||||||
|
<IconParkSolidLog />
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[18px] text-black text-nowrap tracking-[0.36px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">Redemption Logs</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem1() {
|
||||||
|
return (
|
||||||
|
<div className="box-border content-stretch flex gap-1 h-[68px] items-center justify-start p-[12px] relative rounded-[32px] shrink-0 w-[327px]" data-name="menu item">
|
||||||
|
<TextIcon1 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MdiUsers() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 size-11" data-name="mdi:users">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 44 44">
|
||||||
|
<g id="mdi:users">
|
||||||
|
<path d={svgPaths.p23818a00} fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextIcon2() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-1 items-center justify-start relative shrink-0" data-name="text+icon">
|
||||||
|
<MdiUsers />
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[18px] text-black text-nowrap tracking-[0.36px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">Staff Management</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem2() {
|
||||||
|
return (
|
||||||
|
<div className="box-border content-stretch flex gap-1 h-[68px] items-center justify-start p-[12px] relative rounded-[32px] shrink-0 w-[327px]" data-name="menu item">
|
||||||
|
<TextIcon2 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LsiconOrderFilled() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 size-11" data-name="lsicon:order-filled">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 44 44">
|
||||||
|
<g id="lsicon:order-filled">
|
||||||
|
<path clipRule="evenodd" d={svgPaths.p17e82500} fill="var(--fill-0, black)" fillRule="evenodd" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextIcon3() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-1 items-center justify-start relative shrink-0" data-name="text+icon">
|
||||||
|
<LsiconOrderFilled />
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[18px] text-black text-nowrap tracking-[0.36px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">Booking Management</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex items-center justify-between relative shrink-0 w-[303px]" data-name="header">
|
||||||
|
<TextIcon3 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem3() {
|
||||||
|
return (
|
||||||
|
<div className="box-border content-stretch flex gap-1 h-[68px] items-center justify-start p-[12px] relative rounded-[32px] shrink-0 w-[327px]" data-name="menu item">
|
||||||
|
<Header />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FluentPersonSupport20Filled() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 size-11" data-name="fluent:person-support-20-filled">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 44 44">
|
||||||
|
<g id="fluent:person-support-20-filled">
|
||||||
|
<path d={svgPaths.p1b7f1f00} fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextIcon4() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-1 items-center justify-start relative shrink-0" data-name="text+icon">
|
||||||
|
<FluentPersonSupport20Filled />
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[18px] text-black text-nowrap tracking-[0.36px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">Support</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem4() {
|
||||||
|
return (
|
||||||
|
<div className="box-border content-stretch flex gap-1 h-[68px] items-center justify-start p-[12px] relative rounded-[32px] shrink-0 w-[327px]" data-name="menu item">
|
||||||
|
<TextIcon4 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItemMdiBell() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 size-6" data-name="menu item/mdi:bell">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 24 24">
|
||||||
|
<g id="menu item/mdi:bell">
|
||||||
|
<path d={svgPaths.pc1a2580} fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Icon() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-2 items-center justify-center relative shrink-0 size-11" data-name="icon">
|
||||||
|
<MenuItemMdiBell />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextIcon5() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-1 items-center justify-start relative shrink-0 w-[226px]" data-name="text+icon">
|
||||||
|
<Icon />
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[18px] text-black text-nowrap tracking-[0.36px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">Notifications</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem5() {
|
||||||
|
return (
|
||||||
|
<div className="box-border content-stretch flex gap-1 h-[68px] items-center justify-start p-[12px] relative rounded-[32px] shrink-0 w-[327px]" data-name="menu item">
|
||||||
|
<TextIcon5 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopContainer() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex flex-col items-start justify-start relative shrink-0 w-full" data-name="top container">
|
||||||
|
<MenuItem />
|
||||||
|
<MenuItem1 />
|
||||||
|
<MenuItem2 />
|
||||||
|
<MenuItem3 />
|
||||||
|
<MenuItem4 />
|
||||||
|
<MenuItem5 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MaterialSymbolsLogoutRounded() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 size-11" data-name="material-symbols:logout-rounded">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 44 44">
|
||||||
|
<g id="material-symbols:logout-rounded">
|
||||||
|
<path d={svgPaths.p2d300300} fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextIcon6() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex gap-1 items-center justify-start relative shrink-0" data-name="text+icon">
|
||||||
|
<MaterialSymbolsLogoutRounded />
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[18px] text-black text-nowrap tracking-[0.36px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">Log out</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
return (
|
||||||
|
<div className="h-[68px] relative rounded-[12px] shrink-0 w-full" data-name="dashboard">
|
||||||
|
<div aria-hidden="true" className="absolute border border-[rgba(0,0,0,0.12)] border-solid inset-0 pointer-events-none rounded-[12px]" />
|
||||||
|
<div className="flex flex-row items-center relative size-full">
|
||||||
|
<div className="box-border content-stretch flex gap-1 h-[68px] items-center justify-start p-[12px] relative w-full">
|
||||||
|
<TextIcon6 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DashboardMenu() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex flex-col h-[861px] items-center justify-between relative shrink-0 w-full" data-name="dashboard menu">
|
||||||
|
<TopContainer />
|
||||||
|
<Dashboard />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardMenu1() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#effcff] relative shadow-[0px_24px_48px_-12px_rgba(0,0,0,0.18)] size-full" data-name="dashboard menu">
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="box-border content-stretch flex flex-col gap-6 items-start justify-start px-6 py-[31px] relative size-full">
|
||||||
|
<Logo />
|
||||||
|
<div className="h-0 relative shrink-0 w-[327px]">
|
||||||
|
<div className="absolute inset-[-1px_-0.31%]">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 329 2">
|
||||||
|
<path d="M1 1H328" id="Line 1" stroke="var(--stroke-0, black)" strokeLinecap="round" strokeOpacity="0.12" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DashboardMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
313
src/imports/FlightBookingRafiki.tsx
Normal file
313
src/imports/FlightBookingRafiki.tsx
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
import svgPaths from "./svg-fswka8rhrw";
|
||||||
|
import flightBookingImage from "figma:asset/fe850ec33f7da20cadeecc91695cda7ad837415e.png";
|
||||||
|
|
||||||
|
function BackgroundComplete() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bottom-[7.01%] left-0 right-0 top-0" data-name="background-complete">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 500 347">
|
||||||
|
<g id="background-complete">
|
||||||
|
<path d={svgPaths.p1791d480} fill="var(--fill-0, #EBEBEB)" id="Vector" />
|
||||||
|
<path d={svgPaths.p16b03f00} fill="var(--fill-0, #EBEBEB)" id="Vector_2" />
|
||||||
|
<path d={svgPaths.p1d98900} fill="var(--fill-0, #EBEBEB)" id="Vector_3" />
|
||||||
|
<path d={svgPaths.p27718480} fill="var(--fill-0, #EBEBEB)" id="Vector_4" />
|
||||||
|
<path d={svgPaths.pae23400} fill="var(--fill-0, #EBEBEB)" id="Vector_5" />
|
||||||
|
<path d={svgPaths.pfa23700} fill="var(--fill-0, #EBEBEB)" id="Vector_6" />
|
||||||
|
<path d={svgPaths.p2c6b0c00} fill="var(--fill-0, #EBEBEB)" id="Vector_7" />
|
||||||
|
<path d={svgPaths.p15520500} fill="var(--fill-0, #EBEBEB)" id="Vector_8" />
|
||||||
|
<path d={svgPaths.p21fa0100} fill="var(--fill-0, #EBEBEB)" id="Vector_9" />
|
||||||
|
<path d={svgPaths.p8568380} fill="var(--fill-0, #E0E0E0)" id="Vector_10" />
|
||||||
|
<path d={svgPaths.p2d76180} fill="var(--fill-0, #F0F0F0)" id="Vector_11" />
|
||||||
|
<g id="Vector_12">
|
||||||
|
<path d={svgPaths.p399c4100} fill="var(--fill-0, #F0F0F0)" />
|
||||||
|
<path d={svgPaths.p399c4100} stroke="var(--stroke-0, #E0E0E0)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
</g>
|
||||||
|
<path d={svgPaths.p7d81480} fill="var(--fill-0, #E0E0E0)" id="Vector_13" />
|
||||||
|
<path d={svgPaths.p3e10fa80} fill="var(--fill-0, #E0E0E0)" id="Vector_14" />
|
||||||
|
<path d={svgPaths.p287e9580} fill="var(--fill-0, #E0E0E0)" id="Vector_15" />
|
||||||
|
<path d={svgPaths.p1b063c00} fill="var(--fill-0, #E0E0E0)" id="Vector_16" />
|
||||||
|
<path d={svgPaths.p19dd0a30} fill="var(--fill-0, #E0E0E0)" id="Vector_17" />
|
||||||
|
<path d={svgPaths.p3269cc80} fill="var(--fill-0, #E0E0E0)" id="Vector_18" />
|
||||||
|
<path d={svgPaths.pfff5dc0} fill="var(--fill-0, #EBEBEB)" id="Vector_19" />
|
||||||
|
<path d={svgPaths.pb065d40} fill="var(--fill-0, #EBEBEB)" id="Vector_20" />
|
||||||
|
<path d={svgPaths.p13d75400} fill="var(--fill-0, #EBEBEB)" id="Vector_21" />
|
||||||
|
<path d={svgPaths.p133c9700} fill="var(--fill-0, #EBEBEB)" id="Vector_22" />
|
||||||
|
<path d={svgPaths.p1ddb780} fill="var(--fill-0, #EBEBEB)" id="Vector_23" />
|
||||||
|
<path d={svgPaths.p1c624700} fill="var(--fill-0, #EBEBEB)" id="Vector_24" />
|
||||||
|
<path d={svgPaths.p2eb8c500} fill="var(--fill-0, #EBEBEB)" id="Vector_25" />
|
||||||
|
<path d={svgPaths.p3e566380} fill="var(--fill-0, #EBEBEB)" id="Vector_26" />
|
||||||
|
<path d={svgPaths.p275aac00} fill="var(--fill-0, #F0F0F0)" id="Vector_27" />
|
||||||
|
<path d={svgPaths.p1d2af780} fill="var(--fill-0, #E6E6E6)" id="Vector_28" />
|
||||||
|
<path d={svgPaths.p2767f380} fill="var(--fill-0, #E0E0E0)" id="Vector_29" />
|
||||||
|
<path d={svgPaths.p3c918700} fill="var(--fill-0, #EBEBEB)" id="Vector_30" />
|
||||||
|
<path d={svgPaths.p31fa4c80} fill="var(--fill-0, #EBEBEB)" id="Vector_31" />
|
||||||
|
<path d={svgPaths.pd55e600} fill="var(--fill-0, #EBEBEB)" id="Vector_32" />
|
||||||
|
<path d={svgPaths.p36f50f00} fill="var(--fill-0, #EBEBEB)" id="Vector_33" />
|
||||||
|
<path d={svgPaths.p1afc5b80} fill="var(--fill-0, #EBEBEB)" id="Vector_34" />
|
||||||
|
<path d={svgPaths.p16feb000} fill="var(--fill-0, #EBEBEB)" id="Vector_35" />
|
||||||
|
<path d={svgPaths.p2edec780} fill="var(--fill-0, #EBEBEB)" id="Vector_36" />
|
||||||
|
<path d={svgPaths.p7432a00} fill="var(--fill-0, #EBEBEB)" id="Vector_37" />
|
||||||
|
<path d={svgPaths.pd30380} fill="var(--fill-0, #EBEBEB)" id="Vector_38" />
|
||||||
|
<path d={svgPaths.p16f8f300} fill="var(--fill-0, #EBEBEB)" id="Vector_39" />
|
||||||
|
<path d={svgPaths.p3f441df0} fill="var(--fill-0, #E0E0E0)" id="Vector_40" />
|
||||||
|
<path d={svgPaths.p1d97f380} fill="var(--fill-0, #EBEBEB)" id="Vector_41" />
|
||||||
|
<path d={svgPaths.p39639500} fill="var(--fill-0, #EBEBEB)" id="Vector_42" />
|
||||||
|
<path d={svgPaths.p27979e80} fill="var(--fill-0, #EBEBEB)" id="Vector_43" />
|
||||||
|
<path d={svgPaths.p9c5fcf0} fill="var(--fill-0, #EBEBEB)" id="Vector_44" />
|
||||||
|
<path d={svgPaths.p2c469500} fill="var(--fill-0, #EBEBEB)" id="Vector_45" />
|
||||||
|
<path d={svgPaths.p18bb9900} fill="var(--fill-0, #EBEBEB)" id="Vector_46" />
|
||||||
|
<path d={svgPaths.p1cc02300} fill="var(--fill-0, #EBEBEB)" id="Vector_47" />
|
||||||
|
<path d={svgPaths.p5bb3c80} fill="var(--fill-0, #EBEBEB)" id="Vector_48" />
|
||||||
|
<path d={svgPaths.p217d8700} fill="var(--fill-0, #EBEBEB)" id="Vector_49" />
|
||||||
|
<path d={svgPaths.p3ef46600} fill="var(--fill-0, #EBEBEB)" id="Vector_50" />
|
||||||
|
<path d={svgPaths.p37008900} fill="var(--fill-0, #E6E6E6)" id="Vector_51" />
|
||||||
|
<path d={svgPaths.p4aed700} fill="var(--fill-0, #F0F0F0)" id="Vector_52" />
|
||||||
|
<path d={svgPaths.p395bfe80} fill="var(--fill-0, white)" id="Vector_53" />
|
||||||
|
<path d={svgPaths.p10252e00} fill="var(--fill-0, white)" id="Vector_54" />
|
||||||
|
<path d={svgPaths.p3f28f940} fill="var(--fill-0, white)" id="Vector_55" />
|
||||||
|
<path d={svgPaths.p28ae59c0} fill="var(--fill-0, #E6E6E6)" id="Vector_56" />
|
||||||
|
<path d={svgPaths.p2312b7c0} fill="var(--fill-0, #E6E6E6)" id="Vector_57" />
|
||||||
|
<path d={svgPaths.p3987ae00} fill="var(--fill-0, #E0E0E0)" id="Vector_58" />
|
||||||
|
<path d={svgPaths.pf823500} fill="var(--fill-0, #E6E6E6)" id="Vector_59" />
|
||||||
|
<path d={svgPaths.p19f30980} fill="var(--fill-0, #F0F0F0)" id="Vector_60" />
|
||||||
|
<path d={svgPaths.p26ec7700} fill="var(--fill-0, #F0F0F0)" id="Vector_61" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BackgroundSimple() {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-[7.97%_12.27%_7.77%_21.48%]" data-name="background-simple">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 32 32">
|
||||||
|
<g id="background-simple" opacity="0">
|
||||||
|
<path d={svgPaths.p33c34680} fill="var(--fill-0, #878787)" id="Vector" />
|
||||||
|
<path d={svgPaths.p33c34680} fill="var(--fill-0, white)" id="Vector_2" opacity="0.9" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Shadow() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bottom-0 left-[11.22%] right-[11.22%] top-[93.92%]" data-name="Shadow">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 388 23">
|
||||||
|
<g id="Shadow">
|
||||||
|
<path d={svgPaths.p32fbd720} fill="var(--fill-0, #F5F5F5)" id="path" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Window() {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-[20.36%_53.6%_48.09%_28.11%]" data-name="Window">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 92 118">
|
||||||
|
<g id="Window">
|
||||||
|
<path d={svgPaths.p26423900} fill="var(--fill-0, #878787)" id="Vector" />
|
||||||
|
<path d={svgPaths.p26423900} fill="var(--fill-0, white)" id="Vector_2" opacity="0.8" />
|
||||||
|
<path d={svgPaths.pf19080} fill="var(--fill-0, #878787)" id="Vector_3" />
|
||||||
|
<path d={svgPaths.p17a5c500} fill="var(--fill-0, white)" id="Vector_4" opacity="0.3" />
|
||||||
|
<path d={svgPaths.paea9300} fill="var(--fill-0, #F0F0F0)" id="Vector_5" />
|
||||||
|
<path d={svgPaths.p1c719480} fill="var(--fill-0, #F0F0F0)" id="Vector_6" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Device() {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-[7.46%_12.95%_18.52%_55.08%]" data-name="Device">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 160 276">
|
||||||
|
<g id="Device">
|
||||||
|
<path d={svgPaths.p8347d00} fill="var(--fill-0, #878787)" id="Vector" />
|
||||||
|
<path d={svgPaths.p8347d00} fill="var(--fill-0, black)" id="Vector_2" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p37c1ac00} fill="var(--fill-0, #878787)" id="Vector_3" />
|
||||||
|
<path d={svgPaths.p2abc3f70} fill="var(--fill-0, #263238)" id="Vector_4" />
|
||||||
|
<path d={svgPaths.p349f9f00} fill="var(--fill-0, white)" id="Vector_5" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p24fec7f0} fill="var(--fill-0, white)" id="Vector_6" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p3e7ef800} fill="var(--fill-0, white)" id="Vector_7" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p85f60f0} fill="var(--fill-0, white)" id="Vector_8" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p2a6e6100} fill="var(--fill-0, white)" id="Vector_9" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p32d1caf0} fill="var(--fill-0, white)" id="Vector_10" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p24400600} fill="var(--fill-0, white)" id="Vector_11" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p25d2af00} fill="var(--fill-0, white)" id="Vector_12" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p3bdd9800} fill="var(--fill-0, white)" id="Vector_13" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p122acec0} fill="var(--fill-0, #878787)" id="Vector_14" />
|
||||||
|
<path d={svgPaths.p122acec0} fill="var(--fill-0, white)" id="Vector_15" opacity="0.4" />
|
||||||
|
<path d={svgPaths.pbc9f980} fill="var(--fill-0, #878787)" id="Vector_16" />
|
||||||
|
<path d={svgPaths.p5fd2e80} fill="var(--fill-0, white)" id="Vector_17" opacity="0.6" />
|
||||||
|
<path d={svgPaths.pad2e780} fill="var(--fill-0, white)" id="Vector_18" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p2fc11400} fill="var(--fill-0, white)" id="Vector_19" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p2faec300} fill="var(--fill-0, white)" id="Vector_20" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p3e2a8700} fill="var(--fill-0, #F0F0F0)" id="Vector_21" />
|
||||||
|
<path d={svgPaths.p3123ba00} fill="var(--fill-0, #F0F0F0)" id="Vector_22" />
|
||||||
|
<path d={svgPaths.p2b508700} fill="var(--fill-0, #F0F0F0)" id="Vector_23" />
|
||||||
|
<path d={svgPaths.p100f5e00} fill="var(--fill-0, #F0F0F0)" id="Vector_24" />
|
||||||
|
<path d={svgPaths.p13b05300} fill="var(--fill-0, #878787)" id="Vector_25" />
|
||||||
|
<path d={svgPaths.p28fa780} fill="var(--fill-0, #878787)" id="Vector_26" />
|
||||||
|
<path d={svgPaths.p2519fa00} fill="var(--fill-0, #878787)" id="Vector_27" />
|
||||||
|
<path d={svgPaths.p14315500} fill="var(--fill-0, #878787)" id="Vector_28" />
|
||||||
|
<path d={svgPaths.p297a0ac0} fill="var(--fill-0, white)" id="Vector_29" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p98cf080} fill="var(--fill-0, white)" id="Vector_30" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p5c5dc40} fill="var(--fill-0, white)" id="Vector_31" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p3f497600} fill="var(--fill-0, white)" id="Vector_32" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p1fafef00} fill="var(--fill-0, white)" id="Vector_33" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p2c4d00} fill="var(--fill-0, white)" id="Vector_34" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p1ebc0f00} fill="var(--fill-0, #878787)" id="Vector_35" />
|
||||||
|
<path d={svgPaths.p2b87ea80} fill="var(--fill-0, white)" id="Vector_36" />
|
||||||
|
<path d={svgPaths.p3b000000} fill="var(--fill-0, white)" id="Vector_37" />
|
||||||
|
<path d={svgPaths.p239464c0} fill="var(--fill-0, white)" id="Vector_38" />
|
||||||
|
<path d={svgPaths.p2d12a880} fill="var(--fill-0, white)" id="Vector_39" />
|
||||||
|
<path d={svgPaths.p3747b180} fill="var(--fill-0, white)" id="Vector_40" />
|
||||||
|
<path d={svgPaths.p30281a00} fill="var(--fill-0, white)" id="Vector_41" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Character() {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-[32.43%_43.9%_2.71%_13.16%]" data-name="Character">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 215 242">
|
||||||
|
<g id="Character">
|
||||||
|
<path d={svgPaths.p39bc5500} fill="var(--fill-0, #263238)" id="Vector" />
|
||||||
|
<path d={svgPaths.p39bc5500} fill="var(--fill-0, white)" id="Vector_2" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p26024c0} fill="var(--fill-0, #263238)" id="Vector_3" />
|
||||||
|
<path d={svgPaths.p39e5970} fill="var(--fill-0, #263238)" id="Vector_4" />
|
||||||
|
<path d={svgPaths.p3e491780} fill="var(--fill-0, #263238)" id="Vector_5" />
|
||||||
|
<path d={svgPaths.p3e491780} fill="var(--fill-0, white)" id="Vector_6" opacity="0.3" />
|
||||||
|
<path d={svgPaths.p15c1f180} fill="var(--fill-0, #263238)" id="Vector_7" />
|
||||||
|
<path d={svgPaths.p27f81500} fill="var(--fill-0, #263238)" id="Vector_8" />
|
||||||
|
<path d={svgPaths.p27f81500} fill="var(--fill-0, white)" id="Vector_9" opacity="0.3" />
|
||||||
|
<path d={svgPaths.p21515c30} fill="var(--fill-0, #263238)" id="Vector_10" />
|
||||||
|
<path d={svgPaths.p1e867600} fill="var(--fill-0, #263238)" id="Vector_11" />
|
||||||
|
<path d={svgPaths.p1e867600} fill="var(--fill-0, white)" id="Vector_12" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p11005380} fill="var(--fill-0, #263238)" id="Vector_13" />
|
||||||
|
<path d={svgPaths.p34e8900} fill="var(--fill-0, white)" id="Vector_14" opacity="0.4" />
|
||||||
|
<path d={svgPaths.p11854a00} fill="var(--fill-0, black)" id="Vector_15" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p26290c80} fill="var(--fill-0, #E4897B)" id="Vector_16" />
|
||||||
|
<path d={svgPaths.p280ffe80} fill="var(--fill-0, #2E353A)" id="Vector_17" />
|
||||||
|
<path d={svgPaths.p6372df1} fill="var(--fill-0, #E4897B)" id="Vector_18" />
|
||||||
|
<path d={svgPaths.p6861500} fill="var(--fill-0, #2E353A)" id="Vector_19" />
|
||||||
|
<path d={svgPaths.p845300} fill="var(--fill-0, #E4897B)" id="Vector_20" />
|
||||||
|
<path d={svgPaths.p845300} fill="var(--fill-0, black)" id="Vector_21" opacity="0.1" />
|
||||||
|
<path d={svgPaths.p3efa5800} fill="var(--fill-0, black)" id="Vector_22" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p6483d00} fill="var(--fill-0, #E4897B)" id="Vector_23" />
|
||||||
|
<path d={svgPaths.p3a585000} fill="var(--fill-0, black)" id="Vector_24" opacity="0.1" />
|
||||||
|
<path d={svgPaths.p7937200} fill="var(--fill-0, black)" id="Vector_25" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p31bf4600} fill="var(--fill-0, #263238)" id="Vector_26" />
|
||||||
|
<path d={svgPaths.p172f8500} fill="var(--fill-0, white)" id="Vector_27" opacity="0.1" />
|
||||||
|
<path d={svgPaths.p10cece80} fill="var(--fill-0, black)" id="Vector_28" opacity="0.3" />
|
||||||
|
<path d={svgPaths.p30e37500} fill="var(--fill-0, #878787)" id="Vector_29" />
|
||||||
|
<path d={svgPaths.p30e37500} fill="var(--fill-0, black)" id="Vector_30" opacity="0.2" />
|
||||||
|
<path d={svgPaths.pb3d8080} fill="var(--fill-0, #263238)" id="Vector_31" />
|
||||||
|
<path d={svgPaths.p3c78b000} fill="var(--fill-0, white)" id="Vector_32" opacity="0.5" />
|
||||||
|
<path d={svgPaths.p9279600} fill="var(--fill-0, #E4897B)" id="Vector_33" />
|
||||||
|
<path d={svgPaths.p144c44a0} fill="var(--fill-0, #E4897B)" id="Vector_34" />
|
||||||
|
<path d={svgPaths.p1d892300} fill="var(--fill-0, black)" id="Vector_35" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p31fbdc10} fill="var(--fill-0, #878787)" id="Vector_36" />
|
||||||
|
<path d={svgPaths.p31fbdc10} fill="var(--fill-0, black)" id="Vector_37" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p39d63980} fill="var(--fill-0, black)" id="Vector_38" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p1a4d4600} fill="var(--fill-0, black)" id="Vector_39" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p30fd91f0} fill="var(--fill-0, white)" id="Vector_40" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p34aaa00} fill="var(--fill-0, black)" id="Vector_41" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p3b9e2ac0} fill="var(--fill-0, #263238)" id="Vector_42" />
|
||||||
|
<path d={svgPaths.p3bc5d200} fill="var(--fill-0, black)" id="Vector_43" opacity="0.2" />
|
||||||
|
<path d={svgPaths.pad53b00} fill="var(--fill-0, white)" id="Vector_44" opacity="0.4" />
|
||||||
|
<path d={svgPaths.p12456a80} fill="var(--fill-0, #878787)" id="Vector_45" />
|
||||||
|
<path d={svgPaths.p12456a80} fill="var(--fill-0, white)" id="Vector_46" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p15eef400} fill="var(--fill-0, #263238)" id="Vector_47" />
|
||||||
|
<path d={svgPaths.p30a7ae00} fill="var(--fill-0, #263238)" id="Vector_48" />
|
||||||
|
<path d={svgPaths.pffde580} fill="var(--fill-0, #E4897B)" id="Vector_49" />
|
||||||
|
<path d={svgPaths.p29726300} fill="var(--fill-0, black)" id="Vector_50" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p1cef5000} fill="var(--fill-0, #263238)" id="Vector_51" />
|
||||||
|
<path d={svgPaths.p3914700} fill="var(--fill-0, #E4897B)" id="Vector_52" />
|
||||||
|
<path d={svgPaths.pd5db800} fill="var(--fill-0, #DE5753)" id="Vector_53" />
|
||||||
|
<path d={svgPaths.p2854c400} fill="var(--fill-0, #263238)" id="Vector_54" />
|
||||||
|
<path d={svgPaths.p2f8eef00} fill="var(--fill-0, #263238)" id="Vector_55" />
|
||||||
|
<path d={svgPaths.p244d1480} fill="var(--fill-0, #263238)" id="Vector_56" />
|
||||||
|
<path d={svgPaths.p80c3a00} fill="var(--fill-0, #263238)" id="Vector_57" />
|
||||||
|
<path d={svgPaths.pbd90290} id="Vector_58" stroke="var(--stroke-0, #263238)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="0.72468" />
|
||||||
|
<path d={svgPaths.p3c06700} id="Vector_59" stroke="var(--stroke-0, #263238)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="0.72468" />
|
||||||
|
<path d={svgPaths.p37166600} fill="var(--fill-0, #263238)" id="Vector_60" />
|
||||||
|
<path d={svgPaths.p14f5200} fill="var(--fill-0, #E4897B)" id="Vector_61" />
|
||||||
|
<path d={svgPaths.p94c3c0} fill="var(--fill-0, #263238)" id="Vector_62" />
|
||||||
|
<path d={svgPaths.p321ca900} fill="var(--fill-0, white)" id="Vector_63" />
|
||||||
|
<path d={svgPaths.p16c47380} fill="var(--fill-0, #DE5753)" id="Vector_64" />
|
||||||
|
<path d={svgPaths.p1b36b200} fill="var(--fill-0, #878787)" id="Vector_65" />
|
||||||
|
<path d={svgPaths.p1b36b200} fill="var(--fill-0, black)" id="Vector_66" opacity="0.2" />
|
||||||
|
<g id="Group" opacity="0.3">
|
||||||
|
<path d={svgPaths.p36da600} fill="var(--fill-0, white)" id="Vector_67" />
|
||||||
|
<path d={svgPaths.p1a314f80} fill="var(--fill-0, white)" id="Vector_68" />
|
||||||
|
<path d={svgPaths.p8726000} fill="var(--fill-0, white)" id="Vector_69" />
|
||||||
|
<path d={svgPaths.p34c9a380} fill="var(--fill-0, white)" id="Vector_70" />
|
||||||
|
</g>
|
||||||
|
<path d={svgPaths.p3149aa00} id="Vector_71" stroke="var(--stroke-0, #263238)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="0.5" />
|
||||||
|
<path d={svgPaths.p178a2100} fill="var(--fill-0, #263238)" id="Vector_72" />
|
||||||
|
<path d={svgPaths.pa8d6300} fill="var(--fill-0, #878787)" id="Vector_73" />
|
||||||
|
<path d={svgPaths.pa8d6300} fill="var(--fill-0, black)" id="Vector_74" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p36b03900} fill="var(--fill-0, white)" id="Vector_75" />
|
||||||
|
<path d={svgPaths.p36b03900} fill="var(--fill-0, black)" id="Vector_76" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p35f3d170} fill="var(--fill-0, white)" id="Vector_77" />
|
||||||
|
<path d={svgPaths.p35f3d170} fill="var(--fill-0, black)" id="Vector_78" opacity="0.1" />
|
||||||
|
<path d={svgPaths.pc63db00} fill="var(--fill-0, #263238)" id="Vector_79" />
|
||||||
|
<path d={svgPaths.p2f8bd080} fill="var(--fill-0, black)" id="Vector_80" opacity="0.2" />
|
||||||
|
<path d={svgPaths.pb0c8bf0} fill="var(--fill-0, white)" id="Vector_81" opacity="0.4" />
|
||||||
|
<path d={svgPaths.p32ff6780} fill="var(--fill-0, white)" id="Vector_82" opacity="0.4" />
|
||||||
|
<path d={svgPaths.p3f229600} fill="var(--fill-0, white)" id="Vector_83" opacity="0.4" />
|
||||||
|
<path d={svgPaths.p3fbd8e00} fill="var(--fill-0, white)" id="Vector_84" />
|
||||||
|
<path d={svgPaths.p3fbd8e00} fill="var(--fill-0, black)" id="Vector_85" opacity="0.2" />
|
||||||
|
<path d={svgPaths.pc3c7b00} fill="var(--fill-0, white)" id="Vector_86" />
|
||||||
|
<path d={svgPaths.pc3c7b00} fill="var(--fill-0, black)" id="Vector_87" opacity="0.1" />
|
||||||
|
<path d={svgPaths.p1be4f00} fill="var(--fill-0, #878787)" id="Vector_88" />
|
||||||
|
<path d={svgPaths.p3d945900} fill="var(--fill-0, black)" id="Vector_89" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p528a700} fill="var(--fill-0, white)" id="Vector_90" opacity="0.4" />
|
||||||
|
<path d={svgPaths.p2cc4840} fill="var(--fill-0, white)" id="Vector_91" opacity="0.4" />
|
||||||
|
<path d={svgPaths.p3946bb40} fill="var(--fill-0, white)" id="Vector_92" opacity="0.4" />
|
||||||
|
<path d={svgPaths.p16909200} fill="var(--fill-0, #263238)" id="Vector_93" />
|
||||||
|
<path d={svgPaths.p1072e180} fill="var(--fill-0, white)" id="Vector_94" opacity="0.5" />
|
||||||
|
<path d={svgPaths.p36836700} fill="var(--fill-0, black)" id="Vector_95" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p2e195d00} fill="var(--fill-0, #263238)" id="Vector_96" />
|
||||||
|
<path d={svgPaths.p9186440} fill="var(--fill-0, white)" id="Vector_97" opacity="0.5" />
|
||||||
|
<path d={svgPaths.p2a953080} fill="var(--fill-0, black)" id="Vector_98" opacity="0.2" />
|
||||||
|
<path d={svgPaths.pd1dd780} fill="var(--fill-0, #263238)" id="Vector_99" />
|
||||||
|
<path d={svgPaths.p7e68900} fill="var(--fill-0, white)" id="Vector_100" opacity="0.5" />
|
||||||
|
<path d={svgPaths.p1c6b3980} fill="var(--fill-0, black)" id="Vector_101" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p1f3b9980} fill="var(--fill-0, #878787)" id="Vector_102" />
|
||||||
|
<path d={svgPaths.p1f3b9980} fill="var(--fill-0, white)" id="Vector_103" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p2e5a2600} fill="var(--fill-0, black)" id="Vector_104" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p3a1e4c00} fill="var(--fill-0, #878787)" id="Vector_105" />
|
||||||
|
<path d={svgPaths.p3a1e4c00} fill="var(--fill-0, white)" id="Vector_106" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p1d9a6680} fill="var(--fill-0, #263238)" id="Vector_107" />
|
||||||
|
<path d={svgPaths.p1d9a6680} fill="var(--fill-0, white)" id="Vector_108" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p62be880} fill="var(--fill-0, black)" id="Vector_109" opacity="0.2" />
|
||||||
|
<path d={svgPaths.pb15fd00} fill="var(--fill-0, #263238)" id="Vector_110" />
|
||||||
|
<path d={svgPaths.pb15fd00} fill="var(--fill-0, white)" id="Vector_111" opacity="0.6" />
|
||||||
|
<path d={svgPaths.p1ad54580} fill="var(--fill-0, white)" id="Vector_112" opacity="0.4" />
|
||||||
|
<path d={svgPaths.pb6e5930} fill="var(--fill-0, black)" id="Vector_113" opacity="0.2" />
|
||||||
|
<path d={svgPaths.p31e2fb80} fill="var(--fill-0, #263238)" id="Vector_114" />
|
||||||
|
<g id="Group_2" opacity="0.2">
|
||||||
|
<path d={svgPaths.p31e2fb80} fill="var(--fill-0, white)" id="Vector_115" />
|
||||||
|
</g>
|
||||||
|
<path d={svgPaths.p6032f80} fill="var(--fill-0, #263238)" id="Vector_116" />
|
||||||
|
<path d={svgPaths.p6032f80} fill="var(--fill-0, white)" id="Vector_117" opacity="0.6" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FlightBookingRafiki() {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={flightBookingImage}
|
||||||
|
alt="Person working on laptop with travel app"
|
||||||
|
data-name="flight-booking/rafiki"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/imports/Group58845.tsx
Normal file
67
src/imports/Group58845.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import svgPaths from "./svg-wyhfl2ip3y";
|
||||||
|
|
||||||
|
function LightGray() {
|
||||||
|
return (
|
||||||
|
<div className="relative size-full" data-name="LightGray">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 43 70">
|
||||||
|
<g id="LightGray">
|
||||||
|
<path d={svgPaths.p276f1b00} fill="var(--fill-0, white)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Group58839() {
|
||||||
|
return (
|
||||||
|
<div className="h-[19.106px] relative w-[19.346px]">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 20 20">
|
||||||
|
<g id="Group 58839">
|
||||||
|
<path d={svgPaths.p27401e00} fill="var(--fill-0, white)" id="Rectangle 120" />
|
||||||
|
<path d={svgPaths.p158d9f00} fill="var(--fill-0, white)" id="Rectangle 121" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Group58841() {
|
||||||
|
return (
|
||||||
|
<div className="absolute contents left-[92.44px] top-[50.85px]">
|
||||||
|
<div className="absolute flex inset-[33.33%_71.56%_21.43%_19.49%] items-center justify-center">
|
||||||
|
<div className="flex-none h-[69.394px] scale-y-[-100%] w-[42.458px]">
|
||||||
|
<LightGray />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute flex h-[25.621px] items-center justify-center left-[116.32px] top-[50.85px] w-[25.737px]">
|
||||||
|
<div className="flex-none rotate-[25.872deg]">
|
||||||
|
<Group58839 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Group58845() {
|
||||||
|
return (
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="absolute font-['Chillax:Medium',_sans-serif] leading-[0] left-0 not-italic text-[109.04px] text-nowrap text-white top-[0.38px] tracking-[-6.5424px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">{`Ci `}</p>
|
||||||
|
</div>
|
||||||
|
<div className="absolute font-['Chillax:Medium',_sans-serif] leading-[0] left-[139.38px] not-italic text-[109.04px] text-nowrap text-white top-0 tracking-[-7.6328px]">
|
||||||
|
<p className="leading-[normal] whitespace-pre">yCards</p>
|
||||||
|
</div>
|
||||||
|
<Group58841 />
|
||||||
|
<div className="absolute left-[281.21px] size-[9.644px] top-[47.02px]">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 10 10">
|
||||||
|
<circle cx="4.82221" cy="4.82221" fill="var(--fill-0, white)" id="Ellipse 4" r="4.82221" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="absolute h-[10.238px] left-[297.23px] top-[42.2px] w-[9.644px]">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 10 11">
|
||||||
|
<ellipse cx="4.82221" cy="5.11877" fill="var(--fill-0, white)" id="Ellipse 5" rx="4.82221" ry="5.11877" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
254
src/imports/Notifications.tsx
Normal file
254
src/imports/Notifications.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import svgPaths from "./svg-xewg6e9vz";
|
||||||
|
|
||||||
|
function MdiAccount() {
|
||||||
|
return (
|
||||||
|
<div className="absolute left-5 size-[60px] top-5" data-name="mdi:account">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 60 60">
|
||||||
|
<g id="mdi:account">
|
||||||
|
<path d={svgPaths.p343fd100} fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Details() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex flex-col gap-2 items-start justify-start leading-[0] relative shrink-0 text-black w-[449px]" data-name="details">
|
||||||
|
<div className="font-['Figtree:SemiBold',_sans-serif] font-semibold relative shrink-0 text-[16px] w-full">
|
||||||
|
<p className="leading-[23.556px]">Account Update Notification</p>
|
||||||
|
</div>
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal relative shrink-0 text-[14px] w-full">
|
||||||
|
<p className="leading-[20px]">Your account has been successfully updated. Please check your email for confirmation.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationDetails() {
|
||||||
|
return (
|
||||||
|
<div className="absolute content-stretch flex gap-3.5 items-start justify-start left-3 top-3" data-name="notification details">
|
||||||
|
<div className="bg-[#f6f6f6] rounded-[12px] shrink-0 size-[100px]" data-name="image" />
|
||||||
|
<MdiAccount />
|
||||||
|
<Details />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Close() {
|
||||||
|
return (
|
||||||
|
<div className="absolute left-[1061px] size-5 top-3" data-name="close">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 20 20">
|
||||||
|
<g id="close">
|
||||||
|
<path d={svgPaths.p1f279900} fill="var(--fill-0, #656D76)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Notification() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bg-white h-[124px] left-0 overflow-clip rounded-[12px] shadow-[0px_20px_24px_-4px_rgba(0,0,0,0.08),0px_8px_8px_-4px_rgba(0,0,0,0.03)] top-0 w-[1093px]" data-name="notification">
|
||||||
|
<NotificationDetails />
|
||||||
|
<Close />
|
||||||
|
<div className="absolute flex flex-col font-['Figtree:Regular',_sans-serif] font-normal h-[15px] justify-center leading-[0] left-[1016px] text-[16px] text-[rgba(0,0,0,0.42)] top-[104.5px] translate-y-[-50%] w-[65px]">
|
||||||
|
<p className="leading-[23.556px]">10/01/23</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Details1() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex flex-col gap-2 items-start justify-start leading-[0] relative shrink-0 text-black w-[449px]" data-name="details">
|
||||||
|
<div className="font-['Figtree:SemiBold',_sans-serif] font-semibold relative shrink-0 text-[16px] w-full">
|
||||||
|
<p className="leading-[23.556px]">New Message Alert</p>
|
||||||
|
</div>
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal relative shrink-0 text-[14px] w-full">
|
||||||
|
<p className="leading-[20px]">We have received your request and are processing it. Expect an update shortly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WpfMessage() {
|
||||||
|
return (
|
||||||
|
<div className="absolute left-5 size-[60px] top-5" data-name="wpf:message">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 60 60">
|
||||||
|
<g id="wpf:message">
|
||||||
|
<path d={svgPaths.paa2b800} fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationDetails1() {
|
||||||
|
return (
|
||||||
|
<div className="absolute content-stretch flex gap-3.5 items-start justify-start left-3 top-3" data-name="notification details">
|
||||||
|
<div className="bg-[#f6f6f6] rounded-[12px] shrink-0 size-[100px]" data-name="image" />
|
||||||
|
<Details1 />
|
||||||
|
<WpfMessage />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Close1() {
|
||||||
|
return (
|
||||||
|
<div className="absolute left-[1061px] size-5 top-3" data-name="close">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 20 20">
|
||||||
|
<g id="close">
|
||||||
|
<path d={svgPaths.p1f279900} fill="var(--fill-0, #656D76)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Notification1() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bg-white h-[124px] left-0 overflow-clip rounded-[12px] shadow-[0px_20px_24px_-4px_rgba(0,0,0,0.08),0px_8px_8px_-4px_rgba(0,0,0,0.03)] top-[136px] w-[1093px]" data-name="notification">
|
||||||
|
<NotificationDetails1 />
|
||||||
|
<Close1 />
|
||||||
|
<div className="absolute flex flex-col font-['Figtree:Regular',_sans-serif] font-normal h-[15px] justify-center leading-[0] left-[1016px] text-[16px] text-[rgba(0,0,0,0.42)] top-[104.5px] translate-y-[-50%] w-[65px]">
|
||||||
|
<p className="leading-[23.556px]">10/01/23</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Details2() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex flex-col gap-2 items-start justify-start leading-[0] relative shrink-0 text-black w-[449px]" data-name="details">
|
||||||
|
<div className="font-['Figtree:SemiBold',_sans-serif] font-semibold relative shrink-0 text-[16px] w-full">
|
||||||
|
<p className="leading-[23.556px]">System Maintenance Notice</p>
|
||||||
|
</div>
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal relative shrink-0 text-[14px] w-full">
|
||||||
|
<p className="leading-[23.556px]">{`A new message has arrived in your inbox. Don't forget to check it out!`}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function IcRoundEngineering() {
|
||||||
|
return (
|
||||||
|
<div className="absolute left-5 size-[60px] top-5" data-name="ic:round-engineering">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 60 60">
|
||||||
|
<g id="ic:round-engineering">
|
||||||
|
<path d={svgPaths.p2aacbc00} fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationDetails2() {
|
||||||
|
return (
|
||||||
|
<div className="absolute content-stretch flex gap-3.5 items-start justify-start left-3 top-3" data-name="notification details">
|
||||||
|
<div className="bg-[#f6f6f6] rounded-[12px] shrink-0 size-[100px]" data-name="image" />
|
||||||
|
<Details2 />
|
||||||
|
<IcRoundEngineering />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Close2() {
|
||||||
|
return (
|
||||||
|
<div className="absolute left-[1061px] size-5 top-3" data-name="close">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 20 20">
|
||||||
|
<g id="close">
|
||||||
|
<path d={svgPaths.p1f279900} fill="var(--fill-0, #656D76)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Notification2() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bg-white h-[124px] left-0 opacity-[0.42] overflow-clip rounded-[12px] top-[272px] w-[1093px]" data-name="notification">
|
||||||
|
<NotificationDetails2 />
|
||||||
|
<Close2 />
|
||||||
|
<div className="absolute flex flex-col font-['Figtree:Regular',_sans-serif] font-normal h-[15px] justify-center leading-[0] left-[1016px] text-[16px] text-[rgba(0,0,0,0.42)] top-[104.5px] translate-y-[-50%] w-[65px]">
|
||||||
|
<p className="leading-[23.556px]">10/01/23</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Details3() {
|
||||||
|
return (
|
||||||
|
<div className="content-stretch flex flex-col gap-2 items-start justify-start leading-[0] relative shrink-0 text-black w-[449px]" data-name="details">
|
||||||
|
<div className="font-['Figtree:SemiBold',_sans-serif] font-semibold relative shrink-0 text-[16px] w-full">
|
||||||
|
<p className="leading-[23.556px]">Event Registration Confirmation</p>
|
||||||
|
</div>
|
||||||
|
<div className="font-['Figtree:Regular',_sans-serif] font-normal relative shrink-0 text-[14px] w-full">
|
||||||
|
<p className="leading-[23.556px]">Scheduled maintenance will occur this weekend. We appreciate your understanding</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LineMdCalendar() {
|
||||||
|
return (
|
||||||
|
<div className="absolute left-5 size-[60px] top-5" data-name="line-md:calendar">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 60 60">
|
||||||
|
<g id="line-md:calendar">
|
||||||
|
<path d={svgPaths.p1d319800} fill="var(--fill-0, black)" id="Vector" />
|
||||||
|
<g id="Group">
|
||||||
|
<path d={svgPaths.p3a49e400} id="Vector_2" stroke="var(--stroke-0, black)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
<path d="M17.5 10V5M42.5 10V5" id="Vector_3" stroke="var(--stroke-0, black)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
<path d="M17.5 27.5H42.5" id="Vector_4" stroke="var(--stroke-0, black)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
<path d="M17.5 37.5H35" id="Vector_5" stroke="var(--stroke-0, black)" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationDetails3() {
|
||||||
|
return (
|
||||||
|
<div className="absolute content-stretch flex gap-3.5 items-start justify-start left-3 top-3" data-name="notification details">
|
||||||
|
<div className="bg-[#f6f6f6] rounded-[12px] shrink-0 size-[100px]" data-name="image" />
|
||||||
|
<Details3 />
|
||||||
|
<LineMdCalendar />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Close3() {
|
||||||
|
return (
|
||||||
|
<div className="absolute left-[1061px] size-5 top-3" data-name="close">
|
||||||
|
<svg className="block size-full" fill="none" preserveAspectRatio="none" viewBox="0 0 20 20">
|
||||||
|
<g id="close">
|
||||||
|
<path d={svgPaths.p1f279900} fill="var(--fill-0, #656D76)" id="Vector" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Notification3() {
|
||||||
|
return (
|
||||||
|
<div className="absolute bg-white h-[124px] left-0 opacity-[0.42] overflow-clip rounded-[12px] top-[408px] w-[1093px]" data-name="notification">
|
||||||
|
<NotificationDetails3 />
|
||||||
|
<Close3 />
|
||||||
|
<div className="absolute flex flex-col font-['Figtree:Regular',_sans-serif] font-normal h-[15px] justify-center leading-[0] left-[1016px] text-[16px] text-[rgba(0,0,0,0.42)] top-[104.5px] translate-y-[-50%] w-[65px]">
|
||||||
|
<p className="leading-[23.556px]">10/01/23</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Notifications() {
|
||||||
|
return (
|
||||||
|
<div className="relative size-full" data-name="notifications">
|
||||||
|
<Notification />
|
||||||
|
<Notification1 />
|
||||||
|
<Notification2 />
|
||||||
|
<Notification3 />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
315
src/imports/TimeSlot.tsx
Normal file
315
src/imports/TimeSlot.tsx
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
function Header() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-full" data-name="header">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col items-start justify-start relative w-full">
|
||||||
|
<div className="css-jxtxn5 font-['Figtree:Medium',_sans-serif] font-medium leading-[0] relative shrink-0 text-[#1c0d0d] text-[16px] w-full">
|
||||||
|
<p className="leading-[24px]">Time-slot Usage</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Main() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-full" data-name="main">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col items-start justify-start overflow-clip relative w-full">
|
||||||
|
<div className="css-18hjcb font-['Figtree:Bold',_sans-serif] font-bold leading-[0] overflow-ellipsis overflow-hidden relative shrink-0 text-[#1c0d0d] text-[48px] text-nowrap w-full">
|
||||||
|
<p className="[white-space-collapse:collapse] leading-[40px] overflow-ellipsis overflow-hidden">+8%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame0() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0" data-name="Depth 7, Frame 0">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col items-start justify-start relative">
|
||||||
|
<div className="css-o1kdnh font-['Figtree:Regular',_sans-serif] font-normal leading-[0] relative shrink-0 text-[16px] text-black text-nowrap w-full">
|
||||||
|
<p className="leading-[24px] whitespace-pre">Last 30 Days</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame1() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0" data-name="Depth 7, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col items-start justify-start relative">
|
||||||
|
<div className="css-ne5zjj font-['Figtree:Medium',_sans-serif] font-medium leading-[0] relative shrink-0 text-[16px] text-[rgba(0,0,0,0.42)] text-nowrap w-full">
|
||||||
|
<p className="leading-[24px] whitespace-pre">+8%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stat() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-full" data-name="stat">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex gap-3 items-start justify-start relative w-full">
|
||||||
|
<Depth7Frame0 />
|
||||||
|
<Depth7Frame1 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame0() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#f2e8e8] h-[137px] relative shrink-0 w-full" data-name="Depth 8, Frame 0">
|
||||||
|
<div aria-hidden="true" className="absolute border-[#757575] border-[2px_0px_0px] border-solid inset-0 pointer-events-none" />
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[137px] w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame1() {
|
||||||
|
return (
|
||||||
|
<div className="h-5 relative shrink-0" data-name="Depth 8, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col h-5 items-start justify-start relative">
|
||||||
|
<div className="css-tbwqt8 font-['Plus_Jakarta_Sans:Bold',_sans-serif] font-bold leading-[0] relative shrink-0 text-[#994d52] text-[13px] text-nowrap w-full">
|
||||||
|
<p className="leading-[20px] whitespace-pre">9 AM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame7() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-[33px]" data-name="Depth 7, Frame 0">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col gap-6 items-center justify-center relative w-[33px]">
|
||||||
|
<Depth8Frame0 />
|
||||||
|
<Depth8Frame1 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame2() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#f2e8e8] h-[137px] relative shrink-0 w-full" data-name="Depth 8, Frame 0">
|
||||||
|
<div aria-hidden="true" className="absolute border-[#757575] border-[2px_0px_0px] border-solid inset-0 pointer-events-none" />
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[137px] w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame3() {
|
||||||
|
return (
|
||||||
|
<div className="h-5 relative shrink-0" data-name="Depth 8, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col h-5 items-start justify-start relative">
|
||||||
|
<div className="css-tbwqt8 font-['Plus_Jakarta_Sans:Bold',_sans-serif] font-bold leading-[0] relative shrink-0 text-[#994d52] text-[13px] text-nowrap w-full">
|
||||||
|
<p className="leading-[20px] whitespace-pre">10 AM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame8() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-10" data-name="Depth 7, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col gap-6 items-center justify-center relative w-10">
|
||||||
|
<Depth8Frame2 />
|
||||||
|
<Depth8Frame3 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame4() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#f2e8e8] h-[137px] relative shrink-0 w-full" data-name="Depth 8, Frame 0">
|
||||||
|
<div aria-hidden="true" className="absolute border-[#757575] border-[2px_0px_0px] border-solid inset-0 pointer-events-none" />
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[137px] w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame5() {
|
||||||
|
return (
|
||||||
|
<div className="h-5 relative shrink-0" data-name="Depth 8, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col h-5 items-start justify-start relative">
|
||||||
|
<div className="css-tbwqt8 font-['Plus_Jakarta_Sans:Bold',_sans-serif] font-bold leading-[0] relative shrink-0 text-[#994d52] text-[13px] text-nowrap w-full">
|
||||||
|
<p className="leading-[20px] whitespace-pre">11 AM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame2() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-9" data-name="Depth 7, Frame 2">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col gap-6 items-center justify-center relative w-9">
|
||||||
|
<Depth8Frame4 />
|
||||||
|
<Depth8Frame5 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame6() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#f2e8e8] h-[137px] relative shrink-0 w-full" data-name="Depth 8, Frame 0">
|
||||||
|
<div aria-hidden="true" className="absolute border-[#757575] border-[2px_0px_0px] border-solid inset-0 pointer-events-none" />
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[137px] w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame7() {
|
||||||
|
return (
|
||||||
|
<div className="h-5 relative shrink-0" data-name="Depth 8, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col h-5 items-start justify-start relative">
|
||||||
|
<div className="css-tbwqt8 font-['Plus_Jakarta_Sans:Bold',_sans-serif] font-bold leading-[0] relative shrink-0 text-[#994d52] text-[13px] text-nowrap w-full">
|
||||||
|
<p className="leading-[20px] whitespace-pre">12 PM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame3() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-[39px]" data-name="Depth 7, Frame 3">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col gap-6 items-center justify-center relative w-[39px]">
|
||||||
|
<Depth8Frame6 />
|
||||||
|
<Depth8Frame7 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame8() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#f2e8e8] h-[137px] relative shrink-0 w-full" data-name="Depth 8, Frame 0">
|
||||||
|
<div aria-hidden="true" className="absolute border-[#757575] border-[2px_0px_0px] border-solid inset-0 pointer-events-none" />
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[137px] w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame9() {
|
||||||
|
return (
|
||||||
|
<div className="h-5 relative shrink-0" data-name="Depth 8, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col h-5 items-start justify-start relative">
|
||||||
|
<div className="css-tbwqt8 font-['Plus_Jakarta_Sans:Bold',_sans-serif] font-bold leading-[0] relative shrink-0 text-[#994d52] text-[13px] text-nowrap w-full">
|
||||||
|
<p className="leading-[20px] whitespace-pre">1 PM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame4() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-[30px]" data-name="Depth 7, Frame 4">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col gap-6 items-center justify-center relative w-[30px]">
|
||||||
|
<Depth8Frame8 />
|
||||||
|
<Depth8Frame9 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame10() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#f2e8e8] h-[137px] relative shrink-0 w-full" data-name="Depth 8, Frame 0">
|
||||||
|
<div aria-hidden="true" className="absolute border-[#757575] border-[2px_0px_0px] border-solid inset-0 pointer-events-none" />
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[137px] w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame11() {
|
||||||
|
return (
|
||||||
|
<div className="h-5 relative shrink-0" data-name="Depth 8, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col h-5 items-start justify-start relative">
|
||||||
|
<div className="css-tbwqt8 font-['Plus_Jakarta_Sans:Bold',_sans-serif] font-bold leading-[0] relative shrink-0 text-[#994d52] text-[13px] text-nowrap w-full">
|
||||||
|
<p className="leading-[20px] whitespace-pre">2 PM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame5() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-[33px]" data-name="Depth 7, Frame 5">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col gap-6 items-center justify-center relative w-[33px]">
|
||||||
|
<Depth8Frame10 />
|
||||||
|
<Depth8Frame11 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame12() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#f2e8e8] h-[137px] relative shrink-0 w-full" data-name="Depth 8, Frame 0">
|
||||||
|
<div aria-hidden="true" className="absolute border-[#757575] border-[2px_0px_0px] border-solid inset-0 pointer-events-none" />
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[137px] w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth8Frame13() {
|
||||||
|
return (
|
||||||
|
<div className="h-5 relative shrink-0" data-name="Depth 8, Frame 1">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col h-5 items-start justify-start relative">
|
||||||
|
<div className="css-tbwqt8 font-['Plus_Jakarta_Sans:Bold',_sans-serif] font-bold leading-[0] relative shrink-0 text-[#994d52] text-[13px] text-nowrap w-full">
|
||||||
|
<p className="leading-[20px] whitespace-pre">3 PM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Depth7Frame6() {
|
||||||
|
return (
|
||||||
|
<div className="relative shrink-0 w-[33px]" data-name="Depth 7, Frame 6">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex flex-col gap-6 items-center justify-center relative w-[33px]">
|
||||||
|
<Depth8Frame12 />
|
||||||
|
<Depth8Frame13 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Graph() {
|
||||||
|
return (
|
||||||
|
<div className="h-[222px] min-h-[180px] relative shrink-0 w-full" data-name="graph">
|
||||||
|
<div className="flex flex-row items-center justify-center min-h-inherit relative size-full">
|
||||||
|
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border content-stretch flex gap-6 h-[222px] items-center justify-center min-h-inherit px-3 py-0 relative w-full">
|
||||||
|
<Depth7Frame7 />
|
||||||
|
<Depth7Frame8 />
|
||||||
|
<Depth7Frame2 />
|
||||||
|
<Depth7Frame3 />
|
||||||
|
<Depth7Frame4 />
|
||||||
|
<Depth7Frame5 />
|
||||||
|
<Depth7Frame6 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TimeSlot() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white relative rounded-[8px] shadow-[0px_12px_16px_-4px_rgba(0,0,0,0.08),0px_4px_6px_-2px_rgba(0,0,0,0.03)] size-full" data-name="time slot">
|
||||||
|
<div className="min-w-inherit relative size-full">
|
||||||
|
<div className="box-border content-stretch flex flex-col gap-3 items-start justify-start min-w-inherit p-[24px] relative size-full">
|
||||||
|
<Header />
|
||||||
|
<Main />
|
||||||
|
<Stat />
|
||||||
|
<Graph />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/imports/svg-1dw6tu7s10.ts
Normal file
17
src/imports/svg-1dw6tu7s10.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export default {
|
||||||
|
p1179ed00: "M0.548117 92.5816C15.9675 92.5816 15.9675 17.8368 31.3869 17.8368C46.8062 17.8368 46.8062 34.8243 62.2256 34.8243C77.645 34.8243 77.645 78.9916 93.0643 78.9916C108.484 78.9916 108.484 28.0293 123.903 28.0293C139.323 28.0293 139.323 85.7866 154.741 85.7866C170.161 85.7866 170.161 51.8117 185.58 51.8117C201 51.8117 201 38.2218 216.42 38.2218C231.839 38.2218 231.839 102.774 247.259 102.774C262.677 102.774 262.677 126.556 278.097 126.556C293.516 126.556 293.516 0.849372 308.936 0.849372C324.355 0.849372 324.355 68.7992 339.775 68.7992C355.193 68.7992 355.193 109.569 370.613 109.569C386.032 109.569 386.032 21.2343 401.452 21.2343V126.556H278.097H0.548117V92.5816V92.5816Z",
|
||||||
|
p1398a4f0: "M30.25 0C30.9236 8.8854e-05 31.5737 0.247379 32.077 0.694967C32.5804 1.14255 32.9019 1.75931 32.9808 2.42825L33 2.75C33.0008 3.45092 33.2692 4.12509 33.7503 4.63476C34.2315 5.14444 34.8891 5.45115 35.5888 5.49222C36.2886 5.5333 36.9775 5.30564 37.515 4.85577C38.0525 4.4059 38.398 3.76776 38.4808 3.07175L38.5 2.75C38.5 2.02065 38.7897 1.32118 39.3055 0.805457C39.8212 0.289732 40.5207 0 41.25 0H46.75C48.938 0 51.0365 0.869194 52.5836 2.41637C54.1308 3.96354 55 6.06196 55 8.25V35.75C55 37.938 54.1308 40.0365 52.5836 41.5836C51.0365 43.1308 48.938 44 46.75 44H41.25C40.5764 43.9999 39.9263 43.7526 39.423 43.305C38.9196 42.8574 38.5981 42.2407 38.5192 41.5718L38.5 41.25C38.4992 40.5491 38.2308 39.8749 37.7497 39.3652C37.2685 38.8556 36.6109 38.5489 35.9112 38.5078C35.2114 38.4667 34.5225 38.6944 33.985 39.1442C33.4475 39.5941 33.102 40.2322 33.0192 40.9282L33 41.25C33 41.9793 32.7103 42.6788 32.1945 43.1945C31.6788 43.7103 30.9793 44 30.25 44H8.25C6.06196 44 3.96354 43.1308 2.41637 41.5836C0.869194 40.0365 0 37.938 0 35.75V8.25C0 6.06196 0.869194 3.96354 2.41637 2.41637C3.96354 0.869194 6.06196 0 8.25 0H30.25ZM35.75 24.75C35.0207 24.75 34.3212 25.0397 33.8055 25.5555C33.2897 26.0712 33 26.7707 33 27.5V30.25C33 30.9793 33.2897 31.6788 33.8055 32.1945C34.3212 32.7103 35.0207 33 35.75 33C36.4793 33 37.1788 32.7103 37.6945 32.1945C38.2103 31.6788 38.5 30.9793 38.5 30.25V27.5C38.5 26.7707 38.2103 26.0712 37.6945 25.5555C37.1788 25.0397 36.4793 24.75 35.75 24.75ZM35.75 11C35.0764 11.0001 34.4263 11.2474 33.923 11.695C33.4196 12.1426 33.0981 12.7593 33.0192 13.4283L33 13.75V16.5C33.0008 17.2009 33.2692 17.8751 33.7503 18.3848C34.2315 18.8944 34.8891 19.2011 35.5888 19.2422C36.2886 19.2833 36.9775 19.0556 37.515 18.6058C38.0525 18.1559 38.398 17.5178 38.4808 16.8217L38.5 16.5V13.75C38.5 13.0207 38.2103 12.3212 37.6945 11.8055C37.1788 11.2897 36.4793 11 35.75 11Z",
|
||||||
|
p148f2d80: "M28.5 23.75C33.7467 23.75 38 19.4967 38 14.25C38 9.00329 33.7467 4.75 28.5 4.75C23.2533 4.75 19 9.00329 19 14.25C19 19.4967 23.2533 23.75 28.5 23.75Z",
|
||||||
|
p162bfc00: "M40.8333 35.7292C40.8333 40.8027 40.8333 44.9167 24.5 44.9167C8.16667 44.9167 8.16667 40.8027 8.16667 35.7292C8.16667 30.6556 15.4799 26.5417 24.5 26.5417C33.5201 26.5417 40.8333 30.6556 40.8333 35.7292Z",
|
||||||
|
p1b6b4900: "M7.78433 46.992C8.83667 50.6697 11.671 53.5003 17.336 59.1653L24.046 65.8753C33.9093 75.7423 38.8373 80.6667 44.9607 80.6667C51.0877 80.6667 56.0157 75.7387 65.8753 65.879C75.7387 56.0157 80.6667 51.0877 80.6667 44.9607C80.6667 38.8373 75.7387 33.9057 65.879 24.046L59.169 17.336C53.5003 11.671 50.6697 8.83667 46.992 7.78433C43.3143 6.72833 39.4093 7.63033 31.603 9.43433L27.1003 10.472C20.5297 11.9863 17.2443 12.7453 14.993 14.993C12.7417 17.2407 11.99 20.5333 10.472 27.1003L9.43067 31.603C7.63033 39.413 6.732 43.3143 7.78433 46.992ZM37.114 26.6603C37.8212 27.3424 38.3854 28.1585 38.7737 29.061C39.162 29.9635 39.3666 30.9343 39.3755 31.9168C39.3844 32.8992 39.1975 33.8736 38.8256 34.783C38.4537 35.6924 37.9044 36.5186 37.2097 37.2133C36.5149 37.9081 35.6887 38.4574 34.7793 38.8293C33.8699 39.2011 32.8956 39.3881 31.9131 39.3792C30.9306 39.3703 29.9598 39.1657 29.0573 38.7774C28.1548 38.3891 27.3387 37.8249 26.6567 37.1177C25.3112 35.7225 24.5672 33.8549 24.5848 31.9168C24.6023 29.9786 25.3801 28.1248 26.7506 26.7543C28.1211 25.3837 29.9749 24.606 31.9131 24.5884C33.8513 24.5709 35.7188 25.3149 37.114 26.6603ZM69.85 44.187L44.2603 69.7803C43.7414 70.281 43.0467 70.5579 42.3256 70.5513C41.6046 70.5447 40.915 70.2551 40.4053 69.745C39.8957 69.2349 39.6068 68.545 39.6009 67.824C39.595 67.1029 39.8725 66.4084 40.3737 65.89L65.9597 40.2967C66.4756 39.7808 67.1753 39.491 67.9048 39.491C68.6344 39.491 69.3341 39.7808 69.85 40.2967C70.3659 40.8126 70.6557 41.5123 70.6557 42.2418C70.6557 42.9714 70.3659 43.6711 69.85 44.187Z",
|
||||||
|
p2b0db900: "M34.8333 58.6667C28.1722 58.6667 22.5353 56.3591 17.9227 51.744C13.31 47.1289 11.0024 41.492 11 34.8333C10.9976 28.1747 13.3051 22.5378 17.9227 17.9227C22.5402 13.3076 28.1771 11 34.8333 11C41.4896 11 47.1277 13.3076 51.7477 17.9227C56.3677 22.5378 58.674 28.1747 58.6667 34.8333C58.6667 37.5222 58.2389 40.0583 57.3833 42.4417C56.5278 44.825 55.3667 46.9333 53.9 48.7667L74.4333 69.3C75.1056 69.9722 75.4417 70.8278 75.4417 71.8667C75.4417 72.9056 75.1056 73.7611 74.4333 74.4333C73.7611 75.1056 72.9056 75.4417 71.8667 75.4417C70.8278 75.4417 69.9722 75.1056 69.3 74.4333L48.7667 53.9C46.9333 55.3667 44.825 56.5278 42.4417 57.3833C40.0583 58.2389 37.5222 58.6667 34.8333 58.6667ZM34.8333 51.3333C39.4167 51.3333 43.3131 49.7298 46.5227 46.5227C49.7322 43.3156 51.3358 39.4191 51.3333 34.8333C51.3309 30.2476 49.7273 26.3523 46.5227 23.1477C43.318 19.943 39.4216 18.3382 34.8333 18.3333C30.2451 18.3284 26.3499 19.9332 23.1477 23.1477C19.9454 26.3621 18.3407 30.2573 18.3333 34.8333C18.326 39.4093 19.9308 43.3058 23.1477 46.5227C26.3646 49.7396 30.2598 51.3431 34.8333 51.3333Z",
|
||||||
|
p2c16a3d0: "M47.5 41.5625C47.5 47.4644 47.5 52.25 28.5 52.25C9.5 52.25 9.5 47.4644 9.5 41.5625C9.5 35.6606 18.0072 30.875 28.5 30.875C38.9928 30.875 47.5 35.6606 47.5 41.5625Z",
|
||||||
|
p2d604c00: "M17 17L12.3333 12.3333M13.8889 8.44444C13.8889 11.4513 11.4513 13.8889 8.44444 13.8889C5.43756 13.8889 3 11.4513 3 8.44444C3 5.43756 5.43756 3 8.44444 3C11.4513 3 13.8889 5.43756 13.8889 8.44444Z",
|
||||||
|
p2e23cbe0: "M0.548117 93.5816C15.9675 93.5816 15.9675 18.8368 31.3869 18.8368C46.8062 18.8368 46.8062 35.8243 62.2256 35.8243C77.645 35.8243 77.645 79.9916 93.0643 79.9916C108.484 79.9916 108.484 29.0293 123.903 29.0293C139.323 29.0293 139.323 86.7866 154.741 86.7866C170.161 86.7866 170.161 52.8117 185.58 52.8117C201 52.8117 201 39.2218 216.42 39.2218C231.839 39.2218 231.839 103.774 247.259 103.774C262.677 103.774 262.677 127.556 278.097 127.556C293.516 127.556 293.516 1.84937 308.936 1.84937C324.355 1.84937 324.355 69.7992 339.775 69.7992C355.193 69.7992 355.193 110.569 370.613 110.569C386.032 110.569 386.032 22.2343 401.452 22.2343",
|
||||||
|
p318c7f00: "M44 0C68.3012 0 88 19.6988 88 44C88 68.3012 68.3012 88 44 88C19.6988 88 0 68.3012 0 44C0 19.6988 19.6988 0 44 0ZM39.908 24.5564C39.0942 24.5587 38.3144 24.883 37.7389 25.4585C37.1634 26.034 36.8391 26.8138 36.8368 27.6276V52.184C36.8368 53.8824 38.2096 55.2552 39.908 55.2552H64.4644C64.8772 55.2708 65.2889 55.203 65.6749 55.0558C66.0609 54.9087 66.4132 54.6851 66.7108 54.3986C67.0084 54.1121 67.2451 53.7684 67.4068 53.3883C67.5685 53.0082 67.6518 52.5993 67.6518 52.1862C67.6518 51.7731 67.5685 51.3642 67.4068 50.9841C67.2451 50.604 67.0084 50.2603 66.7108 49.9738C66.4132 49.6873 66.0609 49.4638 65.6749 49.3166C65.2889 49.1694 64.8772 49.1016 64.4644 49.1172H42.9748V27.6276C42.9725 26.8145 42.6488 26.0354 42.0743 25.46C41.4998 24.8847 40.7211 24.5599 39.908 24.5564Z",
|
||||||
|
p31dc680: "M25.0833 22.6944V23.8888H3.58333V22.6944L5.97222 20.3055V13.1388C5.97222 9.43605 8.39694 6.17522 11.9444 5.12411V4.77772C11.9444 4.14415 12.1961 3.53652 12.6441 3.08852C13.0921 2.64052 13.6998 2.38883 14.3333 2.38883C14.9669 2.38883 15.5745 2.64052 16.0225 3.08852C16.4705 3.53652 16.7222 4.14415 16.7222 4.77772V5.12411C20.2697 6.17522 22.6944 9.43605 22.6944 13.1388V20.3055L25.0833 22.6944ZM16.7222 25.0833C16.7222 25.7168 16.4705 26.3245 16.0225 26.7725C15.5745 27.2205 14.9669 27.4722 14.3333 27.4722C13.6998 27.4722 13.0921 27.2205 12.6441 26.7725C12.1961 26.3245 11.9444 25.7168 11.9444 25.0833",
|
||||||
|
p32f12c00: "M19 4H5C3.89543 4 3 4.89543 3 6V20C3 21.1046 3.89543 22 5 22H19C20.1046 22 21 21.1046 21 20V6C21 4.89543 20.1046 4 19 4Z",
|
||||||
|
p3de23540: "M24.5 20.4167C29.0103 20.4167 32.6667 16.7603 32.6667 12.25C32.6667 7.73967 29.0103 4.08333 24.5 4.08333C19.9897 4.08333 16.3333 7.73967 16.3333 12.25C16.3333 16.7603 19.9897 20.4167 24.5 20.4167Z",
|
||||||
|
p3e210c00: "M76 46C76 47.1046 75.1046 48 74 48H70V52C70 53.1046 69.1046 54 68 54C66.8954 54 66 53.1046 66 52V48H62C60.8954 48 60 47.1046 60 46C60 44.8954 60.8954 44 62 44H66V40C66 38.8954 66.8954 38 68 38C69.1046 38 70 38.8954 70 40V44H74C75.1046 44 76 44.8954 76 46ZM61.5325 60.7125C62.2436 61.5589 62.1339 62.8214 61.2875 63.5325C60.4411 64.2436 59.1786 64.1339 58.4675 63.2875C53.4375 57.2975 46.5225 54 39 54C31.4775 54 24.5625 57.2975 19.5325 63.2875C18.8214 64.1332 17.5594 64.2423 16.7137 63.5312C15.8681 62.8202 15.7589 61.5582 16.47 60.7125C20.205 56.2675 24.85 53.11 30.0125 51.42C23.571 47.408 20.5687 39.6125 22.6551 32.3161C24.7415 25.0198 31.4112 19.99 39 19.99C46.5888 19.99 53.2585 25.0198 55.3449 32.3161C57.4313 39.6125 54.429 47.408 47.9875 51.42C53.15 53.11 57.795 56.2675 61.5325 60.7125ZM39 50C46.1797 50 52 44.1797 52 37C52 29.8203 46.1797 24 39 24C31.8203 24 26 29.8203 26 37C26.0083 44.1763 31.8237 49.9917 39 50Z",
|
||||||
|
pcbebe00: "M17.3031 6L19.7796 8.29L14.5022 13.17L10.1764 9.17L2.16293 16.59L3.68776 18L10.1764 12L14.5022 16L21.3152 9.71L23.7917 12V6H17.3031Z",
|
||||||
|
}
|
||||||
206
src/imports/svg-fswka8rhrw.ts
Normal file
206
src/imports/svg-fswka8rhrw.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
export default {
|
||||||
|
p100f5e00: "M49.7954 168.233C49.7941 167.892 49.86 167.555 49.9895 167.24C50.119 166.924 50.3095 166.638 50.5499 166.396C50.7904 166.155 51.0762 165.964 51.3908 165.833C51.7055 165.702 52.0429 165.635 52.3836 165.635C52.6374 165.636 52.8897 165.673 53.1331 165.745C53.0925 165.48 53.0724 165.213 53.0731 164.945C53.0731 164.224 53.2153 163.51 53.4917 162.844C53.768 162.177 54.173 161.572 54.6835 161.063C55.194 160.553 55.8 160.149 56.4667 159.874C57.1335 159.599 57.848 159.458 58.5693 159.459C58.9044 159.46 59.2388 159.49 59.5686 159.549C60.1077 158.655 60.9237 157.962 61.8928 157.574C62.8618 157.186 63.931 157.124 64.9381 157.399C65.9452 157.674 66.8352 158.269 67.473 159.095C68.1109 159.922 68.4617 160.934 68.4724 161.977H68.5424C69.7165 161.977 70.8425 162.444 71.6727 163.274C72.5029 164.104 72.9693 165.23 72.9693 166.404C72.9693 167.578 72.5029 168.704 71.6727 169.535C70.8425 170.365 69.7165 170.831 68.5424 170.831H52.3137C51.6377 170.813 50.9957 170.531 50.5251 170.045C50.0544 169.56 49.7925 168.909 49.7954 168.233Z",
|
||||||
|
p10252e00: "M318.449 60.6878L281.544 198.592H275.648L312.563 60.6878H318.449Z",
|
||||||
|
p1072e180: "M137.026 88.5735L137.186 113.286L138.026 241.467H144.151L144.991 113.286L145.151 88.5735H137.026Z",
|
||||||
|
p10cece80: "M84.6628 107.49L64.9565 111.897C64.9565 111.897 74.7297 106.501 82.1446 104.313L84.6628 107.49Z",
|
||||||
|
p11005380: "M134.998 241.467H141.114L152.116 88.3336H143.992L134.998 241.467Z",
|
||||||
|
p11854a00: "M143.552 95.5686H151.576L152.116 88.5735H143.992L143.552 95.5686Z",
|
||||||
|
p122acec0: "M147.488 23.2438H5.4662V250.366H147.488V23.2438Z",
|
||||||
|
p12456a80: "M51.5758 46.8225C51.5758 46.8225 54.6936 35.95 66.5654 36.9393C78.4371 37.9286 83.004 49.0709 83.004 49.0709L77.4478 46.2329L71.1522 64.5901L59.6802 46.4227L51.5758 46.8225Z",
|
||||||
|
p133c9700: "M73.349 223.635C73.349 223.635 76.4069 225.114 77.2663 223.225C78.1257 221.336 70.0214 210.064 59.069 213.312C59.069 213.312 58.7792 222.945 64.6751 226.752C70.571 230.56 73.349 223.635 73.349 223.635Z",
|
||||||
|
p13b05300: "M118.688 110.893L30.9685 143.67L32.9671 149.026C32.9671 149.026 49.8554 150.765 63.9456 145.479L130.02 120.796C132.018 120.057 136.615 116.659 135.826 114.71C134.666 111.832 127.681 112.582 127.681 112.582C127.681 112.582 123.364 109.134 118.688 110.893Z",
|
||||||
|
p13d75400: "M79.6347 228.621C79.6347 228.621 76.267 228.261 76.5368 226.213C76.8066 224.164 89.6477 218.868 97.2225 227.412C97.2225 227.412 92.3758 235.746 85.3607 235.856C78.3456 235.966 79.6347 228.621 79.6347 228.621Z",
|
||||||
|
p14315500: "M80.8338 140.922L100.94 133.407L99.6807 158.92C99.6461 159.598 99.4139 160.252 99.0127 160.801C98.6114 161.349 98.0587 161.769 97.4223 162.007L95.0939 162.877C94.8788 162.955 94.6432 162.957 94.4268 162.883C94.2103 162.808 94.026 162.661 93.9047 162.467L80.8338 140.922Z",
|
||||||
|
p144c44a0: "M97.334 34.1613L92.987 38.908L89.6194 34.561C89.6194 34.561 89.0298 36.04 88.1204 36.1199C87.211 36.1999 81.6149 39.1178 80.6056 37.9886C80.2044 37.3813 79.9229 36.7029 79.7762 35.99C79.3985 34.479 79.1082 32.9476 78.9068 31.4032C78.9068 31.4032 88.3402 25.967 89.4295 26.8164C89.5494 26.9063 89.7293 27.0662 89.9591 27.2661C91.8978 28.9849 97.334 34.1613 97.334 34.1613Z",
|
||||||
|
p14f5200: "M67.3348 22.7392C67.1502 21.7682 66.5904 20.909 65.7768 20.3479C64.9631 19.7867 63.9611 19.5688 62.9879 19.7413C60.3897 20.2909 59.6702 25.8071 66.3355 26.4566C67.2949 26.5466 67.7346 25.5772 67.3348 22.7392Z",
|
||||||
|
p15520500: "M236.835 282.604H43.8795C42.3679 282.601 40.9191 281.999 39.8512 280.929C38.7833 279.859 38.1835 278.409 38.1835 276.898V5.65607C38.1966 4.15314 38.8022 2.71609 39.8687 1.65707C40.9353 0.598043 42.3765 0.0025746 43.8795 0H236.835C238.349 0 239.8 0.60117 240.87 1.67126C241.94 2.74135 242.541 4.1927 242.541 5.70603V276.898C242.541 278.411 241.94 279.862 240.87 280.932C239.8 282.002 238.349 282.604 236.835 282.604ZM43.8795 0.199862C42.4342 0.202509 41.0489 0.778524 40.0279 1.80148C39.0068 2.82443 38.4333 4.21072 38.4333 5.65607V276.898C38.4333 278.343 39.0068 279.729 40.0279 280.752C41.0489 281.775 42.4342 282.351 43.8795 282.354H236.835C238.282 282.351 239.668 281.775 240.691 280.753C241.713 279.73 242.289 278.344 242.292 276.898V5.65607C242.289 4.2098 241.713 2.82353 240.691 1.80087C239.668 0.7782 238.282 0.202503 236.835 0.199862H43.8795Z",
|
||||||
|
p15c1f180: "M59.3104 51.6491C59.4803 79.2899 62.3083 135.821 82.1945 148.112H26.9729C25.9986 148.112 25.042 147.853 24.2009 147.361C23.3598 146.869 22.6645 146.163 22.1862 145.314C17.2996 136.65 3.85895 107.42 6.13737 51.1295C6.30249 47.1519 7.99962 43.3923 10.8736 40.6376C13.7476 37.8828 17.5756 36.3463 21.5567 36.3498H43.8811C47.9516 36.347 51.8581 37.9539 54.7485 40.82C57.6389 43.686 59.2788 47.5788 59.3104 51.6491Z",
|
||||||
|
p15eef400: "M61.399 5.97086C61.399 5.97086 60.8394 0.974336 54.2539 0.62458C47.6685 0.274823 42.1324 4.9016 46.0796 12.8461C50.0269 20.7906 50.1568 29.6844 44.7605 30.7736C44.7605 30.7736 55.3532 35.6802 57.2818 19.0018L61.399 5.97086Z",
|
||||||
|
p16909200: "M138.036 241.467H144.151L145.161 88.3336H137.036L138.036 241.467Z",
|
||||||
|
p16b03f00: "M449.588 343.252H416.491V343.501H449.588V343.252Z",
|
||||||
|
p16c47380: "M81.6649 28.815C79.1766 28.735 78.0474 27.8756 77.5577 26.9363C78.5009 26.9947 79.427 27.2144 80.2958 27.5858C80.8697 27.8419 81.3487 28.2719 81.6649 28.815Z",
|
||||||
|
p16f8f300: "M127.541 75.6375H108.095V89.0581H127.541V75.6375Z",
|
||||||
|
p16feb000: "M91.0767 74.3184H87.0895V89.218H91.0767V74.3184Z",
|
||||||
|
p172f8500: "M105.129 104.622L88.4402 150.79C88.4402 150.79 48.2381 149.791 40.3336 141.707C32.4291 133.622 35.1972 120.152 41.3329 110.668C47.4687 101.185 78.3072 99.406 78.3072 99.406L82.1446 104.313L84.6628 107.49L84.8227 107.69C84.8227 107.69 95.955 104.622 105.129 104.622Z",
|
||||||
|
p178a2100: "M73.5006 6.54046C73.5006 6.54046 71.5019 20.8205 62.6681 21.53C53.8342 22.2396 62.2084 6.30063 65.666 3.21277C67.2649 1.79376 72.831 -0.154886 73.5006 6.54046Z",
|
||||||
|
p1791d480: "M499.653 327.173H0V327.423H499.653V327.173Z",
|
||||||
|
p17a5c500: "M88.3786 36.4047V81.1036C88.3786 88.9778 85.2506 96.5294 79.6828 102.097C74.1149 107.665 66.5633 110.793 58.6892 110.793H37.1342C29.2592 110.793 21.7065 107.665 16.1371 102.098C10.5677 96.5302 7.43749 88.9787 7.43484 81.1036V36.4047C7.43484 28.5306 10.5628 20.979 16.1307 15.4111C21.6985 9.8433 29.2501 6.71532 37.1242 6.71532H58.6792C62.5789 6.71401 66.4407 7.48098 70.0439 8.97242C73.6472 10.4639 76.9213 12.6506 79.6792 15.4076C82.4372 18.1646 84.625 21.438 86.1177 25.0408C87.6103 28.6435 88.3786 32.505 88.3786 36.4047Z",
|
||||||
|
p18bb9900: "M90.7969 123.964H94.7841V109.064H90.7969V123.964Z",
|
||||||
|
p19dd0a30: "M75.9472 292.857L76.9465 292.797C76.9465 292.527 75.4876 265.915 82.6726 256.532C84.4114 254.263 86.3 253.174 88.2987 253.314C93.0054 253.644 96.9526 260.529 96.9926 260.599L97.862 260.109C97.6921 259.81 93.615 252.684 88.3686 252.325C86.0302 252.155 83.8417 253.324 81.8831 255.922C74.4683 265.595 75.8873 291.797 75.9472 292.857Z",
|
||||||
|
p19f30980: "M400.442 276.698H449.927C450.833 276.698 451.566 275.964 451.566 275.059V272.77C451.566 271.865 450.833 271.132 449.927 271.132H400.442C399.537 271.132 398.803 271.865 398.803 272.77V275.059C398.803 275.964 399.537 276.698 400.442 276.698Z",
|
||||||
|
p1a314f80: "M85.9319 14.2651L84.6029 19.2616C84.6029 19.2616 83.7634 19.0018 83.7534 18.682L84.8527 14.435C84.8527 14.435 85.6221 14.2951 85.9319 14.2651Z",
|
||||||
|
p1a4d4600: "M105.129 58.8841C105.129 58.8841 96.3847 55.9661 95.4253 48.6012L101.751 52.6584L105.129 58.8841Z",
|
||||||
|
p1ad54580: "M180.476 44.614L173.081 78.0708C172.955 78.6164 172.654 79.1062 172.225 79.4666C171.797 79.827 171.262 80.0385 170.703 80.0694H131.95C131.692 80.046 131.443 79.9658 131.22 79.8344C130.997 79.703 130.806 79.5238 130.661 79.3097C130.515 79.0955 130.419 78.8518 130.38 78.5961C130.34 78.3404 130.358 78.0791 130.431 77.8309L137.786 44.7339C137.936 44.2282 138.255 43.7893 138.689 43.4909C139.124 43.1925 139.649 43.0528 140.174 43.0951H178.957C180.077 43.155 180.716 43.5348 180.476 44.614Z",
|
||||||
|
p1afc5b80: "M67.9228 74.3184H63.9356V89.218H67.9228V74.3184Z",
|
||||||
|
p1b063c00: "M72.1898 292.797H73.1891C73.1891 291.358 72.7794 257.331 63.1162 251.825C62.1979 251.231 61.1508 250.866 60.0624 250.76C58.974 250.654 57.8761 250.81 56.8605 251.215C51.0146 253.634 48.1166 264.027 47.9967 264.476L48.996 264.736C48.996 264.636 51.884 254.363 57.2702 252.135C58.1462 251.793 59.091 251.666 60.0261 251.763C60.9612 251.861 61.8596 252.18 62.6465 252.694C71.7901 257.931 72.1898 292.477 72.1898 292.797Z",
|
||||||
|
p1b36b200: "M87.5008 13.8854C87.5008 13.8854 85.6321 13.2058 82.1646 14.435C81.6516 14.2793 81.1122 14.23 80.5795 14.2902C80.0468 14.3503 79.532 14.5186 79.0667 14.7847C76.8868 14.7941 74.7278 15.211 72.7011 16.0139L66.0757 18.5321V19.2516L72.831 17.5128C73.3906 19.1717 74.8996 21.7099 77.178 21.2403C80.1759 20.6207 80.026 17.4829 79.7962 16.0139C80.4357 15.2744 81.6449 15.1645 81.8847 15.7341C82.1346 17.0332 82.964 20.0311 86.1418 19.8512C89.3196 19.6713 87.5008 13.8854 87.5008 13.8854Z",
|
||||||
|
p1be4f00: "M186.182 37.5389V81.0187H157.322V48.6212L172.302 37.5389H186.182Z",
|
||||||
|
p1c624700: "M60.0483 228.321C60.0483 228.321 62.3767 226.443 59.8684 225.523C57.3601 224.604 45.0287 224.864 44.8788 237.075C44.8788 237.075 59.2388 239.464 61.0076 234.807C62.7764 230.15 60.0483 228.321 60.0483 228.321Z",
|
||||||
|
p1c6b3980: "M202.151 88.5735H194.027L194.566 95.5686H202.591L202.151 88.5735Z",
|
||||||
|
p1c719480: "M43.7496 86.9896C43.7477 88.451 43.1772 89.8543 42.1588 90.9025C41.1404 91.9507 39.7542 92.5615 38.2934 92.6056H9.78321C8.23514 88.9685 7.43642 85.0565 7.43484 81.1036V65.2047C8.59894 64.3446 9.92985 63.7371 11.3423 63.4211C12.7548 63.1052 14.2177 63.0878 15.6373 63.37C17.057 63.6523 18.402 64.2279 19.5862 65.0601C20.7704 65.8923 21.7678 66.9626 22.5144 68.2026C23.2394 68.067 23.9752 67.9968 24.7128 67.9928C27.8729 67.9954 30.9027 69.2526 33.1363 71.4881C35.3699 73.7235 36.6246 76.7544 36.6246 79.9145C36.6251 80.4895 36.585 81.0639 36.5046 81.6333C37.0272 81.4802 37.569 81.4028 38.1135 81.4034C39.6005 81.4008 41.028 81.9868 42.0842 83.0336C43.1403 84.0803 43.739 85.5026 43.7496 86.9896Z",
|
||||||
|
p1cc02300: "M100.319 123.086L104.212 123.951L107.444 109.407L103.552 108.542L100.319 123.086Z",
|
||||||
|
p1cef5000: "M68.6839 3.37267C68.6839 3.37267 75.679 -2.62317 83.7534 1.37405C91.8278 5.37128 91.7479 17.4329 86.3216 21.55C80.8954 25.6672 68.6839 3.37267 68.6839 3.37267Z",
|
||||||
|
p1d2af780: "M47.6669 289.479H98.1518V280.255H47.6669V289.479Z",
|
||||||
|
p1d892300: "M89.9591 27.2661C89.3496 30.264 87.201 34.8108 79.7762 35.99C79.3985 34.479 79.1082 32.9476 78.9068 31.4032C78.9068 31.4032 88.3402 25.967 89.4295 26.8164C89.5494 26.9063 89.7293 27.0662 89.9591 27.2661Z",
|
||||||
|
p1d97f380: "M128.081 123.954H132.068V106.796H128.081V123.954Z",
|
||||||
|
p1d98900: "M330.99 345.97H322.306V346.219H330.99V345.97Z",
|
||||||
|
p1d9a6680: "M170.573 77.1314V79.7796C170.573 80.569 169.214 81.1286 168.425 81.1286H106.678C105.888 81.1286 104.619 80.569 104.619 79.7796V77.1314H170.573Z",
|
||||||
|
p1ddb780: "M89.3179 255.702C89.3179 255.702 86.9896 253.824 89.5078 252.904C92.0261 251.985 104.338 252.245 104.497 264.456C104.497 264.456 90.1474 266.845 88.3686 262.188C86.5898 257.531 89.3179 255.702 89.3179 255.702Z",
|
||||||
|
p1e867600: "M108.286 146.403H29.0315C27.9221 146.403 27.0229 147.303 27.0229 148.412V150.231C27.0229 151.34 27.9221 152.239 29.0315 152.239H108.286C109.396 152.239 110.295 151.34 110.295 150.231V148.412C110.295 147.303 109.396 146.403 108.286 146.403Z",
|
||||||
|
p1ebc0f00: "M96.463 83.462H56.7306C55.8644 83.462 55.0068 83.2914 54.2066 82.96C53.4064 82.6285 52.6793 82.1427 52.0669 81.5302C51.4545 80.9178 50.9687 80.1907 50.6372 79.3905C50.3058 78.5904 50.1351 77.7327 50.1351 76.8666C50.1378 75.1182 50.8335 73.4421 52.0698 72.2058C53.3061 70.9695 54.9822 70.2738 56.7306 70.2712H96.463C98.2114 70.2738 99.8874 70.9695 101.124 72.2058C102.36 73.4421 103.056 75.1182 103.058 76.8666C103.058 78.6158 102.364 80.2934 101.127 81.5302C99.8898 82.7671 98.2122 83.462 96.463 83.462Z",
|
||||||
|
p1f3b9980: "M206.428 92.171L77.7576 92.171C77.4742 92.171 77.1935 92.1151 76.9316 92.0067C76.6697 91.8982 76.4318 91.7392 76.2313 91.5388C76.0309 91.3383 75.8719 91.1004 75.7634 90.8385C75.655 90.5766 75.5991 90.2959 75.5991 90.0125V84.5463L208.587 84.5463V90.0525C208.576 90.618 208.344 91.1568 207.941 91.553C207.537 91.9492 206.994 92.1711 206.428 92.171Z",
|
||||||
|
p1fafef00: "M127.651 227.862H14.5799C14.2848 227.859 14.0027 227.74 13.7949 227.53C13.5872 227.321 13.4706 227.038 13.4707 226.742C13.4707 226.448 13.5875 226.166 13.7955 225.958C14.0036 225.75 14.2857 225.633 14.5799 225.633H127.651C127.945 225.633 128.228 225.75 128.436 225.958C128.644 226.166 128.761 226.448 128.761 226.742C128.761 227.038 128.644 227.321 128.436 227.53C128.229 227.74 127.946 227.859 127.651 227.862Z",
|
||||||
|
p21515c30: "M35.1772 140.567H102.131C103.456 140.567 104.727 141.094 105.664 142.031C106.601 142.968 107.127 144.239 107.127 145.564V152.249H30.1807V145.564C30.1807 144.239 30.7071 142.968 31.6441 142.031C32.5811 141.094 33.852 140.567 35.1772 140.567Z",
|
||||||
|
p217d8700: "M95.9333 123.954H97.932V105.946H95.9333V123.954Z",
|
||||||
|
p21fa0100: "M452.995 282.604H260.029C258.517 282.601 257.067 281.999 255.997 280.929C254.928 279.86 254.326 278.41 254.323 276.898V5.65607C254.339 4.15223 254.947 2.71519 256.015 1.65644C257.083 0.597693 258.525 0.00254652 260.029 0H452.995C454.497 0.00520088 455.935 0.601831 457 1.66057C458.064 2.71932 458.668 4.15487 458.681 5.65607V276.898C458.681 278.407 458.083 279.856 457.017 280.925C455.951 281.995 454.505 282.598 452.995 282.604ZM260.029 0.199862C258.583 0.202503 257.197 0.7782 256.174 1.80087C255.151 2.82353 254.576 4.2098 254.573 5.65607V276.898C254.576 278.344 255.151 279.73 256.174 280.753C257.197 281.775 258.583 282.351 260.029 282.354H452.995C454.442 282.351 455.828 281.775 456.85 280.753C457.873 279.73 458.449 278.344 458.451 276.898V5.65607C458.449 4.2098 457.873 2.82353 456.85 1.80087C455.828 0.7782 454.442 0.202503 452.995 0.199862H260.029Z",
|
||||||
|
p2312b7c0: "M374.17 58.2096H371.542V201.67H374.17V58.2096Z",
|
||||||
|
p239464c0: "M74.0685 80.2142C73.6229 80.2224 73.1815 80.1266 72.7794 79.9344C72.3993 79.7587 72.0593 79.5071 71.7801 79.1949C71.4982 78.8864 71.2778 78.5268 71.1306 78.1357C70.9796 77.7469 70.9016 77.3336 70.9007 76.9165C70.8977 76.4907 70.9827 76.0688 71.1505 75.6774C71.3053 75.2877 71.5287 74.929 71.8101 74.6181C72.0961 74.3189 72.4349 74.075 72.8094 73.8986C73.2077 73.7171 73.6409 73.625 74.0785 73.6288C74.524 73.6218 74.9651 73.7175 75.3676 73.9086C75.9194 74.1938 76.3836 74.6231 76.7108 75.151C77.0381 75.6789 77.2162 76.2855 77.2263 76.9065C77.229 77.3314 77.1475 77.7525 76.9865 78.1457C76.8287 78.5158 76.6053 78.8544 76.327 79.145C76.0443 79.4481 75.7048 79.6926 75.3277 79.8645C74.9381 80.0704 74.5085 80.1897 74.0685 80.2142ZM72.1898 76.9165C72.1891 77.1941 72.2295 77.4703 72.3097 77.736C72.3919 77.9876 72.5207 78.2215 72.6895 78.4255C72.8448 78.6328 73.0468 78.8005 73.2791 78.9151C73.5307 79.0336 73.8054 79.095 74.0835 79.095C74.3617 79.095 74.6363 79.0336 74.888 78.9151C75.1187 78.7896 75.3225 78.6198 75.4876 78.4155C75.6484 78.2057 75.7702 77.9688 75.8473 77.716C75.9349 77.4586 75.9788 77.1884 75.9772 76.9165C75.9746 76.6417 75.9308 76.3689 75.8473 76.1071C75.7613 75.8541 75.6329 75.6176 75.4676 75.4076C75.3052 75.2057 75.1005 75.042 74.868 74.9279C74.6236 74.8041 74.3525 74.7423 74.0785 74.748C73.7974 74.7457 73.5199 74.8108 73.2691 74.9379C73.0382 75.0589 72.8342 75.2255 72.6695 75.4276C72.5087 75.6373 72.3868 75.8743 72.3097 76.1271C72.231 76.3829 72.1906 76.6489 72.1898 76.9165Z",
|
||||||
|
p24400600: "M140.512 11.4121H138.744V16.1188H140.512V11.4121Z",
|
||||||
|
p244d1480: "M84.5928 16.6634C84.6828 17.3629 84.373 17.9725 83.9133 18.0225C83.4536 18.0724 83.004 17.5628 82.914 16.8733C82.8241 16.1837 83.1339 15.5642 83.6035 15.5142C84.0732 15.4642 84.5029 15.9739 84.5928 16.6634Z",
|
||||||
|
p24fec7f0: "M73.8687 6.11574C73.8687 6.3652 73.9428 6.60903 74.0816 6.8163C74.2204 7.02357 74.4176 7.18494 74.6483 7.27994C74.879 7.37494 75.1326 7.39929 75.3771 7.34989C75.6216 7.30049 75.846 7.17957 76.0217 7.00248C76.1974 6.82539 76.3165 6.6001 76.3639 6.3552C76.4114 6.1103 76.385 5.85683 76.2882 5.62693C76.1913 5.39704 76.0284 5.20108 75.82 5.06394C75.6117 4.92679 75.3672 4.85464 75.1178 4.85662C74.9529 4.85661 74.7897 4.88925 74.6375 4.95265C74.4853 5.01605 74.3471 5.10895 74.231 5.226C74.1149 5.34306 74.0231 5.48194 73.9609 5.63465C73.8987 5.78735 73.8673 5.95087 73.8687 6.11574Z",
|
||||||
|
p2519fa00: "M96.5929 124.823L47.0473 96.413C46.7086 96.2155 46.3302 96.096 45.9395 96.0632C45.5488 96.0304 45.1558 96.085 44.7889 96.2231L40.8516 97.6921C40.7214 97.7405 40.6048 97.8197 40.5119 97.9229C40.419 98.0262 40.3525 98.1504 40.3181 98.285C40.2838 98.4196 40.2825 98.5605 40.3145 98.6957C40.3465 98.8309 40.4108 98.9563 40.5019 99.0612L70.6009 133.118L96.5929 124.823Z",
|
||||||
|
p25d2af00: "M20.9155 15.2594H21.9647V12.911H20.9155V15.2594Z",
|
||||||
|
p26024c0: "M104.279 241.477C108.402 241.477 111.744 238.135 111.744 234.012C111.744 229.89 108.402 226.548 104.279 226.548C100.156 226.548 96.8144 229.89 96.8144 234.012C96.8144 238.135 100.156 241.477 104.279 241.477Z",
|
||||||
|
p26290c80: "M152.755 201.865L145.82 201.195C145.82 201.195 137.676 178.981 129.552 159.094C122.996 143.106 116.461 128.546 114.232 128.456C113.278 128.53 112.337 128.728 111.434 129.045C99.0328 132.673 53.8442 151.41 42.3422 139.708C29.5911 126.717 48.0283 95.4987 48.0283 95.4987C48.0283 95.4987 78.6969 91.1217 81.0053 98.0469C81.5735 100.088 82.0074 102.165 82.3044 104.263C82.6542 106.381 82.894 108.16 82.894 108.16C83.8434 107.92 114.632 99.9755 126.764 108.16C139.105 116.494 152.755 201.865 152.755 201.865Z",
|
||||||
|
p26423900: "M55.0817 0H36.2948C16.2497 0 0 16.2497 0 36.2948V81.1736C0 101.219 16.2497 117.468 36.2948 117.468H55.0817C75.1268 117.468 91.3765 101.219 91.3765 81.1736V36.2948C91.3765 16.2497 75.1268 0 55.0817 0Z",
|
||||||
|
p26ec7700: "M446.13 321.107H404.249C403.344 321.107 402.61 321.841 402.61 322.746V325.034C402.61 325.939 403.344 326.673 404.249 326.673H446.13C447.035 326.673 447.769 325.939 447.769 325.034V322.746C447.769 321.841 447.035 321.107 446.13 321.107Z",
|
||||||
|
p275aac00: "M53.8126 326.663H91.9861L96.1032 284.872H49.6955L53.8126 326.663Z",
|
||||||
|
p2767f380: "M136.235 89.218H45.6483V93.8748H136.235V89.218Z",
|
||||||
|
p27718480: "M415.491 333.978H396.315V334.228H415.491V333.978Z",
|
||||||
|
p27979e80: "M83.9718 123.283L87.9011 123.96L90.8156 107.051L86.8863 106.374L83.9718 123.283Z",
|
||||||
|
p27f81500: "M65.3063 51.6491C65.4761 79.2899 68.3042 135.821 88.1904 148.112H29.9009C29.9009 148.112 9.35513 119.692 12.1332 51.1795C12.2983 47.2019 13.9955 43.4423 16.8694 40.6875C19.7434 37.9327 23.5715 36.3963 27.5525 36.3997H49.877C53.9389 36.3969 57.8379 37.9969 60.7269 40.8522C63.6159 43.7075 65.2615 47.5875 65.3063 51.6491Z",
|
||||||
|
p280ffe80: "M163.488 213.587C162.486 213.129 161.409 212.855 160.31 212.777C160.31 212.777 156.513 210.549 152.755 201.865H148.528C147.813 201.874 147.117 202.102 146.535 202.518C145.952 202.935 145.511 203.519 145.271 204.193C145.201 204.382 145.161 204.581 145.151 204.783C145.203 205.788 145.313 206.789 145.481 207.781C146.099 211.159 146.924 214.496 147.949 217.774L148.668 217.594L147.369 211.288C146.93 209.14 149.188 211.208 150.227 213.377C151.267 215.545 152.875 217.714 157.552 216.644C162.998 215.355 165.187 214.566 163.488 213.587Z",
|
||||||
|
p2854c400: "M75.9788 17.8726C76.0588 18.5621 75.749 19.1717 75.2893 19.2316C74.8296 19.2916 74.3799 18.772 74.29 18.0724C74.2001 17.3729 74.5099 16.7734 74.9695 16.7134C75.4292 16.6534 75.8889 17.1731 75.9788 17.8726Z",
|
||||||
|
p287e9580: "M73.8187 292.857L74.818 292.787C74.818 292.267 71.4903 239.753 83.9817 228.261L83.3121 227.522C70.4511 239.344 73.6688 290.678 73.8187 292.857Z",
|
||||||
|
p28ae59c0: "M323.285 58.2096H320.657V201.67H323.285V58.2096Z",
|
||||||
|
p28fa780: "M37.1142 146.768L23.2139 136.705C22.858 136.45 22.4449 136.287 22.0109 136.229C21.577 136.172 21.1356 136.222 20.7256 136.375L19.0868 136.985C19.0058 137.014 18.9328 137.062 18.8738 137.125C18.8148 137.188 18.7714 137.264 18.7471 137.346C18.7227 137.429 18.7182 137.516 18.7338 137.601C18.7494 137.686 18.7848 137.766 18.8369 137.834L27.5709 149.236L25.1825 150.136C25.0498 150.178 24.9276 150.249 24.8236 150.342C24.7197 150.434 24.6363 150.548 24.5789 150.675C24.5215 150.802 24.4913 150.94 24.4903 151.079C24.4892 151.219 24.5174 151.357 24.5729 151.485C24.6158 151.617 24.6859 151.74 24.7788 151.844C24.8717 151.948 24.9854 152.031 25.1124 152.088C25.2395 152.146 25.3772 152.176 25.5166 152.177C25.656 152.178 25.7941 152.15 25.922 152.094L36.9743 147.967L37.1142 146.768Z",
|
||||||
|
p29726300: "M77.148 33.7116C76.1996 34.3107 75.4294 35.1535 74.9178 36.1519C74.4063 37.1504 74.1722 38.2679 74.24 39.3876C72.5216 38.8814 70.9385 37.9965 69.6069 36.7981C68.2754 35.5997 67.2292 34.1182 66.5454 32.4625C66.9648 31.1531 67.2986 29.8179 67.5447 28.4652H67.8345L77.148 33.7116Z",
|
||||||
|
p297a0ac0: "M32.9671 200.191H15.9789C15.6486 200.191 15.3217 200.126 15.0167 199.999C14.7117 199.872 14.4348 199.687 14.2017 199.453C13.9687 199.219 13.7841 198.941 13.6587 198.636C13.5332 198.33 13.4693 198.003 13.4706 197.673C13.4706 197.007 13.7349 196.369 14.2053 195.899C14.6757 195.429 15.3136 195.164 15.9789 195.164H32.9671C33.2973 195.163 33.6246 195.227 33.93 195.352C34.2355 195.478 34.5132 195.662 34.7471 195.895C34.9811 196.129 35.1668 196.405 35.2935 196.71C35.4201 197.015 35.4853 197.342 35.4853 197.673C35.4853 198.341 35.22 198.981 34.7478 199.453C34.2755 199.926 33.635 200.191 32.9671 200.191Z",
|
||||||
|
p2a6e6100: "M134.766 12.981H132.998V16.1188H134.766V12.981Z",
|
||||||
|
p2a953080: "M81.5949 95.5686H89.6194L90.159 88.5735H82.0346L81.5949 95.5686Z",
|
||||||
|
p2abc3f70: "M5.46619 9.20359V265.825C5.43338 266.287 5.50038 266.751 5.66268 267.185C5.82498 267.619 6.07875 268.013 6.40676 268.34C6.73477 268.667 7.12933 268.919 7.56364 269.08C7.99795 269.241 8.46184 269.307 8.92378 269.273H144.59C146.538 269.273 147.488 268.074 147.488 266.135V8.92379C147.488 7.24495 146.758 6.04578 145.079 6.04578H111.163C110.701 6.02525 110.244 6.1476 109.854 6.39616C109.464 6.64472 109.161 7.00741 108.984 7.43482L107.575 10.7525C107.393 11.1735 107.087 11.529 106.698 11.7718C106.309 12.0145 105.855 12.1331 105.397 12.1116H47.6469C47.1835 12.1262 46.7267 11.9992 46.3375 11.7474C45.9482 11.4957 45.6451 11.1311 45.4684 10.7025L44.0594 7.40484C43.8808 6.98128 43.5755 6.62333 43.1855 6.38002C42.7955 6.13672 42.3398 6.01997 41.8809 6.04578H8.45411C8.04403 6.03455 7.6362 6.1099 7.25721 6.26694C6.87823 6.42398 6.5366 6.65916 6.25465 6.95714C5.97269 7.25513 5.75676 7.60922 5.6209 7.9963C5.48504 8.38338 5.43232 8.79476 5.46619 9.20359Z",
|
||||||
|
p2b508700: "M78.7053 97.6022C78.7053 97.0721 78.9159 96.5637 79.2907 96.1889C79.6655 95.8141 80.1739 95.6035 80.7039 95.6035C80.8966 95.6042 81.0882 95.6311 81.2735 95.6835C81.2632 95.4804 81.2632 95.277 81.2735 95.0739C81.2735 93.9502 81.7199 92.8725 82.5145 92.0779C83.3091 91.2833 84.3869 90.8369 85.5106 90.8369C85.7721 90.8346 86.0331 90.858 86.29 90.9068C86.7051 90.2171 87.3339 89.6817 88.0809 89.3819C88.828 89.0821 89.6524 89.0343 90.4291 89.2457C91.2058 89.4571 91.8923 89.9162 92.3843 90.5533C92.8763 91.1904 93.147 91.9706 93.1553 92.7755H93.2252C94.0984 92.8228 94.9202 93.2029 95.5216 93.8377C96.1229 94.4725 96.4581 95.3137 96.4581 96.1881C96.4581 97.0626 96.1229 97.9037 95.5216 98.5385C94.9202 99.1733 94.0984 99.5535 93.2252 99.6008H80.6439C80.1243 99.5852 79.6313 99.3677 79.2693 98.9946C78.9074 98.6215 78.7051 98.122 78.7053 97.6022Z",
|
||||||
|
p2b87ea80: "M63.3859 78.4954C63.3934 78.7448 63.3384 78.9922 63.2261 79.2149C63.1153 79.4243 62.9581 79.6055 62.7664 79.7446C62.5655 79.8865 62.3427 79.9946 62.1068 80.0644C61.8398 80.1372 61.5642 80.1742 61.2874 80.1743H58.1496V73.6688H61.6272C61.843 73.6661 62.056 73.7177 62.2468 73.8187C62.4251 73.9113 62.5818 74.0407 62.7064 74.1984C62.8389 74.3544 62.9407 74.5342 63.0062 74.7281C63.0765 74.9203 63.1137 75.123 63.1161 75.3276C63.1186 75.6404 63.0357 75.9479 62.8763 76.217C62.7198 76.4966 62.4732 76.715 62.1768 76.8366C62.5292 76.9318 62.8412 77.1387 63.0662 77.4262C63.286 77.7384 63.3983 78.1138 63.3859 78.4954ZM59.3887 74.748V76.3869H61.0675C61.1694 76.3901 61.2707 76.3722 61.3653 76.3344C61.4599 76.2965 61.5457 76.2396 61.6172 76.1671C61.6936 76.0879 61.7526 75.9938 61.7904 75.8905C61.8283 75.7873 61.8442 75.6772 61.837 75.5675C61.8461 75.3499 61.7749 75.1365 61.6372 74.9679C61.5717 74.8984 61.4927 74.8431 61.4051 74.8053C61.3174 74.7674 61.223 74.748 61.1275 74.748H59.3887ZM62.0869 78.2556C62.0873 78.1429 62.0669 78.0312 62.0269 77.9258C61.992 77.8232 61.9377 77.7283 61.867 77.646C61.7988 77.5692 61.7176 77.505 61.6272 77.4562C61.535 77.4072 61.4317 77.383 61.3274 77.3862H59.3887V79.095H61.2574C61.3702 79.0964 61.4821 79.076 61.5872 79.0351C61.6867 78.9964 61.7757 78.9347 61.847 78.8552C61.9205 78.7771 61.9783 78.6855 62.0169 78.5854C62.0764 78.4852 62.1107 78.372 62.1168 78.2556H62.0869Z",
|
||||||
|
p2c469500: "M113.961 123.964H117.948V109.064H113.961V123.964Z",
|
||||||
|
p2c4d00: "M65.1247 234.567H14.5799C14.2857 234.567 14.0036 234.45 13.7955 234.242C13.5875 234.034 13.4707 233.752 13.4707 233.458C13.4707 233.164 13.5875 232.881 13.7955 232.673C14.0036 232.465 14.2857 232.349 14.5799 232.349H65.1148C65.409 232.349 65.6911 232.465 65.8991 232.673C66.1071 232.881 66.224 233.164 66.224 233.458C66.224 233.75 66.1085 234.031 65.9026 234.239C65.6968 234.446 65.4172 234.564 65.1247 234.567Z",
|
||||||
|
p2c6b0c00: "M224.994 339.874H131.379V340.124H224.994V339.874Z",
|
||||||
|
p2cc4840: "M185.283 49.3607H178.757V59.2239H185.283V49.3607Z",
|
||||||
|
p2d12a880: "M78.2157 80.1543V73.6588H79.5348V76.7467L82.2629 73.6488H83.6519L81.1636 76.5368L83.8018 80.1543H82.3328L80.3342 77.3362L79.5048 78.2057V80.2043L78.2157 80.1543Z",
|
||||||
|
p2d76180: "M188.934 54.397C199.846 52.6257 207.256 42.3441 205.485 31.4324C203.714 20.5206 193.432 13.1108 182.52 14.882C171.608 16.6532 164.199 26.9348 165.97 37.8466C167.741 48.7583 178.023 56.1682 188.934 54.397Z",
|
||||||
|
p2e195d00: "M73.0409 241.467H79.1566L90.159 88.3336H82.0346L73.0409 241.467Z",
|
||||||
|
p2e5a2600: "M75.5991 88.3336L208.587 88.3336V84.4963L75.5991 84.4963V88.3336Z",
|
||||||
|
p2eb8c500: "M67.643 238.155C67.643 238.155 67.8429 235.866 65.9942 236.766C64.1454 237.665 57.7299 244.59 64.3853 251.166C64.3853 251.166 73.379 244.53 71.7402 241.073C70.1013 237.615 67.643 238.155 67.643 238.155Z",
|
||||||
|
p2edec780: "M78.3514 73.8043L74.4592 74.6694L77.6916 89.2141L81.5839 88.3491L78.3514 73.8043Z",
|
||||||
|
p2f8bd080: "M201.172 37.5389H192.408V81.0187H201.172V37.5389Z",
|
||||||
|
p2f8eef00: "M74.7897 16.7734L76.2287 15.9939C76.2287 15.9939 75.6391 17.363 74.7897 16.7734Z",
|
||||||
|
p2faec300: "M147.488 61.1675H5.4662V181.504H147.488V61.1675Z",
|
||||||
|
p2fc11400: "M77.9958 50.345H42.0208C41.7257 50.3424 41.4436 50.2233 41.2359 50.0137C41.0281 49.8041 40.9116 49.5209 40.9116 49.2258C40.9116 49.0801 40.9403 48.9359 40.996 48.8013C41.0517 48.6667 41.1335 48.5444 41.2365 48.4414C41.3395 48.3384 41.4617 48.2567 41.5963 48.201C41.7309 48.1452 41.8751 48.1166 42.0208 48.1166H77.9958C78.1415 48.1166 78.2857 48.1452 78.4203 48.201C78.5549 48.2567 78.6772 48.3384 78.7802 48.4414C78.8832 48.5444 78.9649 48.6667 79.0206 48.8013C79.0764 48.9359 79.1051 49.0801 79.1051 49.2258C79.1064 49.3723 79.0787 49.5176 79.0235 49.6533C78.9684 49.7891 78.8868 49.9125 78.7837 50.0166C78.6806 50.1206 78.5579 50.2033 78.4226 50.2596C78.2874 50.316 78.1423 50.345 77.9958 50.345Z",
|
||||||
|
p30281a00: "M89.8476 80.1543V73.6488H92.2059C92.685 73.6406 93.1608 73.729 93.6049 73.9086C93.9833 74.0648 94.324 74.2999 94.6042 74.5982C94.8711 74.8862 95.0753 75.2265 95.2038 75.5975C95.3453 75.9986 95.4163 76.4212 95.4137 76.8466C95.419 77.3065 95.3411 77.7635 95.1839 78.1957C95.0464 78.5794 94.8267 78.9285 94.5401 79.2185C94.2536 79.5085 93.9071 79.7324 93.525 79.8745C93.1052 80.04 92.6571 80.1215 92.2059 80.1143L89.8476 80.1543ZM94.1346 76.8866C94.1385 76.5949 94.0947 76.3046 94.0047 76.0272C93.9303 75.7803 93.8042 75.5521 93.6349 75.3576C93.4676 75.1669 93.2592 75.0165 93.0254 74.9179C92.7665 74.8075 92.4873 74.7531 92.2059 74.7581H91.1067V79.0451H92.2059C92.4914 79.0483 92.7742 78.9904 93.0354 78.8752C93.2664 78.7721 93.4713 78.6184 93.6349 78.4255C93.8026 78.2261 93.9283 77.995 94.0047 77.746C94.0925 77.4749 94.1364 77.1915 94.1346 76.9066V76.8866Z",
|
||||||
|
p30a7ae00: "M79.1466 3.68245C79.1466 3.68245 73.0009 -3.84232 61.1591 4.68176C49.3174 13.2058 58.6009 31.1634 67.8445 30.6637C77.0881 30.1641 79.1466 3.68245 79.1466 3.68245Z",
|
||||||
|
p30e37500: "M81.0053 98.0469L84.8227 107.69L75.5391 109.689L70.6126 104.552L59.6202 115.275L34.5876 116.474L47.9783 95.4887L46.7791 79.13L45.3901 60.8527L44.2309 45.6033C44.2309 45.6033 56.8821 40.4569 67.4548 41.0065C78.0274 41.5561 86.4416 42.9052 86.4416 42.9052C93.007 58.6842 81.0053 98.0469 81.0053 98.0469Z",
|
||||||
|
p30fd91f0: "M59.6602 75.4326H63.5974C64.0334 75.4326 64.3869 75.0792 64.3869 74.6432V67.2083C64.3869 66.7723 64.0334 66.4189 63.5974 66.4189H59.6602C59.2242 66.4189 58.8707 66.7723 58.8707 67.2083V74.6432C58.8707 75.0792 59.2242 75.4326 59.6602 75.4326Z",
|
||||||
|
p3123ba00: "M17.0481 131.309C17.045 130.737 17.1764 130.172 17.4319 129.66C17.6875 129.148 18.0598 128.703 18.519 128.362C18.9781 128.02 19.5112 127.791 20.0751 127.694C20.639 127.597 21.2178 127.633 21.7649 127.801C21.7131 127.427 21.6863 127.05 21.6849 126.672C21.6849 124.615 22.5019 122.643 23.9562 121.189C25.4105 119.734 27.3829 118.917 29.4395 118.917C29.9192 118.918 30.3977 118.965 30.8685 119.057C31.6325 117.803 32.7834 116.83 34.148 116.287C35.5125 115.743 37.0168 115.658 38.4341 116.043C39.8514 116.429 41.105 117.265 42.0061 118.425C42.9071 119.585 43.4068 121.006 43.4298 122.475H43.5597C45.2162 122.475 46.8048 123.133 47.9761 124.304C49.1474 125.476 49.8054 127.064 49.8054 128.721C49.8054 130.377 49.1474 131.966 47.9761 133.137C46.8048 134.308 45.2162 134.966 43.5597 134.966H20.5757C19.6292 134.93 18.7334 134.529 18.0758 133.847C17.4183 133.166 17.05 132.256 17.0481 131.309Z",
|
||||||
|
p3149aa00: "M73.3207 5.55115C73.3207 5.55115 75.3193 14.9746 68.6839 19.0018",
|
||||||
|
p31bf4600: "M105.129 104.622L88.4402 150.79C88.4402 150.79 48.2381 149.791 40.3336 141.707C32.4291 133.622 35.1972 120.152 41.3329 110.668C47.4687 101.185 78.3072 99.406 78.3072 99.406L84.8727 107.69C84.8727 107.69 95.955 104.622 105.129 104.622Z",
|
||||||
|
p31e2fb80: "M34.5077 148.352C33.9776 148.352 33.4692 148.141 33.0944 147.767C32.7196 147.392 32.509 146.883 32.509 146.353C32.9288 123.549 40.3836 108.689 43.3015 103.723C43.4426 103.487 43.6309 103.282 43.8548 103.122C44.0786 102.961 44.3332 102.849 44.6024 102.792C44.8717 102.735 45.1499 102.734 45.4196 102.789C45.6892 102.844 45.9446 102.955 46.1696 103.113L47.7285 104.203L46.0896 103.053L46.1795 103.113C46.592 103.398 46.8825 103.827 46.9936 104.316C47.1046 104.804 47.0281 105.317 46.7791 105.752C44.141 110.308 36.936 124.688 36.5163 146.403C36.5032 146.926 36.2855 147.423 35.9099 147.788C35.5344 148.152 35.0309 148.355 34.5077 148.352Z",
|
||||||
|
p31fa4c80: "M73.0992 72.0599H69.112V89.218H73.0992V72.0599Z",
|
||||||
|
p31fbdc10: "M115.811 63.0312C112.813 69.7465 101.441 67.3082 93.6266 64.6601C90.9937 63.7683 88.4087 62.741 85.882 61.5822L86.0718 55.4765L86.4616 42.8752L95.3953 48.6012L101.721 52.6584L90.139 36.1599L95.6352 30.8036C95.6352 30.8036 120.558 52.4985 115.811 63.0312Z",
|
||||||
|
p321ca900: "M82.1446 25.7771L81.8848 26.6565C79.5364 26.5666 77.8875 26.1968 77.188 25.4573C77.1796 25.1675 77.2132 24.878 77.2879 24.5979C78.8051 25.3303 80.4605 25.7322 82.1446 25.7771Z",
|
||||||
|
p3269cc80: "M74.3783 296.474C74.5682 293.896 79.0751 233.118 71.1506 222.405L70.3511 222.995C78.0558 233.408 73.429 295.765 73.349 296.394L74.3783 296.474Z",
|
||||||
|
p32d1caf0: "M137.634 12.1915H135.866V16.1188H137.634V12.1915Z",
|
||||||
|
p32fbd720: "M193.755 22.6243C300.764 22.6243 387.511 17.5596 387.511 11.3121C387.511 5.06459 300.764 0 193.755 0C86.7472 0 0 5.06459 0 11.3121C0 17.5596 86.7472 22.6243 193.755 22.6243Z",
|
||||||
|
p32ff6780: "M200.272 49.3607H193.747V59.2239H200.272V49.3607Z",
|
||||||
|
p33c34680: "M10.4817 27.1876C12.0512 24.819 13.7913 22.5679 15.6881 20.4523C33.6756 0.466156 65.3336 -5.82947 89.6367 5.84242C115.239 18.1439 130.229 46.4442 155.741 58.9056C178.845 70.1977 206.076 66.5303 231.618 63.5423C257.16 60.5544 285.151 59.0854 306.336 73.6653C341.212 97.6486 335.856 149.333 310.463 178.153C295.234 195.441 273.089 206.133 250.185 208.612C233.547 210.4 214.21 209.051 203.018 221.503C197.712 227.408 195.463 235.353 192.875 242.858C183.681 269.549 167.013 295.201 141.571 307.413C116.128 319.624 81.1626 314.158 66.1331 290.275C53.9015 270.828 57.2792 245.746 61.866 223.241C66.4528 200.737 71.6492 176.474 62.1958 155.538C51.1235 131.015 23.1529 118.384 9.00275 95.5001C-3.69843 75.0144 -2.7391 47.1637 10.4817 27.1876Z",
|
||||||
|
p349f9f00: "M79.7946 8.07437C80.8763 8.07437 81.7532 7.19746 81.7532 6.11574C81.7532 5.03401 80.8763 4.15709 79.7946 4.15709C78.7128 4.15709 77.8359 5.03401 77.8359 6.11574C77.8359 7.19746 78.7128 8.07437 79.7946 8.07437Z",
|
||||||
|
p34aaa00: "M59.5802 73.1242H64.0571V66.9985H59.5802V73.1242Z",
|
||||||
|
p34c9a380: "M77.4578 15.4743L76.1387 20.4708C76.1387 20.4708 75.2893 20.211 75.2793 19.8912L76.3786 15.6441C76.3786 15.6441 77.148 15.5042 77.4578 15.4743Z",
|
||||||
|
p34e8900: "M134.988 241.467H141.114L151.576 95.9384L152.116 88.5735H143.992L143.552 95.9384L134.988 241.467Z",
|
||||||
|
p35f3d170: "M194.047 35.8001H173.221V68.4974H194.047V35.8001Z",
|
||||||
|
p36836700: "M137.026 88.5735L137.186 113.556H144.991L145.151 88.5735H137.026Z",
|
||||||
|
p36b03900: "M199.593 35.8001H178.767V68.4974H199.593V35.8001Z",
|
||||||
|
p36da600: "M86.3616 14.3051L85.0326 19.2316C85.0326 19.2316 86.9113 19.8712 87.4109 18.3623C87.6694 17.017 87.625 15.631 87.281 14.3051C86.977 14.2506 86.6657 14.2506 86.3616 14.3051Z",
|
||||||
|
p36f50f00: "M58.7492 74.3184H54.762V89.218H58.7492V74.3184Z",
|
||||||
|
p37008900: "M429.122 56.6906H264.786V202.509H429.122V56.6906Z",
|
||||||
|
p37166600: "M65.7659 21.0804C65.7659 21.0804 69.0437 13.7255 67.9045 8.40917C67.9045 8.40917 84.0233 -0.114911 86.5415 16.8533C86.5415 16.8533 86.6814 1.54394 72.0816 2.65317C57.4817 3.7624 59.6502 25.0776 67.8045 30.6837C67.8045 30.6837 63.7473 25.6872 65.7659 21.0804Z",
|
||||||
|
p3747b180: "M88.8583 79.0651V80.1543H84.3414V73.6488H88.7683V74.758H85.6006V76.327H88.3386V77.3263H85.6006V79.0251L88.8583 79.0651Z",
|
||||||
|
p37c1ac00: "M9.74321 275.579H143.47C148.851 275.579 153.214 271.216 153.214 265.835L153.214 9.74323C153.214 4.36218 148.851 0 143.47 0H9.74321C4.36218 0 -1.52588e-05 4.36218 -1.52588e-05 9.74323L-1.52588e-05 265.835C-1.52588e-05 271.216 4.36218 275.579 9.74321 275.579Z",
|
||||||
|
p3914700: "M73.6205 5.88092C82.0946 5.0415 85.2724 9.30854 86.7613 17.9825C88.6201 28.815 86.9712 36.8194 76.1387 35.5303C61.429 33.8015 59.5403 7.27995 73.6205 5.88092Z",
|
||||||
|
p3946bb40: "M185.283 61.902H178.757V64.4602H185.283V61.902Z",
|
||||||
|
p395bfe80: "M305.488 60.6878L268.903 198.292V133.637L288.22 60.6878H305.488Z",
|
||||||
|
p39639500: "M108.784 123.954H112.772V106.796H108.784V123.954Z",
|
||||||
|
p3987ae00: "M442.453 301.89C442.658 301.459 442.757 300.984 442.741 300.506C442.726 300.028 442.597 299.561 442.364 299.143C442.131 298.726 441.802 298.37 441.403 298.106C441.005 297.841 440.549 297.676 440.074 297.623C440.494 296.393 440.528 295.063 440.171 293.813C439.813 292.562 439.082 291.451 438.076 290.628C437.833 290.405 437.524 290.268 437.196 290.238C436.848 290.275 436.52 290.418 436.257 290.648L430.681 294.645C430.235 294.918 429.858 295.291 429.582 295.735C428.442 297.943 432.579 300.731 430.861 302.59C430.371 303.109 429.592 303.219 428.992 303.589C427.663 304.419 427.503 306.237 426.883 307.676C426.264 309.115 424.115 310.224 423.286 308.895C423.896 307.516 422.057 306.337 420.558 306.157C419.059 305.977 417.5 305.897 416.671 304.498C414.552 306.347 409.795 301.181 409.905 299.092C408.166 299.222 406.977 300.861 406.598 302.49C406.353 304.211 406.292 305.953 406.418 307.686C406.358 311.074 405.268 314.382 405.328 317.759C405.378 320.387 406.398 323.305 408.796 324.365C409.993 324.792 411.266 324.962 412.533 324.864L439.944 324.694C440.603 324.737 441.263 324.627 441.873 324.375C443.112 323.725 443.372 322.096 443.482 320.707C443.975 314.751 444.468 308.799 444.961 302.85L442.453 301.89Z",
|
||||||
|
p399c4100: "M186.66 20.9155V34.9058L195.934 29.6594",
|
||||||
|
p39bc5500: "M33.1786 218.743L61.9686 215.915V175.453H73.3806V215.905L102.291 218.743C103.676 218.882 104.96 219.53 105.895 220.562C106.829 221.594 107.347 222.937 107.347 224.329V228.646H28.1021V224.329C28.1031 222.934 28.6235 221.589 29.562 220.556C30.5004 219.523 31.7897 218.877 33.1786 218.743Z",
|
||||||
|
p39d63980: "M93.6266 64.6601C90.9937 63.7683 88.4087 62.741 85.882 61.5822L86.0718 55.4765L88.0704 50.7298C88.0704 50.7298 92.3974 57.615 93.6266 64.6601Z",
|
||||||
|
p39e5970: "M31.15 241.477C35.2727 241.477 38.6148 238.135 38.6148 234.012C38.6148 229.89 35.2727 226.548 31.15 226.548C27.0273 226.548 23.6852 229.89 23.6852 234.012C23.6852 238.135 27.0273 241.477 31.15 241.477Z",
|
||||||
|
p3a1e4c00: "M71.8017 85.4956H212.384C212.668 85.4956 212.948 85.4398 213.21 85.3313C213.472 85.2228 213.71 85.0638 213.91 84.8634C214.111 84.663 214.27 84.425 214.378 84.1631C214.487 83.9012 214.543 83.6206 214.543 83.3371C214.543 83.0536 214.487 82.773 214.378 82.5111C214.27 82.2492 214.111 82.0113 213.91 81.8108C213.71 81.6104 213.472 81.4514 213.21 81.3429C212.948 81.2344 212.668 81.1786 212.384 81.1786H71.8017C71.5183 81.1786 71.2376 81.2344 70.9757 81.3429C70.7138 81.4514 70.4759 81.6104 70.2754 81.8108C70.075 82.0113 69.916 82.2492 69.8075 82.5111C69.6991 82.773 69.6432 83.0536 69.6432 83.3371C69.6432 83.6206 69.6991 83.9012 69.8075 84.1631C69.916 84.425 70.075 84.663 70.2754 84.8634C70.4759 85.0638 70.7138 85.2228 70.9757 85.3313C71.2376 85.4398 71.5183 85.4956 71.8017 85.4956Z",
|
||||||
|
p3a585000: "M123.536 138.629C123.786 148.872 97.6638 225.998 97.6638 225.998L91.0783 223.69C97.374 152.239 106.548 145.604 102.75 142.316C101.931 141.607 99.4825 141.247 96.2048 141.117C89.1244 141 82.0428 141.257 74.9895 141.886C68.9937 142.346 64.3669 142.826 64.3669 142.826C67.4348 142.296 70.4827 141.617 73.4706 140.827C86.723 137.214 99.4976 132.034 111.524 125.398C111.524 125.398 123.286 128.386 123.536 138.629Z",
|
||||||
|
p3b000000: "M67.1533 80.2142C66.7045 80.2221 66.26 80.1264 65.8542 79.9344C65.4741 79.7587 65.1341 79.5071 64.8549 79.1949C64.5762 78.8858 64.3592 78.5263 64.2154 78.1357C64.0597 77.7483 63.9816 77.334 63.9855 76.9165C63.9839 76.4918 64.0654 76.0709 64.2254 75.6774C64.3835 75.2872 64.6103 74.9284 64.8949 74.6181C65.1782 74.3158 65.5176 74.0715 65.8942 73.8986C66.2925 73.7171 66.7257 73.625 67.1633 73.6288C67.6088 73.6218 68.0499 73.7175 68.4524 73.9086C68.8293 74.0983 69.1681 74.3558 69.4517 74.6681C69.7214 74.9838 69.9374 75.3417 70.0913 75.7274C70.2404 76.1132 70.3183 76.5229 70.3211 76.9365C70.3238 77.3613 70.2423 77.7825 70.0813 78.1757C69.9235 78.5458 69.7 78.8843 69.4218 79.175C69.1384 79.4773 68.799 79.7216 68.4224 79.8945C68.0281 80.0932 67.5947 80.2024 67.1533 80.2142ZM65.2646 76.9165C65.2622 77.195 65.3061 77.4719 65.3946 77.736C65.4733 77.987 65.5987 78.221 65.7643 78.4255C65.922 78.6305 66.1234 78.7978 66.3539 78.9151C66.6072 79.0338 66.8836 79.0953 67.1633 79.0953C67.4431 79.0953 67.7194 79.0338 67.9727 78.9151C68.1999 78.7885 68.4002 78.6188 68.5623 78.4155C68.7261 78.2061 68.8514 77.9693 68.9321 77.716C69.0117 77.457 69.0521 77.1875 69.052 76.9165C69.0545 76.6413 69.0106 76.3677 68.9221 76.1071C68.84 75.8524 68.7112 75.6152 68.5424 75.4076C68.3814 75.2084 68.1806 75.045 67.9528 74.9279C67.7076 74.8063 67.437 74.7447 67.1633 74.748C66.879 74.7457 66.5982 74.8107 66.3439 74.9379C66.1141 75.0563 65.9129 75.2235 65.7543 75.4276C65.5942 75.6393 65.4694 75.8755 65.3846 76.1271C65.3058 76.3829 65.2654 76.6489 65.2646 76.9165Z",
|
||||||
|
p3b9e2ac0: "M62.2084 64.2404H61.2191V67.8479H62.2084V64.2404Z",
|
||||||
|
p3bc5d200: "M46.7791 79.13L45.3901 60.8527L48.388 58.1146C48.388 58.1146 51.6757 68.9971 46.7791 79.13Z",
|
||||||
|
p3bdd9800: "M20.206 12.0516H11.8718V16.1188H20.206V12.0516Z",
|
||||||
|
p3c06700: "M81.435 11.0573C81.9998 10.8547 82.603 10.7818 83.1999 10.8441C83.7967 10.9063 84.3718 11.1022 84.8827 11.4171",
|
||||||
|
p3c78b000: "M9.48504 24.2182L17.4795 40.7167C17.5404 40.8082 17.5785 40.9128 17.5908 41.022C17.603 41.1311 17.589 41.2417 17.5499 41.3443C17.5108 41.447 17.4477 41.5388 17.3659 41.6121C17.2842 41.6855 17.186 41.7382 17.0798 41.766L9.77484 44.3242C9.28518 44.4941 8.28588 44.5341 8.06603 44.0744L0.071584 27.5159C-0.148263 27.0462 0.551251 26.3667 1.07089 26.1968L8.37581 23.6386C8.59962 23.582 8.83641 23.6098 9.04101 23.7167C9.2456 23.8237 9.40367 24.0022 9.48504 24.2182Z",
|
||||||
|
p3c918700: "M53.8026 72.0599H49.8154V89.218H53.8026V72.0599Z",
|
||||||
|
p3d945900: "M186.182 37.5389H177.418V81.0187H186.182V37.5389Z",
|
||||||
|
p3e10fa80: "M73.1891 297.793C73.1891 297.323 77.5461 250.666 60.508 231.119L59.7585 231.769C76.5168 251.006 72.2398 297.233 72.1898 297.723L73.1891 297.793Z",
|
||||||
|
p3e2a8700: "M96.5929 154.313C96.5955 153.02 97.1102 151.781 98.0241 150.867C98.9381 149.953 100.177 149.439 101.47 149.436C101.944 149.439 102.415 149.509 102.869 149.646C102.796 149.15 102.76 148.649 102.759 148.147C102.761 145.41 103.85 142.786 105.785 140.851C107.72 138.915 110.344 137.827 113.081 137.824C113.722 137.823 114.361 137.883 114.99 138.004C115.994 136.312 117.526 134.995 119.35 134.257C121.174 133.518 123.19 133.399 125.089 133.916C126.987 134.433 128.664 135.559 129.861 137.12C131.059 138.682 131.711 140.593 131.718 142.561H131.868C134.073 142.561 136.188 143.437 137.747 144.996C139.307 146.555 140.183 148.67 140.183 150.875C140.183 153.08 139.307 155.195 137.747 156.754C136.188 158.313 134.073 159.189 131.868 159.189H101.33C100.06 159.155 98.8536 158.626 97.9686 157.715C97.0835 156.804 96.5898 155.583 96.5929 154.313Z",
|
||||||
|
p3e491780: "M71.1522 184.177L64.1571 184.177C63.0625 184.177 62.0127 183.742 61.2387 182.968C60.4647 182.194 60.0299 181.144 60.0299 180.05V148.142H75.3293V180.02C75.3333 180.569 75.2279 181.113 75.0191 181.62C74.8104 182.128 74.5025 182.588 74.1136 182.976C73.7246 183.363 73.2624 183.668 72.7539 183.875C72.2454 184.081 71.7009 184.184 71.1522 184.177Z",
|
||||||
|
p3e566380: "M91.4065 241.412C91.4065 241.412 90.0075 239.584 92.0461 239.364C94.0846 239.144 103.218 241.572 101.04 250.686C101.04 250.686 89.9175 249.846 89.4379 246.069C88.9582 242.292 91.4065 241.412 91.4065 241.412Z",
|
||||||
|
p3e7ef800: "M79.9445 258.131V262.887H72.4397V258.131H79.9445ZM80.0444 256.402H72.3397C71.9077 256.402 71.4935 256.573 71.188 256.879C70.8825 257.184 70.7109 257.599 70.7109 258.031V263.027C70.7109 263.459 70.8825 263.874 71.188 264.179C71.4935 264.484 71.9077 264.656 72.3397 264.656H80.0444C80.4764 264.656 80.8907 264.484 81.1962 264.179C81.5017 263.874 81.6733 263.459 81.6733 263.027V258.031C81.6733 257.599 81.5017 257.184 81.1962 256.879C80.8907 256.573 80.4764 256.402 80.0444 256.402Z",
|
||||||
|
p3ef46600: "M54.3422 123.804H73.7887V110.383H54.3422V123.804Z",
|
||||||
|
p3efa5800: "M129.552 159.074C122.996 143.086 116.461 128.526 114.232 128.436C113.278 128.51 112.337 128.708 111.434 129.025L111.524 125.378C117.202 125.849 122.509 128.392 126.434 132.523C130.721 137.19 130.281 150.82 129.552 159.074Z",
|
||||||
|
p3f229600: "M200.272 61.902H193.747V64.4602H200.272V61.902Z",
|
||||||
|
p3f28f940: "M424.795 147.707V169.742L417.33 198.592H411.444L424.795 147.707Z",
|
||||||
|
p3f441df0: "M45.6483 128.611H136.235V123.954L45.6483 123.954V128.611Z",
|
||||||
|
p3f497600: "M23.5736 221.146H14.5799C14.2909 221.144 14.0142 221.028 13.8089 220.825C13.6036 220.622 13.4858 220.346 13.4806 220.057C13.4806 219.762 13.5972 219.479 13.8049 219.269C14.0126 219.06 14.2948 218.941 14.5899 218.938H23.5836C23.8752 218.946 24.1523 219.067 24.3557 219.276C24.5592 219.485 24.673 219.765 24.6729 220.057C24.6652 220.345 24.5466 220.619 24.3418 220.822C24.1371 221.025 23.8619 221.141 23.5736 221.146Z",
|
||||||
|
p3fbd8e00: "M184.603 34.8008H163.778V67.4981H184.603V34.8008Z",
|
||||||
|
p4aed700: "M424.795 60.6878H268.903V198.592H424.795V60.6878Z",
|
||||||
|
p528a700: "M185.283 44.3642H178.757V47.4121H185.283V44.3642Z",
|
||||||
|
p5bb3c80: "M119.677 123.954H121.675V105.946H119.677V123.954Z",
|
||||||
|
p5c5dc40: "M118.987 214.441H14.5799C14.2848 214.438 14.0027 214.319 13.7949 214.11C13.5872 213.9 13.4706 213.617 13.4707 213.322C13.4707 213.028 13.5875 212.745 13.7955 212.537C14.0036 212.329 14.2857 212.213 14.5799 212.213H118.987C119.282 212.213 119.566 212.329 119.775 212.537C119.985 212.745 120.104 213.027 120.107 213.322C120.107 213.619 119.989 213.903 119.779 214.113C119.569 214.323 119.284 214.441 118.987 214.441Z",
|
||||||
|
p5fd2e80: "M60.388 36.7445H43.3998C43.0696 36.7445 42.7426 36.6793 42.4376 36.5526C42.1327 36.4259 41.8557 36.2402 41.6227 36.0063C41.3896 35.7723 41.2051 35.4946 41.0796 35.1891C40.9542 34.8837 40.8902 34.5564 40.8916 34.2262C40.8916 33.561 41.1558 32.923 41.6262 32.4526C42.0966 31.9822 42.7346 31.7179 43.3998 31.7179H60.388C61.0533 31.7179 61.6912 31.9822 62.1616 32.4526C62.632 32.923 62.8963 33.561 62.8963 34.2262C62.8976 34.5564 62.8337 34.8837 62.7082 35.1891C62.5828 35.4946 62.3982 35.7723 62.1652 36.0063C61.9321 36.2402 61.6552 36.4259 61.3502 36.5526C61.0452 36.6793 60.7183 36.7445 60.388 36.7445Z",
|
||||||
|
p6032f80: "M59.92 106.191H4.56846C4.13587 106.191 3.70753 106.106 3.30799 105.94C2.90845 105.774 2.54557 105.531 2.24015 105.225C1.93473 104.918 1.69276 104.555 1.52813 104.155C1.36349 103.755 1.27943 103.326 1.28074 102.894C1.27943 102.461 1.36349 102.032 1.52813 101.632C1.69276 101.232 1.93473 100.869 2.24015 100.562C2.54557 100.256 2.90845 100.013 3.30799 99.847C3.70753 99.6812 4.13587 99.5958 4.56846 99.5958H59.92C60.3526 99.5958 60.7809 99.6812 61.1805 99.847C61.58 100.013 61.9429 100.256 62.2483 100.562C62.5537 100.869 62.7957 101.232 62.9603 101.632C63.125 102.032 63.209 102.461 63.2077 102.894C63.209 103.326 63.125 103.755 62.9603 104.155C62.7957 104.555 62.5537 104.918 62.2483 105.225C61.9429 105.531 61.58 105.774 61.1805 105.94C60.7809 106.106 60.3526 106.191 59.92 106.191Z",
|
||||||
|
p62be880: "M170.573 77.1314V79.7796C170.573 80.569 169.214 81.1286 168.425 81.1286H127.703C126.903 81.1286 125.604 80.569 125.604 79.7796V77.1314H170.573Z",
|
||||||
|
p6372df1: "M151.986 199.596C151.986 199.596 157.272 210.649 160.31 212.777C160.31 212.777 156.233 215.216 151.986 209.779C146.5 202.784 145.151 204.783 145.151 204.783C145.265 203.576 145.489 202.382 145.82 201.215C146.64 198.937 151.986 199.596 151.986 199.596Z",
|
||||||
|
p6483d00: "M64.3669 142.826C64.3669 142.826 99.1228 139.178 102.75 142.316C106.548 145.604 97.374 152.239 91.0783 223.69L97.6638 225.998C97.6638 225.998 123.786 148.872 123.536 138.629C123.286 128.386 111.544 125.388 111.544 125.388C111.544 125.388 88.6301 138.629 64.3669 142.826Z",
|
||||||
|
p6861500: "M105.298 239.918C104.437 239.237 103.458 238.718 102.41 238.389C102.41 238.389 99.2527 235.322 97.6638 225.998L93.5566 224.999C92.8606 224.836 92.1312 224.891 91.4678 225.158C90.8044 225.424 90.2392 225.889 89.8492 226.488C89.7388 226.655 89.6514 226.837 89.5894 227.027C89.3895 228.017 89.2527 229.018 89.1797 230.025C88.9875 233.441 89.0042 236.865 89.2296 240.278H89.9691L90.199 233.833C90.2889 231.644 91.9777 234.192 92.4874 236.541C92.997 238.889 94.0263 241.387 98.823 241.457C104.409 241.517 106.727 241.277 105.298 239.918Z",
|
||||||
|
p7432a00: "M62.2068 71.2105H60.2082V89.218H62.2068V71.2105Z",
|
||||||
|
p7937200: "M111.524 125.398C111.044 125.808 101.381 134.072 96.2048 141.117C89.1244 141 82.0428 141.257 74.9895 141.886L73.4706 140.837C86.7235 137.221 99.4981 132.038 111.524 125.398Z",
|
||||||
|
p7d81480: "M73.7288 290.078C73.7288 289.679 75.0778 250.426 81.4634 244.22C87.6691 238.224 94.7941 244 95.0939 244.22L95.7335 243.451C95.6535 243.381 87.6091 236.815 80.7439 243.451C74.0685 249.946 72.7495 288.36 72.7495 289.999L73.7288 290.078Z",
|
||||||
|
p7e68900: "M194.027 88.5735L194.566 95.9384L205.029 241.467H211.145L202.591 95.9384L202.151 88.5735H194.027Z",
|
||||||
|
p80c3a00: "M83.4137 15.5642L84.8527 14.7847C84.8527 14.7847 84.2531 16.1738 83.4137 15.5642Z",
|
||||||
|
p8347d00: "M16.2487 275.579H149.976C155.357 275.579 159.719 271.216 159.719 265.835L159.719 9.74323C159.719 4.36218 155.357 0 149.976 0H16.2487C10.8677 0 6.50546 4.36218 6.50546 9.74323L6.50546 265.835C6.50546 271.216 10.8677 275.579 16.2487 275.579Z",
|
||||||
|
p845300: "M97.4539 223.6C97.4539 223.6 99.9622 235.591 102.45 238.389C102.45 238.389 97.9136 239.778 95.0756 233.523C91.3981 225.418 89.6294 226.997 89.6294 226.997C90.0262 225.853 90.5245 224.746 91.1183 223.69C92.4174 221.691 97.4539 223.6 97.4539 223.6Z",
|
||||||
|
p8568380: "M191.125 57.5608C203.775 54.5752 211.609 41.9003 208.624 29.2505C205.638 16.6008 192.963 8.76652 180.313 11.7521C167.664 14.7377 159.829 27.4127 162.815 40.0624C165.801 52.7121 178.475 60.5464 191.125 57.5608Z",
|
||||||
|
p85f60f0: "M131.888 13.7604H130.12V16.1088H131.888V13.7604Z",
|
||||||
|
p8726000: "M77.8875 15.5242L76.5584 20.4508C76.5584 20.4508 78.4371 21.0804 78.9468 19.5714C79.1989 18.2285 79.1511 16.8465 78.8069 15.5242C78.5028 15.4698 78.1916 15.4698 77.8875 15.5242Z",
|
||||||
|
p9186440: "M73.0309 241.467H79.1566L89.6194 95.9384L90.159 88.5735H82.0346L81.5949 95.9384L73.0309 241.467Z",
|
||||||
|
p9279600: "M15.8906 58.8841L10.0646 47.2222C10.0646 47.2222 14.1918 38.1585 12.9326 36.8794C11.6735 35.6003 10.2645 31.1334 7.93612 30.7736C5.60773 30.4139 2.54986 41.2364 2.54986 42.1557C2.54986 43.0751 4.72834 48.5813 4.72834 48.5813L9.86478 64.5701L15.8906 58.8841Z",
|
||||||
|
p94c3c0: "M83.014 25.8171C82.6735 26.7952 82.4063 27.7972 82.2145 28.815H81.6649C79.1766 28.735 78.0474 27.8756 77.5578 26.9363C77.3225 26.4791 77.2024 25.9714 77.208 25.4573C77.1996 25.1675 77.2332 24.878 77.3079 24.5979C78.8251 25.3303 80.4805 25.7322 82.1646 25.7771C82.6842 25.8171 83.014 25.8171 83.014 25.8171Z",
|
||||||
|
p98cf080: "M134.407 207.726H14.5799C14.2857 207.726 14.0036 207.609 13.7955 207.401C13.5875 207.193 13.4707 206.911 13.4707 206.616C13.4706 206.321 13.5872 206.038 13.7949 205.829C14.0027 205.619 14.2848 205.5 14.5799 205.497H134.397C134.692 205.5 134.974 205.619 135.182 205.829C135.389 206.038 135.506 206.321 135.506 206.616C135.506 206.909 135.39 207.19 135.185 207.397C134.979 207.605 134.699 207.723 134.407 207.726Z",
|
||||||
|
p9c5fcf0: "M123.134 123.964H127.122V109.064H123.134V123.964Z",
|
||||||
|
pa8d6300: "M44.2309 45.5734C40.4335 46.103 28.312 70.7459 25.414 70.7559C22.516 70.7658 13.4223 51.5792 13.4223 51.5792L5.90753 57.1453C5.90753 57.1453 14.9013 87.9039 25.0742 86.4649C36.0666 84.906 48.0582 66.9485 48.0582 66.9485C48.5081 63.0141 48.5081 59.0412 48.0582 55.1067C47.5481 51.6727 46.237 48.4068 44.2309 45.5734Z",
|
||||||
|
pad2e780: "M93.4151 44.2792H42.0108C41.7166 44.2792 41.4345 44.1624 41.2265 43.9543C41.0184 43.7463 40.9016 43.4642 40.9016 43.17C40.9016 42.8749 41.0181 42.5917 41.2258 42.3821C41.4336 42.1725 41.7157 42.0534 42.0108 42.0508H93.4151C93.5621 42.0521 93.7073 42.0823 93.8426 42.1398C93.9779 42.1972 94.1006 42.2808 94.2036 42.3856C94.3066 42.4905 94.3879 42.6146 94.443 42.7509C94.498 42.8872 94.5256 43.033 94.5243 43.18C94.5217 43.4724 94.4037 43.752 94.1959 43.9579C93.9882 44.1637 93.7076 44.2792 93.4151 44.2792Z",
|
||||||
|
pad53b00: "M57.7015 45.5734C57.7015 45.5734 69.4034 73.2041 70.2728 83.3371C70.2728 83.3371 77.9775 59.2539 77.0581 47.3621L57.7015 45.5734Z",
|
||||||
|
pae23400: "M95.5836 335.657H52.4236V335.907H95.5836V335.657Z",
|
||||||
|
paea9300: "M88.3786 42.3606V53.4828H54.5021C53.2156 53.5243 51.9339 53.3068 50.7331 52.8431C49.5323 52.3794 48.437 51.6791 47.5122 50.7837C46.5875 49.8883 45.8522 48.8162 45.35 47.631C44.8478 46.4458 44.5891 45.1717 44.5891 43.8845C44.5891 42.5973 44.8478 41.3232 45.35 40.138C45.8522 38.9528 46.5875 37.8807 47.5122 36.9853C48.437 36.0899 49.5323 35.3895 50.7331 34.9259C51.9339 34.4622 53.2156 34.2446 54.5021 34.2862H54.672C54.704 32.0274 55.4696 29.8403 56.8534 28.0548C58.2372 26.2693 60.1642 24.9822 62.3435 24.3877C64.5229 23.7932 66.8365 23.9236 68.9352 24.7592C71.034 25.5947 72.8041 27.0901 73.9786 29.0198C74.7039 28.8871 75.4397 28.8202 76.1771 28.82C79.3363 28.82 82.3661 30.0749 84.5999 32.3088C86.8338 34.5427 88.0888 37.5725 88.0888 40.7317C88.086 41.3103 88.0426 41.888 87.9589 42.4605L88.3786 42.3606Z",
|
||||||
|
pb065d40: "M75.3576 249.497C75.3576 249.497 70.3012 249.277 70.0114 253.644C69.7216 258.011 81.0037 270.062 81.0037 270.062C81.0037 270.062 88.9982 267.164 87.3093 256.282C85.6205 245.399 76.6168 246.829 75.3576 249.497Z",
|
||||||
|
pb0c8bf0: "M200.272 44.3642H193.747V47.4121H200.272V44.3642Z",
|
||||||
|
pb15fd00: "M180.476 44.614L173.081 78.0708C172.955 78.6164 172.654 79.1062 172.225 79.4666C171.797 79.827 171.262 80.0385 170.703 80.0694H130.581C130.324 80.0447 130.076 79.9634 129.854 79.8314C129.632 79.6995 129.443 79.5201 129.298 79.3061C129.154 79.0922 129.059 78.8489 129.02 78.5939C128.981 78.3388 128.999 78.0783 129.072 77.8309L136.417 44.7339C136.578 44.2394 136.901 43.8139 137.335 43.5272C137.769 43.2404 138.287 43.1092 138.805 43.155H178.957C180.077 43.155 180.716 43.5348 180.476 44.614Z",
|
||||||
|
pb3d8080: "M18.3489 40.337C18.4024 40.4412 18.4326 40.5558 18.4374 40.6729C18.4423 40.79 18.4217 40.9067 18.377 41.015C18.3324 41.1233 18.2647 41.2207 18.1788 41.3003C18.0928 41.3799 17.9906 41.4399 17.8792 41.4762L9.2552 44.4741C9.01391 44.554 8.75197 44.5444 8.51715 44.4472C8.28233 44.3499 8.09029 44.1715 7.97609 43.9445L0.10156 27.6158C0.0445976 27.5056 0.0107801 27.385 0.00218704 27.2612C-0.00640606 27.1375 0.0104062 27.0133 0.0515912 26.8963C0.0927762 26.7793 0.157459 26.6719 0.241668 26.5809C0.325876 26.4898 0.427826 26.4169 0.541249 26.3667L9.16526 23.3688C9.42612 23.3135 9.698 23.3474 9.93723 23.4652C10.1765 23.583 10.3691 23.7778 10.4843 24.0183L18.3489 40.337Z",
|
||||||
|
pb6e5930: "M156.853 66.0591C158.721 66.0591 160.64 64.0605 161.13 61.5822C161.619 59.1039 160.49 57.0953 158.611 57.0953C158.269 57.1026 157.931 57.1703 157.612 57.2952C157.439 58.1789 157.06 59.0093 156.506 59.7194C155.952 60.4294 155.239 60.9991 154.424 61.3824C154.42 61.4489 154.42 61.5157 154.424 61.5822C153.845 64.0505 154.974 66.0591 156.853 66.0591Z",
|
||||||
|
pbc9f980: "M23.7235 52.9632C30.3132 52.9632 35.6552 47.6212 35.6552 41.0315C35.6552 34.4418 30.3132 29.0998 23.7235 29.0998C17.1338 29.0998 11.7918 34.4418 11.7918 41.0315C11.7918 47.6212 17.1338 52.9632 23.7235 52.9632Z",
|
||||||
|
pbd90290: "M75.3093 12.0267C74.6314 12.0129 73.9594 12.1542 73.3445 12.44C72.7296 12.7258 72.1883 13.1484 71.7618 13.6755",
|
||||||
|
pc3c7b00: "M179.057 34.8008H158.232V67.4981H179.057V34.8008Z",
|
||||||
|
pc63db00: "M201.172 37.5389V81.0187H172.312V48.6212L187.291 37.5389H201.172Z",
|
||||||
|
pd1dd780: "M205.029 241.467H211.155L202.151 88.3336H194.027L205.029 241.467Z",
|
||||||
|
pd30380: "M85.9403 71.2105H83.9417V89.218H85.9403V71.2105Z",
|
||||||
|
pd55e600: "M95.0166 71.6338L91.0873 72.3111L94.0019 89.2198L97.9312 88.5425L95.0166 71.6338Z",
|
||||||
|
pd5db800: "M81.2352 18.0525C82.0355 19.2513 82.9784 20.3486 84.0432 21.3202C83.8309 21.6955 83.5186 22.0044 83.141 22.2126C82.7634 22.4208 82.3354 22.5201 81.9047 22.4994L81.2352 18.0525Z",
|
||||||
|
pf19080: "M58.6892 6.71532H37.1342C20.7372 6.71532 7.44482 20.0077 7.44482 36.4047V81.0637C7.44482 97.4607 20.7372 110.753 37.1342 110.753H58.6892C75.0862 110.753 88.3786 97.4607 88.3786 81.0637V36.4047C88.3786 20.0077 75.0862 6.71532 58.6892 6.71532Z",
|
||||||
|
pf823500: "M444.051 272.92H401.521L401.871 277.217L402.121 280.285L402.77 288.28L403.14 292.867L403.63 298.972L403.839 301.59L403.909 302.37L404.159 305.448L404.499 309.665L404.709 312.283L404.929 314.941L405.178 318.019L405.368 320.347L405.578 322.966L405.668 324.105H444.691L444.781 322.966L444.991 320.357L445.181 318.009L445.43 314.931L445.65 312.283L445.86 309.665L446.2 305.438L446.45 302.37L446.51 301.59L446.72 298.972L447.219 292.867L447.589 288.29L448.229 280.295L448.478 277.227L448.828 272.93L444.051 272.92ZM446.61 275.738L445.79 274.919H446.69L446.61 275.738ZM446.4 278.347L446.32 279.346L442.513 283.153L438.146 278.806L442.053 274.899H442.972L446.4 278.347ZM445.301 291.957L442.513 294.795L438.146 290.428L442.513 286.071L445.51 289.069L445.301 291.957ZM404.829 289.039L407.827 286.041L412.194 290.398L407.847 294.795L405.069 292.007L404.829 289.039ZM441.104 284.602L436.697 288.969L432.33 284.602L436.697 280.235L441.104 284.602ZM434.238 274.919H439.235L436.737 277.417L434.238 274.919ZM435.318 290.378L430.961 294.795L426.594 290.428L430.961 286.071L435.318 290.378ZM420.818 296.154L425.185 291.797L429.542 296.154L425.185 300.521L420.818 296.154ZM423.766 301.93L419.399 306.297L415.042 301.94L419.409 297.573L423.766 301.93ZM430.961 297.573L435.318 301.94L430.961 306.297L426.594 301.93L430.961 297.573ZM425.185 288.969L420.818 284.602L425.185 280.235L429.542 284.602L425.185 288.969ZM423.766 290.378L419.409 294.795L415.042 290.428L419.409 286.071L423.766 290.378ZM413.623 288.969L409.266 284.602L413.623 280.235L417.99 284.602L413.623 288.969ZM413.623 291.797L417.99 296.154L413.623 300.521L409.266 296.154L413.623 291.797ZM417.99 307.716L413.623 312.073L409.266 307.716L413.623 303.349L417.99 307.716ZM419.399 309.125L423.766 313.492L419.409 317.859L415.042 313.492L419.399 309.125ZM420.818 307.716L425.185 303.349L429.542 307.716L425.185 312.073L420.818 307.716ZM430.961 309.125L435.318 313.492L430.961 317.859L426.594 313.492L430.961 309.125ZM436.737 314.901L441.104 319.268L438.275 322.096H435.198L432.37 319.268L436.737 314.901ZM432.37 307.716L436.737 303.349L441.104 307.716L436.697 312.073L432.37 307.716ZM432.37 296.154L436.697 291.797L441.064 296.154L436.697 300.521L432.37 296.154ZM435.318 278.806L430.961 283.173L426.594 278.806L430.501 274.899H431.42L435.318 278.806ZM425.185 277.397L422.686 274.899H427.683L425.185 277.397ZM423.766 278.806L419.409 283.173L415.042 278.806L418.949 274.899H419.868L423.766 278.806ZM416.121 274.899L413.623 277.397L411.134 274.899H416.121ZM408.306 274.899L412.214 278.806L407.847 283.173L404.049 279.366L403.969 278.367L407.387 274.939L408.306 274.899ZM404.569 274.899L403.759 275.708L403.69 274.899H404.569ZM404.299 282.444L406.438 284.582L404.619 286.401L404.299 282.444ZM405.488 297.084L405.318 295.015L406.438 296.134L405.438 297.084H405.488ZM405.698 299.712L407.847 297.553L412.214 301.92L407.847 306.277L406.088 304.518L405.698 299.712ZM406.348 307.786V307.596L406.448 307.696L406.358 307.786H406.348ZM407.107 317.09L406.558 310.394L407.847 309.105L412.214 313.472L407.847 317.829L407.107 317.09ZM407.507 322.086L407.427 321.087L407.847 320.667L409.266 322.086H407.507ZM409.266 319.258L413.623 314.891L417.99 319.258L415.162 322.086H412.084L409.266 319.258ZM417.99 322.086L419.409 320.667L420.828 322.086H417.99ZM423.646 322.086L420.818 319.258L425.185 314.891L429.542 319.258L426.704 322.096L423.646 322.086ZM429.542 322.086L430.961 320.667L432.38 322.086H429.542ZM442.852 322.086H441.094L442.513 320.667L442.932 321.087L442.852 322.086ZM443.262 317.09L442.513 317.829L438.146 313.472L442.513 309.105L443.802 310.394L443.262 317.09ZM444.031 307.586V307.786L443.942 307.696L444.051 307.586H444.031ZM444.281 304.518L442.513 306.277L438.146 301.92L442.513 297.553L444.671 299.712L444.281 304.518ZM444.881 297.094L443.882 296.094L445.011 294.975L444.881 297.094ZM446.07 282.434L445.75 286.431L443.922 284.602L446.07 282.454V282.434Z",
|
||||||
|
pfa23700: "M110.813 335.657H104.487V335.907H110.813V335.657Z",
|
||||||
|
pffde580: "M74.27 39.3876C74.3282 40.6776 74.6477 41.9423 75.2094 43.1051L70.3528 57.0953L58.8408 41.826C62.9479 41.2963 65.3462 36.2299 66.5754 32.4625C66.9947 31.1531 67.3285 29.8179 67.5747 28.4652V28.3353L67.8345 28.4852L77.1081 33.7116C76.1725 34.3194 75.4162 35.1659 74.9173 36.1638C74.4184 37.1616 74.1949 38.2746 74.27 39.3876Z",
|
||||||
|
pfff5dc0: "M54.702 257.361C54.702 257.361 59.4587 259.11 58.0397 263.247C56.6207 267.384 41.6111 274.189 41.6111 274.189C41.6111 274.189 35.3155 268.424 41.1014 259.04C46.8874 249.657 54.5721 254.403 54.702 257.361Z",
|
||||||
|
}
|
||||||
12
src/imports/svg-iw8mz5qegz.ts
Normal file
12
src/imports/svg-iw8mz5qegz.ts
Normal file
File diff suppressed because one or more lines are too long
5
src/imports/svg-wyhfl2ip3y.ts
Normal file
5
src/imports/svg-wyhfl2ip3y.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
p158d9f00: "M13.106 19.1062V6.15595H19.2861L19.3459 17.2568L13.106 19.1062Z",
|
||||||
|
p27401e00: "M1.9006 0.0397738L19.1986 -1.64677e-05L19.2482 6.15737H-3.40723e-05L1.9006 0.0397738Z",
|
||||||
|
p276f1b00: "M10.7004 62.4059V53.5937L4.95192 51.4107L4.56825e-05 49.6767V38.4622L5.37937 40.3663L10.9407 42.2763L10.7004 32.6632C10.7004 24.2339 10.7878 18.5562 10.9407 17.3114C11.8142 10.2579 15.3082 4.92955 20.8549 2.24353C23.3444 1.02063 25.5936 0.452853 29.044 0.147128C31.3369 -0.0712476 32.1449 -0.04941 34.0884 0.234478C36.6653 0.627554 39.2858 1.41371 40.9454 2.30904L42.0373 2.89866L42.1028 7.85578L42.1465 12.8129L40.5742 11.9612C37.9318 10.5636 35.5079 9.93035 32.4288 9.90851C30.2014 9.88667 29.5899 9.97402 28.3233 10.3889C25.4626 11.3935 23.257 13.5772 22.2525 16.4379C21.51 18.5562 21.379 21.2422 21.4445 33.908L21.51 45.9186L31.8392 49.5776L42.4583 53.2691L42.1465 56.1161L41.504 64.1588L31.8392 60.7066L21.4008 57.1625V62.4059V69.394H16.0506H10.7004V62.4059Z",
|
||||||
|
}
|
||||||
8
src/imports/svg-xewg6e9vz.ts
Normal file
8
src/imports/svg-xewg6e9vz.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
p1d319800: "M43.75 12.5H16.25C14.1789 12.5 12.5 14.1789 12.5 16.25C12.5 18.3211 14.1789 20 16.25 20H43.75C45.8211 20 47.5 18.3211 47.5 16.25C47.5 14.1789 45.8211 12.5 43.75 12.5Z",
|
||||||
|
p1f279900: "M14.7154 4.1074C15.0408 3.78197 15.5685 3.78196 15.8939 4.1074C16.2193 4.43283 16.2193 4.96047 15.8939 5.28591L11.1798 10L15.8939 14.714C16.2193 15.0394 16.2193 15.5671 15.8939 15.8925C15.5685 16.2179 15.0408 16.2179 14.7154 15.8925L10.0013 11.1785L5.28734 15.8925C5.28225 15.8976 5.27712 15.9026 5.27193 15.9075C4.94546 16.2178 4.42917 16.2128 4.10882 15.8925C3.86475 15.6484 3.80372 15.2906 3.92577 14.9897C3.96644 14.8893 4.02747 14.7953 4.10882 14.714L8.82283 10L4.10882 5.28595C3.78338 4.96052 3.78338 4.43287 4.10882 4.10744C4.43426 3.782 4.9619 3.782 5.28733 4.10744L10.0013 8.82142L14.7154 4.1074Z",
|
||||||
|
p2aacbc00: "M22.5 37.5C15.825 37.5 2.5 40.85 2.5 47.5V50C2.5 51.375 3.625 52.5 5 52.5H40C41.375 52.5 42.5 51.375 42.5 50V47.5C42.5 40.85 29.175 37.5 22.5 37.5ZM11.85 22.5H33.175C33.85 22.5 34.4 21.95 34.4 21.275V21.225C34.4 20.9001 34.2709 20.5885 34.0412 20.3588C33.8115 20.1291 33.4999 20 33.175 20H32.5C32.5 16.3 30.475 13.125 27.5 11.375V13.75C27.5 14.45 26.95 15 26.25 15C25.55 15 25 14.45 25 13.75V10.35C24.2 10.15 23.375 10 22.5 10C21.625 10 20.8 10.15 20 10.35V13.75C20 14.45 19.45 15 18.75 15C18.05 15 17.5 14.45 17.5 13.75V11.375C14.525 13.125 12.5 16.3 12.5 20H11.85C11.6891 20 11.5298 20.0317 11.3812 20.0932C11.2326 20.1548 11.0975 20.245 10.9838 20.3588C10.87 20.4725 10.7798 20.6076 10.7182 20.7562C10.6567 20.9048 10.625 21.0641 10.625 21.225V21.3C10.625 21.95 11.175 22.5 11.85 22.5ZM22.5 32.5C27.15 32.5 31.025 29.3 32.15 25H12.85C13.975 29.3 17.85 32.5 22.5 32.5ZM54.95 15.575L57.275 13.5L55.4 10.25L52.425 11.225C52.075 10.95 51.675 10.725 51.25 10.55L50.625 7.5H46.875L46.25 10.55C45.825 10.725 45.425 10.95 45.05 11.225L42.1 10.25L40.225 13.5L42.55 15.575C42.5 16 42.5 16.45 42.55 16.875L40.225 19L42.1 22.25L45.1 21.3C45.425 21.55 45.8 21.75 46.175 21.925L46.875 25H50.625L51.3 21.95C51.7 21.775 52.05 21.575 52.4 21.325L55.375 22.275L57.25 19.025L54.925 16.9C55 16.425 54.975 16 54.95 15.575ZM48.75 19.375C47.9212 19.375 47.1263 19.0458 46.5403 18.4597C45.9542 17.8737 45.625 17.0788 45.625 16.25C45.625 15.4212 45.9542 14.6263 46.5403 14.0403C47.1263 13.4542 47.9212 13.125 48.75 13.125C49.5788 13.125 50.3737 13.4542 50.9597 14.0403C51.5458 14.6263 51.875 15.4212 51.875 16.25C51.875 17.0788 51.5458 17.8737 50.9597 18.4597C50.3737 19.0458 49.5788 19.375 48.75 19.375ZM48.5 26.975L46.375 27.675C46.125 27.475 45.85 27.325 45.55 27.2L45.1 25H42.425L41.975 27.175C41.675 27.3 41.375 27.475 41.125 27.65L39.025 26.95L37.675 29.275L39.325 30.75C39.3 31.075 39.3 31.375 39.325 31.675L37.675 33.2L39.025 35.525L41.175 34.85C41.425 35.025 41.675 35.175 41.95 35.3L42.4 37.5H45.075L45.55 35.325C45.825 35.2 46.1 35.05 46.35 34.875L48.475 35.55L49.825 33.225L48.175 31.7C48.2 31.375 48.2 31.075 48.175 30.775L49.825 29.3L48.5 26.975ZM43.75 33.475C42.525 33.475 41.525 32.475 41.525 31.25C41.525 30.025 42.525 29.025 43.75 29.025C44.975 29.025 45.975 30.025 45.975 31.25C45.975 32.475 44.975 33.475 43.75 33.475Z",
|
||||||
|
p343fd100: "M30 10C32.6522 10 35.1957 11.0536 37.0711 12.9289C38.9464 14.8043 40 17.3478 40 20C40 22.6522 38.9464 25.1957 37.0711 27.0711C35.1957 28.9464 32.6522 30 30 30C27.3478 30 24.8043 28.9464 22.9289 27.0711C21.0536 25.1957 20 22.6522 20 20C20 17.3478 21.0536 14.8043 22.9289 12.9289C24.8043 11.0536 27.3478 10 30 10ZM30 35C41.05 35 50 39.475 50 45V50H10V45C10 39.475 18.95 35 30 35Z",
|
||||||
|
p3a49e400: "M30 10H47.5C48.875 10 50 11.125 50 12.5V47.5C50 48.875 48.875 50 47.5 50H12.5C11.125 50 10 48.875 10 47.5V12.5C10 11.125 11.125 10 12.5 10H30Z",
|
||||||
|
paa2b800: "M53.0769 9.23077H6.92308C3 9.23077 0 12.2308 0 16.1538V43.8462C0 47.7692 3 50.7692 6.92308 50.7692H53.0769C57 50.7692 60 47.7692 60 43.8462V16.1538C60 12.2308 57 9.23077 53.0769 9.23077ZM54.9231 44.7692L36.9231 31.8462L30 36.4615L22.8462 31.8462L5.07692 44.7692L19.6154 29.7692L1.84615 15.9231L30 31.1538L57.9231 16.1538L40.3846 30L54.9231 44.7692Z",
|
||||||
|
}
|
||||||
3624
src/index.css
Normal file
3624
src/index.css
Normal file
File diff suppressed because it is too large
Load Diff
7
src/main.tsx
Normal file
7
src/main.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App.tsx";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
190
src/styles/globals.css
Normal file
190
src/styles/globals.css
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-size: 16px;
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: #ffffff;
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: #030213;
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--secondary: oklch(0.95 0.0058 264.53);
|
||||||
|
--secondary-foreground: #030213;
|
||||||
|
--muted: #ececf0;
|
||||||
|
--muted-foreground: #717182;
|
||||||
|
--accent: #e9ebef;
|
||||||
|
--accent-foreground: #030213;
|
||||||
|
--destructive: #d4183d;
|
||||||
|
--destructive-foreground: #ffffff;
|
||||||
|
--border: rgba(0, 0, 0, 0.1);
|
||||||
|
--input: transparent;
|
||||||
|
--input-background: #f3f3f5;
|
||||||
|
--switch-background: #cbced4;
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: #030213;
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.145 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.145 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.985 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.396 0.141 25.723);
|
||||||
|
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||||
|
--border: oklch(0.269 0 0);
|
||||||
|
--input: oklch(0.269 0 0);
|
||||||
|
--ring: oklch(0.439 0 0);
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-normal: 400;
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(0.269 0 0);
|
||||||
|
--sidebar-ring: oklch(0.439 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-input-background: var(--input-background);
|
||||||
|
--color-switch-background: var(--switch-background);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base typography. This is not applied to elements which have an ancestor with a Tailwind text class.
|
||||||
|
*/
|
||||||
|
@layer base {
|
||||||
|
:where(:not(:has([class*=" text-"]), :not(:has([class^="text-"])))) {
|
||||||
|
h1 {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: var(--font-size);
|
||||||
|
}
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
8
tsconfig.node.json
Normal file
8
tsconfig.node.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node"
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
62
vite.config.ts
Normal file
62
vite.config.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
|
||||||
|
alias: {
|
||||||
|
'vaul@1.1.2': 'vaul',
|
||||||
|
'sonner@2.0.3': 'sonner',
|
||||||
|
'recharts@2.15.2': 'recharts',
|
||||||
|
'react-resizable-panels@2.1.7': 'react-resizable-panels',
|
||||||
|
'react-hook-form@7.55.0': 'react-hook-form',
|
||||||
|
'react-day-picker@8.10.1': 'react-day-picker',
|
||||||
|
'next-themes@0.4.6': 'next-themes',
|
||||||
|
'lucide-react@0.487.0': 'lucide-react',
|
||||||
|
'input-otp@1.4.2': 'input-otp',
|
||||||
|
'figma:asset/fe850ec33f7da20cadeecc91695cda7ad837415e.png': path.resolve(__dirname, './src/assets/fe850ec33f7da20cadeecc91695cda7ad837415e.png'),
|
||||||
|
'figma:asset/fc17f89f308e91a12011ae1dc4ea7b32cda951d8.png': path.resolve(__dirname, './src/assets/fc17f89f308e91a12011ae1dc4ea7b32cda951d8.png'),
|
||||||
|
'embla-carousel-react@8.6.0': 'embla-carousel-react',
|
||||||
|
'cmdk@1.1.1': 'cmdk',
|
||||||
|
'class-variance-authority@0.7.1': 'class-variance-authority',
|
||||||
|
'@radix-ui/react-tooltip@1.1.8': '@radix-ui/react-tooltip',
|
||||||
|
'@radix-ui/react-toggle@1.1.2': '@radix-ui/react-toggle',
|
||||||
|
'@radix-ui/react-toggle-group@1.1.2': '@radix-ui/react-toggle-group',
|
||||||
|
'@radix-ui/react-tabs@1.1.3': '@radix-ui/react-tabs',
|
||||||
|
'@radix-ui/react-switch@1.1.3': '@radix-ui/react-switch',
|
||||||
|
'@radix-ui/react-slot@1.1.2': '@radix-ui/react-slot',
|
||||||
|
'@radix-ui/react-slider@1.2.3': '@radix-ui/react-slider',
|
||||||
|
'@radix-ui/react-separator@1.1.2': '@radix-ui/react-separator',
|
||||||
|
'@radix-ui/react-select@2.1.6': '@radix-ui/react-select',
|
||||||
|
'@radix-ui/react-scroll-area@1.2.3': '@radix-ui/react-scroll-area',
|
||||||
|
'@radix-ui/react-radio-group@1.2.3': '@radix-ui/react-radio-group',
|
||||||
|
'@radix-ui/react-progress@1.1.2': '@radix-ui/react-progress',
|
||||||
|
'@radix-ui/react-popover@1.1.6': '@radix-ui/react-popover',
|
||||||
|
'@radix-ui/react-navigation-menu@1.2.5': '@radix-ui/react-navigation-menu',
|
||||||
|
'@radix-ui/react-menubar@1.1.6': '@radix-ui/react-menubar',
|
||||||
|
'@radix-ui/react-label@2.1.2': '@radix-ui/react-label',
|
||||||
|
'@radix-ui/react-hover-card@1.1.6': '@radix-ui/react-hover-card',
|
||||||
|
'@radix-ui/react-dropdown-menu@2.1.6': '@radix-ui/react-dropdown-menu',
|
||||||
|
'@radix-ui/react-dialog@1.1.6': '@radix-ui/react-dialog',
|
||||||
|
'@radix-ui/react-context-menu@2.2.6': '@radix-ui/react-context-menu',
|
||||||
|
'@radix-ui/react-collapsible@1.1.3': '@radix-ui/react-collapsible',
|
||||||
|
'@radix-ui/react-checkbox@1.1.4': '@radix-ui/react-checkbox',
|
||||||
|
'@radix-ui/react-avatar@1.1.3': '@radix-ui/react-avatar',
|
||||||
|
'@radix-ui/react-aspect-ratio@1.1.2': '@radix-ui/react-aspect-ratio',
|
||||||
|
'@radix-ui/react-alert-dialog@1.1.6': '@radix-ui/react-alert-dialog',
|
||||||
|
'@radix-ui/react-accordion@1.2.3': '@radix-ui/react-accordion',
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
outDir: 'build',
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user