This commit is contained in:
@@ -5,10 +5,12 @@ import { format, startOfWeek, addDays, parseISO } from 'date-fns';
|
||||
import { cs } from 'date-fns/locale';
|
||||
import { LocalData, SubstitutionData, ChangeEntry, Hour } from '@/lib/types';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { capitalizeFirstLetter } from '@/lib/utils';
|
||||
|
||||
interface ScheduleViewerProps {
|
||||
localData: LocalData;
|
||||
substitutionData: SubstitutionData | null;
|
||||
hideSubstitutions?: boolean;
|
||||
}
|
||||
|
||||
function getChangesForClass(changes: Record<string, (ChangeEntry | null)[]> | undefined, className: string): (ChangeEntry | null)[] {
|
||||
@@ -22,7 +24,7 @@ function getChangesForClass(changes: Record<string, (ChangeEntry | null)[]> | un
|
||||
return [];
|
||||
}
|
||||
|
||||
export default function ScheduleViewer({ localData, substitutionData }: ScheduleViewerProps) {
|
||||
export default function ScheduleViewer({ localData, substitutionData, hideSubstitutions = false }: ScheduleViewerProps) {
|
||||
const referenceDate = useMemo(() => {
|
||||
if (substitutionData?.schedule) {
|
||||
const dates = Object.keys(substitutionData.schedule).sort();
|
||||
@@ -51,7 +53,7 @@ export default function ScheduleViewer({ localData, substitutionData }: Schedule
|
||||
|
||||
const currentWeekMaxHours = useMemo(() => {
|
||||
let max = maxHours;
|
||||
if (substitutionData?.schedule) {
|
||||
if (!hideSubstitutions && substitutionData?.schedule) {
|
||||
weekDays.forEach(date => {
|
||||
const dateStr = format(date, 'yyyy-MM-dd');
|
||||
const dayData = substitutionData.schedule[dateStr];
|
||||
@@ -62,7 +64,7 @@ export default function ScheduleViewer({ localData, substitutionData }: Schedule
|
||||
});
|
||||
}
|
||||
return max;
|
||||
}, [maxHours, substitutionData, weekDays, localData.class]);
|
||||
}, [maxHours, substitutionData, weekDays, localData.class, hideSubstitutions]);
|
||||
|
||||
const hours = Array.from({ length: currentWeekMaxHours }, (_, i) => i + 1);
|
||||
|
||||
@@ -115,7 +117,7 @@ export default function ScheduleViewer({ localData, substitutionData }: Schedule
|
||||
<td key={hourIndex} className="border-r min-w-[120px] h-full align-top relative p-0">
|
||||
<CellContent
|
||||
staticLessons={staticLessons}
|
||||
change={change}
|
||||
change={hideSubstitutions ? null : change}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
@@ -157,7 +159,7 @@ function CellContent({ staticLessons, change }: { staticLessons: Hour[], change:
|
||||
<div key={idx} className="flex-1 flex flex-col justify-between p-1 text-[10px] border-b min-h-[40px]">
|
||||
<div className='flex justify-between'>
|
||||
<div className="font-bold truncate">{lesson.subject}</div>
|
||||
<span className="truncate opacity-70">{lesson.teacher.code}</span>
|
||||
<span className="truncate opacity-70">{capitalizeFirstLetter(lesson.teacher.code)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="truncate max-w-[40px]">{lesson.room}</span>
|
||||
@@ -175,7 +177,7 @@ function CellContent({ staticLessons, change }: { staticLessons: Hour[], change:
|
||||
<div className="font-bold text-lg text-primary">{lesson.subject}</div>
|
||||
<div className="flex justify-between items-end mt-1">
|
||||
<div className="font-mono font-medium">{lesson.room}</div>
|
||||
<div className="text-[10px] opacity-80" title={lesson.teacher.name}>{lesson.teacher.code}</div>
|
||||
<div className="text-[10px] opacity-80" title={lesson.teacher.name}>{capitalizeFirstLetter(lesson.teacher.code)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
52
viewer/components/own/update-status.tsx
Normal file
52
viewer/components/own/update-status.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { SubstitutionData } from "@/lib/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface UpdateStatusProps {
|
||||
data: SubstitutionData | null;
|
||||
}
|
||||
|
||||
export default function UpdateStatus({ data }: UpdateStatusProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const handleRefresh = () => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 text-sm text-muted-foreground p-4 rounded-lg border">
|
||||
<div>
|
||||
<span className="font-medium">Poslední aktualizace:</span> {data.status.lastUpdated}
|
||||
<span className="mx-2 hidden sm:inline">•</span>
|
||||
<br className="sm:hidden" />
|
||||
<span>
|
||||
Aktualizace každých{" "}
|
||||
{data.status.currentUpdateSchedule < 60
|
||||
? `${data.status.currentUpdateSchedule} min`
|
||||
: `${data.status.currentUpdateSchedule / 60} hod`}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isPending}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 mr-2 ${isPending ? "animate-spin" : ""}`}
|
||||
/>
|
||||
Aktualizovat
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
viewer/components/site-header.tsx
Normal file
103
viewer/components/site-header.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Menu, X, AlertTriangle, Home, List } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function SiteHeader() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const routes = [
|
||||
{
|
||||
href: "/",
|
||||
label: "Třída",
|
||||
active: pathname === "/",
|
||||
icon: Home
|
||||
},
|
||||
{
|
||||
href: "/all",
|
||||
label: "Vše",
|
||||
active: pathname === "/all",
|
||||
icon: List
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-14 items-center px-4 md:px-8">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||
<span className="sr-only">Toggle Menu</span>
|
||||
</Button>
|
||||
|
||||
<div className="mr-4 hidden md:flex items-center">
|
||||
<Link href="/" className="mr-6 flex items-center space-x-2">
|
||||
<span className="hidden font-bold sm:inline-block">
|
||||
Mimořádný rozvrh
|
||||
</span>
|
||||
</Link>
|
||||
<nav className="flex items-center space-x-6 text-sm font-medium">
|
||||
{routes.map((route) => (
|
||||
<Link
|
||||
key={route.href}
|
||||
href={route.href}
|
||||
className={cn(
|
||||
"transition-colors hover:text-foreground/80 flex items-center gap-2",
|
||||
route.active ? "text-foreground" : "text-foreground/60"
|
||||
)}
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end hidden">
|
||||
<div className="w-full flex-1 md:w-auto md:flex-none">
|
||||
<span className="font-bold md:hidden">Mimořádný rozvrh</span>
|
||||
</div>
|
||||
<nav className="flex items-center">
|
||||
<Button variant="ghost" size="icon" title="Nahlásit chybu" asChild>
|
||||
<Link href="https://github.com/jzitnik/tablescraper/issues/new" target="_blank" rel="noreferrer">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-500" />
|
||||
<span className="sr-only">Nahlásit chybu</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="md:hidden border-t p-4 space-y-4 bg-background animate-in slide-in-from-top-5">
|
||||
<nav className="flex flex-col space-y-4">
|
||||
{routes.map((route) => (
|
||||
<Link
|
||||
key={route.href}
|
||||
href={route.href}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={cn(
|
||||
"text-sm font-medium transition-colors hover:text-primary p-2 rounded-md hover:bg-muted",
|
||||
route.active ? "bg-muted text-foreground" : "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<route.icon className="h-5 w-5" />
|
||||
{route.label}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user