Skip to content

Commit

Permalink
Merge branch 'main' into feat--add-section-certificate
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinEichinger authored Nov 4, 2024
2 parents 9b2a772 + 6ce733c commit d3fec45
Show file tree
Hide file tree
Showing 14 changed files with 235 additions and 90 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"pre-commit": "^1.2.2",
"prettier": "^2.8.1",
"prompts": "^2.4.2",
"qrcode.react": "^4.0.1",
"react": "^18.3.1",
"react-app-polyfill": "^3.0.0",
"react-day-picker": "^9.2.1",
Expand Down
12 changes: 4 additions & 8 deletions src/components/VideoButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import { Lecture_Appointmenttype_Enum } from '../gql/graphql';
import { useMutation } from '@apollo/client';
import { gql } from '../gql';
import ZoomMeetingModal from '../modals/ZoomMeetingModal';
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { useQuery } from '@apollo/client';
import { canJoinMeeting } from '../widgets/AppointmentDay';
import { DateTime } from 'luxon';
import { Button } from './Button';
import { IconVideo } from '@tabler/icons-react';
import { useCanJoinMeeting } from '@/hooks/useCanJoinMeeting';

type VideoButtonProps = {
isInstructor?: boolean;
Expand Down Expand Up @@ -71,10 +70,7 @@ const VideoButton: React.FC<VideoButtonProps> = ({
}
};

const canStartMeeting = useMemo(
() => canJoin ?? (startDateTime && duration && canJoinMeeting(startDateTime, duration, isInstructor ? 240 : 10, DateTime.now())),
[canJoin, duration, isInstructor, startDateTime]
);
const canStartMeeting = useCanJoinMeeting(isInstructor ? 240 : 10, startDateTime, duration);

return (
<>
Expand All @@ -86,7 +82,7 @@ const VideoButton: React.FC<VideoButtonProps> = ({
zoomUrl={zoomUrl ?? undefined}
/>
<Button
disabled={!canStartMeeting || isOver}
disabled={!(canJoin ?? canStartMeeting) || isOver}
reasonDisabled={isInstructor ? t('course.meeting.hint.student') : t('course.meeting.hint.pupil')}
onClick={openMeeting}
className={className}
Expand Down
4 changes: 4 additions & 0 deletions src/components/appointment/AppointmentDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = ({ appointment }) =>
return participants + organizers;
}, [appointment.organizers, appointment.participants]);

// eslint-disable-next-line react-hooks/exhaustive-deps
const attendeesCount = useMemo(() => countAttendees(), [appointment.participants, appointment.organizers]);

const isPastAppointment = useMemo(() => {
Expand All @@ -83,13 +84,15 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = ({ appointment }) =>
setCanceled(true);
cancelAppointment({ variables: { appointmentId: appointment.id } });
navigate(-1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleDeclineClick = useCallback(() => {
toast.show({ description: t('appointment.detail.canceledToast'), placement: 'top' });
setCanceled(true);
declineAppointment({ variables: { appointmentId: appointment.id } });
setShowDeclineModal(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const attendees = useMemo(() => {
Expand All @@ -98,6 +101,7 @@ const AppointmentDetail: React.FC<AppointmentDetailProps> = ({ appointment }) =>

const isLastAppointment = useMemo(
() => (appointment.appointmentType === Lecture_Appointmenttype_Enum.Group && appointment.total === 1 ? true : false),
// eslint-disable-next-line react-hooks/exhaustive-deps
[appointment.total]
);

Expand Down
200 changes: 133 additions & 67 deletions src/components/appointment/AppointmentMetaDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Circle, HStack, Modal, Pressable, Spacer, Stack, Text, Tooltip, useBreakpointValue } from 'native-base';
import { Circle, HStack, VStack, Modal, Pressable, Spacer, Stack, Text, Tooltip, useBreakpointValue } from 'native-base';
import InformationBadge from '../notifications/preferences/InformationBadge';
import DateIcon from '../../assets/icons/lernfair/appointments/appointment_date.svg';
import TimeIcon from '../../assets/icons/lernfair/appointments/appointment_time.svg';
Expand All @@ -8,12 +8,17 @@ import CamerIcon from '../../assets/icons/lf-camera-icon.svg';
import { useLayoutHelper } from '../../hooks/useLayoutHelper';
import { useTranslation } from 'react-i18next';
import AttendeesModal from '../../modals/AttendeesModal';
import { useMemo, useState } from 'react';
import { useMemo, useState, useEffect } from 'react';
import { useMutation } from '@apollo/client';
import { AppointmentParticipant, Lecture_Appointmenttype_Enum, Organizer } from '../../gql/graphql';
import { Appointment } from '../../types/lernfair/Appointment';
import { DateTime } from 'luxon';
import useInterval from '../../hooks/useInterval';
import VideoButton from '../VideoButton';
import { IconDeviceMobileMessage, IconPointFilled, IconArrowNarrowRight } from '@tabler/icons-react';
import { useCanJoinMeeting } from '@/hooks/useCanJoinMeeting';
import { QRCodeSVG } from 'qrcode.react';
import { gql } from '../../gql';

type MetaProps = {
date: string;
Expand Down Expand Up @@ -52,8 +57,13 @@ const AppointmentMetaDetails: React.FC<MetaProps> = ({
zoomMeetingUrl,
}) => {
const [showModal, setShowModal] = useState<boolean>(false);
const [_, setCurrentTime] = useState(0);
const [loginURL, setLoginURL] = useState<string>('empty');
const [, setCurrentTime] = useState(0);
const { isMobile } = useLayoutHelper();
const isMobilePhone = useBreakpointValue({
base: true,
sm: false,
});
const { t } = useTranslation();

const buttonWidth = useBreakpointValue({
Expand All @@ -67,80 +77,136 @@ const AppointmentMetaDetails: React.FC<MetaProps> = ({
const isAppointmentOver = useMemo(() => {
const end = DateTime.fromISO(startDateTime).plus({ minutes: duration + 15 });
return end < DateTime.now();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const canStartMeeting = useCanJoinMeeting(isOrganizer ? 240 : 10, startDateTime, duration);

useEffect(() => {
canStartMeeting && createShortTimeLoginData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const [createShortTimeLoginToken] = useMutation(
gql(`
mutation LoginTokenForSecondDevice($expiresAt: DateTime!, $description: String!) { tokenCreate(expiresAt: $expiresAt, description: $description) }
`)
);

const createShortTimeLoginData = async () => {
const expiresAt = DateTime.now().plus({ hours: 1 });
const res = await createShortTimeLoginToken({ variables: { expiresAt: expiresAt, description: `` } });
const token = res?.data?.tokenCreate;

setLoginURL(`${window.location.origin}/login-token?secret_token=${token}&referrer=qr_code`);
};

return (
<>
<Modal isOpen={showModal} backgroundColor="transparent" onClose={() => setShowModal(false)}>
<AttendeesModal organizers={organizers} participants={participants} declinedBy={declinedBy} onClose={() => setShowModal(false)} />
</Modal>

<Stack direction={isMobile ? 'column' : 'row'} space={isMobile ? 5 : 7}>
<HStack space={2} alignItems="center">
<DateIcon />
<Text fontWeight="normal">{date}</Text>
</HStack>
<HStack space={2} alignItems="center">
<TimeIcon />
<Text fontWeight="normal">{t('appointment.detail.time', { start: startTime, end: endTime, duration: duration })}</Text>
</HStack>
<HStack space={2} alignItems="center">
<RepeatIcon />
<Text fontWeight="normal">{t('appointment.detail.repeatDate', { appointmentCount: count, appointmentsTotal: total })}</Text>
</HStack>
<HStack space={2} alignItems="center">
<PersonIcon />
<Text fontWeight="normal">
{t('appointment.detail.participants', {
participantsTotal: attendeesCount,
})}
</Text>
<Pressable onPress={() => setShowModal(true)}>
<InformationBadge />
</Pressable>
</HStack>
</Stack>
<Spacer py={3} />
{(overrideMeetingLink || zoomMeetingUrl) && (
<HStack space={2} alignItems="center">
<CamerIcon />
<Text fontWeight="normal">{overrideMeetingLink ?? zoomMeetingUrl?.split('?')[0]}</Text>
{zoomMeetingUrl && (
<Tooltip
maxWidth={270}
label={isOrganizer ? t('appointment.detail.zoomTooltipStudent') : t('appointment.detail.zoomTooltipPupil')}
bg={'primary.900'}
_text={{ textAlign: 'center' }}
p={3}
hasArrow
children={
<Circle rounded="full" bg="danger.100" size={4} ml={2}>
<Text color={'white'}>i</Text>
</Circle>
}
></Tooltip>
<HStack alignItems={'flex-start'} space={4} flexWrap="wrap" mt="-20px" flexShrink={1}>
<VStack alignItems={'flex-start'} mt="20px" flexShrink={1}>
<Stack space={isMobile ? 5 : 7} alignItems="flex-start" flexWrap={'wrap'} flexShrink={1} direction={isMobile ? 'column' : 'row'} mt="-10px">
<HStack space={2} alignItems="center">
<DateIcon />
<Text fontWeight="normal">{date}</Text>
</HStack>
<HStack space={2} alignItems="center">
<TimeIcon />
<Text fontWeight="normal">{t('appointment.detail.time', { start: startTime, end: endTime, duration: duration })}</Text>
</HStack>
<HStack space={2} alignItems="center">
<RepeatIcon />
<Text fontWeight="normal">{t('appointment.detail.repeatDate', { appointmentCount: count, appointmentsTotal: total })}</Text>
</HStack>
<HStack space={2} alignItems="center">
<PersonIcon />
<Text fontWeight="normal">
{t('appointment.detail.participants', {
participantsTotal: attendeesCount,
})}
</Text>
<Pressable onPress={() => setShowModal(true)}>
<InformationBadge />
</Pressable>
</HStack>
</Stack>
<Spacer py={3} />
{(overrideMeetingLink || zoomMeetingUrl) && (
<HStack space={2} alignItems="center">
<CamerIcon />
<Text fontWeight="normal">{overrideMeetingLink ?? zoomMeetingUrl?.split('?')[0]}</Text>
{zoomMeetingUrl && (
<Tooltip
maxWidth={270}
label={isOrganizer ? t('appointment.detail.zoomTooltipStudent') : t('appointment.detail.zoomTooltipPupil')}
bg={'primary.900'}
_text={{ textAlign: 'center' }}
p={3}
hasArrow
children={
<Circle rounded="full" bg="danger.100" size={4} ml={2}>
<Text color={'white'}>i</Text>
</Circle>
}
></Tooltip>
)}
</HStack>
)}
<Spacer py={3} />
{appointmentId && appointmentType && (
<>
<VideoButton
isInstructor={isOrganizer}
appointmentId={appointmentId}
appointmentType={appointmentType}
startDateTime={startDateTime}
duration={duration}
buttonText={t('appointment.detail.videochatButton')}
width={buttonWidth}
isOver={isAppointmentOver}
overrideLink={overrideMeetingLink ?? undefined}
/>
</>
)}
</HStack>
)}
<Spacer py={3} />
<div className="flex flex-col items-stretch md:items-start">
{appointmentId && appointmentType && (
<>
<VideoButton
isInstructor={isOrganizer}
appointmentId={appointmentId}
appointmentType={appointmentType}
startDateTime={startDateTime}
duration={duration}
buttonText={t('appointment.detail.videochatButton')}
width={buttonWidth}
isOver={isAppointmentOver}
overrideLink={overrideMeetingLink ?? undefined}
className="w-full md:w-fit"
/>
</>
</VStack>
{/* QR Code */}
{canStartMeeting && !isMobilePhone && (
<HStack backgroundColor={'primary.100'} padding="16px" borderRadius="15px" space={4} mt="20px" flexWrap="wrap">
<VStack>
<HStack alignItems={'center'} space={2} mb={2} ml={-1}>
<IconDeviceMobileMessage />
<Text fontSize="sm" fontWeight={'bold'}>
{t('appointment.detail.qrcode.title')}
</Text>
</HStack>
<Text fontSize="xs">{t('appointment.detail.qrcode.header')}</Text>
<HStack space={2} alignItems={'center'}>
<IconPointFilled size="6" />
<Text fontSize="xs">{t('appointment.detail.qrcode.bp1')}</Text>
</HStack>
<HStack space={2} alignItems={'center'}>
<IconPointFilled size="6" />
<Text fontSize="xs">{t('appointment.detail.qrcode.bp2')}</Text>
</HStack>
<HStack space={2} alignItems={'center'}>
<IconPointFilled size="6" />
<Text fontSize="xs">{t('appointment.detail.qrcode.bp3')}</Text>
</HStack>
<HStack space={2} alignItems={'center'} mt={2}>
<Text fontSize="xs">{t('appointment.detail.qrcode.footer')}</Text>
<IconArrowNarrowRight size="24" stroke={2} />
</HStack>
</VStack>
<VStack>
<QRCodeSVG value={loginURL} />
</VStack>
</HStack>
)}
</div>
</HStack>
</>
);
};
Expand Down
25 changes: 25 additions & 0 deletions src/hooks/useCanJoinMeeting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { DateTime } from 'luxon';
import { useMemo, useState } from 'react';
import useInterval from './useInterval';

export const useCanJoinMeeting = (joinBeforeMinutes: number, start?: string, duration?: number) => {
const [now, setNow] = useState(DateTime.now());

useInterval(() => {
setNow(DateTime.now());
}, 60000);

const valCanJoinMeeting = useMemo(() => canJoinMeeting(joinBeforeMinutes, now, start, duration), [duration, joinBeforeMinutes, start, now]) as boolean;

return valCanJoinMeeting;
};

const canJoinMeeting = (joinBeforeMinutes: number, now: DateTime, start?: string, duration?: number): boolean => {
if (start && duration) {
const startDate = DateTime.fromISO(start).minus({ minutes: joinBeforeMinutes });
const end = DateTime.fromISO(start).plus({ minutes: duration });
return now.toUnixInteger() >= startDate.toUnixInteger() && now.toUnixInteger() <= end.toUnixInteger();
} else {
return false;
}
};
10 changes: 9 additions & 1 deletion src/lang/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@
"canceledToast": "تم إلغاء الموعد",
"zoomTooltipStudent": "إذا قمت بالنقر على هذا الرابط، ستنضم إلى الاجتماع كمشارك. للانضمام إلى الاجتماع كمضيف، استخدم زر \"الانضمام إلى دردشة الفيديو الآن\".",
"zoomTooltipPupil": "لا تشارك هذا الرابط مع أشخاص آخرين. هذه هي الطريقة الوحيدة التي يمكننا من خلالها ضمان أمن المنصة.",
"qrcode": {
"title": "سجّل الدخول باستخدام هاتفك الذكي",
"header": "مزاياك",
"bp1": "التقاط صور للملاحظات أو الكتاب المدرسي أو أوراق العمل",
"bp2": "الدردشة وإرسال الملفات مباشرةً من هاتفك الذكي",
"bp3": "تلقي إشعارات على الجهاز",
"footer": "ما عليك سوى مسح رمز الاستجابة السريعة ضوئياً باستخدام هاتفك الذكي"
},
"cancelledBy": "موعد {{name}} ألغيت هذه",
"rescheduleButton": "موعد المناوبة",
"rescheduleDescription": "موعد إذا قمت بتحريك، سيتم تحميل {{name}}} مرة أخرى.",
Expand Down Expand Up @@ -2061,10 +2069,10 @@
"createAppointment": "موعد إنشاء",
"editAppointment": "موعد تعديل",
"settings": "اعدادات",
"notifications": "الإشعارات",
"systemNotifications": "إشعارات النظام",
"newsletterNotifications": "إشعارات الرسائل الإخبارية",
"profile": "ملف تعريف",
"notifications": "الإشعارات",
"manageSessions": "إدارة الأجهزة",
"helpCenter": "مركز المساعدة",
"newEmail": "تغيير البريد الإلكتروني",
Expand Down
Loading

0 comments on commit d3fec45

Please sign in to comment.