Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions frontend/src/assets/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,29 @@ img {
animation: sidebar-enter 220ms ease-out;
will-change: opacity, filter;
}

/* Scrollbars */
* {
scrollbar-width: thin;
scrollbar-color: var(--color-primary) transparent;
}

*::-webkit-scrollbar {
width: 10px;
height: 10px;
}

*::-webkit-scrollbar-track {
background: transparent;
}

*::-webkit-scrollbar-thumb {
background-color: var(--color-primary);
border-radius: 999px;
border: 3px solid transparent;
background-clip: content-box;
}

*::-webkit-scrollbar-thumb:hover {
filter: brightness(1.08);
}
254 changes: 232 additions & 22 deletions frontend/src/components/DashboardOverview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState } from "react";
import { useEffect, useMemo, useState } from "react";

import { useNavigate } from "@tanstack/react-router";

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
Expand All @@ -15,13 +17,77 @@ import { isWoman, vocative } from "czech-vocative";

type DashboardOverviewProps = {
username: string;
classId?: string;
className?: string;
};

type OverviewAssignment = {
id: string | number;
name: string;
subjectName: string;
description: string;
dueDate: string;
};

type AssignmentsResponse = {
success: boolean;
statusCode: number;
data?: unknown;
message?: string;
};

function normalizeOverviewAssignment(item: any): OverviewAssignment {
const nestedSubject =
typeof item?.subject === "object" && item?.subject !== null ? item.subject : null;

return {
id: item?.id ?? "",
name: item?.name ?? item?.title ?? "Bez názvu",
subjectName:
item?.subjectName ??
(typeof item?.subject === "string" ? item.subject : null) ??
nestedSubject?.name ??
"—",
description: item?.description ?? "",
dueDate: item?.dueDate ?? new Date().toISOString(),
};
}

function normalizeOverviewAssignments(payload: AssignmentsResponse): OverviewAssignment[] {
const data = payload?.data;
if (!data) return [];
const items = Array.isArray(data) ? data : [data];
return items.map((item) => normalizeOverviewAssignment(item));
}

function formatAssignmentDate(date: string): string {
return new Date(date).toLocaleDateString("cs-CZ", { timeZone: "UTC" });
}

function getDueDateStatus(
dueDate: string,
now: Date,
): "overdue" | "today" | "future" {
const due = new Date(dueDate);
if (Number.isNaN(due.getTime())) {
return "future";
}

const nowUtcDay = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const dueUtcDay = Date.UTC(due.getUTCFullYear(), due.getUTCMonth(), due.getUTCDate());

if (dueUtcDay < nowUtcDay) return "overdue";
if (dueUtcDay === nowUtcDay) return "today";
return "future";
}

export function DashboardOverview({
username,
classId,
className,
}: DashboardOverviewProps) {
const navigate = useNavigate({ from: "/dashboard/$classId" });

const usernameVocative = username
? capitalizeFirstLetter(vocative(username, isWoman(username)))
: "Uživateli";
Expand All @@ -48,7 +114,7 @@ export function DashboardOverview({
});
const dayLabels = ["Po", "Út", "St", "Čt", "Pá", "So", "Ne"];

// Build 5x7 grid (35 cells) and include previous/next month days.
// Build 6x7 grid (42 cells) and include previous/next month days.
type Cell = { day: number; inMonth: boolean };
const cells: Cell[] = [];

Expand All @@ -64,7 +130,7 @@ export function DashboardOverview({

// next month leading days
let nextDay = 1;
while (cells.length < 35) {
while (cells.length < 42) {
cells.push({ day: nextDay++, inMonth: false });
}

Expand All @@ -76,8 +142,78 @@ export function DashboardOverview({
setDisplayedDate((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1));
}

const [assignments, setAssignments] = useState<OverviewAssignment[]>([]);
const [assignmentsLoading, setAssignmentsLoading] = useState(false);
const [assignmentsError, setAssignmentsError] = useState<string | null>(null);

useEffect(() => {
if (!classId) {
setAssignments([]);
setAssignmentsError("Chybí ID třídy.");
setAssignmentsLoading(false);
return;
}

const token = localStorage.getItem("auth_token");
if (!token) {
setAssignments([]);
setAssignmentsError("Chybí přihlašovací token.");
setAssignmentsLoading(false);
return;
}

const controller = new AbortController();
const loadAssignments = async () => {
setAssignmentsLoading(true);
setAssignmentsError(null);

try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/assignments/${classId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
signal: controller.signal,
},
);

const payload = (await response.json().catch(() => null)) as
| AssignmentsResponse
| null;

if (!response.ok) {
throw new Error(payload?.message ?? "Nepodařilo se načíst assignmenty.");
}

setAssignments(normalizeOverviewAssignments(payload ?? { success: true, statusCode: 200 }));
} catch (error) {
if ((error as Error).name === "AbortError") {
return;
}

setAssignments([]);
setAssignmentsError(
(error as Error).message || "Nepodařilo se načíst assignmenty.",
);
} finally {
setAssignmentsLoading(false);
}
};

loadAssignments();
return () => controller.abort();
}, [classId]);

const sortedAssignments = useMemo(() => {
return [...assignments].sort(
(a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(),
);
}, [assignments]);

return (
<main className="h-full w-full flex flex-col ">
<main className="w-full flex flex-col min-h-0 h-[calc(100dvh-48px)] overflow-y-auto pb-6">
<h1 className="text-3xl shrink-0 mb-1 font-semibold">
Ahoj {usernameVocative}
{className && ` - ${className}`},
Expand All @@ -91,15 +227,15 @@ export function DashboardOverview({
})}
</p>

<article className="grid min-h-0 flex-1 w-full grid-cols-2 grid-rows-[1.9fr_1.2fr] grid-cols-[1fr_1.5fr] gap-6">
<section className="min-h-0 bg-[var(--card-bg)] rounded-xl border border-[#18b4a6] flex flex-col p-4 overflow-hidden">
<article className="flex items-center justify-between ">
<h3 className="font-bold text-3xl capitalize">{monthName}</h3>
<section className="flex items-center gap-2">
<article className="grid min-h-0 flex-1 w-full gap-6 grid-cols-1 xl:grid-cols-[minmax(0,460px)_minmax(0,1fr)] 2xl:grid-cols-[minmax(0,520px)_minmax(0,1fr)] xl:grid-rows-[minmax(0,1.9fr)_minmax(0,1.2fr)]">
<section className="min-h-0 min-w-0 bg-[var(--card-bg)] rounded-xl border border-[#18b4a6] flex flex-col p-3 sm:p-4 overflow-hidden">
<article className="flex items-center justify-between gap-2">
<h3 className="font-bold capitalize text-xl sm:text-2xl lg:text-3xl truncate">{monthName}</h3>
<section className="flex items-center gap-1 sm:gap-2 shrink-0">
<button
onClick={prevMonth}
aria-label="Previous month"
className="p-1"
className="p-1 sm:p-1.5"
>
<FontAwesomeIcon
icon={faLessThan}
Expand All @@ -110,7 +246,7 @@ export function DashboardOverview({
<button
onClick={nextMonth}
aria-label="Next month"
className="p-1"
className="p-1 sm:p-1.5"
>
<FontAwesomeIcon
icon={faGreaterThan}
Expand All @@ -121,18 +257,16 @@ export function DashboardOverview({
</section>
</article>

<div className="mt-2 grid grid-cols-7 gap-0 text-center text-sm text-[#18b4a6]">
<div className="mt-2 grid grid-cols-7 gap-0 text-center text-[#18b4a6]">
{dayLabels.map((d) => (
<div key={d} className="pb-1 font-semibold text-xl">
<div key={d} className="pb-1 font-semibold text-xs sm:text-sm lg:text-base">
{d}
</div>
))}
</div>

<div
className={`grid grid-cols-7 ${
cells.length > 35 ? "grid-rows-6" : "grid-rows-5"
} gap-0.5 flex-1 min-h-0 text-center`}
className="grid grid-cols-7 grid-rows-6 gap-0.5 sm:gap-1 flex-1 min-h-0 text-center"
>
{cells.map((cell, i) => {
const isToday =
Expand All @@ -143,16 +277,20 @@ export function DashboardOverview({
return (
<div
key={i}
className={` flex items-center justify-center rounded ${
className={`aspect-square w-full flex items-center justify-center rounded-md cursor-pointer ${
isToday
? "bg-[#18b4a6] text-white hover:scale-95 transition-colors duration-50 cursor-pointer"
? "bg-[#18b4a6] text-white transition-colors duration-150 "
: cell.inMonth
? "hover:bg-black/5 transition-colors duration-50 cursor-pointer text-[#e6e6e6] hover:scale-95"
? "hover:bg-black/3 transition-colors duration-150 text-[#e6e6e6]"
: "text-gray-400"
}`}
>
Comment on lines 278 to 287
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calendar day cells are rendered as <div>s styled with cursor-pointer, which implies interactivity, but they have no click handler and no keyboard semantics. Either remove the pointer styling or switch to a semantic interactive element (button/role="button" + tabIndex + keyboard handlers) if days are intended to be clickable.

Copilot uses AI. Check for mistakes.
<span
className={`${cell.inMonth ? "text-3xl font-bold" : "text-base"}`}
className={`${
cell.inMonth
? "font-bold text-base sm:text-xl lg:text-3xl leading-none"
: "text-xs sm:text-sm lg:text-base leading-none"
}`}
>
{cell.day}
</span>
Expand All @@ -162,9 +300,81 @@ export function DashboardOverview({
</div>
</section>

<section className="min-h-0 bg-[var(--card-bg)] rounded-xl border border-[#18b4a6]"></section>
<section className="min-h-0 bg-[var(--card-bg)] rounded-xl border border-[#18b4a6] flex flex-col p-4 overflow-hidden">
<header className="flex items-start justify-between gap-4">
<h2 className="font-bold text-3xl">Deadliny</h2>
<button
type="button"
onClick={() => navigate({ to: "/dashboard/todo", search: { create: "1" } })}
className="flex items-center gap-2 rounded-lg px-3 py-2 text-[#18b4a6] hover:bg-black/5 transition-colors"
>
<FontAwesomeIcon
icon={faSquarePlus}
className="text-[#18b4a6] scale-200 hover:scale-190 cursor-pointer transition-transform duration-100"
/>

</button>
Comment on lines +304 to +316
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "add deadline" control is an icon-only <button> without an accessible label. Screen readers will announce it poorly, and it’s hard to discover. Add an aria-label (and optionally a visible tooltip/title) describing the action, e.g. "Vytvořit deadline".

Copilot uses AI. Check for mistakes.
</header>

<div className="mt-3 flex-1 min-h-0 overflow-y-auto rounded-lg border border-[#353535]">
{assignmentsLoading && (
<p className="p-3 text-sm text-gray-400">Načítám assignmenty...</p>
)}
{!assignmentsLoading && assignmentsError && (
<p className="p-3 text-sm text-red-400">{assignmentsError}</p>
)}
{!assignmentsLoading && !assignmentsError && sortedAssignments.length === 0 && (
<p className="p-3 text-sm text-gray-400">Žádné aktuální deadliny.</p>
)}

{!assignmentsLoading && !assignmentsError && sortedAssignments.length > 0 && (
<ul className="divide-y divide-[#353535]">
{sortedAssignments.map((assignment) => {
const dueStatus = getDueDateStatus(assignment.dueDate, now);
const dueDateClassName =
dueStatus === "overdue"
? "text-red-400 font-semibold"
: dueStatus === "today"
? "text-amber-400"
: "text-gray-300";

return (
<li
key={assignment.id}
className="px-1"
>
<button
type="button"
onClick={() =>
navigate({
to: "/dashboard/todo",
search: { open: String(assignment.id) },
})
}
className="grid w-full grid-cols-1 sm:grid-cols-[minmax(0,1.1fr)_minmax(0,0.8fr)_minmax(0,1.4fr)_minmax(0,0.6fr)] items-center gap-1 sm:gap-3 rounded-lg px-2 py-2 text-left hover:bg-black/5 hover:cursor-pointer transition-colors duration-150"
>
<p className="truncate text-lg font-semibold text-[#e6e6e6]">
{assignment.name}
</p>
<p className="truncate text-sm text-gray-300">
{assignment.subjectName || "—"}
</p>
<p className="truncate text-sm text-gray-400">
{assignment.description || "—"}
</p>
<p className={`text-sm sm:text-right ${dueDateClassName}`}>
{formatAssignmentDate(assignment.dueDate)}
</p>
</button>
</li>
);
})}
</ul>
)}
</div>
</section>

<section className="col-span-2 min-h-0 bg-[var(--card-bg)] rounded-xl border border-[#18b4a6] grid grid-cols-2 grid-rows-[1fr_3fr] gap-4 overflow-hidden">
<section className="xl:col-span-2 min-h-0 bg-[var(--card-bg)] rounded-xl border border-[#18b4a6] grid grid-cols-1 md:grid-cols-2 grid-rows-[auto_auto_auto_auto] md:grid-rows-[1fr_3fr] gap-4 overflow-hidden">
<h2 className="font-bold text-3xl capitalize p-4">Poznámky</h2>
<div className="flex justify-end items-start p-6">
<FontAwesomeIcon
Expand Down
Loading
Loading