Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/xi.web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
@source "../../../packages/pages.email/src";
@source "../../../packages/pages.notes/src";
@source "../../../packages/pages.email-confirm/src";
@source "../../../packages/features.lesson.add/src";

@source "../../../node_modules/@xipkg";

Expand Down
30 changes: 30 additions & 0 deletions packages/features.lesson.add/README.md
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} />
</>
);
}
```
3 changes: 3 additions & 0 deletions packages/features.lesson.add/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import config from 'common.eslint';

export default config;
1 change: 1 addition & 0 deletions packages/features.lesson.add/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AddingLessonModal } from './src';
55 changes: 55 additions & 0 deletions packages/features.lesson.add/package.json
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"
}
2 changes: 2 additions & 0 deletions packages/features.lesson.add/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useAddingForm } from './useAddingForm';
export { useConstants } from './useConstants';
66 changes: 66 additions & 0 deletions packages/features.lesson.add/src/hooks/useAddingForm.ts
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);
};
Comment on lines +48 to +56
Copy link
Contributor

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?


return {
form,
control,
eventDate,
handleSubmit,
onSubmit,
handleClearForm,
};
};
24 changes: 24 additions & 0 deletions packages/features.lesson.add/src/hooks/useConstants.ts
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 };
};
3 changes: 3 additions & 0 deletions packages/features.lesson.add/src/index.ts
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';
48 changes: 48 additions & 0 deletions packages/features.lesson.add/src/model/formSchema.ts
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>;
1 change: 1 addition & 0 deletions packages/features.lesson.add/src/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { formSchema, type FormData } from './formSchema';
48 changes: 48 additions & 0 deletions packages/features.lesson.add/src/ui/AddingLessonModal.tsx
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}>
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
);
};
13 changes: 13 additions & 0 deletions packages/features.lesson.add/src/ui/AddingModal.css
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Возможно ли как-то иначе поправить проблему, например патчем самого компонента в xi.kit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

если проблема в том, что не выбирается дата в календаре, то это из-за того что модальное окно при появлении навешивает pointer-events: none на body
соответственно, когда открывается календарь нужно снимать pointer-events
тут как вариант можно завернуть daypicker в поповер или исправить сам daypicker что бы он себя вел как modal (навешивал/снимал pointer-events)

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;
}
Loading
Loading