Initial commit: TKB Timeshift Schedule Management Application

- Next.js 15.2.4 with React 19 and TypeScript
- Monthly schedule generator with Excel-like formatting
- Complete employee list from example.xlsx (18 employees)
- Excel-style column naming (A, B, C, ..., Z, AA, AB, ...)
- Counter-clockwise text rotation for data values
- Weekend highlighting and shift color coding
- Team selection for TKB, METRO, D8
- 7 days from previous month + full current month date range
- jspreadsheet integration for Excel-like interface

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Docker Config Backup
2025-07-28 20:09:31 +02:00
commit 3216f74229
118 changed files with 16774 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
"use client"
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Search, Download, Eye, Calendar, Clock } from "lucide-react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
interface ScheduleHistoryProps {
teamId: string
teamName: string
}
// Sample historical data
const getHistoricalSchedules = (teamId: string) => [
{
id: "1",
name: "Week 45 - November 2024",
dateRange: "Nov 4-10, 2024",
createdBy: "John Manager",
createdAt: "2024-11-01",
status: "completed",
employees: 8,
},
{
id: "2",
name: "Week 44 - October 2024",
dateRange: "Oct 28 - Nov 3, 2024",
createdBy: "Sarah Admin",
createdAt: "2024-10-25",
status: "completed",
employees: 7,
},
{
id: "3",
name: "Week 43 - October 2024",
dateRange: "Oct 21-27, 2024",
createdBy: "John Manager",
createdAt: "2024-10-18",
status: "completed",
employees: 8,
},
{
id: "4",
name: "Holiday Schedule - Christmas",
dateRange: "Dec 23-29, 2023",
createdBy: "Admin",
createdAt: "2023-12-15",
status: "archived",
employees: 5,
},
]
export function ScheduleHistory({ teamId, teamName }: ScheduleHistoryProps) {
const [searchTerm, setSearchTerm] = React.useState("")
const historicalSchedules = getHistoricalSchedules(teamId)
const filteredSchedules = historicalSchedules.filter(
(schedule) =>
schedule.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
schedule.dateRange.toLowerCase().includes(searchTerm.toLowerCase()) ||
schedule.createdBy.toLowerCase().includes(searchTerm.toLowerCase()),
)
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "bg-green-100 text-green-800"
case "archived":
return "bg-gray-100 text-gray-800"
default:
return "bg-blue-100 text-blue-800"
}
}
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="size-5" />
{teamName} - Schedule History
</CardTitle>
<CardDescription>Browse and restore previous schedules for your team</CardDescription>
<div className="flex items-center gap-4 mt-4">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 size-4 text-gray-400" />
<Input
placeholder="Search schedules..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Schedule Name</TableHead>
<TableHead>Date Range</TableHead>
<TableHead>Created By</TableHead>
<TableHead>Created Date</TableHead>
<TableHead>Employees</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredSchedules.map((schedule) => (
<TableRow key={schedule.id}>
<TableCell className="font-medium">{schedule.name}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Calendar className="size-4 text-gray-400" />
{schedule.dateRange}
</div>
</TableCell>
<TableCell>{schedule.createdBy}</TableCell>
<TableCell>{new Date(schedule.createdAt).toLocaleDateString()}</TableCell>
<TableCell>{schedule.employees}</TableCell>
<TableCell>
<Badge className={getStatusColor(schedule.status)}>{schedule.status}</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Eye className="size-4 mr-1" />
View
</Button>
<Button variant="outline" size="sm">
<Download className="size-4 mr-1" />
Export
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{filteredSchedules.length === 0 && (
<div className="text-center py-8">
<Clock className="size-12 mx-auto mb-4 text-gray-400" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No schedules found</h3>
<p className="text-gray-500">
{searchTerm ? "Try adjusting your search terms" : "No historical schedules available yet"}
</p>
</div>
)}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,58 @@
"use client"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { TimeshiftSpreadsheet } from "./timeshift-spreadsheet"
import { ScheduleHistory } from "./schedule-history"
import { ScheduleTemplates } from "./schedule-templates"
import { Calendar, History, FileIcon as FileTemplate, Table } from "lucide-react"
interface ScheduleTabsProps {
teamId: string
teamName: string
}
export function ScheduleTabs({ teamId, teamName }: ScheduleTabsProps) {
return (
<Tabs defaultValue="current" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="current" className="flex items-center gap-2">
<Table className="size-4" />
Current Schedule
</TabsTrigger>
<TabsTrigger value="history" className="flex items-center gap-2">
<History className="size-4" />
History
</TabsTrigger>
<TabsTrigger value="templates" className="flex items-center gap-2">
<FileTemplate className="size-4" />
Templates
</TabsTrigger>
<TabsTrigger value="calendar" className="flex items-center gap-2">
<Calendar className="size-4" />
Calendar View
</TabsTrigger>
</TabsList>
<TabsContent value="current" className="mt-4">
<TimeshiftSpreadsheet teamId={teamId} teamName={teamName} />
</TabsContent>
<TabsContent value="history" className="mt-4">
<ScheduleHistory teamId={teamId} teamName={teamName} />
</TabsContent>
<TabsContent value="templates" className="mt-4">
<ScheduleTemplates teamId={teamId} teamName={teamName} />
</TabsContent>
<TabsContent value="calendar" className="mt-4">
<div className="flex items-center justify-center h-96 border-2 border-dashed border-gray-300 rounded-lg">
<div className="text-center">
<Calendar className="size-12 mx-auto mb-4 text-gray-400" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Calendar View</h3>
<p className="text-gray-500">Calendar view coming soon...</p>
</div>
</div>
</TabsContent>
</Tabs>
)
}

View File

@@ -0,0 +1,130 @@
"use client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { FileIcon as FileTemplate, Plus, Copy, Edit, Trash2 } from "lucide-react"
interface ScheduleTemplatesProps {
teamId: string
teamName: string
}
// Sample template data
const getTemplates = (teamId: string) => [
{
id: "1",
name: "Standard Week",
description: "Regular 5-day work week with weekend coverage",
shifts: ["08:00-16:00", "16:00-00:00", "00:00-08:00"],
employees: 8,
coverage: "24/7",
},
{
id: "2",
name: "Holiday Schedule",
description: "Reduced staffing for holidays and special events",
shifts: ["08:00-20:00", "20:00-08:00"],
employees: 4,
coverage: "24/7",
},
{
id: "3",
name: "Maintenance Week",
description: "Extended maintenance periods with adjusted shifts",
shifts: ["06:00-14:00", "14:00-22:00", "22:00-06:00"],
employees: 6,
coverage: "24/7",
},
{
id: "4",
name: "Emergency Response",
description: "Emergency staffing template for critical situations",
shifts: ["12-hour rotating"],
employees: 12,
coverage: "24/7",
},
]
export function ScheduleTemplates({ teamId, teamName }: ScheduleTemplatesProps) {
const templates = getTemplates(teamId)
return (
<Card className="w-full">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<FileTemplate className="size-5" />
{teamName} - Schedule Templates
</CardTitle>
<CardDescription>Pre-configured schedule templates for quick deployment</CardDescription>
</div>
<Button>
<Plus className="size-4 mr-2" />
Create Template
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2">
{templates.map((template) => (
<Card key={template.id} className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{template.name}</CardTitle>
<CardDescription className="mt-1">{template.description}</CardDescription>
</div>
<Badge variant="outline">{template.coverage}</Badge>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-3">
<div>
<p className="text-sm font-medium text-gray-700 mb-2">Shift Patterns:</p>
<div className="flex flex-wrap gap-1">
{template.shifts.map((shift, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{shift}
</Badge>
))}
</div>
</div>
<div className="flex items-center justify-between text-sm text-gray-600">
<span>{template.employees} employees</span>
</div>
<div className="flex items-center gap-2 pt-2">
<Button size="sm" className="flex-1">
<Copy className="size-4 mr-1" />
Use Template
</Button>
<Button variant="outline" size="sm">
<Edit className="size-4" />
</Button>
<Button variant="outline" size="sm">
<Trash2 className="size-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
{templates.length === 0 && (
<div className="text-center py-8">
<FileTemplate className="size-12 mx-auto mb-4 text-gray-400" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No templates found</h3>
<p className="text-gray-500 mb-4">Create your first schedule template to get started</p>
<Button>
<Plus className="size-4 mr-2" />
Create Template
</Button>
</div>
)}
</CardContent>
</Card>
)
}

108
components/team-sidebar.tsx Normal file
View File

@@ -0,0 +1,108 @@
"use client"
import type * as React from "react"
import { TrainFrontTunnel, Calendar, Users } from "lucide-react"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
} from "@/components/ui/sidebar"
// Sample team data
const teams = [
{
id: "tkb",
name: "TKB",
color: "bg-blue-500",
},
{
id: "metro",
name: "METRO",
color: "bg-green-500",
},
{
id: "d8",
name: "D8",
color: "bg-orange-500",
},
]
interface TeamSidebarProps extends React.ComponentProps<typeof Sidebar> {
selectedTeam: string
onTeamSelect: (teamId: string) => void
}
export function TeamSidebar({ selectedTeam, onTeamSelect, ...props }: TeamSidebarProps) {
return (
<Sidebar {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<div className="flex items-center gap-2">
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<TrainFrontTunnel className="size-4" />
</div>
<div className="flex flex-col gap-0.5 leading-none">
<span className="font-semibold">TimeShift 161</span>
<span className="text-xs">Team Scheduling</span>
</div>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Teams</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{teams.map((team) => (
<SidebarMenuItem key={team.id}>
<SidebarMenuButton
onClick={() => onTeamSelect(team.id)}
isActive={selectedTeam === team.id}
className="w-full"
>
<div className="flex items-center gap-3 w-full">
<div className={`w-3 h-3 rounded-full ${team.color}`} />
<span className="font-medium">{team.name}</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Quick Actions</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>
<Calendar className="size-4" />
<span>View All Schedules</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<Users className="size-4" />
<span>Manage Teams</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
)
}

View File

@@ -0,0 +1,472 @@
"use client"
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Save, Download, Plus } from "lucide-react"
interface TimeshiftSpreadsheetProps {
teamId: string
teamName: string
}
export function TimeshiftSpreadsheet({ teamId: _teamId, teamName }: TimeshiftSpreadsheetProps) {
const spreadsheetRef = React.useRef<HTMLDivElement>(null)
const jspreadsheetInstance = React.useRef<unknown>(null)
const currentDate = new Date()
const [selectedMonth, setSelectedMonth] = React.useState<string>((currentDate.getMonth() + 1).toString())
const [selectedYear, setSelectedYear] = React.useState<string>(currentDate.getFullYear().toString())
const [dialogOpen, setDialogOpen] = React.useState(false)
const [forceUpdate, setForceUpdate] = React.useState(0)
// Function to generate Excel-style column names (A, B, C, ..., Z, AA, AB, ...)
const getExcelColumnName = (index: number): string => {
let result = ''
while (index >= 0) {
result = String.fromCharCode(65 + (index % 26)) + result
index = Math.floor(index / 26) - 1
}
return result
}
// Generate monthly schedule data
const generateMonthlyData = (month: number, year: number) => {
console.log("Generating data for month:", month, "year:", year)
// Get first and last day of selected month
const firstDay = new Date(year, month - 1, 1)
const lastDay = new Date(year, month, 0)
console.log("Date range:", firstDay, "to", lastDay)
// Get exactly 7 days from previous month
const prevMonthStart = new Date(firstDay)
prevMonthStart.setDate(firstDay.getDate() - 7)
// End at the last day of the selected month (no next month days)
const nextMonthEnd = new Date(lastDay)
console.log("Full date range:", prevMonthStart.toDateString(), "to", nextMonthEnd.toDateString())
console.log("Previous month start:", prevMonthStart.toDateString(), "Current month end:", nextMonthEnd.toDateString())
// Generate header rows
const titleRow = [`${String(month).padStart(2, '0')}.${year} v.1`, "Uzávěry ostatní"]
const dayNameRow = [""]
const yearRow = ["rok"]
const monthRow = ["měsíc"]
const dateRow = ["den"]
const shiftRow = [`Pohotovost ${_teamId.toUpperCase()}`]
// Generate columns for each day from prevMonthStart to nextMonthEnd
const currentDate = new Date(prevMonthStart)
let dayCount = 0
const maxDays = 50 // Safety limit to prevent infinite loops
console.log("Starting date loop from:", currentDate.toDateString(), "to:", nextMonthEnd.toDateString())
while (currentDate <= nextMonthEnd && dayCount < maxDays) {
const dayName = ["Neděle", "Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota"][currentDate.getDay()]
titleRow.push("", "")
dayNameRow.push(dayName, "")
yearRow.push(currentDate.getFullYear().toString(), "")
monthRow.push((currentDate.getMonth() + 1).toString(), "")
dateRow.push(currentDate.getDate().toString(), "")
shiftRow.push("den", "noc")
currentDate.setDate(currentDate.getDate() + 1)
dayCount++
}
console.log("Generated", dayCount, "days of data")
// Complete employee data from Excel file
const employees = [
"Pauzer Libor (all in one)",
"Vörös Pavel (NN)",
"Janouš Petr (VN)",
"Dvořák Václav (VN)",
"Vondrák Pavel (NN)",
"Čeleda Olda (NN)",
"Hanzlík Marek (VN)",
"Kohl David (VN)",
"Dittrich Vladimír (VN)",
"Toman Milan (VN)",
"Glaser Ondřej (NN)",
"Herbst David (NN)",
"Ryba Ondřej (NN)",
"Zábranský Petr (NN)",
"Žemlička Miroslav (NN)",
"Teslík Hynek (NN)",
"X BEZ zkušeností s VN",
"Pohotovost IT"
]
const employeeRows = employees.map(name => {
const row = [name]
// Add empty cells for each day/night pair
for (let i = 1; i < shiftRow.length; i++) {
row.push("")
}
return row
})
const result = [titleRow, dayNameRow, yearRow, monthRow, dateRow, shiftRow, [""], ...employeeRows]
console.log("Generated", result.length, "rows with", result[0]?.length, "columns")
console.log("Title row:", titleRow.slice(0, 10))
console.log("Day row:", dayNameRow.slice(0, 10))
return result
}
// Sample data for different teams (fallback)
const getTeamData = (_teamId: string) => {
const baseData = [
["Employee", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
["John Smith", "08:00-16:00", "08:00-16:00", "08:00-16:00", "08:00-16:00", "08:00-16:00", "OFF", "OFF"],
["Jane Doe", "16:00-00:00", "16:00-00:00", "16:00-00:00", "16:00-00:00", "16:00-00:00", "OFF", "OFF"],
["Mike Johnson", "00:00-08:00", "00:00-08:00", "00:00-08:00", "00:00-08:00", "00:00-08:00", "OFF", "OFF"],
["Sarah Wilson", "08:00-16:00", "OFF", "08:00-16:00", "OFF", "08:00-16:00", "08:00-16:00", "08:00-16:00"],
["", "", "", "", "", "", "", ""],
["", "", "", "", "", "", "", ""],
]
return baseData
}
React.useEffect(() => {
console.log("useEffect triggered - Month:", selectedMonth, "Year:", selectedYear, "Force:", forceUpdate)
const loadJSpreadsheet = async () => {
console.log("Starting loadJSpreadsheet function")
// Check if scripts are already loaded
const windowWithJExcel = window as unknown as { jexcel?: unknown, jspreadsheet?: unknown }
if (!windowWithJExcel.jexcel && !windowWithJExcel.jspreadsheet) {
console.log("Loading jspreadsheet scripts...")
// Load CSS if not already loaded
if (!document.querySelector('link[href*="jexcel.css"]')) {
const cssLink = document.createElement("link")
cssLink.rel = "stylesheet"
cssLink.href = "https://bossanova.uk/jspreadsheet/v4/jexcel.css"
document.head.appendChild(cssLink)
}
if (!document.querySelector('link[href*="jsuites.css"]')) {
const jSuiteCssLink = document.createElement("link")
jSuiteCssLink.rel = "stylesheet"
jSuiteCssLink.href = "https://jsuites.net/v4/jsuites.css"
document.head.appendChild(jSuiteCssLink)
}
// Load JavaScript files if not already loaded
if (!document.querySelector('script[src*="jsuites.js"]')) {
const jSuitesScript = document.createElement("script")
jSuitesScript.src = "https://jsuites.net/v4/jsuites.js"
document.head.appendChild(jSuitesScript)
}
if (!document.querySelector('script[src*="jexcel.js"]')) {
const jSpreadsheetScript = document.createElement("script")
jSpreadsheetScript.src = "https://bossanova.uk/jspreadsheet/v4/jexcel.js"
document.head.appendChild(jSpreadsheetScript)
// Wait for scripts to load
console.log("Waiting for scripts to load...")
await new Promise((resolve) => {
jSpreadsheetScript.onload = () => {
console.log("Scripts loaded successfully")
setTimeout(resolve, 200) // Longer delay
}
jSpreadsheetScript.onerror = () => {
console.error("Failed to load jspreadsheet script")
resolve(null)
}
})
}
} else {
console.log("Scripts already loaded")
}
// Initialize spreadsheet
const windowObj = window as unknown as { jexcel?: unknown, jspreadsheet?: unknown }
const jexcelLib = windowObj.jexcel || windowObj.jspreadsheet
console.log("Checking for jexcel availability:", !!windowObj.jexcel)
console.log("Checking for jspreadsheet availability:", !!windowObj.jspreadsheet)
console.log("Using library:", jexcelLib ? (windowObj.jexcel ? 'jexcel' : 'jspreadsheet') : 'none')
console.log("Checking spreadsheet ref:", !!spreadsheetRef.current)
if (spreadsheetRef.current && jexcelLib) {
console.log("Initializing spreadsheet...")
// Clear previous instance
if (jspreadsheetInstance.current) {
console.log("Destroying previous instance")
jspreadsheetInstance.current.destroy()
}
// Use monthly data if month/year selected, otherwise use basic data
let data
try {
if (selectedMonth && selectedYear) {
console.log("About to generate monthly data for:", parseInt(selectedMonth), parseInt(selectedYear))
data = generateMonthlyData(parseInt(selectedMonth), parseInt(selectedYear))
console.log("Monthly data generated successfully")
} else {
console.log("Using basic team data")
data = getTeamData(_teamId)
}
} catch (error) {
console.error("Error generating data:", error)
data = getTeamData(_teamId)
}
console.log("Data generated:", data.length, "rows, first row:", data[0]?.length, "columns")
console.log("First few rows:", data.slice(0, 3))
// Generate column configuration based on data
const columns = data[0]?.map((_, index) => ({
type: "text",
title: getExcelColumnName(index), // A, B, C, ..., Z, AA, AB, ...
width: index === 0 ? 200 : 25 // First column 200px, all others 25px
})) || []
// Generate styles for day/night shifts based on the Excel pattern
const styles: Record<string, string> = {}
if (selectedMonth && selectedYear) {
// Style header rows
for (let col = 1; col < (data[0]?.length || 0); col++) {
const colLetter = getExcelColumnName(col)
if (col % 2 === 1) { // Day shifts (odd columns after first)
styles[`${colLetter}6`] = "background-color: #ffff00; font-weight: bold;" // Yellow for day
} else { // Night shifts (even columns after first)
styles[`${colLetter}6`] = "background-color: #00b0f0; font-weight: bold;" // Blue for night
}
}
// Style weekend days (Saturday/Sunday in day name row)
for (let col = 1; col < (data[0]?.length || 0); col += 2) {
const dayName = data[1]?.[col]
if (dayName === "Sobota" || dayName === "Neděle") {
const colLetter = getExcelColumnName(col)
const nextColLetter = getExcelColumnName(col + 1)
styles[`${colLetter}2`] = "background-color: #ffd966;" // Weekend day name
styles[`${nextColLetter}2`] = "background-color: #ffd966;"
}
}
}
console.log("Initializing jspreadsheet with config:", {
dataRows: data.length,
dataCols: data[0]?.length,
columns: columns.length,
stylesCount: Object.keys(styles).length
})
jspreadsheetInstance.current = (jexcelLib as (el: HTMLElement, config: unknown) => unknown)(spreadsheetRef.current, {
data: data,
columns: columns,
minDimensions: [data[0]?.length || 8, Math.max(data.length + 3, 10)],
allowInsertRow: true,
allowInsertColumn: false,
allowDeleteRow: true,
allowDeleteColumn: false,
contextMenu: true,
tableOverflow: true,
tableWidth: "100%",
tableHeight: "500px",
style: styles,
})
console.log("jspreadsheet initialized:", !!jspreadsheetInstance.current)
// Apply counter-clockwise rotation to data values only (exclude second column which contains field labels)
setTimeout(() => {
const table = spreadsheetRef.current?.querySelector('.jexcel tbody')
if (table) {
const rows = table.querySelectorAll('tr')
// Rotate data values in rows 2, 3, 4, 5, 6 (day names, year, month, day, shifts) excluding first two columns
;[1, 2, 3, 4, 5].forEach(rowIndex => {
if (rows[rowIndex]) {
const cells = rows[rowIndex].querySelectorAll('td')
cells.forEach((cell, cellIndex) => {
// Skip first two columns (row numbers and field labels)
if (cellIndex > 1) {
const originalText = cell.textContent
cell.innerHTML = `<div style="transform: rotate(-90deg); font-size: 9px; height: 20px; display: flex; align-items: center; justify-content: center; white-space: nowrap;">${originalText}</div>`
}
})
}
})
// Make only "Pohotovost TKB" bold, keep other field labels regular and ensure they're not rotated
;[0, 2, 3, 4, 5].forEach(rowIndex => {
if (rows[rowIndex]) {
const cells = rows[rowIndex].querySelectorAll('td')
if (cells[1]) { // Second column (index 1)
cells[1].style.transform = 'none'
// Only make "Pohotovost TKB" bold (row index 5)
if (rowIndex === 5) {
cells[1].style.fontWeight = 'bold'
} else {
cells[1].style.fontWeight = 'normal'
}
}
}
})
}
}, 200)
} else {
console.log("Cannot initialize - missing ref or jexcel library")
}
}
console.log("Calling loadJSpreadsheet...")
loadJSpreadsheet().catch(error => {
console.error("Error in loadJSpreadsheet:", error)
})
return () => {
if (jspreadsheetInstance.current) {
jspreadsheetInstance.current.destroy()
}
}
}, [_teamId, selectedMonth, selectedYear, forceUpdate])
const handleSave = () => {
if (jspreadsheetInstance.current) {
const data = jspreadsheetInstance.current.getData()
console.log("Saving data:", data)
// Here you would typically save to your backend
alert("Schedule saved successfully!")
}
}
const handleExport = () => {
if (jspreadsheetInstance.current) {
jspreadsheetInstance.current.download()
}
}
const handleCreateSchedule = () => {
setDialogOpen(true)
}
const handleGenerateSchedule = () => {
console.log("Generate clicked - Month:", selectedMonth, "Year:", selectedYear)
if (selectedMonth && selectedYear) {
setDialogOpen(false)
console.log("Generating schedule for:", selectedMonth, selectedYear)
// Force useEffect to trigger by updating the forceUpdate counter
setForceUpdate(prev => prev + 1)
} else {
console.log("Month or year not selected")
alert("Please select both month and year")
}
}
const handleAddEmployee = () => {
if (jspreadsheetInstance.current) {
jspreadsheetInstance.current.insertRow()
}
}
return (
<Card className="w-full">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">{teamName} - Weekly Schedule</CardTitle>
<CardDescription>Manage work shifts and schedules for your team members</CardDescription>
</div>
<div className="flex gap-2">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button onClick={handleCreateSchedule} variant="outline" size="sm">
<Plus className="size-4 mr-2" />
Create
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create Monthly Schedule</DialogTitle>
<DialogDescription>
Select the month and year for which you want to generate the timetable.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="month" className="text-right">
Month
</label>
<Select value={selectedMonth} onValueChange={setSelectedMonth}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select month" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">January</SelectItem>
<SelectItem value="2">February</SelectItem>
<SelectItem value="3">March</SelectItem>
<SelectItem value="4">April</SelectItem>
<SelectItem value="5">May</SelectItem>
<SelectItem value="6">June</SelectItem>
<SelectItem value="7">July</SelectItem>
<SelectItem value="8">August</SelectItem>
<SelectItem value="9">September</SelectItem>
<SelectItem value="10">October</SelectItem>
<SelectItem value="11">November</SelectItem>
<SelectItem value="12">December</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label htmlFor="year" className="text-right">
Year
</label>
<Select value={selectedYear} onValueChange={setSelectedYear}>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Select year" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={handleGenerateSchedule}>Generate Schedule</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Button onClick={handleSave} variant="outline" size="sm">
<Save className="size-4 mr-2" />
Save
</Button>
<Button onClick={handleExport} variant="outline" size="sm">
<Download className="size-4 mr-2" />
Export
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="border rounded-lg overflow-hidden">
<div ref={spreadsheetRef} className="w-full" />
</div>
<div className="mt-4 text-sm text-muted-foreground">
<p>
<strong>Instructions:</strong>
</p>
<ul className="list-disc list-inside mt-2 space-y-1">
<li>Click on any cell to edit shift times (e.g., &quot;08:00-16:00&quot;)</li>
<li>Use &quot;OFF&quot; for days off</li>
<li>Right-click for context menu options</li>
<li>Use the &quot;Add Employee&quot; button to add new team members</li>
</ul>
</div>
</CardContent>
</Card>
)
}

46
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const 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 }

View File

@@ -0,0 +1,109 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/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,
}

59
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium 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 shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs 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 shadow-xs 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",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

92
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/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 py-6 shadow-sm",
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 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", 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", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

122
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

21
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/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 bg-transparent px-3 py-1 text-base shadow-xs 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 }

26
components/ui/label.tsx Normal file
View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

160
components/ui/select.tsx Normal file
View File

@@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
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
components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,139 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/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
components/ui/sidebar.tsx Normal file
View File

@@ -0,0 +1,726 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/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,
}

View File

@@ -0,0 +1,13 @@
import { cn } from "@/lib/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 }

116
components/ui/table.tsx Normal file
View File

@@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/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
components/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/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-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background 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-md 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 data-[state=active]:shadow-sm [&_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 }

61
components/ui/tooltip.tsx Normal file
View File

@@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/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 }