Skip to content

Commit

Permalink
Merge pull request #58 from Project-Betoniera/feat/skeletons
Browse files Browse the repository at this point in the history
Feat/skeletons
  • Loading branch information
Genio2003 authored Mar 8, 2024
2 parents b98d33e + 80708e0 commit 30e2594
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 113 deletions.
40 changes: 40 additions & 0 deletions src/components/skeletons/ClassroomDetailsSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Card, Skeleton, SkeletonItem, makeStyles } from "@fluentui/react-components";
import { FunctionComponent } from "react";
import { useGlobalStyles } from "../../globalStyles";

type ClassroomDetailsSkeletonProps = {
lines?: number;
};

const useStyles = makeStyles({
skeletonRoot: {
display: "flex",
flexDirection: "column",
rowGap: "0.8rem"
},
skeletonBody: {
display: "flex",
flexDirection: "column",
rowGap: "0.5rem"
}
});

const ClassroomDetailsSkeleton: FunctionComponent<ClassroomDetailsSkeletonProps> = (props) => {
const styles = useStyles();
const globalStyles = useGlobalStyles();

return (
<Card className={globalStyles.card}>
<Skeleton>
<div className={styles.skeletonRoot}>
<SkeletonItem size={24} />
<div className={styles.skeletonBody}>
{new Array(props.lines || 1).fill(null).map((_, index) => <SkeletonItem key={index} size={16} />)}
</div>
</div>
</Skeleton>
</Card>
);
};

export default ClassroomDetailsSkeleton;
51 changes: 51 additions & 0 deletions src/components/skeletons/EventDetailsSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { FunctionComponent } from "react";
import { useGlobalStyles } from "../../globalStyles";
import { Card, Skeleton, SkeletonItem, makeStyles, } from "@fluentui/react-components";

export type EventDetailsSkeletonProps = {
lines?: number,
as?: "card";
};

const useSkeletonStyles = makeStyles({
root: {
display: "flex",
flexDirection: "column",
rowGap: "0.8rem"
},
body: {
display: "flex",
flexDirection: "column",
rowGap: "0.5rem"
}
});

/**
* A skeleton component used to display a loading state of the EventDetails component.
* @param props the properties of the component.
*/
const EventDetailsSkeleton: FunctionComponent<EventDetailsSkeletonProps> = (props: EventDetailsSkeletonProps) => {
const styles = useSkeletonStyles();
const globalStyles = useGlobalStyles();

const content = (
<div className={styles.root}>
<SkeletonItem size={24} />
<div className={styles.body}>
{new Array(props.lines || 1).fill(null).map((_, index) => <SkeletonItem key={index} size={16} />)}
</div>
</div>
);

return props.as === "card" ? (
<Card className={globalStyles.card}>
<Skeleton>
{content}
</Skeleton>
</Card>
) : (
content
);
};

export default EventDetailsSkeleton;
20 changes: 17 additions & 3 deletions src/routes/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Badge, Button, Caption1, Caption2, Card, Dialog, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, DialogTrigger, Drawer, DrawerBody, DrawerHeader, DrawerHeaderTitle, Subtitle1, Subtitle2, Title3, Tooltip, Tree, TreeItem, TreeItemLayout, makeStyles, mergeClasses, shorthands, tokens } from "@fluentui/react-components";
import { Badge, Button, Caption1, Caption2, Card, Dialog, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, DialogTrigger, Drawer, DrawerBody, DrawerHeader, DrawerHeaderTitle, Skeleton, SkeletonItem, Subtitle1, Subtitle2, Title3, Tooltip, Tree, TreeItem, TreeItemLayout, makeStyles, mergeClasses, shorthands, tokens } from "@fluentui/react-components";
import { ArrowExportRegular, BackpackFilled, BuildingFilled, CalendarMonthRegular, CalendarWeekNumbersRegular, DismissFilled, DismissRegular, EyeFilled, EyeOffFilled, PersonFilled, SettingsRegular, StarFilled, StarRegular } from "@fluentui/react-icons";
import { ReactNode, useContext, useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
Expand Down Expand Up @@ -228,6 +228,7 @@ export function Calendar() {
const [now] = useState(new Date());
const [dateTime, setDateTime] = useState(new Date(now));

const [isLoading, setLoading] = useState<boolean>(true);
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
const [calendarTitle, setCalendarTitle] = useState<string>("");
const [isDefault, setIsDefault] = useState<boolean>(false);
Expand Down Expand Up @@ -274,6 +275,8 @@ export function Calendar() {
* The `from` and `to` parameters can be used to specify a custom range of dates to get events for.
*/
async function getEvents(calendar: Calendar, from: Date = calendarView[0], to: Date = calendarView[calendarView.length - 1]) {
setLoading(true);

let newEvents: EventDto[] = [];

switch (calendar.selection.type) {
Expand All @@ -288,6 +291,8 @@ export function Calendar() {
break;
}

setLoading(false);

// Return only the events that are NOT already in the events list
return newEvents
.filter((event) => events.find((item) =>
Expand Down Expand Up @@ -499,6 +504,16 @@ export function Calendar() {
* Furthermore, each card is clickable and opens a dialog with the detailed list of events for the day.
*/
function renderCurrentCalendarView() {

function renderSkeletons() {
const count = Math.round(Math.random() * 3);
const elements = new Array(count).fill(null).map((_, index) => <SkeletonItem key={index} size={16} />);

return (
<Skeleton className={styles.eventContainer}>{elements}</Skeleton>
);
}

/**
* Renders a preview list of the events passed as parameter.
* @param events The events to render
Expand Down Expand Up @@ -578,8 +593,7 @@ export function Calendar() {
{isCurrentViewMonth && filteredEvents.length > 0 ? <div>{renderBadges(day, filteredCalendars, filteredEvents)}</div> : undefined}
</div>
<div className={styles.eventContainer} style={screenMediaQuery && !isCurrentViewMonth ? { overflowY: "auto" } : undefined}>
{/* TODO Show skeletons when loading events */}
{renderPreviewEvents(filteredEvents)}
{isLoading && filteredEvents.length === 0 ? renderSkeletons() : renderPreviewEvents(filteredEvents)}
</div>
</Card>
</DialogTrigger>
Expand Down
119 changes: 69 additions & 50 deletions src/routes/Classroom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Body1, Button, Card, CardFooter, CardHeader, Dialog, DialogActions, Dia
import { ChangeEvent, useContext, useEffect, useState } from "react";
import { DateSelector } from "../components/DateSelector";
import EventDetails from "../components/EventDetails";
import EventDetailsSkeleton from "../components/skeletons/EventDetailsSkeleton";
import ClassroomDetailsSkeleton from "../components/skeletons/ClassroomDetailsSkeleton";
import { ThemeContext } from "../context/ThemeContext";
import { ClassroomDto } from "../dto/ClassroomDto";
import { ClassroomStatus } from "../dto/ClassroomStatus";
Expand Down Expand Up @@ -98,56 +100,73 @@ export function Classroom() {
}
}, [filter, classrooms]);

const renderEvents = () => eventDialog && eventDialog.events && eventDialog.events.length > 0 ? eventDialog.events.map((event) => (
<EventDetails as="card" linkToCalendar key={event.id} event={event} title="subject" hide={["classroom"]} />
)) : (<Subtitle2>Nessuna</Subtitle2>);

const renderClassrooms = () => {
return filteredClassrooms.length === 0 ? [
<Card key={0} className={globalStyles.card}>🚫 Nessuna aula {filter === "free" ? "libera" : "occupata"}</Card>
] : filteredClassrooms.map((item) => {

const status = item.status.isFree ? (<>🟢 <strong>Libera</strong></>) : <>🔴 <strong>Occupata</strong></>;
let changeTime = "";

if (!item.status.statusChangeAt)
changeTime = "Nessun evento programmato.";
else if (item.status.statusChangeAt.getDate() == dateTime.getDate())
changeTime = "Fino alle " + item.status.statusChangeAt.toLocaleTimeString([], { timeStyle: "short" });
else
changeTime = item.status.statusChangeAt.toLocaleString([], { dateStyle: "medium", timeStyle: "short" });

const renderEvents = () => {
if (!eventDialog || !eventDialog.events) {
return (
<Card key={item.classroom.id} className={mergeClasses(globalStyles.card, item.status.isFree ? themeStyles.cardFree : themeStyles.cardBusy)} onClick={() => {
setEventDialog({
classroom: item.classroom,
events: null,
open: true
});

const start = new Date(dateTime);
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(end.getDate() + 1);

requests.event.byClassroom(start, end, item.classroom.id)
.then(events => setEventDialog(eventDialog => {
if (!eventDialog) return null;
return {
...eventDialog,
events,
};
}));
}}>
<CardHeader header={<Subtitle2>🏫 {item.classroom.name}</Subtitle2>} />
<div>
<Body1>{status}</Body1>
<br />
<Body1>{getClockEmoji(item.status.statusChangeAt)} {changeTime}</Body1>
</div>
</Card>
<>
<EventDetailsSkeleton as="card" lines={3} />
<EventDetailsSkeleton as="card" lines={3} />
</>
);
});
} else if (eventDialog.events.length === 0) {
return <Subtitle2>Nessuna</Subtitle2>;
} else {
return eventDialog.events.map((event) => (
<EventDetails as="card" linkToCalendar key={event.id} event={event} title="subject" hide={["classroom"]} />
));
}
};

const renderClassrooms = () => {
if (!classrooms) {
return new Array(30).fill(null).map((_, index) => <ClassroomDetailsSkeleton key={index} lines={2} />);
} else if (filteredClassrooms.length === 0) {
return <Card className={globalStyles.card}>🚫 Nessuna aula {filter === "free" ? "libera" : "occupata"}</Card>;
} else {
return filteredClassrooms.map((item) => {

const status = item.status.isFree ? (<>🟢 <strong>Libera</strong></>) : <>🔴 <strong>Occupata</strong></>;
let changeTime = "";

if (!item.status.statusChangeAt)
changeTime = "Nessun evento programmato.";
else if (item.status.statusChangeAt.getDate() == dateTime.getDate())
changeTime = "Fino alle " + item.status.statusChangeAt.toLocaleTimeString([], { timeStyle: "short" });
else
changeTime = item.status.statusChangeAt.toLocaleString([], { dateStyle: "medium", timeStyle: "short" });

return (
<Card key={item.classroom.id} className={mergeClasses(globalStyles.card, item.status.isFree ? themeStyles.cardFree : themeStyles.cardBusy)} onClick={() => {
setEventDialog({
classroom: item.classroom,
events: null,
open: true
});

const start = new Date(dateTime);
start.setHours(0, 0, 0, 0);
const end = new Date(start);
end.setDate(end.getDate() + 1);

requests.event.byClassroom(start, end, item.classroom.id)
.then(events => setEventDialog(eventDialog => {
if (!eventDialog) return null;
return {
...eventDialog,
events,
};
}));
}}>
<CardHeader header={<Subtitle2>🏫 {item.classroom.name}</Subtitle2>} />
<div>
<Body1>{status}</Body1>
<br />
<Body1>{getClockEmoji(item.status.statusChangeAt)} {changeTime}</Body1>
</div>
</Card>
);
});
}
};

function onFilterChange(_event: ChangeEvent<HTMLSelectElement>, data: SelectOnChangeData) {
Expand Down Expand Up @@ -184,7 +203,7 @@ export function Classroom() {
</Card>

<div className={globalStyles.grid}>
{classrooms ? renderClassrooms() : <Spinner size="huge" />}
{renderClassrooms()}
</div>

{eventDialog && (
Expand All @@ -203,7 +222,7 @@ export function Classroom() {
<Subtitle2>📅 {dateTime.toLocaleDateString([], { dateStyle: "medium" })}</Subtitle2>
</DialogTitle>
<DialogContent className={globalStyles.list}>
{eventDialog.events === null ? <Spinner size="huge" /> : renderEvents()}
{renderEvents()}
</DialogContent>
<DialogActions>
<DialogTrigger>
Expand Down
61 changes: 30 additions & 31 deletions src/routes/Grade.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Card, CardHeader, DataGrid, DataGridBody, DataGridCell, DataGridHeader, DataGridHeaderCell, DataGridRow, Spinner, Subtitle2, Tab, TabList, Title2, createTableColumn } from "@fluentui/react-components";
import { Card, CardHeader, DataGrid, DataGridBody, DataGridCell, DataGridHeader, DataGridHeaderCell, DataGridRow, Skeleton, SkeletonItem, Subtitle2, Tab, TabList, Title2, createTableColumn, makeStyles } from "@fluentui/react-components";
import { useContext, useEffect, useState } from "react";
import { UserContext } from "../context/UserContext";
import { GradeDto, GradeGroupDto } from "../dto/GradeDto";
import { useGlobalStyles } from "../globalStyles";
import useRequests from "../libraries/requests/requests";

const useStyles = makeStyles({
flexGrow: {
flexGrow: 1
}
});

export function Grade() {
const requests = useRequests();
const styles = useStyles();
const globalStyles = useGlobalStyles();
const { data } = useContext(UserContext);
const user = data?.user || { name: "", email: "", year: 0, isAdmin: false };
Expand Down Expand Up @@ -100,36 +107,28 @@ export function Grade() {
<div className={globalStyles.list}>
{(selectedGroup !== undefined || grades !== undefined) && (
<Card className={globalStyles.card}>
{
grades === undefined ? (
<Spinner size="huge" />
) : (
<DataGrid
items={grades}
columns={dataGridColumns}
sortable
>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell }) => (
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody<GradeDto>>
{({ item, rowId }) => (
<DataGridRow<GradeDto>
key={rowId}
>
{({ renderCell }) => (
<DataGridCell>{renderCell(item)}</DataGridCell>
)}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
)
}
<DataGrid
items={grades || new Array<GradeDto>(20).fill({ grade: null, module: { code: "", name: "" } })}
columns={dataGridColumns}
sortable
>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell }) => (
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody<GradeDto>>
{({ item, rowId }) => (
<DataGridRow<GradeDto>
key={rowId}
>
{({ renderCell }) => (<DataGridCell>{grades ? renderCell(item) : (<Skeleton className={styles.flexGrow}><SkeletonItem size={16} /></Skeleton>)}</DataGridCell>)}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
</Card>
)}
</div>
Expand Down
Loading

0 comments on commit 30e2594

Please sign in to comment.