-
Notifications
You must be signed in to change notification settings - Fork 0
19: Adding lesson modal #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: calendar
Are you sure you want to change the base?
Changes from all commits
52ac8f2
e235b75
5fc3233
cb6bbb6
36e5f99
2f58b45
33daa6b
7d92b66
6bf2ed1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Модуль создания счетов на оплату (features.invoice) | ||
|
|
||
| Модуль предназначен для создания и отправки счетов на оплату в приложении. | ||
|
|
||
| ## Структура модуля | ||
|
|
||
| ``` | ||
| src/ | ||
| ├── hooks/ - хуки для работы с формой счета и утилитарные хуки | ||
| ├── model/ - схемы и вспомогательные функции для формы счета | ||
| ├── types/ - типы TypeScript для модуля | ||
| ├── ui/ - React-компоненты UI (InvoiceModal, InputWithHelper и др.) | ||
| ├── locales/ - файлы локализации | ||
| ``` | ||
|
|
||
| ## Использование | ||
|
|
||
| ```tsx | ||
| import { InvoiceModal } from 'features.invoice'; | ||
|
|
||
| export default function Page() { | ||
| const [open, setOpen] = useState(false); | ||
| return ( | ||
| <> | ||
| <button onClick={() => setOpen(true)}>Создать счет</button> | ||
| <InvoiceModal open={open} onOpenChange={setOpen} /> | ||
| </> | ||
| ); | ||
| } | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import config from 'common.eslint'; | ||
|
|
||
| export default config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { AddingLessonModal } from './src'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| { | ||
| "name": "features.lesson.add", | ||
| "version": "0.0.0", | ||
| "type": "module", | ||
| "exports": { | ||
| ".": "./index.ts" | ||
| }, | ||
| "license": "MIT", | ||
| "scripts": { | ||
| "dev": "tsc --watch", | ||
| "lint": "eslint \"**/*.{ts,tsx}\"" | ||
| }, | ||
| "dependencies": { | ||
| "@tanstack/react-router": "1.120.11", | ||
| "@tanstack/react-query": "^5.73.3", | ||
| "sonner": "^1.4.0", | ||
| "common.services": "*", | ||
| "common.utils": "*", | ||
| "common.config": "*", | ||
| "common.api": "*", | ||
| "common.env": "*", | ||
| "common.types": "*", | ||
| "@xipkg/modal": "4.1.0", | ||
| "@xipkg/select": "2.2.5", | ||
| "@xipkg/utils": "1.8.0", | ||
| "@xipkg/form": "4.2.1", | ||
| "@xipkg/button": "3.2.0", | ||
| "@xipkg/input": "2.2.9", | ||
| "@xipkg/icons": "^2.5.4", | ||
| "@xipkg/datepicker": "2.2.0", | ||
| "@xipkg/inputmask": "2.0.12" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "^9.19.0", | ||
| "common.typescript": "*", | ||
| "common.eslint": "*", | ||
| "@types/node": "^20.3.1", | ||
| "@types/react": "^19.0.2", | ||
| "@types/react-dom": "^19.0.2", | ||
| "@xipkg/eslint": "3.2.0", | ||
| "@xipkg/tailwind": "0.8.1", | ||
| "@xipkg/typescript": "latest", | ||
| "eslint": "^9.19.0", | ||
| "eslint-plugin-react-hooks": "^5.0.0", | ||
| "eslint-plugin-react-refresh": "^0.4.18", | ||
| "globals": "^15.14.0", | ||
| "typescript": "~5.7.2", | ||
| "typescript-eslint": "^8.22.0" | ||
| }, | ||
| "peerDependencies": { | ||
| "react": "19" | ||
| }, | ||
| "description": "adding lesson feature", | ||
| "author": "xi.effect" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { useAddingForm } from './useAddingForm'; | ||
| export { useConstants } from './useConstants'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { useForm } from '@xipkg/form'; | ||
| import { zodResolver } from '@hookform/resolvers/zod'; | ||
| import { formSchema, type FormData } from '../model/formSchema'; | ||
| import { useFetchClassrooms } from 'common.services'; | ||
|
|
||
| const DEFAULT_VALUES: FormData = { | ||
| title: '', | ||
| description: '', | ||
| studentId: '', | ||
| startTime: '09:00', | ||
| endTime: '10:00', | ||
| startDate: new Date(), | ||
| shouldRepeat: 'dont_repeat', | ||
| }; | ||
|
|
||
| export const useAddingForm = () => { | ||
| const { data: classrooms } = useFetchClassrooms(); | ||
|
|
||
| const form = useForm<FormData>({ | ||
| resolver: zodResolver(formSchema), | ||
| mode: 'onSubmit', | ||
| defaultValues: DEFAULT_VALUES, | ||
| }); | ||
|
|
||
| const { control, handleSubmit, setValue, formState, watch } = form; | ||
| const eventDate = watch('startDate'); | ||
|
|
||
| const onSubmit = (data: FormData) => { | ||
| const student = classrooms?.find((c) => c.id === Number(data.studentId)); | ||
|
|
||
| const student_ids = student?.kind === 'individual' ? [student.student_id] : []; | ||
|
|
||
| const payload = { | ||
| title: data.title, | ||
| description: data.description || '', | ||
| student_ids, | ||
| startTime: data.startTime, | ||
| endTime: data.endTime, | ||
| startDate: data.startDate, | ||
| shouldRepeat: data.shouldRepeat, | ||
| }; | ||
|
|
||
| console.log('errors', formState.errors); | ||
|
|
||
| console.log('payload', payload); | ||
| }; | ||
|
|
||
| const handleClearForm = () => { | ||
| setValue('title', DEFAULT_VALUES.title); | ||
| setValue('description', DEFAULT_VALUES.description); | ||
| setValue('studentId', DEFAULT_VALUES.studentId); | ||
| setValue('startTime', DEFAULT_VALUES.startTime); | ||
| setValue('endTime', DEFAULT_VALUES.endTime); | ||
| setValue('startDate', DEFAULT_VALUES.startDate); | ||
| setValue('shouldRepeat', DEFAULT_VALUES.shouldRepeat); | ||
| }; | ||
|
|
||
| return { | ||
| form, | ||
| control, | ||
| eventDate, | ||
| handleSubmit, | ||
| onSubmit, | ||
| handleClearForm, | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { useMemo } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| type RepeatVariant = { | ||
| value: string; | ||
| label: string; | ||
| }; | ||
|
|
||
| export const useConstants = () => { | ||
| const { t } = useTranslation('calendar'); | ||
|
|
||
| const repeatVariants: RepeatVariant[] = useMemo(() => { | ||
| return [ | ||
| { value: 'dont_repeat', label: `${t('repeat_settings.dont_repeat')}` }, | ||
| { value: 'every_day', label: `${t('repeat_settings.every_day')}` }, | ||
| { value: 'every_work_day', label: `${t('repeat_settings.every_work_day')}` }, | ||
| { value: 'every_week', label: `${t('repeat_settings.every_week')}` }, | ||
| { value: 'every_2_weeks', label: `${t('repeat_settings.every_2_weeks')}` }, | ||
| { value: 'every_month', label: `${t('repeat_settings.every_month')}` }, | ||
| ]; | ||
| }, [t]); | ||
|
|
||
| return { repeatVariants }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export { AddingLessonModal } from './ui/AddingLessonModal'; | ||
| export * from './hooks'; | ||
| export * from './model'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import * as z from 'zod'; | ||
|
|
||
| const timeToMinutes = (time: string): number => { | ||
| const [hours, minutes] = time.split(':').map(Number); | ||
| return hours * 60 + minutes; | ||
| }; | ||
|
|
||
| // Валидация времени | ||
| const timeValidation = z.string().refine((time) => { | ||
| const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; | ||
| return timeRegex.test(time); | ||
| }, 'Неверный формат времени'); | ||
|
|
||
| export const formSchema = z | ||
| .object({ | ||
| title: z.string(), | ||
| description: z.string().optional(), | ||
| studentId: z.string().min(1, 'Выберите студента'), | ||
| startTime: timeValidation, | ||
| endTime: timeValidation, | ||
| startDate: z.date({ required_error: 'Укажите дату' }), | ||
| shouldRepeat: z | ||
| .enum([ | ||
| 'dont_repeat', | ||
| 'every_day', | ||
| 'every_work_day', | ||
| 'every_week', | ||
| 'every_2_weeks', | ||
| 'every_month', | ||
| ]) | ||
| .default('dont_repeat'), | ||
| }) | ||
| .refine( | ||
| (data) => { | ||
| if (data.startTime && data.endTime) { | ||
| const startMinutes = timeToMinutes(data.startTime); | ||
| const endMinutes = timeToMinutes(data.endTime); | ||
| return startMinutes <= endMinutes; | ||
| } | ||
| return true; | ||
| }, | ||
| { | ||
| message: 'Время начала не может быть позже времени окончания', | ||
| path: ['startTime'], | ||
| }, | ||
| ); | ||
|
|
||
| export type FormData = z.infer<typeof formSchema>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { formSchema, type FormData } from './formSchema'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { useState } from 'react'; | ||
| import { Modal, ModalContent, ModalFooter, ModalCloseButton } from '@xipkg/modal'; | ||
| import { Button } from '@xipkg/button'; | ||
| import { Close } from '@xipkg/icons'; | ||
|
|
||
| import { AddingForm } from './components/AddingForm'; | ||
| import { ModalContentWrapper } from './components/ModalContentWrapper'; | ||
|
|
||
| import './AddingModal.css'; | ||
|
|
||
| type AddingLessonModalProps = { | ||
| open: boolean; | ||
| onOpenChange: (open: boolean) => void; | ||
| }; | ||
|
|
||
| export const AddingLessonModal = ({ open, onOpenChange }: AddingLessonModalProps) => { | ||
| const [eventDate, setEventDate] = useState<Date>(new Date()); | ||
| const handleCloseModal = () => { | ||
| onOpenChange(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <Modal open={open} onOpenChange={handleCloseModal}> | ||
| <ModalContent className="z-30 h-screen max-w-[1100px] overflow-hidden p-4 sm:w-[700px] sm:overflow-auto lg:w-[1100px]"> | ||
| <ModalCloseButton> | ||
| <Close className="fill-gray-80 sm:fill-gray-0 dark:fill-gray-100" /> | ||
| </ModalCloseButton> | ||
| <ModalContentWrapper currentDay={eventDate}> | ||
| <AddingForm onClose={handleCloseModal} onDateChange={setEventDate}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Не совсем понимаю, зачем делать AddingForm как wrapper? Если дело в submit, то можно использовать id для формы |
||
| <ModalFooter className="flex flex-col-reverse items-center justify-center gap-4 p-0 sm:flex-row sm:justify-end sm:py-6"> | ||
| <Button | ||
| className="w-full rounded-2xl sm:w-[128px]" | ||
| size="m" | ||
| variant="secondary" | ||
| type="reset" | ||
| > | ||
| Отменить | ||
| </Button> | ||
| <Button className="w-full rounded-2xl sm:w-[128px]" type="submit" size="m"> | ||
| Назначить | ||
| </Button> | ||
| </ModalFooter> | ||
| </AddingForm> | ||
| </ModalContentWrapper> | ||
| </ModalContent> | ||
| </Modal> | ||
| ); | ||
| }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Возможно ли как-то иначе поправить проблему, например патчем самого компонента в xi.kit?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. если проблема в том, что не выбирается дата в календаре, то это из-за того что модальное окно при появлении навешивает pointer-events: none на body |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /* Делаем календарь в DatePicker кликабельным */ | ||
| [data-radix-popover-content], | ||
| [data-radix-popper-content-wrapper] { | ||
| pointer-events: auto !important; | ||
| } | ||
|
|
||
| /* Стили для всех элементов календаря */ | ||
| [data-radix-calendar], | ||
| [data-radix-calendar-cell], | ||
| [data-radix-calendar-day], | ||
| [data-radix-calendar-cell] button { | ||
| pointer-events: auto !important; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Мне не очень нравится повторение и вызов функции 7 раз подряд. А ты не смотрела на метод reset в react-hook-form?