Skip to content

Commit

Permalink
fix: loading state for session modal
Browse files Browse the repository at this point in the history
also some minor cleanup of passing unnecessary props
  • Loading branch information
a-game committed May 24, 2024
1 parent f4e86c5 commit fc613f5
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 94 deletions.
34 changes: 16 additions & 18 deletions client/src/components/Calendar/AdminCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import dayjs from 'dayjs';
import { useCallback, useRef } from 'react';
import { ScheduleSessionDataSet } from '../Schedule/ScheduleSession';
import Calendar from './Calendar';
import { Event } from '@competence-assistant/shared';

const getAddPayload = (e: ScheduleSessionDataSet, startDateStr: string, resourceId: string, eventId: string) => {
const duration = parseInt(e.duration, 10);
Expand Down Expand Up @@ -39,59 +38,58 @@ const isOutside = (dropArea: HTMLDivElement, { clientX, clientY }: MouseEvent) =
return clientX < left || clientX > right || clientY < top || clientY > bottom;
};

const AdminCalendar = ({ event }: { event: Event }) => {
const { removeScheduleSession, editScheduleSession, createScheduleSession } = useScheduleMutations(event.id);
const { editScheduleBreak, removeScheduleBreak, createScheduleBreak } = useBreakMutations(event.id);
const AdminCalendar = ({ eventId, disabled = false }: { eventId: string; disabled?: boolean }) => {
const { removeScheduleSession, editScheduleSession, createScheduleSession } = useScheduleMutations(eventId);
const { editScheduleBreak, removeScheduleBreak, createScheduleBreak } = useBreakMutations(eventId);

const { sessions, breaks } = useSchedule(event.id, { includeConflicts: true });
const { sessions, breaks } = useSchedule(eventId, { includeConflicts: true });

const calendarRef = useRef<FullCalendar>(null);
const dropArea = useRef<HTMLDivElement>(null);
const isPastEvent = dayjs(event.endDate).isBefore(dayjs());

const onAdd = (e: ScheduleSessionDataSet, startDateStr: string, resourceId?: string) => {
if (!resourceId) return;
if (e.type === CalendarEventType.BREAK) {
return createScheduleBreak(getAddBreakPayload(e, startDateStr, event.id));
return createScheduleBreak(getAddBreakPayload(e, startDateStr, eventId));
}
createScheduleSession(getAddPayload(e, startDateStr, resourceId, event.id));
createScheduleSession(getAddPayload(e, startDateStr, resourceId, eventId));
};

const onEventEdit = useCallback(
({ id, startStr, endStr, title, extendedProps }: EventApi, resourceId?: string) => {
if (extendedProps.type === CalendarEventType.BREAK) {
return editScheduleBreak({ id, title, start: startStr, end: endStr, eventId: event.id });
return editScheduleBreak({ id, title, start: startStr, end: endStr, eventId });
}
editScheduleSession({
sessionId: extendedProps.sessionId,
start: startStr,
end: endStr,
roomId: resourceId || extendedProps.roomId,
eventId: event.id,
eventId,
});
},
[editScheduleBreak, editScheduleSession, event.id],
[editScheduleBreak, editScheduleSession, eventId],
);

const onDragStop = (evt: EventApi, mouseEvent: MouseEvent) => {
if (!dropArea.current || !isOutside(dropArea.current, mouseEvent)) return;
if (evt.extendedProps.type === CalendarEventType.BREAK) {
return removeScheduleBreak({ id: evt.id, eventId: event.id });
return removeScheduleBreak({ id: evt.id, eventId });
}
removeScheduleSession({ sessionId: evt.extendedProps.sessionId, eventId: event.id });
removeScheduleSession({ sessionId: evt.extendedProps.sessionId, eventId });
};

return (
<Box h="100%" ref={dropArea}>
<Calendar
ref={calendarRef}
eventId={event.id}
eventId={eventId}
calendarEvents={[...sessions, ...breaks]}
showHelpers
editable={!isPastEvent}
droppable={!isPastEvent}
eventDrop={(info) => onEventEdit(info.event, info.newResource?.id)}
eventDragStop={({ event: evt, jsEvent }) => onDragStop(evt, jsEvent)}
editable={!disabled}
droppable={!disabled}
eventDrop={({ event, newResource }) => onEventEdit(event, newResource?.id)}
eventDragStop={({ event, jsEvent }) => onDragStop(event, jsEvent)}
eventResize={({ event }) => onEventEdit(event)}
drop={({ draggedEl, dateStr, resource }) =>
onAdd(draggedEl.dataset as ScheduleSessionDataSet, dateStr, resource?.id)
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Events/EventActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import IconButton from '../IconButton';
const EventActions = ({ event }: { event: EventWithUniqueVoters }) => {
const { t } = useTranslation('event', { keyPrefix: 'actions' });
const { removeEvent } = useEventMutations();
const toggleActiveMutation = useToggleEventActive(event);
const toggleActiveMutation = useToggleEventActive(event.active);
const toggleActive = useCallback(() => toggleActiveMutation(event.id), [event, toggleActiveMutation]);

const url = `${new URL(window.location.href).origin}/events/${event.id}/sessions?layout=minimal`;
Expand Down
14 changes: 5 additions & 9 deletions client/src/components/Schedule/ScheduleAdminPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { Accordion, Stack, Text, Alert, Skeleton } from '@mantine/core';
import React from 'react';

import dayjs from 'dayjs';
import ScheduleSession from './ScheduleSession';
import ScheduleSessionCardInfo from './ScheduleSessionCardInfo';

import Breaks from './Breaks';
import { useTranslation } from 'react-i18next';
import Rooms from './Rooms';
import { IconAlertCircle } from '@tabler/icons-react';
import { Event } from '@competence-assistant/shared';
import { CalendarEventType, useSchedule } from '@/hooks/schedule';

const AccordionSection = ({ title, children }: { title: string; children: React.ReactNode }) => {
Expand All @@ -27,23 +25,21 @@ const AccordionSection = ({ title, children }: { title: string; children: React.
);
};

const ScheduleAdminPanel = ({ event }: { event: Event }) => {
const ScheduleAdminPanel = ({ eventId, disabled = false }: { eventId: string; disabled?: boolean }) => {
const { t } = useTranslation('schedule');
const { isLoading, unscheduledSessions } = useSchedule(event.id);

const isPastEvent = dayjs(event.endDate).isBefore(dayjs());
const { isLoading, unscheduledSessions } = useSchedule(eventId);

return (
<Stack spacing="sm">
{isPastEvent && (
{disabled && (
<Alert color="yellow" icon={<IconAlertCircle />}>
{t('pastAlert')}
</Alert>
)}
{!isPastEvent && (
{!disabled && (
<Accordion variant="filled" styles={{ content: { padding: 0 } }}>
<AccordionSection title={t('rooms.title')}>
<Rooms eventId={event.id} />
<Rooms eventId={eventId} />
</AccordionSection>

<AccordionSection title={t('sessions')}>
Expand Down
30 changes: 15 additions & 15 deletions client/src/components/Session/SessionInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Badge from '@/components/Badge';
import DurationDisplay from '@/components/DurationDisplay';
import { SessionLevelEnum, Session } from '@competence-assistant/shared';
import { ActionIcon, Box, Flex, Group, MantineNumberSize, Stack, Text, Tooltip } from '@mantine/core';
import { ActionIcon, Box, Group, MantineNumberSize, Stack, Text, Tooltip } from '@mantine/core';
import {
IconCalendarEvent,
IconMasksTheater,
Expand All @@ -25,36 +25,36 @@ const SessionInfo = ({ session, showVoters }: Props) => {
const { t } = useTranslation('session', { keyPrefix: 'info' });
return (
<Stack>
<Flex wrap="nowrap" align="center" gap="sm">
<Flex gap={4}>
<Group spacing="sm">
{session.level && <SessionLevel level={session.level} />}

<Group spacing={4}>
<IconPresentation size={IconSize.md} />
<Text size="sm" inline>
{capitalize(session.type)}
</Text>
</Flex>
</Group>

<DurationDisplay duration={session.duration} size="sm" />

{showVoters && session.voters && (
<Flex gap={4}>
<Group spacing={4}>
<IconUsers size={IconSize.md} />
<VotersCount inline size="sm" voters={session.voters} maxParticipants={session.maxParticipants} />
</Flex>
</Group>
)}
</Group>

{session.level && <SessionLevel level={session.level} />}
</Flex>

<Flex gap="xs">
<Group spacing="xs">
{session.tracks &&
session.tracks.map((track) => <Badge key={track.id} color={track.color} label={track.name} />)}
</Flex>
<Flex gap="xs">
</Group>
<Group spacing="xs">
<LinkUrl label={t('meeting')} url={session.meetingUrl} icon={<IconCalendarEvent size={IconSize.md} />} />
<LinkUrl label={t('slides')} url={session.slidesUrl} icon={<IconSlideshow size={IconSize.md} />} />
<LinkUrl label={t('recording')} url={session.recordingUrl} icon={<IconVideo size={IconSize.md} />} />
<LinkUrl label={t('feedback')} url={session.feedbackUrl} icon={<IconMasksTheater size={IconSize.md} />} />
</Flex>
</Group>
</Stack>
);
};
Expand All @@ -69,7 +69,7 @@ export const SessionLevel = ({ level, size = 'sm' }: SessionLevelProps) => {
const levelIndex = levelArray.indexOf(level);
return (
<Group spacing="xs">
<Flex gap={2}>
<Group spacing={2}>
{levelArray.map((v, index) => (
<Box
key={v}
Expand All @@ -81,7 +81,7 @@ export const SessionLevel = ({ level, size = 'sm' }: SessionLevelProps) => {
})}
/>
))}
</Flex>
</Group>
<Text size={size}>{capitalize(level)}</Text>
</Group>
);
Expand Down
13 changes: 11 additions & 2 deletions client/src/components/Session/SessionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Divider, Group, Stack } from '@mantine/core';
import { Divider, Group, Skeleton, Stack } from '@mantine/core';

import SessionInfo from './SessionInfo';
import UsersList from '../UserList';
Expand All @@ -15,11 +15,20 @@ export type Props = {
event?: EventWithUniqueVoters | Event;
};

const LoadingModal = () => (
<Stack>
<Skeleton height={22} />
<Skeleton height={20} width={250} />
<Skeleton height={28} width={142} />
<Skeleton height={300} />
</Stack>
);

const SessionModal = ({ sessionId, event }: Props) => {
const { data: session, isLoading } = useSession(sessionId);
const { canVote } = useSessionHelpers({ session, event });

if (isLoading) return 'loading...';
if (isLoading) return <LoadingModal />;
if (!session) return null;

return (
Expand Down
75 changes: 36 additions & 39 deletions client/src/components/Wishes/WishCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,55 +36,52 @@ const WishCard = ({ data: wish }: WishCardProps) => {
}, [wish]);

return (
<Card withBorder radius="md" style={{ display: 'flex', flexDirection: 'column' }}>
<Group position="apart" align="top">
<Box>
<Title size="lg">{wish.name}</Title>
<Text c="dimmed" fz="sm">
{dateLong(wish.createdAt)}
</Text>
</Box>
{canEdit && (
<Card withBorder radius="md">
<Stack>
<Group position="apart" align="top">
<Box>
<WishActions data={wish} />
<Title size="lg">{wish.name}</Title>
<Text c="dimmed" fz="sm">
{dateLong(wish.createdAt)}
</Text>
</Box>
)}
</Group>
{canEdit && <WishActions data={wish} />}
</Group>

<Divider my="sm" />
<Divider />

<Stack mt="0" style={{ flexGrow: '1' }}>
{wish.type && (
<Group spacing="xs">
<IconPresentation size={IconSize.md} />
<Text inline fw="bold">
{capitalize(wish.type)}
</Text>
</Group>
)}
{wish.level && <SessionLevel level={wish.level} />}
<Group spacing="sm">
{wish.level && <SessionLevel level={wish.level} />}
{wish.type && (
<Group spacing={4}>
<IconPresentation size={IconSize.md} />
<Text inline>{capitalize(wish.type)}</Text>
</Group>
)}
</Group>

<Box>
<Box style={{ flexGrow: '1' }}>
<Text lineClamp={wish.type || wish.level ? 5 : 6} ref={refDescription}>
<Markdown>{wish.description}</Markdown>
</Text>
</Box>

{showReadMore && (
<Flex mt="xs" justify="center">
<Button variant="subtle" size="xs" onClick={user ? () => openWishModal({ wish, user }) : undefined}>
{t('readMore')}
</Button>
</Flex>
)}
{user && (
<>
<Divider />
<Card.Section inheritPadding>
<UserStack users={[user]} />
</Card.Section>
</>
)}
</Stack>
{showReadMore && (
<Flex mt="xs" justify="center">
<Button variant="subtle" size="xs" onClick={user ? () => openWishModal({ wish, user }) : undefined}>
{t('readMore')}
</Button>
</Flex>
)}
{user && (
<>
<Divider my="sm" />
<Card.Section inheritPadding>
<UserStack users={[user]} />
</Card.Section>
</>
)}
</Card>
);
};
Expand Down
13 changes: 5 additions & 8 deletions client/src/hooks/eventMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,29 +53,26 @@ const useEditEvent = () => {
const useRemoveEvent = () => {
const { t } = useTranslation('event', { keyPrefix: 'notifications' });

const queryClient = useQueryClient();
const updater = (oldData: Event[] | undefined, id: Event['id']) => {
return oldData?.filter((event) => event.id !== id) || [];
};
const onSuccess = () => {
queryClient.invalidateQueries({ queryKey: ['events'] });
sendNotification({ status: 'INFO', message: t('deleted') });
};
return useOptimisticMutation(Api.deleteEvent, ['events'], updater, { onSuccess });
};

export const useToggleEventActive = (event: Event) => {
export const useToggleEventActive = (active: boolean) => {
const { t } = useTranslation('event', { keyPrefix: 'notifications' });

const queryClient = useQueryClient();
const endpoint = event.active ? Api.deactivateEvent : Api.activateEvent;
const updater = (oldData: Event[] | undefined, selectedEventId: string) => {
return oldData?.map((event) => (event.id === selectedEventId ? { ...event, active: !event.active } : event)) || [];
};
const onSuccess = () => {
queryClient.invalidateQueries({ queryKey: ['events'] });
sendNotification({ status: 'INFO', message: t(event.active ? 'hidden' : 'shown') });
sendNotification({ status: 'INFO', message: t(active ? 'hidden' : 'shown') });
};
const mutation = useOptimisticMutation(endpoint, ['events'], updater, { onSuccess });

const fn = active ? Api.deactivateEvent : Api.activateEvent;
const mutation = useOptimisticMutation(fn, ['events'], updater, { onSuccess });
return mutation.mutate;
};
6 changes: 4 additions & 2 deletions client/src/pages/admin/schedule/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ScheduleAdminPanel from '@/components/Schedule/ScheduleAdminPanel';
import PageHeader from '@/components/PageHeader';
import { useEvent } from '@/hooks/events';
import { dateShort } from '@/utils/dates';
import dayjs from 'dayjs';

const Layout = ({ children }: { children: React.ReactNode[] }) => {
const [panel, calendar] = children;
Expand Down Expand Up @@ -42,13 +43,14 @@ const PageAdminSchedule = () => {
);
}
if (!event) return null;
const isPastEvent = dayjs(event.endDate).isBefore(dayjs());

return (
<>
<PageHeader title={event.name} subtitle={dateShort(event.startDate)} />
<Layout>
<ScheduleAdminPanel event={event} />
<AdminCalendar event={event} />
<ScheduleAdminPanel eventId={event.id} disabled={isPastEvent} />
<AdminCalendar eventId={event.id} disabled={isPastEvent} />
</Layout>
</>
);
Expand Down

0 comments on commit fc613f5

Please sign in to comment.