From 1622a4c1b2ebfb8e645566b064a59578deac607b Mon Sep 17 00:00:00 2001 From: Michal Masrna Date: Sat, 11 Nov 2023 15:04:51 +0100 Subject: [PATCH 1/4] Merge master --- package.json | 3 +- src/components/FileUploader/FileUploader.tsx | 6 +- .../PageLayout/LoginForm/LoginForm.tsx | 4 +- src/components/Problems/Discussion.tsx | 2 +- src/components/Profile/ProfileDetail.tsx | 29 ++++- src/components/Profile/ProfileForm.tsx | 121 ++++++++++++++++++ .../PublicationUploader.tsx | 43 +++++++ src/components/RegisterForm/RegisterForm.tsx | 108 ++-------------- .../SchoolSubForm/SchoolSubForm.tsx | 118 +++++++++++++++++ .../SemesterAdministration.tsx | 8 ++ src/pages/strom/profil/uprava.tsx | 12 ++ src/types/api/competition.ts | 1 + src/types/api/generated/competition.ts | 1 + yarn.lock | 11 ++ 14 files changed, 357 insertions(+), 110 deletions(-) create mode 100644 src/components/Profile/ProfileForm.tsx create mode 100644 src/components/PublicationUploader/PublicationUploader.tsx create mode 100644 src/components/SchoolSubForm/SchoolSubForm.tsx create mode 100644 src/pages/strom/profil/uprava.tsx diff --git a/package.json b/package.json index 51b787d0..4cde8223 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "typescript": "^5.0.4", - "unstated-next": "^1.1.0" + "unstated-next": "^1.1.0", + "usehooks-ts": "^2.9.1" }, "devDependencies": { "@svgr/webpack": "^8.0.1", diff --git a/src/components/FileUploader/FileUploader.tsx b/src/components/FileUploader/FileUploader.tsx index 8550df1e..52b617af 100644 --- a/src/components/FileUploader/FileUploader.tsx +++ b/src/components/FileUploader/FileUploader.tsx @@ -7,10 +7,11 @@ import {Accept, DropzoneOptions, useDropzone} from 'react-dropzone' interface FileUploaderProps { uploadLink: string acceptedFormats?: Accept + adjustFormData?: (formData: FormData) => void refetch: () => void } -export const FileUploader: FC = ({uploadLink, acceptedFormats, refetch}) => { +export const FileUploader: FC = ({uploadLink, acceptedFormats, adjustFormData, refetch}) => { const {mutate: fileUpload} = useMutation({ mutationFn: (formData: FormData) => axios.post(uploadLink, formData), onSuccess: () => refetch(), @@ -23,9 +24,10 @@ export const FileUploader: FC = ({uploadLink, acceptedFormats } const formData = new FormData() formData.append('file', acceptedFiles[0]) + adjustFormData?.(formData) fileUpload(formData) }, - [fileUpload], + [adjustFormData, fileUpload], ) const {getRootProps, getInputProps} = useDropzone({ diff --git a/src/components/PageLayout/LoginForm/LoginForm.tsx b/src/components/PageLayout/LoginForm/LoginForm.tsx index 2bed7b6c..12e67c3e 100644 --- a/src/components/PageLayout/LoginForm/LoginForm.tsx +++ b/src/components/PageLayout/LoginForm/LoginForm.tsx @@ -24,8 +24,8 @@ export const LoginForm: FC = ({closeOverlay}) => { const {login} = AuthContainer.useContainer() const {handleSubmit, control} = useForm({defaultValues}) - const onSubmit: SubmitHandler = async (data) => { - await login({data, onSuccess: closeOverlay}) + const onSubmit: SubmitHandler = (data) => { + login({data, onSuccess: closeOverlay}) } const requiredRule = {required: '* Toto pole nemôže byť prázdne.'} diff --git a/src/components/Problems/Discussion.tsx b/src/components/Problems/Discussion.tsx index e2857d0ad..3ee05298 100644 --- a/src/components/Problems/Discussion.tsx +++ b/src/components/Problems/Discussion.tsx @@ -96,7 +96,7 @@ export const Discussion: FC = ({problemId, problemNumber, close const close = () => setDeleteDialogId(undefined) const agree = () => { - deleteDialogId !== undefined ? confirmDeleteComment(deleteDialogId) : undefined + if (deleteDialogId !== undefined) confirmDeleteComment(deleteDialogId) close() } diff --git a/src/components/Profile/ProfileDetail.tsx b/src/components/Profile/ProfileDetail.tsx index 31cf4547..f2985763 100644 --- a/src/components/Profile/ProfileDetail.tsx +++ b/src/components/Profile/ProfileDetail.tsx @@ -5,7 +5,9 @@ import {FC} from 'react' import {Profile} from '@/types/api/personal' import {AuthContainer} from '@/utils/AuthContainer' +import {useSeminarInfo} from '@/utils/useSeminarInfo' +import {Button, Link} from '../Clickable/Clickable' import styles from './ProfileDetail.module.scss' type ProfileLineInput = { @@ -25,6 +27,7 @@ const ProfileLine: FC = ({label, value}) => { export const ProfileDetail: FC = () => { const {isAuthed} = AuthContainer.useContainer() + const {seminar} = useSeminarInfo() const {data} = useQuery({ queryKey: ['personal', 'profiles', 'myprofile'], @@ -34,13 +37,25 @@ export const ProfileDetail: FC = () => { const profile = data?.data return ( - - - - - - - + + + + + + + + + + + upraviť údaje + + ) } diff --git a/src/components/Profile/ProfileForm.tsx b/src/components/Profile/ProfileForm.tsx new file mode 100644 index 00000000..73e78afb --- /dev/null +++ b/src/components/Profile/ProfileForm.tsx @@ -0,0 +1,121 @@ +import {useMutation, useQuery} from '@tanstack/react-query' +import axios from 'axios' +import {useRouter} from 'next/router' +import {FC} from 'react' +import {SubmitHandler, useForm} from 'react-hook-form' + +import styles from '@/components/FormItems/Form.module.scss' +import {FormInput} from '@/components/FormItems/FormInput/FormInput' +import {SelectOption} from '@/components/FormItems/FormSelect/FormSelect' +import {IGeneralPostResponse} from '@/types/api/general' +import {Profile} from '@/types/api/personal' +import {AuthContainer} from '@/utils/AuthContainer' +import {useSeminarInfo} from '@/utils/useSeminarInfo' + +import {Button} from '../Clickable/Clickable' +import {SchoolSubForm, SchoolSubFormValues} from '../SchoolSubForm/SchoolSubForm' + +interface ProfileFormValues extends SchoolSubFormValues { + first_name?: string + last_name?: string + phone?: string + parent_phone?: string +} + +const defaultValues: ProfileFormValues = { + first_name: '', + last_name: '', + phone: '', + parent_phone: '', + new_school_description: '', + without_school: false, + school: null, + school_not_found: false, + grade: '', +} + +export const ProfileForm: FC = () => { + const {isAuthed} = AuthContainer.useContainer() + + const {data} = useQuery({ + queryKey: ['personal', 'profiles', 'myprofile'], + queryFn: () => axios.get(`/api/personal/profiles/myprofile`), + enabled: isAuthed, + }) + const profile = data?.data + const profileValues: ProfileFormValues = { + first_name: profile?.first_name, + last_name: profile?.last_name, + phone: profile?.phone ?? '', + parent_phone: profile?.parent_phone ?? '', + new_school_description: '', + without_school: profile?.school_id === 1, + school: ({id: profile?.school.code, label: profile?.school.verbose_name} as SelectOption) ?? null, + school_not_found: profile?.school_id === 0, + grade: profile?.grade ?? '', + } + + const {handleSubmit, control, watch, setValue} = useForm({ + defaultValues, + values: profileValues, + }) + + watch(['first_name']) + + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }) + } + + const router = useRouter() + + const {seminar} = useSeminarInfo() + + const transformFormData = (data: ProfileFormValues) => ({ + profile: { + first_name: data.first_name, + last_name: data.last_name, + school: data.school?.id, + phone: data.phone, + parent_phone: data.parent_phone, + grade: data.grade, + }, + new_school_description: data.new_school_description || '', + }) + + const {mutate: submitFormData} = useMutation({ + mutationFn: (data: ProfileFormValues) => { + return axios.put(`/api/user/user`, transformFormData(data)) + }, + onSuccess: () => router.push(`/${seminar}/profil`), + }) + + const onSubmit: SubmitHandler = (data) => { + submitFormData(data) + } + + const requiredRule = {required: '* Toto pole nemôže byť prázdne.'} + const phoneRule = { + validate: (val?: string) => { + if (val && !/^(\+\d{10,12})$/u.test(val.replaceAll(/\s+/gu, ''))) + return '* Zadaj telefónne číslo vo formáte validnom formáte +421 123 456 789 alebo +421123456789.' + }, + } + return ( +
+
+ + + + + +

* takto označéné polia sú povinné

+ + +
+ ) +} diff --git a/src/components/PublicationUploader/PublicationUploader.tsx b/src/components/PublicationUploader/PublicationUploader.tsx new file mode 100644 index 00000000..8c959d47 --- /dev/null +++ b/src/components/PublicationUploader/PublicationUploader.tsx @@ -0,0 +1,43 @@ +import {Stack} from '@mui/material' +import {useQueryClient} from '@tanstack/react-query' +import {FC} from 'react' + +import {SemesterWithProblems} from '@/types/api/generated/competition' + +import {Link} from '../Clickable/Clickable' +import {FileUploader} from '../FileUploader/FileUploader' + +interface PublicationUploaderProps { + semesterId: string + order: number + semesterData: SemesterWithProblems | undefined +} + +export const PublicationUploader: FC = ({semesterId, order, semesterData}) => { + const queryClient = useQueryClient() + + const refetch = () => queryClient.invalidateQueries({queryKey: ['competition', 'semester', semesterId]}) + + const appendFormData = (formData: FormData) => { + formData.append('publication_type', 'Časopisy') + formData.append('event', semesterId) + formData.append('order', order.toString()) + } + + const publication = semesterData?.publication_set.find((publication) => publication.order === order) + + return ( + +

{order}. Časopis:

+ {publication && ( + {publication.name}.pdf + )} + +
+ ) +} diff --git a/src/components/RegisterForm/RegisterForm.tsx b/src/components/RegisterForm/RegisterForm.tsx index 4ca310d4..90bdc2fc 100644 --- a/src/components/RegisterForm/RegisterForm.tsx +++ b/src/components/RegisterForm/RegisterForm.tsx @@ -1,22 +1,19 @@ -import {useMutation, useQuery} from '@tanstack/react-query' +import {useMutation} from '@tanstack/react-query' import axios from 'axios' import {useRouter} from 'next/router' -import {FC, useEffect, useRef} from 'react' +import {FC} from 'react' import {SubmitHandler, useForm} from 'react-hook-form' import styles from '@/components/FormItems/Form.module.scss' -import {FormAutocomplete} from '@/components/FormItems/FormAutocomplete/FormAutocomplete' import {FormCheckbox} from '@/components/FormItems/FormCheckbox/FormCheckbox' import {FormInput} from '@/components/FormItems/FormInput/FormInput' -import {FormSelect, SelectOption} from '@/components/FormItems/FormSelect/FormSelect' -import {IGrade} from '@/types/api/competition' import {IGeneralPostResponse} from '@/types/api/general' -import {ISchool} from '@/types/api/personal' import {useSeminarInfo} from '@/utils/useSeminarInfo' import {Button} from '../Clickable/Clickable' +import {SchoolSubForm, SchoolSubFormValues} from '../SchoolSubForm/SchoolSubForm' -type RegisterFormValues = { +interface RegisterFormValues extends SchoolSubFormValues { email?: string password1?: string password2?: string @@ -24,12 +21,7 @@ type RegisterFormValues = { last_name?: string phone?: string parent_phone?: string - new_school_description?: string - without_school: boolean - school?: SelectOption | null - school_not_found: boolean - grade: number | '' - gdpr: boolean + gdpr?: boolean } const defaultValues: RegisterFormValues = { @@ -49,8 +41,10 @@ const defaultValues: RegisterFormValues = { } export const RegisterForm: FC = () => { - const {handleSubmit, control, watch, setValue, getValues} = useForm({defaultValues}) - const [school_not_found, without_school] = watch(['school_not_found', 'without_school']) + const {handleSubmit, control, watch, setValue, getValues} = useForm({ + defaultValues, + values: defaultValues, + }) const scrollToTop = () => { window.scrollTo({ @@ -59,58 +53,8 @@ export const RegisterForm: FC = () => { }) } - const otherSchoolItem = useRef() - const withoutSchoolItem = useRef() - const router = useRouter() - // načítanie ročníkov z BE, ktorými vyplníme FormSelect s ročníkmi - const {data: gradesData} = useQuery({ - queryKey: ['competition', 'grade'], - queryFn: () => axios.get(`/api/competition/grade`), - }) - const grades = gradesData?.data ?? [] - const gradeItems: SelectOption[] = grades.map(({id, name}) => ({id, label: name})) - - // načítanie škôl z BE, ktorými vyplníme FormAutocomplete so školami - const {data: schoolsData} = useQuery({ - queryKey: ['personal', 'schools'], - queryFn: () => axios.get(`/api/personal/schools`), - }) - const schools = schoolsData?.data ?? [] - const allSchoolItems: SelectOption[] = schools.map(({code, city, name, street}) => ({ - id: code, - label: city ? `${name} ${street}, ${city}` : name, - })) - const emptySchoolItems = allSchoolItems.filter(({id}) => [0, 1].includes(id)) - otherSchoolItem.current = emptySchoolItems.find(({id}) => id === 0) - withoutSchoolItem.current = emptySchoolItems.find(({id}) => id === 1) - const schoolItems = allSchoolItems.filter(({id}) => ![0, 1].includes(id)) - - // predvyplnenie/zmazania hodnôt pri zakliknutí checkboxu pre užívateľa po škole - useEffect(() => { - if (without_school) { - setValue('school', withoutSchoolItem.current) - setValue('grade', 13) - setValue('school_not_found', false) - } else { - setValue('school', null) - setValue('grade', '') - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [without_school]) - - // predvyplnenie/zmazania hodnôt pri zakliknutí checkboxu pre neznámu školu - useEffect(() => { - if (school_not_found) { - setValue('school', otherSchoolItem.current) - } else if (!without_school) { - setValue('school', null) - setValue('grade', '') - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [school_not_found]) - const {seminar} = useSeminarInfo() const transformFormData = (data: RegisterFormValues) => ({ @@ -136,7 +80,7 @@ export const RegisterForm: FC = () => { onSuccess: () => router.push(`${router.asPath}/../verifikacia`), }) - const onSubmit: SubmitHandler = async (data) => { + const onSubmit: SubmitHandler = (data) => { submitFormData(data) } @@ -189,37 +133,7 @@ export const RegisterForm: FC = () => { /> - - - - {school_not_found && ( - - )} - id !== 13 || without_school)} - disabled={!gradeItems.length || without_school} - rules={requiredRule} - /> + = { + control: Control + watch: UseFormWatch + setValue: UseFormSetValue +} + +export const SchoolSubForm = ({control, watch, setValue}: SchoolSubFormProps) => { + const [school_not_found, without_school] = watch(['school_not_found', 'without_school']) + + const otherSchoolItem = useRef() + const withoutSchoolItem = useRef() + + // načítanie ročníkov z BE, ktorými vyplníme FormSelect s ročníkmi + const {data: gradesData} = useQuery({ + queryKey: ['competition', 'grade'], + queryFn: () => axios.get(`/api/competition/grade`), + }) + const grades = gradesData?.data ?? [] + const gradeItems: SelectOption[] = grades.map(({id, name}) => ({id, label: name})) + + // načítanie škôl z BE, ktorými vyplníme FormAutocomplete so školami + const {data: schoolsData} = useQuery({ + queryKey: ['personal', 'schools'], + queryFn: () => axios.get(`/api/personal/schools`), + }) + const schools = schoolsData?.data ?? [] + const allSchoolItems: SelectOption[] = schools.map(({code, city, name, street}) => ({ + id: code, + label: city ? `${name} ${street}, ${city}` : name, + })) + const emptySchoolItems = allSchoolItems.filter(({id}) => [0, 1].includes(id)) + otherSchoolItem.current = emptySchoolItems.find(({id}) => id === 0) + withoutSchoolItem.current = emptySchoolItems.find(({id}) => id === 1) + const schoolItems = allSchoolItems.filter(({id}) => ![0, 1].includes(id)) + + // predvyplnenie/zmazania hodnôt pri zakliknutí checkboxu pre užívateľa po škole + useUpdateEffect(() => { + if (without_school) { + setValue('school', withoutSchoolItem.current) + setValue('grade', 13) + setValue('school_not_found', false) + } else { + setValue('school', null) + setValue('grade', '') + } + }, [without_school]) + + // predvyplnenie/zmazania hodnôt pri zakliknutí checkboxu pre neznámu školu + useUpdateEffect(() => { + if (school_not_found) { + setValue('school', otherSchoolItem.current) + } else if (!without_school) { + setValue('school', null) + setValue('grade', '') + } + }, [school_not_found]) + + const requiredRule = {required: '* Toto pole nemôže byť prázdne.'} + return ( +
+ + + + {school_not_found && ( + + )} + id !== 13 || without_school)} + disabled={!gradeItems.length || without_school} + rules={requiredRule} + /> +
+ ) +} diff --git a/src/components/SemesterAdministration/SemesterAdministration.tsx b/src/components/SemesterAdministration/SemesterAdministration.tsx index 78fd8a2b..329749c1 100644 --- a/src/components/SemesterAdministration/SemesterAdministration.tsx +++ b/src/components/SemesterAdministration/SemesterAdministration.tsx @@ -1,3 +1,4 @@ +import {Stack} from '@mui/material' import {useQuery} from '@tanstack/react-query' import axios from 'axios' import {useRouter} from 'next/router' @@ -9,6 +10,7 @@ import {useHasPermissions} from '@/utils/useHasPermissions' import {Button, Link} from '../Clickable/Clickable' import {Loading} from '../Loading/Loading' import styles from '../Problems/Problems.module.scss' +import {PublicationUploader} from '../PublicationUploader/PublicationUploader' import {Result} from '../Results/ResultsRow' interface PostalCard { @@ -125,6 +127,12 @@ export const SemesterAdministration: FC = () => { ) : ( <> )} + +

Nahrávanie časopisov

+ {[1, 2, 3].map((order) => ( + + ))} +
) } diff --git a/src/pages/strom/profil/uprava.tsx b/src/pages/strom/profil/uprava.tsx new file mode 100644 index 00000000..fd1f514e --- /dev/null +++ b/src/pages/strom/profil/uprava.tsx @@ -0,0 +1,12 @@ +import {NextPage} from 'next' + +import {PageLayout} from '@/components/PageLayout/PageLayout' +import {ProfileForm} from '@/components/Profile/ProfileForm' + +const Profil: NextPage = () => ( + + + +) + +export default Profil diff --git a/src/types/api/competition.ts b/src/types/api/competition.ts index fc7243f2..45875149 100644 --- a/src/types/api/competition.ts +++ b/src/types/api/competition.ts @@ -13,6 +13,7 @@ export interface Publication { file: string publication_type: number | null event: number | null + order: number | null } export interface RegistrationLink { diff --git a/src/types/api/generated/competition.ts b/src/types/api/generated/competition.ts index 6dcb24af..62c5f460 100644 --- a/src/types/api/generated/competition.ts +++ b/src/types/api/generated/competition.ts @@ -4,6 +4,7 @@ export interface Publication { file: any publication_type?: any | null event?: any | null + order: number | null } export interface RegistrationLink { diff --git a/yarn.lock b/yarn.lock index 1e0ca0a7..5ad9f273 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10703,6 +10703,16 @@ __metadata: languageName: node linkType: hard +"usehooks-ts@npm:^2.9.1": + version: 2.9.1 + resolution: "usehooks-ts@npm:2.9.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 36f1e4142ce23bc019b81d2e93aefd7f2c350abcf255598c21627114a69a2f2f116b35dc3a353375f09c6e4c9b704a04f104e3d10e98280545c097feca66c30a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -10853,6 +10863,7 @@ __metadata: typed-scss-modules: ^7.1.0 typescript: ^5.0.4 unstated-next: ^1.1.0 + usehooks-ts: ^2.9.1 languageName: unknown linkType: soft From e00d5369be74dd2e625ccf731c19cfa6377f8f89 Mon Sep 17 00:00:00 2001 From: Michal Masrna Date: Sat, 11 Nov 2023 15:05:39 +0100 Subject: [PATCH 2/4] remove unused code upload is not in the side panel anymore --- src/components/Problems/Problems.tsx | 455 +++++++++++++-------------- 1 file changed, 223 insertions(+), 232 deletions(-) diff --git a/src/components/Problems/Problems.tsx b/src/components/Problems/Problems.tsx index 11784984..9e6b5291 100644 --- a/src/components/Problems/Problems.tsx +++ b/src/components/Problems/Problems.tsx @@ -1,232 +1,223 @@ -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' -import axios from 'axios' -import Image from 'next/image' -import {Dispatch, FC, SetStateAction, useState} from 'react' - -import {Button, Link} from '@/components/Clickable/Clickable' -import {Problem, SeriesWithProblems} from '@/types/api/competition' -import {useDataFromURL} from '@/utils/useDataFromURL' -import {useHasPermissions} from '@/utils/useHasPermissions' - -import {Latex} from '../Latex/Latex' -import {Loading} from '../Loading/Loading' -import {Discussion} from './Discussion' -import styles from './Problems.module.scss' -import {UploadProblemForm} from './UploadProblemForm' - -const Problem: FC<{ - problem: Problem - setDisplaySideContent: Dispatch< - SetStateAction<{ - type: string - problemId: number - problemNumber: number - problemSubmitted?: boolean - }> - > - registered: boolean - canRegister: boolean - canSubmit: boolean - invalidateSeriesQuery: () => Promise -}> = ({problem, registered, setDisplaySideContent, canSubmit, invalidateSeriesQuery}) => { - const handleDiscussionButtonClick = () => { - setDisplaySideContent((prevState) => { - if (prevState.type === 'discussion' && prevState.problemId === problem.id) { - return {type: '', problemId: -1, problemNumber: -1} - } else { - return {type: 'discussion', problemId: problem.id, problemNumber: problem.order} - } - }) - } - const handleUploadClick = () => { - setDisplayProblemUploadForm((prevState) => !prevState) - setDisplayActions(false) - } - - const [displayProblemUploadForm, setDisplayProblemUploadForm] = useState(false) - const [displayActions, setDisplayActions] = useState(true) - - return ( -
-

{problem.order}. ÚLOHA

- {problem.text} - {problem.image && ( -
- {`Obrázok -
- )} - {displayProblemUploadForm && ( - - )} - {displayActions && ( -
- {problem.solution_pdf && ( - - vzorové riešenie - - )} - {registered && ( - <> - - moje riešenie - - - opravené riešenie{!!problem.submitted?.corrected_solution && ` (${problem.submitted.score || '?'})`} - - - )} - - {registered && ( - - )} -
- )} -
- ) -} - -const overrideCycle = (prev: boolean | undefined) => { - if (prev === undefined) return true - if (prev === true) return false - return undefined -} - -export const Problems: FC = () => { - const {id, seminar, loading} = useDataFromURL() - - // used to display discussions - const [displaySideContent, setDisplaySideContent] = useState<{ - type: string - problemId: number - problemNumber: number - problemSubmitted?: boolean - }>({type: '', problemId: -1, problemNumber: -1, problemSubmitted: false}) - - const {data: seriesData, isLoading: seriesIsLoading} = useQuery({ - queryKey: ['competition', 'series', id.seriesId], - queryFn: () => axios.get(`/api/competition/series/${id.seriesId}`), - enabled: id.seriesId !== -1, - }) - const series = seriesData?.data - const problems = series?.problems ?? [] - // const semesterId = series?.semester ?? -1 - const canSubmit = series?.can_submit ?? false - - const [overrideCanRegister, setOverrideCanRegister] = useState() - const [overrideIsRegistered, setOverrideIsRegistered] = useState() - const toggleCanRegister = () => setOverrideCanRegister((prevState) => overrideCycle(prevState)) - const toggleIsRegistered = () => setOverrideIsRegistered((prevState) => overrideCycle(prevState)) - - const canRegister = overrideCanRegister ?? series?.can_participate ?? false - const isRegistered = overrideIsRegistered ?? series?.is_registered ?? false - - const queryClient = useQueryClient() - - const invalidateSeriesQuery = () => queryClient.invalidateQueries({queryKey: ['competition', 'series', id.seriesId]}) - - const {mutate: registerToSemester} = useMutation({ - mutationFn: (id: number) => axios.post(`/api/competition/event/${id}/register`), - onSuccess: () => { - // refetch semestra, nech sa aktualizuje is_registered - invalidateSeriesQuery() - }, - }) - - const {hasPermissions, permissionsIsLoading} = useHasPermissions() - - return ( - <> -
- {(loading.semesterListIsLoading || - loading.currentSeriesIsLoading || - seriesIsLoading || - permissionsIsLoading) && } - {hasPermissions && ( -
- Admin: Opravovanie -
- )} - {problems.map((problem) => ( - - ))} - - {/* TODO: odstranit z produkcie */} -
- debug sekcia: -
- - - {' '} - {overrideIsRegistered === undefined ? 'no override' : overrideIsRegistered ? 'on' : 'off'} - -
-
- - - {' '} - {overrideCanRegister === undefined ? 'no override' : overrideCanRegister ? 'on' : 'off'} - -
-
-
- -
- {!isRegistered && canRegister ? ( -
registerToSemester(id.semesterId)} className={styles.registerButton}> - Chcem riešiť! -
- ) : ( - // sideCointainer grid rata s tymto childom, aj ked prazdnym -
- )} - {displaySideContent.type === 'discussion' && ( - setDisplaySideContent({type: '', problemId: -1, problemNumber: -1})} - invalidateSeriesQuery={invalidateSeriesQuery} - /> - )} - {/* {displaySideContent.type === 'uploadProblemForm' && ( - - )} */} -
- - ) -} +import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' +import axios from 'axios' +import Image from 'next/image' +import {Dispatch, FC, SetStateAction, useState} from 'react' + +import {Button, Link} from '@/components/Clickable/Clickable' +import {Problem, SeriesWithProblems} from '@/types/api/competition' +import {useDataFromURL} from '@/utils/useDataFromURL' +import {useHasPermissions} from '@/utils/useHasPermissions' + +import {Latex} from '../Latex/Latex' +import {Loading} from '../Loading/Loading' +import {Discussion} from './Discussion' +import styles from './Problems.module.scss' +import {UploadProblemForm} from './UploadProblemForm' + +const Problem: FC<{ + problem: Problem + setDisplaySideContent: Dispatch< + SetStateAction<{ + type: string + problemId: number + problemNumber: number + problemSubmitted?: boolean + }> + > + registered: boolean + canRegister: boolean + canSubmit: boolean + invalidateSeriesQuery: () => Promise +}> = ({problem, registered, setDisplaySideContent, canSubmit, invalidateSeriesQuery}) => { + const handleDiscussionButtonClick = () => { + setDisplaySideContent((prevState) => { + if (prevState.type === 'discussion' && prevState.problemId === problem.id) { + return {type: '', problemId: -1, problemNumber: -1} + } else { + return {type: 'discussion', problemId: problem.id, problemNumber: problem.order} + } + }) + } + const handleUploadClick = () => { + setDisplayProblemUploadForm((prevState) => !prevState) + setDisplayActions(false) + } + + const [displayProblemUploadForm, setDisplayProblemUploadForm] = useState(false) + const [displayActions, setDisplayActions] = useState(true) + + return ( +
+

{problem.order}. ÚLOHA

+ {problem.text} + {problem.image && ( +
+ {`Obrázok +
+ )} + {displayProblemUploadForm && ( + + )} + {displayActions && ( +
+ {problem.solution_pdf && ( + + vzorové riešenie + + )} + {registered && ( + <> + + moje riešenie + + + opravené riešenie{!!problem.submitted?.corrected_solution && ` (${problem.submitted.score || '?'})`} + + + )} + + {registered && ( + + )} +
+ )} +
+ ) +} + +const overrideCycle = (prev: boolean | undefined) => { + if (prev === undefined) return true + if (prev === true) return false + return undefined +} + +export const Problems: FC = () => { + const {id, seminar, loading} = useDataFromURL() + + // used to display discussions + const [displaySideContent, setDisplaySideContent] = useState<{ + type: string + problemId: number + problemNumber: number + problemSubmitted?: boolean + }>({type: '', problemId: -1, problemNumber: -1, problemSubmitted: false}) + + const {data: seriesData, isLoading: seriesIsLoading} = useQuery({ + queryKey: ['competition', 'series', id.seriesId], + queryFn: () => axios.get(`/api/competition/series/${id.seriesId}`), + enabled: id.seriesId !== -1, + }) + const series = seriesData?.data + const problems = series?.problems ?? [] + // const semesterId = series?.semester ?? -1 + const canSubmit = series?.can_submit ?? false + + const [overrideCanRegister, setOverrideCanRegister] = useState() + const [overrideIsRegistered, setOverrideIsRegistered] = useState() + const toggleCanRegister = () => setOverrideCanRegister((prevState) => overrideCycle(prevState)) + const toggleIsRegistered = () => setOverrideIsRegistered((prevState) => overrideCycle(prevState)) + + const canRegister = overrideCanRegister ?? series?.can_participate ?? false + const isRegistered = overrideIsRegistered ?? series?.is_registered ?? false + + const queryClient = useQueryClient() + + const invalidateSeriesQuery = () => queryClient.invalidateQueries({queryKey: ['competition', 'series', id.seriesId]}) + + const {mutate: registerToSemester} = useMutation({ + mutationFn: (id: number) => axios.post(`/api/competition/event/${id}/register`), + onSuccess: () => { + // refetch semestra, nech sa aktualizuje is_registered + invalidateSeriesQuery() + }, + }) + + const {hasPermissions, permissionsIsLoading} = useHasPermissions() + + return ( + <> +
+ {(loading.semesterListIsLoading || + loading.currentSeriesIsLoading || + seriesIsLoading || + permissionsIsLoading) && } + {hasPermissions && ( +
+ Admin: Opravovanie +
+ )} + {problems.map((problem) => ( + + ))} + + {/* TODO: odstranit z produkcie */} +
+ debug sekcia: +
+ + + {' '} + {overrideIsRegistered === undefined ? 'no override' : overrideIsRegistered ? 'on' : 'off'} + +
+
+ + + {' '} + {overrideCanRegister === undefined ? 'no override' : overrideCanRegister ? 'on' : 'off'} + +
+
+
+ +
+ {!isRegistered && canRegister ? ( +
registerToSemester(id.semesterId)} className={styles.registerButton}> + Chcem riešiť! +
+ ) : ( + // sideCointainer grid rata s tymto childom, aj ked prazdnym +
+ )} + {displaySideContent.type === 'discussion' && ( + setDisplaySideContent({type: '', problemId: -1, problemNumber: -1})} + invalidateSeriesQuery={invalidateSeriesQuery} + /> + )} +
+ + ) +} From 323a856a7cd82850bc2b870187b59d96b81f5f30 Mon Sep 17 00:00:00 2001 From: Michal Masrna Date: Sat, 11 Nov 2023 15:05:52 +0100 Subject: [PATCH 3/4] removed chcem riesit button --- src/components/Problems/Problems.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/components/Problems/Problems.tsx b/src/components/Problems/Problems.tsx index 9e6b5291..55ae2e77 100644 --- a/src/components/Problems/Problems.tsx +++ b/src/components/Problems/Problems.tsx @@ -199,16 +199,7 @@ export const Problems: FC = () => {
-
- {!isRegistered && canRegister ? ( -
registerToSemester(id.semesterId)} className={styles.registerButton}> - Chcem riešiť! -
- ) : ( - // sideCointainer grid rata s tymto childom, aj ked prazdnym -
- )} {displaySideContent.type === 'discussion' && ( Date: Sat, 11 Nov 2023 15:06:19 +0100 Subject: [PATCH 4/4] Register dialog when user not registered to semester tries to upload their first solution --- src/components/Problems/Problems.tsx | 62 +++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/components/Problems/Problems.tsx b/src/components/Problems/Problems.tsx index 55ae2e77..1c013973 100644 --- a/src/components/Problems/Problems.tsx +++ b/src/components/Problems/Problems.tsx @@ -1,13 +1,17 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import axios from 'axios' import Image from 'next/image' +import {useRouter} from 'next/router' import {Dispatch, FC, SetStateAction, useState} from 'react' import {Button, Link} from '@/components/Clickable/Clickable' import {Problem, SeriesWithProblems} from '@/types/api/competition' +import {Profile} from '@/types/api/personal' +import {AuthContainer} from '@/utils/AuthContainer' import {useDataFromURL} from '@/utils/useDataFromURL' import {useHasPermissions} from '@/utils/useHasPermissions' +import {Dialog} from '../Dialog/Dialog' import {Latex} from '../Latex/Latex' import {Loading} from '../Loading/Loading' import {Discussion} from './Discussion' @@ -28,7 +32,16 @@ const Problem: FC<{ canRegister: boolean canSubmit: boolean invalidateSeriesQuery: () => Promise -}> = ({problem, registered, setDisplaySideContent, canSubmit, invalidateSeriesQuery}) => { + displayRegisterDialog: () => void +}> = ({ + problem, + registered, + setDisplaySideContent, + canRegister, + canSubmit, + invalidateSeriesQuery, + displayRegisterDialog, +}) => { const handleDiscussionButtonClick = () => { setDisplaySideContent((prevState) => { if (prevState.type === 'discussion' && prevState.problemId === problem.id) { @@ -39,8 +52,12 @@ const Problem: FC<{ }) } const handleUploadClick = () => { - setDisplayProblemUploadForm((prevState) => !prevState) - setDisplayActions(false) + if (!registered && canRegister) { + displayRegisterDialog() + } else { + setDisplayProblemUploadForm((prevState) => !prevState) + setDisplayActions(false) + } } const [displayProblemUploadForm, setDisplayProblemUploadForm] = useState(false) @@ -96,7 +113,7 @@ const Problem: FC<{ )} - {registered && ( + {(registered || canRegister) && ( @@ -116,6 +133,17 @@ const overrideCycle = (prev: boolean | undefined) => { export const Problems: FC = () => { const {id, seminar, loading} = useDataFromURL() + const router = useRouter() + + const {isAuthed} = AuthContainer.useContainer() + + const {data} = useQuery({ + queryKey: ['personal', 'profiles', 'myprofile'], + queryFn: () => axios.get(`/api/personal/profiles/myprofile`), + enabled: isAuthed, + }) + const profile = data?.data + // used to display discussions const [displaySideContent, setDisplaySideContent] = useState<{ type: string @@ -131,7 +159,7 @@ export const Problems: FC = () => { }) const series = seriesData?.data const problems = series?.problems ?? [] - // const semesterId = series?.semester ?? -1 + const semesterId = series?.semester ?? -1 const canSubmit = series?.can_submit ?? false const [overrideCanRegister, setOverrideCanRegister] = useState() @@ -156,8 +184,31 @@ export const Problems: FC = () => { const {hasPermissions, permissionsIsLoading} = useHasPermissions() + const [deleteDialogId, setDeleteDialogId] = useState() + const close = () => setDeleteDialogId(undefined) + const editProfile = () => { + close() + router.push(`/${seminar}/profil/uprava`) + } + const agree = () => { + deleteDialogId !== undefined && registerToSemester(semesterId) + close() + } + return ( <> + + + + + } + />
{(loading.semesterListIsLoading || loading.currentSeriesIsLoading || @@ -177,6 +228,7 @@ export const Problems: FC = () => { canRegister={canRegister} canSubmit={canSubmit} invalidateSeriesQuery={invalidateSeriesQuery} + displayRegisterDialog={() => setDeleteDialogId(problem.id)} /> ))}