From b8aefa4c58d41cb26e0d4d2f4d13274b4e469998 Mon Sep 17 00:00:00 2001 From: Mateus Vieira <68292695+mateusvrs@users.noreply.github.com> Date: Sun, 10 Dec 2023 12:11:32 -0300 Subject: [PATCH] =?UTF-8?q?task(front):=20cria=20p=C3=A1gina=20de=20cria?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20grades=20com=20funcionalidades=20propostas?= =?UTF-8?q?=20(#157)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * web(components): cria modal flutuante padrão Co-authored-by: Arthur Ribeiro * web(context): disponibilização das grades p/app * web(comp): versão básica do botão de gerar grades * web(api): integração com a geração de grades * web(schedules): versão simples da grade horária Co-authored-by: Arthur Ribeiro * web(fix): fix the bug of info page * web(comp): arruma nome do setModal * web(comp): define v1 da mostragem de grade * web(contexts): personaliza set de grades do user * web(mygrades): adiciona test index 0 p * web(api): remove import desnecessário * web(tailwind): adiciona nova cor snow * web(fix): changing the modal and schedule base colors * web(icons): add icons for upload, download, delete * web(modal): rm context desnecessário * web(comps): adiciona estado de preview ao schedule * web(schedules): muda layout pra p[a] * fix(app): lógica correta p/atualização de grades * web(schedules): add fnc remover grades locais * web(styles): deixa scroll bar mais fina no chrome * web(comp): cria um set de profs únicos pmostrar * task(frontend): trás as modificações da api para salvamento de grade para continuidade da criação do flow layout (#152) * task(schedule/save): criar a rota de salvamento de grade p/usuários autênticados (#144) * api(save-schedule): Created models and url path. - Still working on views * api(models): add migrations para o modelo d/grades * api(urls): arruma rota da view de salvamento * api(views): organiza imports por grupos de lib Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(settings): desliga adição de slash Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(utils): funcs p/salvar grade horária no db Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(views): modulariza views em uma pasta separada Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(views): fnc p/verificar e salvar grade do user Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(urls): adição da view nas rotas e no admin Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(settings): definição da auth padrão do swagger Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(views): correção do parm status de retorno da * api(utils): add linha de no cover p/tests * refactor(api): add espaço entre classe e função * tests(api): verifica as fnc do salvamento de grade - Tests para os casos de sucesso e erro do salvamento de grade - Verificação da models e do método __str__ da grade * refactor(views): tenta diminuir cog complexidade * refactor(views): diminui cog complexidade - A função de gerar estava com uma complexidade alta assim foi necessário refatorar para diminuir a complexidade * api(admin): deixa json bonito na pág admin * api(views): adiciona validador de request body * tests(api): verifica os casos de body incorreto * refactor(api/views): diminui cog complexidade * refactor(api): reduz cong complexidade do checker * refactor(api): modulariza validade class func --------- Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * task(frontend): alteração do ícone da volta para tela de login (#147) * frontend(feature): criação da nova página de informações para usuários * frontend(fixxing): ajustando tamanho do ícone de informação * frontend(feature): add information page for new users * web(info): organiza layout dos colaboradores * web(info): adição da descrição de utilização do site. * web(info): modulariza página de informações * fix(web): fix information page for new users * web(icons): add icons for google and search * web(icon): mudando o icone da volta para o home * web(icon): add search icon to the search bar * web(fixing): fixing search icon alt text --------- Co-authored-by: mateuvrs Co-authored-by: Mateus Vieira <68292695+mateusvrs@users.noreply.github.com> * models(schedules): add created_at field * serializers(schedules): add serializer for schedule * api(urls): add get_schedules endpoint * models(migrations): make migrations * api(schedules): add get schedules api * utils(db_handler): add get_schedule function * api(test): make tests for get schedule api --------- Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro Co-authored-by: Arthur Ribeiro <109052099+artrsousa1@users.noreply.github.com> * Squashed commit of the following: commit 25375c9254917cde6e7262326c11167c4eebb4b2 Author: Caio Felipe Date: Fri Dec 8 17:29:50 2023 -0300 task(delete-schedule): deleta grades do usuário e altera rotas (#151) * utils(db-handler): add delete_schedule function * api(delete-schedules): delete schedule endpoint * test(deleteschedule): add test for delete schedule * test(delete-schedule): add test for invalid token * Delete api/api/tests/test_schedules_api.py * django(api): rename and delete useless files * api(urls): update urls * utils(test): update test urls * api(save_schedule): remove auth save_schedule * api(views): add schedule view * api(views): rename view generate schedule * fix(typo): nome da classe p/ testar delete --------- Co-authored-by: Mateus Vieira <68292695+mateusvrs@users.noreply.github.com> commit 16813aba93d0ddf2e59fbd0288e182c7ad7814fd Merge: d39a16d 610c520 Author: Caio Felipe Date: Thu Dec 7 23:38:43 2023 -0300 Merge pull request #149 from unb-mds/task/get-schedule task(get-schedule): Cria API para obtenção de grades do usuário commit 610c520530642e2ddad59d29ed3ab03e33fcc719 Author: Caio Date: Thu Dec 7 23:14:08 2023 -0300 api(test): make tests for get schedule api commit fe4dee55eb356d98933344e5a4d7c19b6424fc8c Author: Caio Date: Thu Dec 7 22:34:30 2023 -0300 utils(db_handler): add get_schedule function commit d4b15057a8736328d45ab2601a55cd85946ad2b0 Author: Caio Date: Thu Dec 7 22:33:57 2023 -0300 api(schedules): add get schedules api commit 492c0decc22322b768a27c649ae2820b4ef4a672 Author: Caio Date: Thu Dec 7 22:33:38 2023 -0300 models(migrations): make migrations commit 67ed0ee9b26d29fb2d939cc2838fa49f3657871b Author: Caio Date: Thu Dec 7 22:28:20 2023 -0300 api(urls): add get_schedules endpoint commit 9b754ced794c0eb9965ca4b22e85d7d1cbd71bf4 Author: Caio Date: Thu Dec 7 22:27:59 2023 -0300 serializers(schedules): add serializer for schedule commit e028f6dcede671c5590c75803bc0d1be3a43daca Author: Caio Date: Thu Dec 7 22:27:43 2023 -0300 models(schedules): add created_at field commit d39a16d05310b5f1ceabac12666beb6a0d30de6a Author: Arthur Ribeiro <109052099+artrsousa1@users.noreply.github.com> Date: Thu Dec 7 22:18:30 2023 -0300 task(frontend): alteração do ícone da volta para tela de login (#147) * frontend(feature): criação da nova página de informações para usuários * frontend(fixxing): ajustando tamanho do ícone de informação * frontend(feature): add information page for new users * web(info): organiza layout dos colaboradores * web(info): adição da descrição de utilização do site. * web(info): modulariza página de informações * fix(web): fix information page for new users * web(icons): add icons for google and search * web(icon): mudando o icone da volta para o home * web(icon): add search icon to the search bar * web(fixing): fixing search icon alt text --------- Co-authored-by: mateuvrs Co-authored-by: Mateus Vieira <68292695+mateusvrs@users.noreply.github.com> commit 39c7d1a43b2bf38cb3391905346c942292aa04b3 Author: Mateus Vieira <68292695+mateusvrs@users.noreply.github.com> Date: Thu Dec 7 21:05:46 2023 -0300 task(schedule/save): criar a rota de salvamento de grade p/usuários autênticados (#144) * api(save-schedule): Created models and url path. - Still working on views * api(models): add migrations para o modelo d/grades * api(urls): arruma rota da view de salvamento * api(views): organiza imports por grupos de lib Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(settings): desliga adição de slash Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(utils): funcs p/salvar grade horária no db Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(views): modulariza views em uma pasta separada Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(views): fnc p/verificar e salvar grade do user Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(urls): adição da view nas rotas e no admin Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(settings): definição da auth padrão do swagger Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * api(views): correção do parm status de retorno da * api(utils): add linha de no cover p/tests * refactor(api): add espaço entre classe e função * tests(api): verifica as fnc do salvamento de grade - Tests para os casos de sucesso e erro do salvamento de grade - Verificação da models e do método __str__ da grade * refactor(views): tenta diminuir cog complexidade * refactor(views): diminui cog complexidade - A função de gerar estava com uma complexidade alta assim foi necessário refatorar para diminuir a complexidade * api(admin): deixa json bonito na pág admin * api(views): adiciona validador de request body * tests(api): verifica os casos de body incorreto * refactor(api/views): diminui cog complexidade * refactor(api): reduz cong complexidade do checker * refactor(api): modulariza validade class func --------- Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro * web(comp): rm unicidade de professores * web(comps): add fnc de upload p/cloud * web(utils): reorganiza datas e meses separados * web(api): generate schedule request fnc * web(api): get schedule request fnc com api * web(api): fnc p/salvar grade no cloud * web(utils): fnc p/request com auth * web(contexts): add fnc de deletar grade da nuvem * web(comps): func de fazer download grade com pdf * refactor(app): arrumar erros de layout e ui/ux * web(tailwind): adiciona font mono p/datas * web(icons): adiciona expand_less/expand_more * fix(front): arruma funçao do LayoutJSX * web(fix): fixing LayoutJSX function cognitive complexity * web(fix): improving MyGrades function * web(fix): fix generate schedule function * web(fix): fixing codeclimate issues * fix(mygrades): export incorreto não utilizado * refactor(comps): reduz cog complexidade do preview * refactor(comps): reduz linhas de código Co-authored-by: Arthur Ribeiro * fix(pdf): deixa download mais responsivo e ideal Co-authored-by: Arthur Ribeiro * web(app): coloca index correto nas grades --------- Co-authored-by: Arthur Ribeiro Co-authored-by: GabrielCastelo-31 Co-authored-by: Caio Co-authored-by: Arthur Ribeiro <109052099+artrsousa1@users.noreply.github.com> --- api/api/views/get_schedules.py | 32 +++ api/utils/db_handler.py | 4 +- .../AsideSchedulePopUp/DisciplineFragment.tsx | 9 +- .../components/AsideSchedulePopUp/Tooltip.tsx | 10 +- web/app/components/ClassInfo.tsx | 51 ++-- web/app/components/InfoHeader.tsx | 3 +- web/app/components/Modal/Modal.tsx | 23 ++ web/app/components/Schedule/Schedule.tsx | 141 +++++++++++ .../SchedulePreview/SchedulePreview.tsx | 223 ++++++++++++++++++ web/app/contexts/SchedulesContext.tsx | 82 +++++++ web/app/contexts/UserContext.tsx | 17 ++ web/app/globals.css | 4 +- web/app/hooks/useSchedules.ts | 10 + web/app/layout.tsx | 34 ++- .../components/GenerateScheduleButton.tsx | 28 ++- web/app/schedules/info/page.tsx | 8 +- web/app/schedules/layout.tsx | 10 +- web/app/schedules/mygrades/page.tsx | 50 +++- web/app/schedules/profile/page.tsx | 40 +++- web/app/utils/api/deleteSchedule.ts | 11 + web/app/utils/api/generateSchedule.ts | 20 ++ web/app/utils/api/getSchedules.ts | 8 + web/app/utils/api/saveSchedule.ts | 10 + web/app/utils/dates.ts | 2 + web/app/utils/settingsWithAuth.ts | 11 + web/package-lock.json | 188 ++++++++++++++- web/package.json | 1 + web/public/icons/delete.jpg | Bin 0 -> 289 bytes web/public/icons/download.jpg | Bin 0 -> 295 bytes web/public/icons/expand_less.png | Bin 0 -> 217 bytes web/public/icons/expand_more.png | Bin 0 -> 190 bytes web/public/icons/upload.jpg | Bin 0 -> 420 bytes web/tailwind.config.ts | 2 + web/tsconfig.json | 23 +- 34 files changed, 979 insertions(+), 76 deletions(-) create mode 100644 api/api/views/get_schedules.py create mode 100644 web/app/components/Modal/Modal.tsx create mode 100644 web/app/components/Schedule/Schedule.tsx create mode 100644 web/app/components/SchedulePreview/SchedulePreview.tsx create mode 100644 web/app/contexts/SchedulesContext.tsx create mode 100644 web/app/hooks/useSchedules.ts create mode 100644 web/app/utils/api/deleteSchedule.ts create mode 100644 web/app/utils/api/generateSchedule.ts create mode 100644 web/app/utils/api/getSchedules.ts create mode 100644 web/app/utils/api/saveSchedule.ts create mode 100644 web/app/utils/dates.ts create mode 100644 web/app/utils/settingsWithAuth.ts create mode 100644 web/public/icons/delete.jpg create mode 100644 web/public/icons/download.jpg create mode 100644 web/public/icons/expand_less.png create mode 100644 web/public/icons/expand_more.png create mode 100644 web/public/icons/upload.jpg diff --git a/api/api/views/get_schedules.py b/api/api/views/get_schedules.py new file mode 100644 index 00000000..08cd0df8 --- /dev/null +++ b/api/api/views/get_schedules.py @@ -0,0 +1,32 @@ +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework import status, request, response + +from api.swagger import Errors +from api.serializers import ScheduleSerializer + +from utils.db_handler import get_schedules + +class GetSchedules(APIView): + + permission_classes = [IsAuthenticated] + + @swagger_auto_schema( + operation_description="Retorna as grades horárias do usuário logado.", + security=[{'Bearer': []}], + responses={ + 200: openapi.Response('OK', ScheduleSerializer(many=True)), + **Errors([401, 403]).retrieve_erros() + } + ) + def get(self, request: request.Request) -> response.Response: + """Retorna as grades horárias do usuário logado.""" + + user = request.user + schedules = get_schedules(user) + data = ScheduleSerializer(schedules, many=True).data + + return response.Response(status=status.HTTP_200_OK, data=data) \ No newline at end of file diff --git a/api/utils/db_handler.py b/api/utils/db_handler.py index 2fbd2a54..1779b8ce 100644 --- a/api/utils/db_handler.py +++ b/api/utils/db_handler.py @@ -111,5 +111,5 @@ def delete_schedule(user: User, id: int) -> bool: Schedule.objects.get(user=user, id=id).delete() except Schedule.DoesNotExist: return False - - return True \ No newline at end of file + + return True diff --git a/web/app/components/AsideSchedulePopUp/DisciplineFragment.tsx b/web/app/components/AsideSchedulePopUp/DisciplineFragment.tsx index 96af9441..9478c922 100644 --- a/web/app/components/AsideSchedulePopUp/DisciplineFragment.tsx +++ b/web/app/components/AsideSchedulePopUp/DisciplineFragment.tsx @@ -1,8 +1,11 @@ import { Fragment } from 'react'; +import Image from 'next/image'; +import ClassInfoBox from './ClassInfoBox'; import { ClassType, DisciplineType } from '@/app/utils/api/searchDiscipline'; -import ClassInfoBox from './ClassInfoBox'; +import expand_more from '@/public/icons/expand_more.png'; +import expand_less from '@/public/icons/expand_less.png'; interface DisciplineFragmentPropsType { index: number, @@ -26,9 +29,7 @@ function DisciplineFragmentJSX({ handleDisciplineToggle, ...props }: { onClick={() => handleDisciplineToggle(index)} className='flex items-center gap-3' > - - {discipline.expanded ? 'expand_less' : 'expand_more'} - + expand icon {discipline.name} - {discipline.code} {discipline.expanded && diff --git a/web/app/components/AsideSchedulePopUp/Tooltip.tsx b/web/app/components/AsideSchedulePopUp/Tooltip.tsx index 8da937ca..e2aed464 100644 --- a/web/app/components/AsideSchedulePopUp/Tooltip.tsx +++ b/web/app/components/AsideSchedulePopUp/Tooltip.tsx @@ -9,6 +9,10 @@ interface TooltipPropsType { children: React.ReactNode, }; +export const isMobile = (width?: number) => { + return width && width <= 768; +}; + export default function Tooltip({ children }: TooltipPropsType) { const [active, setActive] = useState(false); const { width } = useWindowDimensions(); @@ -25,10 +29,6 @@ export default function Tooltip({ children }: TooltipPropsType) { }; }, [active]); - const isMobile = () => { - return width && width <= 768; - }; - return (
setActive(false)} - className={`absolute right-2 ${isMobile() ? 'bottom-2' : 'top-2'} material-symbols-rounded`} + className={`absolute right-2 ${isMobile(width) ? 'bottom-2' : 'top-2'} material-symbols-rounded`} > close diff --git a/web/app/components/ClassInfo.tsx b/web/app/components/ClassInfo.tsx index 842ecfe8..5e8655c1 100644 --- a/web/app/components/ClassInfo.tsx +++ b/web/app/components/ClassInfo.tsx @@ -8,6 +8,31 @@ interface ClassInfoPropsType { currentClass: ClassValueType } +export function generateSpecialDates(special_dates: Array>, days: Array) { + return special_dates.map((specialDate, index) => { + const day = specialDate[0]; + const start = parseInt(specialDate[1]) - 1; + const end = parseInt(specialDate[2]) - 1; + + function make_days(start: number, end: number) { + return days.slice(start, end + 1).map((day, index) => +
+ {day} +
+ ); + } + + return ( + + + {day} + + {make_days(start, end)} + + ); + }); +} + export default function ClassInfo({ currentClass }: ClassInfoPropsType) { return (
@@ -24,28 +49,10 @@ export default function ClassInfo({ currentClass }: ClassInfoPropsType) {
{day}
- ) : currentClass.class.special_dates.map((specialDate, index) => { - const day = specialDate[0]; - const start = parseInt(specialDate[1]) - 1; - const end = parseInt(specialDate[2]) - 1; - - function make_days(start: number, end: number) { - return currentClass.class.days.slice(start, end + 1).map((day, index) => -
- {day} -
- ); - } - - return ( - - - {day} - - {make_days(start, end)} - - ); - })} + ) : generateSpecialDates( + currentClass.class.special_dates, + currentClass.class.days + )}
diff --git a/web/app/components/InfoHeader.tsx b/web/app/components/InfoHeader.tsx index ff1234b3..dd2bba2d 100644 --- a/web/app/components/InfoHeader.tsx +++ b/web/app/components/InfoHeader.tsx @@ -4,8 +4,7 @@ import { useState } from 'react'; import useWindowDimensions from '../hooks/useWindowDimensions'; import useUser from '../hooks/useUser'; -const days = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado']; -const months = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; +import { days, months } from '../utils/dates'; const currentDateObject = new Date( new Date().toLocaleString('en', { diff --git a/web/app/components/Modal/Modal.tsx b/web/app/components/Modal/Modal.tsx new file mode 100644 index 00000000..b225069e --- /dev/null +++ b/web/app/components/Modal/Modal.tsx @@ -0,0 +1,23 @@ +interface ModalPropsType { + children: React.ReactNode; + setActiveModal: (active: boolean) => void; + noExit?: boolean; +} + +export default function Modal({ children, setActiveModal, noExit }: ModalPropsType) { + return ( +
+
+ {children} + {!noExit && + + } +
+
+ ); +} \ No newline at end of file diff --git a/web/app/components/Schedule/Schedule.tsx b/web/app/components/Schedule/Schedule.tsx new file mode 100644 index 00000000..f81feaff --- /dev/null +++ b/web/app/components/Schedule/Schedule.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { ScheduleClassType } from '@/app/contexts/SchedulesContext'; +import { HTMLProps, useEffect, useState } from 'react'; +import { generateSpecialDates } from '../ClassInfo'; +import Tooltip from '../AsideSchedulePopUp/Tooltip'; + +interface SchedulePropsType extends HTMLProps { + schedules?: Array; + preview?: boolean; + toDownload?: boolean; +} + +export default function Schedule({ schedules, preview, toDownload, ...props }: SchedulePropsType) { + const [currentSchedule, setCurrentSchedule] = useState>>(new Array(6).fill(new Array(15).fill(null))); + const uniqueTeachers = new Set(); + + const days = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb']; + const times = [ + '08:00 - 08:55', '08:55 - 09:50', '10:00 - 10:55', + '10:55 - 11:50', '12:00 - 12:55', '12:55 - 13:50', + '14:00 - 14:55', '14:55 - 15:50', '16:00 - 16:55', + '16:55 - 17:50', '18:00 - 18:55', '19:00 - 19:50', + '19:50 - 20:40', '20:50 - 21:40', '21:40 - 22:30' + ]; + + useEffect(() => { + const baseSchedule = new Array(6); + for (let i = 0; i < baseSchedule.length; i++) { + baseSchedule[i] = new Array(15).fill(null); + } + + function addClassToSchedule(scheduleStructure: ScheduleClassType) { + const { schedule } = scheduleStructure; + const splitSchedule = schedule.split(' '); + + splitSchedule.forEach((piece) => { + const regex = /^(\d+)([MNT])(\d+)$/; + const match = piece.match(regex); + if (match) { + const [, day, period, time] = match; + + for (let i = 0; i < day.length; i++) { + const x = parseInt(day[i]) - 2; + for (let j = 0; j < time.length; j++) { + let y = parseInt(time[j]) - 1; + + if (period === 'T') y += 5; + else if (period === 'N') y += 11; + + baseSchedule[x][y] = scheduleStructure; + } + } + } + }); + } + + if (schedules) { + schedules.forEach((schedule) => addClassToSchedule(schedule)); + setCurrentSchedule(baseSchedule); + } + }, [schedules]); + + return ( +
+
+
+
+
+ {days.map((day, index) => +
+

{day}

+
+ )} +
+
+
+ {times.map((time, timeIndex) => +
+
+

{time}

+
+
+ {days.map((day, dayIndex) => +
+ {currentSchedule[dayIndex] && + currentSchedule[dayIndex][timeIndex] && + currentSchedule[dayIndex][timeIndex].discipline.code} +
+ )} +
+
+ )} +
+
+ {!preview && +
+
    + {schedules && schedules.map((schedule, index) => { + return ( +
    +
  • + + {schedule.discipline.code} + - {schedule.discipline.name} - ({schedule.classroom}) +
  • + PROFESSORES: +
      + {schedule.teachers.map((teacher, index) => { + if (uniqueTeachers.has(teacher)) return null; + + uniqueTeachers.add(teacher); + return ( +
    • +

      {teacher}

      +
    • + ); + })} +
    + {schedule.special_dates.length ? + <> + DATAS: + {!toDownload ? + {generateSpecialDates(schedule.special_dates, schedule.days)} + : generateSpecialDates(schedule.special_dates, schedule.days)} + : null + } + +
    + ); + })} +
+
+ } +
+ ); +} \ No newline at end of file diff --git a/web/app/components/SchedulePreview/SchedulePreview.tsx b/web/app/components/SchedulePreview/SchedulePreview.tsx new file mode 100644 index 00000000..fa4cb67b --- /dev/null +++ b/web/app/components/SchedulePreview/SchedulePreview.tsx @@ -0,0 +1,223 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import useSchedules from '@/app/hooks/useSchedules'; +import useUser from '@/app/hooks/useUser'; + +import { CloudScheduleType, ScheduleClassType } from '@/app/contexts/SchedulesContext'; + +import Image from 'next/image'; +import Modal from '../Modal/Modal'; +import Schedule from '../Schedule/Schedule'; +import Button from '../Button'; + +import uploadIcon from '@/public/icons/upload.jpg'; +import downloadIcon from '@/public/icons/download.jpg'; +import deleteIcon from '@/public/icons/delete.jpg'; +import saveSchedule from '@/app/utils/api/saveSchedule'; +import getSchedules from '@/app/utils/api/getSchedules'; +import { days, months } from '@/app/utils/dates'; +import deleteSchedule from '@/app/utils/api/deleteSchedule'; +import { errorToast } from '@/app/utils/errorToast'; + +import jsPDF from 'jspdf'; + +const commonError = () => errorToast('Houve um erro na atualização das grades!'); + +function DeleteButton({ + setActiveDeleteModal, +}: { + setActiveDeleteModal: (value: boolean) => void; +}) { + return ( + + ); +} + +function DeleteModalHandler(props: { + deleteModal: { + activeDeleteModal: boolean; + setActiveDeleteModal: (value: boolean) => void; + } + isCloud?: boolean; + index: number; + deleteHandler: { + handleDeleteCloud: () => Promise; + handleDeleteLocal: () => void; + } +}) { + async function handleDelete() { + if (!props.isCloud) props.deleteHandler.handleDeleteLocal(); + else await props.deleteHandler.handleDeleteCloud(); + props.deleteModal.setActiveDeleteModal(false); + } + + return props.deleteModal.activeDeleteModal && + +
+

A grade será deletada para sempre, tem certeza?

+
+ + +
+
+
; +} + +function handleDate(created_at: string) { + const date = new Date(created_at); + + const currentDay = date.getDate().toString(); + const currentMonth = months[date.getMonth()]; + const currentWeekDay = days[date.getDay()]; + + return `${currentWeekDay}, ${currentDay} de ${currentMonth}`; +} + +function BottomPart(props: { + schedules: { + localSchedule?: Array; + cloudSchedule?: CloudScheduleType; + } + index: number; + position: number; + isCloud?: boolean; + handleDelete: () => void; + setters: { + setActiveScheduleModal: (value: boolean) => void; + setActiveDeleteModal: (value: boolean) => void; + setToDownload: (value: boolean) => void; + } +}) { + const { user } = useUser(); + const { setCloudSchedules } = useSchedules(); + + const [changeDate, setChangeDate] = useState(''); + + async function handleUploadToCloud() { + const saveResponse = await saveSchedule(props.schedules.localSchedule, user.access); + + if (saveResponse.status == 201) { + getSchedules(user.access).then(response => { + props.handleDelete(); + setCloudSchedules(response.data); + }).catch(() => commonError()); + } else errorToast('Não foi possível salvar a grade na nuvem!'); + } + + useEffect(() => { + if (props.isCloud && props.schedules.cloudSchedule?.created_at) { + setChangeDate(handleDate(props.schedules.cloudSchedule.created_at)); + } + }, [props.isCloud, props.schedules.cloudSchedule?.created_at]); + + return ( +
+
+ Grade {props.position}
+ {props.isCloud && changeDate && {changeDate}} +
+
+ {!props.isCloud && !user.is_anonymous && + + } + + +
+
+ ); +} + +function handleDownloadPDF(isCloud: boolean, index: number) { + const doc = document.getElementById('download-content')!; + + const pdfManager = new jsPDF('l', 'pt', 'a4'); + pdfManager.html(doc, { + callback: function (doc) { + doc.save(`schedule-${isCloud ? 'cloud' : 'local'}-${index + 1}.pdf`); + }, + x: 0, y: 0, + width: 1150, windowWidth: 2000, + }); +} + +export default function SchedulePreview({ localSchedule, cloudSchedule, index, position, isCloud = false }: { + localSchedule?: Array; + cloudSchedule?: CloudScheduleType; + index: number; + position: number; + isCloud?: boolean; +}) { + const { user } = useUser(); + const { localSchedules, setCloudSchedules, setLocalSchedules } = useSchedules(); + + const [toDownload, setToDownload] = useState(false); + const [activeScheduleModal, setActiveScheduleModal] = useState(false); + const [activeDeleteModal, setActiveDeleteModal] = useState(false); + + async function handleDeleteCloud() { + const response = await deleteSchedule(cloudSchedule?.id, user.access); + + if (response.status == 204) { + getSchedules(user.access).then(response => { + setCloudSchedules(response.data); + }).catch(() => commonError()); + } else errorToast('Não foi possível deletar a grade na nuvem!'); + } + + function handleDeleteLocal() { + const newLocalSchedules = [...localSchedules]; + newLocalSchedules.splice(index, 1); + setLocalSchedules(newLocalSchedules, false); + } + + useEffect(() => { + if (toDownload && activeScheduleModal) { + setTimeout(() => { + handleDownloadPDF(isCloud, index); + setActiveScheduleModal(false); + setToDownload(false); + }, 75); + } + }, [toDownload, activeScheduleModal, isCloud, index]); + + return ( + <> +
+
{ + if (!activeScheduleModal) setActiveScheduleModal(true); + }} + className='flex justify-center items-center bg-snow-tertiary h-48 rounded-3xl'> + + {activeScheduleModal && + + + + } +
+ +
+ + + ); +} \ No newline at end of file diff --git a/web/app/contexts/SchedulesContext.tsx b/web/app/contexts/SchedulesContext.tsx new file mode 100644 index 00000000..47ebe9af --- /dev/null +++ b/web/app/contexts/SchedulesContext.tsx @@ -0,0 +1,82 @@ +'use client'; + +import React, { createContext, useEffect, useState } from 'react'; + +import { ClassType } from '../utils/api/searchDiscipline'; + +export interface ScheduleClassType extends ClassType { + discipline: { + id: number; + name: string; + code: string; + department: number; + unicode_name: string; + } +}; + +export interface CloudScheduleType { + id: number; + created_at: string; + classes: Array; +}; + +type SchedulesType = Array>; +type SchedulesCloudType = Array; + +interface SchedulesContextType { + localSchedules: SchedulesType; + setLocalSchedules: (newSchedules: Array | Array>, add?: boolean) => void; + cloudSchedules: SchedulesCloudType; + setCloudSchedules: (newSchedules: SchedulesCloudType) => void; +} + +export const SchedulesContext = createContext({} as SchedulesContextType); + +export default function SchedulesContextProvider({ children }: { + children: React.ReactNode; +}) { + const [localSchedules, setLocalDefaultSchedules] = useState([] as SchedulesType); + const [cloudSchedules, setCloudDefaultSchedules] = useState([] as SchedulesCloudType); + + useEffect(() => { + const localJSON = JSON.parse(localStorage.getItem('schedules') || '[]'); + const localSchedulesFromJSON: SchedulesType = localJSON; + setLocalDefaultSchedules(localSchedulesFromJSON); + }, []); + + const setLocalSchedules = (newSchedules: Array | Array>, add: boolean = true) => { + if (add) { + const newLocalSchedules = [ + ...newSchedules, + ...localSchedules, + ]; + const uniqueSchedules = newLocalSchedules.filter((schedule, index, self) => { + const stringSchedule = JSON.stringify(schedule); + return index === self.findIndex((s) => JSON.stringify(s) === stringSchedule); + }); + localStorage.setItem('schedules', JSON.stringify(uniqueSchedules)); + } else localStorage.setItem('schedules', JSON.stringify(newSchedules)); + + const localJSON = JSON.parse(localStorage.getItem('schedules') || '[]'); + const localSchedulesFromJSON: SchedulesType = localJSON; + setLocalDefaultSchedules(localSchedulesFromJSON); + }; + + const setCloudSchedules = (newSchedules: SchedulesCloudType) => { + newSchedules.reverse(); + const data: Array = newSchedules; + data.forEach((schedule, index) => { + data[index].classes = JSON.parse(schedule.classes); + }); + setCloudDefaultSchedules(data); + }; + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/web/app/contexts/UserContext.tsx b/web/app/contexts/UserContext.tsx index 78d7907e..8cdf3aa6 100644 --- a/web/app/contexts/UserContext.tsx +++ b/web/app/contexts/UserContext.tsx @@ -6,6 +6,9 @@ import { UserData } from '../components/SignInSection'; import { settings } from '../utils/settings'; import request from '../utils/request'; +import getSchedules from '../utils/api/getSchedules'; +import { errorToast } from '../utils/errorToast'; +import useSchedules from '../hooks/useSchedules'; export interface User { is_anonymous: boolean; @@ -36,6 +39,8 @@ export default function UserContextProvider({ children, ...props }: UserContextP const [user, setUser] = useState(defaultUser); const [isLoading, setLoading] = useState(true); + const { cloudSchedules, setCloudSchedules } = useSchedules(); + useEffect(() => { request.post('/users/login/', {}, settings).then(response => { const userData: UserData = response.data; @@ -53,6 +58,18 @@ export default function UserContextProvider({ children, ...props }: UserContextP }); }, [setUser]); + useEffect(() => { + if (!user.is_anonymous && !cloudSchedules.length) { + getSchedules(user.access).then(response => { + if (response.status == 200 && response.data.length) { + setCloudSchedules(response.data); + } + }).catch(error => { + errorToast('Não foi possível carregar suas grades na nuvem.'); + }); + } + }, [user, cloudSchedules, setCloudSchedules]); + return ( {children} diff --git a/web/app/globals.css b/web/app/globals.css index 2c523752..c97228e3 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -16,10 +16,12 @@ } ::-webkit-scrollbar { - width: 8px; + width: 4px; + height: 4px; } ::-webkit-scrollbar-track { + height: 4px; background: var(--main-white); } diff --git a/web/app/hooks/useSchedules.ts b/web/app/hooks/useSchedules.ts new file mode 100644 index 00000000..ca53f718 --- /dev/null +++ b/web/app/hooks/useSchedules.ts @@ -0,0 +1,10 @@ +'use client'; + +import { useContext } from 'react'; +import { SchedulesContext } from '../contexts/SchedulesContext'; + +export default function useSchedules() { + const value = useContext(SchedulesContext); + + return value; +} \ No newline at end of file diff --git a/web/app/layout.tsx b/web/app/layout.tsx index e341fe7b..5d0e5609 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,22 +1,30 @@ import type { Metadata } from 'next'; -import { Poppins } from 'next/font/google'; +import { Poppins, Chivo_Mono } from 'next/font/google'; import './globals.css'; import UserContextProvider from './contexts/UserContext'; import YearPeriodContextProvider from './contexts/YearPeriodContext'; import ClassesToShowContextProvider from './contexts/ClassesToShowContext'; import SelectedClassesContextProvider from './contexts/SelectedClassesContext/SelectedClassesContext'; +import SchedulesContextProvider from './contexts/SchedulesContext'; import { Toaster } from 'react-hot-toast'; -const poppins = Poppins({ +export const poppins = Poppins({ weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], style: ['normal', 'italic'], subsets: ['latin'], variable: '--font-poppins', }); +export const chivoMono = Chivo_Mono({ + weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], + style: ['normal', 'italic'], + subsets: ['latin'], + variable: '--font-chivo-mono', +}); + export const metadata: Metadata = { title: 'Sua Grade UnB', description: 'Aplicação para gerenciamento de grade de horários da UnB', @@ -29,17 +37,19 @@ export default function RootLayout({ }) { return ( - + - - - - - {children} - - - - + + + + + + {children} + + + + + ); diff --git a/web/app/schedules/home/components/GenerateScheduleButton.tsx b/web/app/schedules/home/components/GenerateScheduleButton.tsx index 91129523..5bdc6cc1 100644 --- a/web/app/schedules/home/components/GenerateScheduleButton.tsx +++ b/web/app/schedules/home/components/GenerateScheduleButton.tsx @@ -2,21 +2,39 @@ import { useRouter } from 'next/navigation'; import useClassesToShow from '@/app/hooks/useClassesToShow'; +import useSelectedClasses from '@/app/hooks/useSelectedClasses'; +import useSchedules from '@/app/hooks/useSchedules'; import Button from '@/app/components/Button'; +import { ScheduleClassType } from '@/app/contexts/SchedulesContext'; + +import generateSchedule from '@/app/utils/api/generateSchedule'; +import { errorToast } from '@/app/utils/errorToast'; + export default function GenerateScheduleButton() { const router = useRouter(); + const { setLocalSchedules } = useSchedules(); + const { selectedClasses } = useSelectedClasses(); const { classesToShow } = useClassesToShow(); + const createSchedule = async () => { + let classes_id = new Array(); + selectedClasses.forEach((_class) => { + Array.from(_class.keys()).forEach((key) => { + classes_id.push(key); + }); + }); + const response = await generateSchedule(classes_id); + if (response.status === 200) { + setLocalSchedules(response.data as Array); + router.replace('/schedules/mygrades'); + } else errorToast('Não foi possível gerar as grades, tente novamente mais tarde!'); + }; return (
-
diff --git a/web/app/schedules/info/page.tsx b/web/app/schedules/info/page.tsx index 461cdbb4..f6b7746a 100644 --- a/web/app/schedules/info/page.tsx +++ b/web/app/schedules/info/page.tsx @@ -54,11 +54,9 @@ function HowToUse() { return ( <>

Como utilizar?

-

-

Na página inicial, clique no botão Buscar Matéria para selecionar as disciplinas desejadas para a sua grade. Escolha o ano/período e insira o nome da disciplina para poder optar por turmas potenciais na criação da grade, permitindo a escolha de até quatro turmas para cada disciplina.

-

Após fazer suas escolhas, clique no botão Gerar Grade e defina a prioridade dos turnos que melhor se adequam à sua rotina. Em seguida, basta escolher a grade mais adequada entre as opções geradas para realizar o download e/ou salvar as disciplinas escolhidas.

-

Compartilhe suas experiências e sugestões. O aplicativo está em constante desenvolvimento, e seu feedback é valioso para aprimorar a experiência de todos os usuários.

-

+

Na página inicial, clique no botão Buscar Matéria para selecionar as disciplinas desejadas para a sua grade. Escolha o ano/período e insira o nome da disciplina para poder optar por turmas potenciais na criação da grade, permitindo a escolha de até quatro turmas para cada disciplina.

+

Após fazer suas escolhas, clique no botão Gerar Grade e defina a prioridade dos turnos que melhor se adequam à sua rotina. Em seguida, basta escolher a grade mais adequada entre as opções geradas para realizar o download e/ou salvar as disciplinas escolhidas.

+

Compartilhe suas experiências e sugestões. O aplicativo está em constante desenvolvimento, e seu feedback é valioso para aprimorar a experiência de todos os usuários.

); } diff --git a/web/app/schedules/layout.tsx b/web/app/schedules/layout.tsx index 7739fb43..0507c428 100644 --- a/web/app/schedules/layout.tsx +++ b/web/app/schedules/layout.tsx @@ -98,16 +98,24 @@ function AsideButtonsJSX() { ); } +function getMainClassName(path: string, breakHeighPoint: boolean) { + const baseClassName = breakHeighPoint ? 'h-[calc(100%-15.75rem)]' : 'h-[calc(100%-9.75rem)]'; + return path !== 'home' ? baseClassName : `pt-${breakHeighPoint ? 3 : 7} ${baseClassName}`; +} + function LayoutJSX({ children }: { children: React.ReactNode }) { const { breakHeighPoint } = useWindowDimensions(); const { isLoading } = useUser(); + const path = usePathname().split('/')[2]; if (isLoading) return ; + const mainClassName = getMainClassName(path, breakHeighPoint); + return ( <> -
+
{children}
diff --git a/web/app/schedules/mygrades/page.tsx b/web/app/schedules/mygrades/page.tsx index 09701fe1..9fb83f1f 100644 --- a/web/app/schedules/mygrades/page.tsx +++ b/web/app/schedules/mygrades/page.tsx @@ -1,11 +1,55 @@ 'use client'; -import useUser from '@/app/hooks/useUser'; +import useSchedules from '@/app/hooks/useSchedules'; + +import SchedulePreview from '@/app/components/SchedulePreview/SchedulePreview'; + +function RenderLocalSchedules() { + const { localSchedules } = useSchedules(); + if (!localSchedules.length) return null; + + return ( + <> +

Grades locais

+
+ {localSchedules.map((schedule, index) => ( + schedule && + ))} +
+ + + ); +} + +function RenderCloudSchedules() { + const { cloudSchedules } = useSchedules(); + if (!cloudSchedules.length) return null; + + return ( + <> +

Grades nuvem

+
+ {cloudSchedules.map((schedule, index) => ( + schedule && + ))} +
+ + ); +} export default function MyGrades() { - const { user } = useUser(); + const { localSchedules, cloudSchedules } = useSchedules(); return ( - <> +
+

Suas Grades

+ + + {localSchedules.length === 0 && cloudSchedules.length === 0 && ( +
+ Você ainda não possui nenhuma grade. +
+ )} +
); } \ No newline at end of file diff --git a/web/app/schedules/profile/page.tsx b/web/app/schedules/profile/page.tsx index 29024923..e932b1fb 100644 --- a/web/app/schedules/profile/page.tsx +++ b/web/app/schedules/profile/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import Image from 'next/image'; import { useRouter } from 'next/navigation'; +import { useState } from 'react'; import useUser from '@/app/hooks/useUser'; import useClassesToShow from '@/app/hooks/useClassesToShow'; @@ -10,21 +10,52 @@ import useSelectedClasses from '@/app/hooks/useSelectedClasses'; import LogoImage from '@/public/logo.png'; import defaultProfile from '@/public/profile.svg'; +import Image from 'next/image'; import Button from '@/app/components/Button'; +import Modal from '@/app/components/Modal/Modal'; import signInWithGoogle from '@/app/utils/signInWithGoogle'; import handleLogout from '@/app/utils/api/logout'; +import useSchedules from '@/app/hooks/useSchedules'; export default function Profile() { + const [activeModal, setActiveModal] = useState(false); + const { setClassesToShow } = useClassesToShow(); const { setSelectedClasses } = useSelectedClasses(); + const { localSchedules, setLocalSchedules } = useSchedules(); const userContext = useUser(); const { user } = userContext; const router = useRouter(); + function handleLogoutAndRedirect() { + setClassesToShow(new Array()); + setSelectedClasses(new Map()); + setLocalSchedules(new Array(), false); + handleLogout({ userContext, router }); + } + return ( <> + {activeModal && + +
+

Vc tem grades não salvas na nuvem e vai perde-las!

+
+

Tem certeza que quer sair?

+
+ + +
+
+
+
+ } -
- ); } \ No newline at end of file diff --git a/web/app/utils/api/deleteSchedule.ts b/web/app/utils/api/deleteSchedule.ts new file mode 100644 index 00000000..f716a09b --- /dev/null +++ b/web/app/utils/api/deleteSchedule.ts @@ -0,0 +1,11 @@ +import request from '../request'; +import { settingsWithAuth } from '../settingsWithAuth'; + +export default async function deleteSchedule(scheduleId?: number, access_token?: string) { + const response = await request.delete( + `/courses/schedules/${scheduleId}/`, + settingsWithAuth(access_token) + ); + + return response; +} \ No newline at end of file diff --git a/web/app/utils/api/generateSchedule.ts b/web/app/utils/api/generateSchedule.ts new file mode 100644 index 00000000..73c72b64 --- /dev/null +++ b/web/app/utils/api/generateSchedule.ts @@ -0,0 +1,20 @@ +import request from '../request'; +import { settings } from '../settings'; + +type EachFieldNumber = 1 | 2 | 3; +type PreferenceType = [EachFieldNumber, EachFieldNumber, EachFieldNumber]; + +//Promise> +export default async function generateSchedule(classes_id: Array, preference?: PreferenceType) { + const body: { + classes: Array, + preference?: PreferenceType, + } = { + classes: classes_id, + }; + if (preference) body.preference = preference; + + const response = await request.post('/courses/schedules/generate/', body, settings); + + return response; +} \ No newline at end of file diff --git a/web/app/utils/api/getSchedules.ts b/web/app/utils/api/getSchedules.ts new file mode 100644 index 00000000..b81f0e83 --- /dev/null +++ b/web/app/utils/api/getSchedules.ts @@ -0,0 +1,8 @@ +import request from '../request'; +import { settingsWithAuth } from '../settingsWithAuth'; + +export default async function getSchedules(access_token?: string) { + const response = request.get('/courses/schedules/', settingsWithAuth(access_token)); + + return response; +} \ No newline at end of file diff --git a/web/app/utils/api/saveSchedule.ts b/web/app/utils/api/saveSchedule.ts new file mode 100644 index 00000000..9f6a7cca --- /dev/null +++ b/web/app/utils/api/saveSchedule.ts @@ -0,0 +1,10 @@ +import { ScheduleClassType } from '@/app/contexts/SchedulesContext'; + +import request from '../request'; +import { settingsWithAuth } from '../settingsWithAuth'; + +export default async function saveSchedule(schedule?: Array, access_token?: string) { + const response = await request.post('/courses/schedules/', schedule, settingsWithAuth(access_token)); + + return response; +} \ No newline at end of file diff --git a/web/app/utils/dates.ts b/web/app/utils/dates.ts new file mode 100644 index 00000000..fab230e3 --- /dev/null +++ b/web/app/utils/dates.ts @@ -0,0 +1,2 @@ +export const days = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado']; +export const months = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro']; diff --git a/web/app/utils/settingsWithAuth.ts b/web/app/utils/settingsWithAuth.ts new file mode 100644 index 00000000..fe551f6b --- /dev/null +++ b/web/app/utils/settingsWithAuth.ts @@ -0,0 +1,11 @@ +import { settings } from '@/app/utils/settings'; + +export const settingsWithAuth = (access_token?: string) => { + return { + ...settings, + headers: { + ...settings.headers, + 'Authorization': `Bearer ${access_token}` + } + }; +}; \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 67d98a32..346d6fc1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/lodash": "^4.14.202", "axios": "^1.6.2", + "jspdf": "^2.5.1", "lodash": "^4.17.21", "next": "14.0.2", "react": "^18", @@ -54,7 +55,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -863,6 +863,12 @@ "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==", "dev": true }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "optional": true + }, "node_modules/@types/react": { "version": "18.2.37", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz", @@ -1248,6 +1254,17 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.16", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", @@ -1331,6 +1348,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1394,6 +1420,17 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1456,6 +1493,31 @@ } ] }, + "node_modules/canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "optional": true + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1579,6 +1641,17 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/core-js": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.34.0.tgz", + "integrity": "sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==", + "hasInstallScript": true, + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1593,6 +1666,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1731,6 +1813,12 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", + "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==", + "optional": true + }, "node_modules/electron-to-chromium": { "version": "1.4.583", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.583.tgz", @@ -2357,6 +2445,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2769,6 +2862,19 @@ "node": ">= 0.4" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3245,6 +3351,23 @@ "json5": "lib/cli.js" } }, + "node_modules/jspdf": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", + "integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==", + "dependencies": { + "@babel/runtime": "^7.14.0", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.4.8" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.2.0", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -3771,6 +3894,12 @@ "node": ">=8" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "optional": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3990,6 +4119,15 @@ } ] }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -4078,8 +4216,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", @@ -4143,6 +4280,15 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4363,6 +4509,15 @@ "node": ">=0.10.0" } }, + "node_modules/stackblur-canvas": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz", + "integrity": "sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -4557,6 +4712,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwindcss": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", @@ -4603,6 +4767,15 @@ "node": ">=6" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4845,6 +5018,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/web/package.json b/web/package.json index 09019c52..f8044f15 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ "dependencies": { "@types/lodash": "^4.14.202", "axios": "^1.6.2", + "jspdf": "^2.5.1", "lodash": "^4.17.21", "next": "14.0.2", "react": "^18", diff --git a/web/public/icons/delete.jpg b/web/public/icons/delete.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f80edd5a187d2c466aa369b335e2196642a6ada6 GIT binary patch literal 289 zcmV++0p9+JP)Px#+et)0R7gwhmC+5tFbqVmo6t>Q6PN@xflXi&x(Vo{x^jY3I}Q13q<*BS?w#G( z9?*FWo%Im~8~_|MGcN#>XV3yrjE`PVLx|!R<2}L4$^mp~iFro#`3b-R`8PnKDgx-_ z4B$Rn8iUpuHPgzJJ=RjB0dx}uyjua{P=NWq3Bb@Pz$O3eqf*F7cfg*aw3?*Qqz<~@ zBH~%u1hLP^o_$N|qUoAOy2WOIIQAe_y-4TW6VA_>vSbLACfW_awNm}3lj`1LX<3u4 n#7w4`*RCs#uYVpL`kSF&HR@X#{9W_V00000NkvXXu0mjf=RST# literal 0 HcmV?d00001 diff --git a/web/public/icons/download.jpg b/web/public/icons/download.jpg new file mode 100644 index 0000000000000000000000000000000000000000..71158a50a1f8abb04b36bf31a4b7f6aa6fe2561e GIT binary patch literal 295 zcmV+?0oeYDP)Px#;Ymb6R7gwhl;I7+KnR6jH>sQGCOU~aiEg5s)J-%mfuu*Ea3)PODStWm?(w)A zuyPD5_s7oxMgXrK)x8QPX}+sMdH}rr98glS0a7cn5pnK&0CxbM2N%hOeq?|ffTOJD z!I;b(;ZFd}xsd_boQ-Vo##F>$b3(oXu!v(g2lXczQRqdcKnp!c+MLBzfK^gz9s%BB zs`nkJ;oi(2;!9I!6>uwfkimnc@4&2ZWBL++2YUc|u|>fAF{Q7xvYDRA(e7tJs=eB^ tjkbtN^dV)9HSa#Qo;9t4nsfgT_yC0AFB$b+BJ=mZK zlvpCi{9rRf$^k}&YsZr~H$4<@{KU7T##2KyaWl&kuNkL5O7R7GoM4(DRpRZCv>-t- z!(yY@f@r2k!l@RTVhYhrNsoDozeK31b!<@OTXfV-S3x3Vv7-r769>Zub6H9E*yo8r Pw=#IT`njxgN@xNAbNxty literal 0 HcmV?d00001 diff --git a/web/public/icons/expand_more.png b/web/public/icons/expand_more.png new file mode 100644 index 0000000000000000000000000000000000000000..0bdbd96df86c440b50ce71959799b702a97e6896 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gjt)4E9As*guCna(>F%+E*bAxJ?qtYdY*k>JKa(@uF}xHl|fBLuvRWmG-z(9A|Pkwy8=GP|4lj!ly5&hIMI^m@cO?}3=0>PHr~3lTY=ettS2Cvs8k2zQL< nc6Lq%>HU0+XEc<4*)NjrbQhQ0<*zdn=pY79S3j3^P6Px$UP(kjR7gwJmFrE!Fc5?%op7CSl>n6hl>n6-oj@m`@8mfKXC3FyNw89sC~NP` z?89DkWm~QsUw;zdM$+eA<$Dy|N_v#U+Xev7!whT&@FMAc!uDCx>j-EI;HfbuK5Pl} zE{POjfcqOC>sK}cVB<$y4Led%St>kKDnj zhOU}qx(4$>maS`s_EkU-x@2JMH1+vkbxPh1+54-DXm%gWQZe=ae}Hc)Fk4x~qPz$I O0000