From c0feea48e52fe17514b7e417745ccc73e5190b35 Mon Sep 17 00:00:00 2001 From: SD Date: Thu, 21 Nov 2024 17:03:49 +0400 Subject: [PATCH] feat: create rooms --- src/actions/hooks/useRoomDetails.ts | 42 +++ src/actions/rooms.ts | 109 ++++++ .../account/(simple)/rooms/[...slug]/page.tsx | 57 +++ .../[locale]/account/(simple)/rooms/page.tsx | 12 + src/components/Account/agora/Agora.tsx | 2 +- src/components/Account/agora/AgoraGroup.tsx | 54 +++ .../agora/components/UsersGroupPanel.tsx | 44 +++ .../Account/agora/components/index.ts | 1 + src/components/Account/agora/index.tsx | 16 + src/components/Account/index.ts | 1 + src/components/Account/rooms/CreateRoom.tsx | 46 +++ src/components/Account/rooms/EditRoomForm.tsx | 220 +++++++++++ src/components/Account/rooms/RoomDetails.tsx | 66 ++++ .../Account/rooms/RoomDetailsContent.tsx | 355 ++++++++++++++++++ src/components/Account/rooms/RoomsTabs.tsx | 173 +++++++++ src/components/Account/rooms/index.tsx | 6 + .../sessions/SessionDetailsContent.tsx | 6 +- src/components/Experts/AdditionalFilter.tsx | 2 - .../Modals/EditExpertEducationModal.tsx | 20 +- src/components/Modals/ScheduleModal.tsx | 28 +- src/components/Modals/UsersListModal.tsx | 113 ++++++ src/components/view/CustomDatePicker.tsx | 60 +++ src/i18nKeys/de.ts | 19 +- src/i18nKeys/en.ts | 19 +- src/i18nKeys/es.ts | 19 +- src/i18nKeys/fr.ts | 19 +- src/i18nKeys/it.ts | 19 +- src/i18nKeys/ru.ts | 19 +- src/styles/_default.scss | 1 + src/styles/_modal.scss | 7 + src/styles/_pages.scss | 4 + src/styles/sessions/_agora.scss | 53 ++- src/styles/sessions/_details.scss | 89 +++++ src/styles/view/_calendar.scss | 1 - src/styles/view/_datepicker.scss | 128 +++++++ src/styles/view/_room.scss | 86 +++++ src/styles/view/_select.scss | 3 +- src/styles/view/_timepicker.scss | 2 +- src/styles/view/style.scss | 2 + src/types/author.ts | 1 - src/types/rooms.ts | 44 +++ src/types/sessions.ts | 3 + src/utils/account.ts | 2 +- src/utils/locale.ts | 28 ++ 44 files changed, 1946 insertions(+), 55 deletions(-) create mode 100644 src/actions/hooks/useRoomDetails.ts create mode 100644 src/actions/rooms.ts create mode 100644 src/app/[locale]/account/(simple)/rooms/[...slug]/page.tsx create mode 100644 src/app/[locale]/account/(simple)/rooms/page.tsx create mode 100644 src/components/Account/agora/AgoraGroup.tsx create mode 100644 src/components/Account/agora/components/UsersGroupPanel.tsx create mode 100644 src/components/Account/rooms/CreateRoom.tsx create mode 100644 src/components/Account/rooms/EditRoomForm.tsx create mode 100644 src/components/Account/rooms/RoomDetails.tsx create mode 100644 src/components/Account/rooms/RoomDetailsContent.tsx create mode 100644 src/components/Account/rooms/RoomsTabs.tsx create mode 100644 src/components/Account/rooms/index.tsx create mode 100644 src/components/Modals/UsersListModal.tsx create mode 100644 src/components/view/CustomDatePicker.tsx create mode 100644 src/styles/view/_datepicker.scss create mode 100644 src/styles/view/_room.scss create mode 100644 src/types/rooms.ts create mode 100644 src/utils/locale.ts diff --git a/src/actions/hooks/useRoomDetails.ts b/src/actions/hooks/useRoomDetails.ts new file mode 100644 index 0000000..029337f --- /dev/null +++ b/src/actions/hooks/useRoomDetails.ts @@ -0,0 +1,42 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../constants/common'; +import { Room } from '../../types/rooms'; +import { getRoomDetails } from '../rooms'; + +export const useRoomDetails = (locale: string, roomId: number) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [room, setRoom] = useState(); + const [errorData, setErrorData] = useState(); + const [loading, setLoading] = useState(false); + + const fetchData = useCallback(() => { + setLoading(true); + setErrorData(undefined); + setRoom(undefined); + + getRoomDetails(locale, jwt, roomId) + .then((room) => { + setRoom(room); + }) + .catch((err) => { + setErrorData(err); + }) + .finally(() => { + setLoading(false); + }) + }, []); + + useEffect(() => { + fetchData(); + }, []); + + return { + fetchData, + loading, + room, + errorData + }; +}; diff --git a/src/actions/rooms.ts b/src/actions/rooms.ts new file mode 100644 index 0000000..dd8f03d --- /dev/null +++ b/src/actions/rooms.ts @@ -0,0 +1,109 @@ +import { apiRequest } from './helpers'; +import {GetUsersForRooms, Room, RoomEdit, RoomEditDTO} from '../types/rooms'; + +export const getUpcomingRooms = (locale: string, token: string): Promise => apiRequest({ + url: '/home/upcomingsessionsall', + method: 'post', + data: { + sessionType: 'room' + }, + locale, + token +}); + +export const getRecentRooms = (locale: string, token: string): Promise => apiRequest({ + url: '/home/historicalmeetings', + method: 'post', + data: { + sessionType: 'room' + }, + locale, + token +}); + +export const getRoomDetails = (locale: string, token: string, id: number): Promise => apiRequest({ + url: '/home/room', + method: 'post', + data: { id }, + locale, + token +}); + +export const deleteRoomClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise => apiRequest({ + url: '/home/deleteclientfromroom', + method: 'post', + data, + locale, + token +}); + +export const deleteRoomSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise => apiRequest({ + url: '/home/deletesupervisorfromroom', + method: 'post', + data, + locale, + token +}); + +export const becomeRoomClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise => apiRequest({ + url: '/home/becomeroomclient', + method: 'post', + data, + locale, + token +}); + +export const becomeRoomSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise => apiRequest({ + url: '/home/becomeroomsupervisor', + method: 'post', + data, + locale, + token +}); + +export const getUsersList = (locale: string, token: string, data: { template: string }): Promise => apiRequest({ + url: '/home/findusersforroom', + method: 'post', + data, + locale, + token +}); + +export const addClient = (locale: string, token: string, data: { sessionId: number, clientUserId: number }): Promise => apiRequest({ + url: '/home/addclienttoroom', + method: 'post', + data, + locale, + token +}); + +export const addSupervisor = (locale: string, token: string, data: { sessionId: number, supervisorUserId: number }): Promise => apiRequest({ + url: '/home/addsupervisortoroom', + method: 'post', + data, + locale, + token +}); + +export const createRoom = (locale: string, token: string): Promise => apiRequest({ + url: '/home/createroom', + method: 'post', + locale, + token +}); + +export const updateRoom = (locale: string, token: string, data: RoomEdit): Promise => apiRequest({ + url: '/home/updateroom', + method: 'post', + data, + locale, + token +}); + +export const getRoomById = (locale: string, token: string, id: number): Promise => apiRequest({ + url: '/home/getroomforedit', + method: 'post', + data: { id }, + locale, + token +}); diff --git a/src/app/[locale]/account/(simple)/rooms/[...slug]/page.tsx b/src/app/[locale]/account/(simple)/rooms/[...slug]/page.tsx new file mode 100644 index 0000000..b6f024c --- /dev/null +++ b/src/app/[locale]/account/(simple)/rooms/[...slug]/page.tsx @@ -0,0 +1,57 @@ +import React, { Suspense } from 'react'; +import { unstable_setRequestLocale } from 'next-intl/server'; +import { notFound } from 'next/navigation'; +import { AccountMenu, RoomDetails, RoomsTabs } from '../../../../../../components/Account'; +import { RoomsType } from '../../../../../../types/rooms'; + +const ROOMS_ROUTES = [RoomsType.UPCOMING, RoomsType.RECENT, RoomsType.NEW]; + +export async function generateStaticParams({ + params: { locale }, +}: { params: { locale: string } }) { + return [{ locale, slug: [RoomsType.UPCOMING] }]; +} + +export default function RoomsDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) { + unstable_setRequestLocale(locale); + const roomType: string = slug?.length > 0 && slug[0] || ''; + const roomId: number | null = slug?.length > 1 && Number(slug[1]) || null; + + if (!slug?.length || slug?.length > 2) { + notFound(); + } + + if (ROOMS_ROUTES.includes(roomType as RoomsType) && Number.isInteger(roomId)) { + return ( + Loading...

}> + +
+ ); + } + + if (ROOMS_ROUTES.includes(roomType as RoomsType) && !Number.isInteger(roomId)) { + return ( + <> +
+ +
+
+
+ Loading...

}> + +
+
+
+ + ); + } + + return notFound(); +}; diff --git a/src/app/[locale]/account/(simple)/rooms/page.tsx b/src/app/[locale]/account/(simple)/rooms/page.tsx new file mode 100644 index 0000000..b4ed706 --- /dev/null +++ b/src/app/[locale]/account/(simple)/rooms/page.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { redirect } from 'next/navigation'; +import { useLocalStorage } from '../../../../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../../../../constants/common'; +import { RoomsType } from '../../../../../types/rooms'; + +export default function RoomsMainPage() { + const [token] = useLocalStorage(AUTH_TOKEN_KEY, ''); + + return token ? redirect(`rooms/${RoomsType.UPCOMING}`) : null; +}; diff --git a/src/components/Account/agora/Agora.tsx b/src/components/Account/agora/Agora.tsx index 6be491d..79c5bbb 100644 --- a/src/components/Account/agora/Agora.tsx +++ b/src/components/Account/agora/Agora.tsx @@ -37,7 +37,7 @@ export const Agora = ({ sessionId, secret, stopCalling, remoteUser }: AgoraProps }; return ( -
+
void; +}; + +export const AgoraGroup = ({ roomId, secret, stopCalling }: AgoraProps) => { + const [calling, setCalling] = useState(false); + const [micOn, setMic] = useState(false); + const [cameraOn, setCamera] = useState(false); + + useEffect(() => { + setCalling(true); + }, []); + + useJoin( + { + appid: process.env.NEXT_PUBLIC_AGORA_APPID, + channel: `${roomId}-${secret}`, + token: null, + }, + calling, + ); + + const stop = () => { + stopCalling(); + setCalling(false); + }; + + return ( + <> +
+ +
+
+ setCamera(a => !a)} + setMic={() => setMic(a => !a)} + /> +
+ + ); +}; diff --git a/src/components/Account/agora/components/UsersGroupPanel.tsx b/src/components/Account/agora/components/UsersGroupPanel.tsx new file mode 100644 index 0000000..1823c3b --- /dev/null +++ b/src/components/Account/agora/components/UsersGroupPanel.tsx @@ -0,0 +1,44 @@ +import { + type IRemoteVideoTrack, + useIsConnected, useLocalCameraTrack, useLocalMicrophoneTrack, usePublish, + useRemoteAudioTracks, + useRemoteUsers, + useRemoteVideoTracks +} from 'agora-rtc-react'; +import { LocalUser } from './LocalUser'; +import { RemoteVideoPlayer } from './RemoteVideoPlayer'; + +type UsersGroupPanelProps = { + calling: boolean; + micOn: boolean; + cameraOn: boolean; +}; + +export const UsersGroupPanel = ({ calling, micOn, cameraOn }: UsersGroupPanelProps) => { + const isConnected = useIsConnected(); + const remoteUsers = useRemoteUsers(); + const { localMicrophoneTrack } = useLocalMicrophoneTrack(micOn); + const { localCameraTrack } = useLocalCameraTrack(cameraOn); + const { audioTracks } = useRemoteAudioTracks(remoteUsers); + + usePublish([localMicrophoneTrack, localCameraTrack]); + audioTracks.map(track => track.play()); + + return calling && isConnected && remoteUsers ? ( +
+
+ +
+ {remoteUsers.length > 0 && remoteUsers.map(({ uid, videoTrack }) => ( +
+ +
+ ))} +
+ ) : null; +} diff --git a/src/components/Account/agora/components/index.ts b/src/components/Account/agora/components/index.ts index 99a5a9d..5e48733 100644 --- a/src/components/Account/agora/components/index.ts +++ b/src/components/Account/agora/components/index.ts @@ -3,3 +3,4 @@ export * from './UserCover'; export * from './RemoteUsers'; export * from './LocalUserPanel'; export * from './RemoteUserPanel'; +export * from './UsersGroupPanel'; diff --git a/src/components/Account/agora/index.tsx b/src/components/Account/agora/index.tsx index cbca252..1daeaf3 100644 --- a/src/components/Account/agora/index.tsx +++ b/src/components/Account/agora/index.tsx @@ -2,7 +2,9 @@ import AgoraRTC, { AgoraRTCProvider } from 'agora-rtc-react'; import { Session } from '../../../types/sessions'; +import { Room } from '../../../types/rooms'; import { Agora } from './Agora'; +import { AgoraGroup } from './AgoraGroup'; export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Session, stopCalling: () => void, isCoach: boolean }) => { const remoteUser = isCoach ? (session?.clients?.length ? session?.clients[0] : undefined) : session?.coach; @@ -20,3 +22,17 @@ export const AgoraClient = ({ session, stopCalling, isCoach }: { session?: Sessi ) : null; }; + +export const AgoraClientGroup = ({ room, stopCalling }: { room?: Room, stopCalling: () => void }) => { + return room ? ( + + {room && ( + + )} + + ) : null; +}; diff --git a/src/components/Account/index.ts b/src/components/Account/index.ts index 8f56865..9ade85d 100644 --- a/src/components/Account/index.ts +++ b/src/components/Account/index.ts @@ -3,3 +3,4 @@ export { AccountMenu } from './AccountMenu'; export { ProfileSettings } from './ProfileSettings'; export * from './sessions'; +export * from './rooms'; diff --git a/src/components/Account/rooms/CreateRoom.tsx b/src/components/Account/rooms/CreateRoom.tsx new file mode 100644 index 0000000..f5fd592 --- /dev/null +++ b/src/components/Account/rooms/CreateRoom.tsx @@ -0,0 +1,46 @@ +'use client' + +import React, { useEffect, useState } from 'react'; +import { EditRoomForm } from './EditRoomForm'; +import debounce from 'lodash/debounce'; +import { createRoom } from '../../../actions/rooms'; +import { Loader } from '../../view/Loader'; +import { useRouter } from '../../../navigation'; +import { RoomsType } from '../../../types/rooms'; + + +export const CreateRoom = ({ locale, jwt }: { locale: string, jwt: string }) => { + const [roomId, setRoomId] = useState(); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const getRoom = debounce(() => { + setRoomId(2556); + // createRoom(locale, jwt) + // .then((data) => { + // setRoomId(data); + // }) + // .finally(() => { + // setLoading(false); + // }) + }, 500); + + useEffect(() => { + // setLoading(true); + getRoom(); + }, []); + + return ( + + {roomId && ( + router.push(`/account/rooms/${RoomsType.UPCOMING}`)} + /> + )} + + ) +}; diff --git a/src/components/Account/rooms/EditRoomForm.tsx b/src/components/Account/rooms/EditRoomForm.tsx new file mode 100644 index 0000000..4b826c8 --- /dev/null +++ b/src/components/Account/rooms/EditRoomForm.tsx @@ -0,0 +1,220 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Form, Input, notification } from 'antd'; +import dayjs, { Dayjs } from 'dayjs'; +import { i18nText } from '../../../i18nKeys'; +import { Tag } from '../../../types/tags'; +import { Slot } from '../../../types/experts'; +import { RoomEdit, RoomEditDTO } from '../../../types/rooms'; +import { getRoomById, updateRoom } from '../../../actions/rooms'; +import { Loader } from '../../view/Loader'; +import { CustomInput } from '../../view/CustomInput'; +import { CustomSelect } from '../../view/CustomSelect'; +import { CustomSwitch } from '../../view/CustomSwitch'; +import { CustomMultiSelect } from '../../view/CustomMultiSelect'; +import { CustomDatePicker } from '../../view/CustomDatePicker'; + +type EditRoomFormProps = { + roomId: number, + locale: string, + jwt: string, + mode: 'create' | 'edit'; + afterSubmit?: () => void; +} + +type RoomFormState = { + title?: string; + description?: string; + date?: Dayjs; + maxCount?: number; + startAt?: string; + supervisor?: boolean; + tags?: number[]; +}; + +export const EditRoomForm = ({ roomId, locale, jwt, mode, afterSubmit }: EditRoomFormProps) => { + const [form] = Form.useForm(); + const [editingRoom, setEditingRoom] = useState(); + const dateValue = Form.useWatch('date', form); + const [loading, setLoading] = useState(false); + const [fetchLoading, setFetchLoading] = useState(false); + + useEffect(() => { + setFetchLoading(true); + getRoomById(locale, jwt, roomId) + .then((data) => { + setEditingRoom(data); + const { item } = data || {}; + + if (mode === 'edit' && item) { + form.setFieldsValue({ + title: item.title, + description: item.description, + date: item?.scheduledStartAtUtc ? dayjs(item.scheduledStartAtUtc) : undefined, + maxCount: item.maxClients, + startAt: item?.scheduledStartAtUtc, + supervisor: item.isNeedSupervisor, + tags: item.tagIds || undefined + }) + } + }) + .finally(() => { + setFetchLoading(false); + }) + }, []); + + const getAvailableSlots = useCallback((): string[] => { + const dateList = new Set(); + if (editingRoom?.availableSlots) { + editingRoom.availableSlots.forEach(({ startTime }) => { + const [date] = startTime.split('T'); + dateList.add(date); + }); + + return Array.from(dateList); + } + + return []; + }, [editingRoom?.availableSlots]); + + const getTimeOptions = (slots?: Slot[], curDate?: Dayjs) => { + const date = curDate ? curDate.utc().format('YYYY-MM-DD') : ''; + if (slots && slots?.length && date) { + return slots.filter(({ startTime }) => startTime.indexOf(date) > -1) + .map(({ startTime, endTime }) => ({ value: startTime, label: `${dayjs(startTime).format('HH:mm')} - ${dayjs(endTime).format('HH:mm')}` })); + } + + return []; + } + + const getTagsOptions = (tags?: Tag[]) => { + if (tags) { + return tags.map(({ id, name }) => ({ value: id, label: {name} })) || []; + } + + return []; + } + + const onSubmit = () => { + setLoading(true); + const { title, description, startAt, maxCount, tags, supervisor } = form.getFieldsValue(); + const result: RoomEdit = { + ...editingRoom, + id: roomId, + title, + scheduledStartAtUtc: startAt, + maxClients: maxCount, + isNeedSupervisor: supervisor, + tagIds: tags || [] + }; + + if (description) { + result.description = description; + } + + updateRoom(locale, jwt, result) + .then(() => { + afterSubmit && afterSubmit(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + .finally(() => { + setLoading(false) + }); + } + + const disabledDate = (current: Dayjs) => current && !getAvailableSlots().includes(current.format('YYYY-MM-DD')); + + return ( + +
+ + + + + + +
+ + + + + + + + ({ value: i+1, label: i+1 }))} + /> + + + + +
+ + + + +
+
+ ); +}; diff --git a/src/components/Account/rooms/RoomDetails.tsx b/src/components/Account/rooms/RoomDetails.tsx new file mode 100644 index 0000000..d3d7a78 --- /dev/null +++ b/src/components/Account/rooms/RoomDetails.tsx @@ -0,0 +1,66 @@ +'use client' + +import React, { useState, useEffect } from 'react'; +import { RoomsType } from '../../../types/rooms'; +import { useSessionTracking } from '../../../actions/hooks/useSessionTracking'; +import { AccountMenu } from '../AccountMenu'; +import { Loader } from '../../view/Loader'; +import { RoomDetailsContent } from './RoomDetailsContent'; +import { useRoomDetails } from '../../../actions/hooks/useRoomDetails'; +import { AgoraClientGroup } from '../agora'; + +type RoomDetailsProps = { + locale: string; + roomId: number; + activeType: RoomsType; +}; + +export const RoomDetails = ({ roomId, locale, activeType }: RoomDetailsProps) => { + const { room, errorData, loading, fetchData } = useRoomDetails(locale, roomId); + const tracking = useSessionTracking(locale, roomId); + const [isCalling, setIsCalling] = useState(false); + + useEffect(() => { + if (isCalling) { + tracking.start(); + } else { + tracking.stop(); + } + }, [isCalling]); + + const stopCalling = () => { + setIsCalling(false); + fetchData(); + } + + return isCalling + ? ( + + ) : ( + <> +
+ +
+
+
+ + setIsCalling(true)} + refresh={fetchData} + /> + +
+
+ + ); +}; diff --git a/src/components/Account/rooms/RoomDetailsContent.tsx b/src/components/Account/rooms/RoomDetailsContent.tsx new file mode 100644 index 0000000..00dcb82 --- /dev/null +++ b/src/components/Account/rooms/RoomDetailsContent.tsx @@ -0,0 +1,355 @@ +'use client' + +import React, { useState } from 'react'; +import { Button, notification, Tag } from 'antd'; +import { DeleteOutlined, LeftOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; +import Image from 'next/image'; +import { useRouter } from '../../../navigation'; +import { Room, RoomsType } from '../../../types/rooms'; +import { i18nText } from '../../../i18nKeys'; +import { LinkButton } from '../../view/LinkButton'; +import { + addClient, + addSupervisor, + becomeRoomClient, + becomeRoomSupervisor, + deleteRoomClient, + deleteRoomSupervisor + } from '../../../actions/rooms'; +import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { UserListModal } from '../../Modals/UsersListModal'; +import { SessionState } from '../../../types/sessions'; +import { EditRoomForm } from './EditRoomForm'; + +type RoomDetailsContentProps = { + locale: string; + activeType: RoomsType; + room?: Room; + startRoom: () => void; + refresh: () => void; +}; + +export const RoomDetailsContent = ({ room, startRoom, locale, activeType, refresh }: RoomDetailsContentProps) => { + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const [userData] = useLocalStorage(AUTH_USER, ''); + const { id: userId = 0 } = userData ? JSON.parse(userData) : {}; + const router = useRouter(); + const [showModal, setShowModal] = useState(false); + const [forSupervisor, setForSupervisor] = useState(false); + const startDate = room?.scheduledStartAtUtc ? dayjs(room?.scheduledStartAtUtc).locale(locale) : null; + const endDate = room?.scheduledEndAtUtc ? dayjs(room?.scheduledEndAtUtc).locale(locale) : null; + const today = startDate ? dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD') : false; + const isCreator = room?.coach && room.coach.id === +userId || false; + const isSupervisor = room?.supervisor && room.supervisor.id === +userId || false; + const isClient = room?.clients && room.clients.length > 0 && room.clients.map(({ id }) => id).includes(+userId) || false; + const isTimeBeforeStart = room?.scheduledStartAtUtc ? dayjs() < dayjs(room.scheduledStartAtUtc) : false; + const [isEdit, setIsEdit] = useState(false); + + const goBack = () => router.push(`/account/rooms/${activeType}`); + + const checkUserApply = (): boolean => (!room?.supervisor || !isSupervisor) && (!room?.clients || room?.clients && room?.clients.length === 0 || !isClient); + + const deleteClient = (clientUserId: number) => { + if (room?.id) { + deleteRoomClient(locale, jwt, { sessionId: room.id, clientUserId }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } + }; + + const deleteSupervisor = (supervisorUserId?: number) => { + if (room?.id && supervisorUserId) { + deleteRoomSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + } + }; + + const becomeClient = () => { + if (room?.id && userId) { + becomeRoomClient(locale, jwt, { sessionId: room.id, clientUserId: +userId }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } + }; + + const becomeSupervisor = () => { + if (room?.id && userId) { + becomeRoomSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId: +userId }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } + }; + + const onInviteSupervisor = () => { + setForSupervisor(true) + setShowModal(true); + }; + + const onAddUser = (id: number) => { + if (room?.id) { + setShowModal(false); + + if (forSupervisor) { + addSupervisor(locale, jwt, { sessionId: room.id, supervisorUserId: id }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } else { + addClient(locale, jwt, { sessionId: room.id, clientUserId: id }) + .then(() => { + refresh(); + }) + .catch((err) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }); + } + } + }; + + const afterEditing = () => { + setIsEdit(false); + refresh(); + } + + return !isEdit ? ( +
+
+ +
+
{room?.title || ''}
+
+ {today + ? `${i18nText('today', locale)} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}` + : `${startDate?.format('D MMMM')} ${startDate?.format('HH:mm')} - ${endDate?.format('HH:mm')}`} +
+ {room?.themesTags && room.themesTags.length > 0 && ( +
+
+ {room.themesTags.map((skill) => {skill?.name})} +
+
+ )} + {room?.description &&
{room.description}
} + {activeType === RoomsType.UPCOMING && (isCreator || isSupervisor || isClient) && ( +
+ {(isCreator || isClient || isSupervisor) && ( + + )} + {isCreator && isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && ( + + )} +
+ )} +
+
+
{i18nText('room.roomCreator', locale)}
+
+
+
+
+ +
+
+
{`${room?.coach?.name} ${room?.coach?.surname || ''}`}
+
+
+
+
+ {room?.isNeedSupervisor && ( +
+
+
{i18nText('room.supervisor', locale)}
+
+ {room?.supervisor && ( +
+
+
+ +
+
+
{`${room?.supervisor?.name} ${room?.supervisor?.surname || ''}`}
+
+ {isCreator && activeType === RoomsType.UPCOMING && isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && ( + } + onClick={() => deleteSupervisor(room?.supervisor?.id)} + /> + )} +
+
+ )} + {room?.supervisor && activeType === RoomsType.RECENT && ( + <> + {room?.supervisorComment && ( +
{room.supervisorComment}
+ )} + + )} + {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !room?.supervisor && isCreator && activeType === RoomsType.UPCOMING && ( + + )} + {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !room?.supervisor && !isCreator && activeType === RoomsType.UPCOMING && checkUserApply() && ( + + )} + {!room?.supervisor && !isCreator && !checkUserApply() && ( +
{i18nText('noData', locale)}
+ )} +
+ )} +
+
+
{i18nText('room.participants', locale)}
+
{`${room?.clients?.length || 0}/${room?.maxClients}`}
+
+ {room?.clients && room?.clients?.length > 0 && ( +
+ {room.clients.map(({id, faceImageUrl, name, surname}) => ( +
+
+ +
+
+
{`${name} ${surname || ''}`}
+
+ {isCreator && room?.state === SessionState.COACH_APPROVED && activeType === RoomsType.UPCOMING && isTimeBeforeStart && ( + } + onClick={() => deleteClient(id)} + /> + )} +
+ ))} +
+ )} + {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && isCreator && activeType === RoomsType.UPCOMING && (!room?.clients || (room?.clients && room?.clients?.length < room.maxClients)) && ( + + )} + {isTimeBeforeStart && room?.state === SessionState.COACH_APPROVED && !isCreator && activeType === RoomsType.UPCOMING && (!room?.clients || (room?.clients && room?.clients?.length < room.maxClients)) && checkUserApply() && ( + + )} +
+ {room && ( + setShowModal(false)} + submit={onAddUser} + afterCloseModal={() => setForSupervisor(false)} + room={room} + /> + )} +
+ ) : ( +
+
+ +
+ +
+ ); +}; diff --git a/src/components/Account/rooms/RoomsTabs.tsx b/src/components/Account/rooms/RoomsTabs.tsx new file mode 100644 index 0000000..8f4dd7d --- /dev/null +++ b/src/components/Account/rooms/RoomsTabs.tsx @@ -0,0 +1,173 @@ +'use client'; + +import React, { MouseEvent, useCallback, useEffect, useState } from 'react'; +import { Empty, Space } from 'antd'; +import dayjs from 'dayjs'; +import 'dayjs/locale/ru'; +import 'dayjs/locale/en'; +import 'dayjs/locale/de'; +import 'dayjs/locale/it'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/es'; +import { RoomsType } from '../../../types/rooms'; +import { getRecentRooms, getUpcomingRooms } from '../../../actions/rooms'; +import { Loader } from '../../view/Loader'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { AUTH_TOKEN_KEY } from '../../../constants/common'; +import { usePathname, useRouter } from '../../../navigation'; +import { i18nText } from '../../../i18nKeys'; +import { CreateRoom } from './CreateRoom'; + +type RoomsTabsProps = { + locale: string; + activeTab: RoomsType; +}; + +export const RoomsTabs = ({ locale, activeTab }: RoomsTabsProps) => { + const [sort, setSort] = useState(); + const [rooms, setRooms] = useState(); + const [loading, setLoading] = useState(true); + const [errorData, setErrorData] = useState(); + const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); + const router = useRouter(); + const pathname = usePathname(); + + const fetchData = () => { + setErrorData(undefined); + setLoading(true); + Promise.all([ + getUpcomingRooms(locale, jwt), + getRecentRooms(locale, jwt) + ]) + .then(([upcoming, recent]) => { + setRooms({ + [RoomsType.UPCOMING]: upcoming || [], + [RoomsType.RECENT]: recent || [] + }); + }) + .catch((err) => { + setErrorData(err); + }) + .finally(() => { + setLoading(false); + }); + }; + + useEffect(() => { + fetchData(); + }, []); + + const onChangeSort = useCallback((value: string) => { + setSort(value); + }, [sort]); + + const onClickSession = (event: MouseEvent, id: number) => { + event.stopPropagation(); + event.preventDefault(); + router.push(`${pathname}/${id}`); + }; + + const getChildren = (list?: any[]) => ( + <> + {/*
+
+ +
+
*/} +
+ {list && list?.length > 0 ? list?.map(({ id, scheduledStartAtUtc, scheduledEndAtUtc, title, coach, clients, supervisor, maxClients }) => { + const startDate = dayjs(scheduledStartAtUtc).locale(locale); + const endDate = dayjs(scheduledEndAtUtc).locale(locale); + const today = dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD'); + + return ( +
) => onClickSession(e, id)}> +
+
+ +
+
+
+
{`${coach?.name} ${coach?.surname || ''}`}
+
{title}
+
+ {today + ? `${i18nText('today', locale)} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}` + : `${startDate.format('D MMMM')} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}`} +
+
+ {supervisor && ( + <> +
{i18nText('room.supervisor', locale)}
+
{`${supervisor?.name} ${supervisor?.surname || ''}`}
+ + )} +
{i18nText('room.members', locale)}
+
{`${clients.length}/${maxClients}`}
+
+
+
+
+
+ ) + }) : ( + + )} +
+ + ); + + const tabs = [ + { + key: RoomsType.UPCOMING, + label: ( + <> + {i18nText('room.upcoming', locale)} + {rooms?.upcoming && rooms?.upcoming?.length > 0 ? ({rooms?.upcoming.length}) : null} + + ), + children: getChildren(rooms?.upcoming) + }, + { + key: RoomsType.RECENT, + label: i18nText('room.recent', locale), + children: getChildren(rooms?.recent) + }, + { + key: RoomsType.NEW, + label: i18nText('room.newRoom', locale), + children: + } + ]; + + return ( + +
+ {tabs.map(({ key, label }) => ( + router.push(`/account/rooms/${key}`)} + > + {label} + + ))} +
+ {tabs.filter(({ key }) => key === activeTab)[0].children} +
+ ); +}; diff --git a/src/components/Account/rooms/index.tsx b/src/components/Account/rooms/index.tsx new file mode 100644 index 0000000..4441047 --- /dev/null +++ b/src/components/Account/rooms/index.tsx @@ -0,0 +1,6 @@ +'use client' + +export * from './RoomDetails'; +export * from './RoomsTabs'; +export * from './RoomDetailsContent'; +export * from './CreateRoom'; diff --git a/src/components/Account/sessions/SessionDetailsContent.tsx b/src/components/Account/sessions/SessionDetailsContent.tsx index f32622e..212b15c 100644 --- a/src/components/Account/sessions/SessionDetailsContent.tsx +++ b/src/components/Account/sessions/SessionDetailsContent.tsx @@ -81,7 +81,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio const CoachCard = (coach?: PublicUser) => coach ? (
- +
@@ -106,7 +106,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio
{session?.themesTags?.slice(0, 2).map((skill) => {skill?.name})} - {session?.themesTags?.length > 2 + {session?.themesTags && session?.themesTags?.length > 2 ? ( @@ -128,7 +128,7 @@ export const SessionDetailsContent = ({ session, locale, activeType, startSessio const StudentCard = (student?: PublicUser | null) => student ? (
- +
{`${student?.name} ${student?.surname || ''}`}
diff --git a/src/components/Experts/AdditionalFilter.tsx b/src/components/Experts/AdditionalFilter.tsx index 5b692ce..383c12d 100644 --- a/src/components/Experts/AdditionalFilter.tsx +++ b/src/components/Experts/AdditionalFilter.tsx @@ -55,8 +55,6 @@ export const ExpertsAdditionalFilter = ({ }; const search = getSearchParamsString(newFilter); - console.log('here1'); - router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`); // router.push({ diff --git a/src/components/Modals/EditExpertEducationModal.tsx b/src/components/Modals/EditExpertEducationModal.tsx index e2720e5..5d52663 100644 --- a/src/components/Modals/EditExpertEducationModal.tsx +++ b/src/components/Modals/EditExpertEducationModal.tsx @@ -1,20 +1,20 @@ 'use client'; -import React, { FC, useEffect, useState } from 'react'; -import {Modal, Button, message, Form, Collapse, GetProp, UploadProps} from 'antd'; +import React, { FC, useState } from 'react'; +import { Modal, Button, message, Form, Collapse } from 'antd'; import type { CollapseProps } from 'antd'; import { CloseOutlined } from '@ant-design/icons'; import { i18nText } from '../../i18nKeys'; -import { PracticePersonData, PracticeDTO, PracticeData, PracticeCase } from '../../types/practice'; +import { PracticePersonData } from '../../types/practice'; import { AUTH_TOKEN_KEY } from '../../constants/common'; import { useLocalStorage } from '../../hooks/useLocalStorage'; -import {setEducation} from '../../actions/profile'; -import {Certificate, Details, EducationData, EducationDTO, Experience} from "../../types/education"; -import {CertificatesContent} from "./educationModalContent/Certificates"; -import {EducationsContent} from "./educationModalContent/Educations"; -import {TrainingsContent} from "./educationModalContent/Trainings"; -import {MbasContent} from "./educationModalContent/Mbas"; -import {ExperiencesContent} from "./educationModalContent/Experiences"; +import { setEducation } from '../../actions/profile'; +import { EducationData, EducationDTO } from '../../types/education'; +import { CertificatesContent } from './educationModalContent/Certificates'; +import { EducationsContent } from './educationModalContent/Educations'; +import { TrainingsContent } from './educationModalContent/Trainings'; +import { MbasContent } from './educationModalContent/Mbas'; +import { ExperiencesContent } from './educationModalContent/Experiences'; type EditExpertEducationModalProps = { open: boolean; diff --git a/src/components/Modals/ScheduleModal.tsx b/src/components/Modals/ScheduleModal.tsx index 93ede9c..5397223 100644 --- a/src/components/Modals/ScheduleModal.tsx +++ b/src/components/Modals/ScheduleModal.tsx @@ -6,12 +6,6 @@ import { Modal, Menu, Calendar, Radio, Button, Input, message, Form } from 'antd import type { CalendarProps, MenuProps } from 'antd'; import { ArrowLeftOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons'; -import locale_ru from 'antd/lib/calendar/locale/ru_RU'; -import locale_en from 'antd/lib/calendar/locale/en_GB'; -import locale_de from 'antd/lib/calendar/locale/de_DE'; -import locale_it from 'antd/lib/calendar/locale/it_IT'; -import locale_es from 'antd/lib/calendar/locale/es_ES'; -import locale_fr from 'antd/lib/calendar/locale/fr_FR'; import dayjs, { Dayjs } from 'dayjs'; import 'dayjs/locale/ru'; import 'dayjs/locale/en'; @@ -19,6 +13,7 @@ import 'dayjs/locale/de'; import 'dayjs/locale/it'; import 'dayjs/locale/fr'; import 'dayjs/locale/es'; +import { getLocale } from '../../utils/locale'; import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common'; import { ExpertScheduler, SignupSessionData } from '../../types/experts'; import { Tag } from '../../types/tags'; @@ -42,27 +37,6 @@ type ScheduleModalProps = { type MenuItem = Required['items'][number]; -const getLocale = (locale: string) => { - if (locale) { - switch (locale) { - case 'ru': - return locale_ru; - case 'de': - return locale_de; - case 'fr': - return locale_fr; - case 'it': - return locale_it; - case 'es': - return locale_es; - default: - return locale_en; - } - } - - return locale_en; -}; - const getCalendarMenu = (start: Dayjs): MenuItem[] => Array.from({ length: 3 }) .map((_: unknown, index: number) => { const date = index ? start.add(index, 'M') : start.clone(); diff --git a/src/components/Modals/UsersListModal.tsx b/src/components/Modals/UsersListModal.tsx new file mode 100644 index 0000000..f81443d --- /dev/null +++ b/src/components/Modals/UsersListModal.tsx @@ -0,0 +1,113 @@ +'use client'; + +import React, { useCallback, useState } from 'react'; +import { Button, Modal, notification } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import debounce from 'lodash/debounce'; +import Image from 'next/image'; +import { i18nText } from '../../i18nKeys'; +import { getUsersList } from '../../actions/rooms'; +import { PublicUser } from '../../types/sessions'; +import { Room } from '../../types/rooms'; +import { CustomInput } from '../view/CustomInput'; +import { Loader } from '../view/Loader'; + +type UserListModalProps = { + room: Room; + isOpen: boolean; + locale: string; + handleCancel: () => void; + jwt: string; + submit: (id: number) => void; + afterCloseModal?: () => void; +}; + +export const UserListModal = ({ room, isOpen, locale, handleCancel, jwt, submit, afterCloseModal }: UserListModalProps) => { + const [users, setUsers] = useState(); + const [loading, seLoading] = useState(false); + + const onSearch = useCallback(debounce((e: any) => { + if (e?.target?.value) { + seLoading(true); + getUsersList(locale, jwt, { template: e.target.value }) + .then(({ items }) => { + const clients = room?.clients?.map(({ id }) => id); + setUsers(items + ? items.filter(({ id }) => !(clients?.length && clients.includes(id) || id === room?.supervisor?.id || id === room?.coach?.id)) + : undefined); + }) + .catch((err: any) => { + notification.error({ + message: 'Error', + description: err?.response?.data?.errMessage + }); + }) + .finally(() => { + seLoading(false); + }); + } else { + setUsers(undefined); + } + + }, 300), []); + + const onAfterClose = () => { + setUsers(undefined); + if (afterCloseModal) afterCloseModal(); + } + + return ( + } + afterClose={onAfterClose} + > +
+ + {users && ( +
+ + {users.length > 0 ? ( +
+ {users.map(({ id, name, surname, faceImageUrl }) => ( +
+
+
+ +
+
+
{`${name} ${surname || ''}`}
+
+
+ +
+ ))} +
+ ) : ( +
{i18nText('noData', locale)}
+ )} +
+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/src/components/view/CustomDatePicker.tsx b/src/components/view/CustomDatePicker.tsx new file mode 100644 index 0000000..96361a6 --- /dev/null +++ b/src/components/view/CustomDatePicker.tsx @@ -0,0 +1,60 @@ +'use client' + +import React, { useEffect, useState } from 'react'; +import { DatePicker } from 'antd'; +import { CalendarOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; +import 'dayjs/locale/ru'; +import 'dayjs/locale/en'; +import 'dayjs/locale/de'; +import 'dayjs/locale/it'; +import 'dayjs/locale/fr'; +import 'dayjs/locale/es'; +import { getLocale } from '../../utils/locale'; + +export const CustomDatePicker = (props: any) => { + const { label, value, locale, ...other } = props; + const [isActiveLabel, setIsActiveLabel] = useState(false); + + dayjs.locale(locale); + + useEffect(() => { + if (label) { + setIsActiveLabel(!!value); + } else { + setIsActiveLabel(false); + } + }, [value]); + + const onOpenChange = (open: boolean) => { + if (open) { + if (!isActiveLabel) setIsActiveLabel(true) + } else { + setIsActiveLabel(!!value) + } + }; + + return ( +
+
+ {label} +
+ } + {...other} + /> +
+ ); +}; diff --git a/src/i18nKeys/de.ts b/src/i18nKeys/de.ts index b34ec39..53e2986 100644 --- a/src/i18nKeys/de.ts +++ b/src/i18nKeys/de.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Kommende & letzte Sitzungen', + rooms: 'Zimmer', notifications: 'Benachrichtigung', support: 'Hilfe & Support', information: 'Rechtliche Informationen', @@ -48,7 +49,23 @@ export default { upcoming: 'Zukünftige Räume', requested: 'Angeforderte Räume', recent: 'Kürzliche Räume', - newRoom: 'Neuer Raum' + newRoom: 'Neuer Raum', + editRoom: 'Raum bearbeiten', + date: 'Datum', + time: 'Zeit', + maxParticipants: 'Max. erlaubte Teilnehmer', + presenceOfSupervisor: 'Anwesenheit eines Supervisors', + supervisor: 'Supervisor', + members: 'Mitglieder', + participants: 'Teilnehmer', + roomCreator: 'Raum-Ersteller', + inviteSupervisor: 'Supervisor einladen', + joinSupervisor: 'Als Supervisor beitreten', + inviteParticipant: 'Teilnehmer einladen', + joinParticipant: 'Als Teilnehmer beitreten', + rapport: 'Rapport', + invite: 'Invite', + save: 'Raum speichern' }, agreementText: 'Folgendes habe ich gelesen und erkläre mich damit einverstanden: Benutzervereinbarung,', userAgreement: 'Benutzervereinbarung', diff --git a/src/i18nKeys/en.ts b/src/i18nKeys/en.ts index e8f55da..bc6b080 100644 --- a/src/i18nKeys/en.ts +++ b/src/i18nKeys/en.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Upcoming & Recent Sessions', + rooms: 'Rooms', notifications: 'Notification', support: 'Help & Support', information: 'Legal Information', @@ -48,7 +49,23 @@ export default { upcoming: 'Upcoming Rooms', requested: 'Rooms Requested', recent: 'Recent Rooms', - newRoom: 'New Room' + newRoom: 'New Room', + editRoom: 'Edit Room', + date: 'Date', + time: 'Time', + maxParticipants: 'Max Participants Allowed', + presenceOfSupervisor: 'Presence of a Supervisor', + supervisor: 'Supervisor', + members: 'Members', + participants: 'Participants', + roomCreator: 'Room Creator', + inviteSupervisor: 'Invite Supervisor', + joinSupervisor: 'Join As A Supervisor', + inviteParticipant: 'Invite Participant', + joinParticipant: 'Join as a participant', + rapport: 'Rapport', + invite: 'Invite', + save: 'Save room' }, agreementText: 'I have read and agree with the terms of the User Agreement,', userAgreement: 'User Agreement', diff --git a/src/i18nKeys/es.ts b/src/i18nKeys/es.ts index 16b9dfd..2922e94 100644 --- a/src/i18nKeys/es.ts +++ b/src/i18nKeys/es.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Próximas y recientes sesiones', + rooms: 'Habitaciones', notifications: 'Notificación', support: 'Ayuda y asistencia', information: 'Información jurídica', @@ -48,7 +49,23 @@ export default { upcoming: 'Próximas salas', requested: 'Salas solicitadas', recent: 'Salas recientes', - newRoom: 'Nueva sala' + newRoom: 'Nueva sala', + editRoom: 'Editar la sala', + date: 'Fecha', + time: 'Tiempo', + maxParticipants: 'Máximo de participantes permitidos', + presenceOfSupervisor: 'Presencia de un supervisor', + supervisor: 'Supervisor', + members: 'Miembros', + participants: 'Participantes', + roomCreator: 'Creador de salas', + inviteSupervisor: 'Invitar al supervisor', + joinSupervisor: 'Unirse como supervisor', + inviteParticipant: 'Invitar a un participante', + joinParticipant: 'Unirse como participante', + rapport: 'Buena relación', + invite: 'Invitar', + save: 'Guardar sala' }, agreementText: 'He leído y acepto las condiciones del Acuerdo de usuario,', userAgreement: 'Acuerdo de usuario', diff --git a/src/i18nKeys/fr.ts b/src/i18nKeys/fr.ts index b4ee9e6..ab93f21 100644 --- a/src/i18nKeys/fr.ts +++ b/src/i18nKeys/fr.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Sessions futures et récentes', + rooms: 'Chambres', notifications: 'Notification', support: 'Aide et support', information: 'Informations légales', @@ -48,7 +49,23 @@ export default { upcoming: 'Salles futures', requested: 'Salles demandées', recent: 'Salles récentes', - newRoom: 'Nouvelle salle' + newRoom: 'Nouvelle salle', + editRoom: 'Modifier la salle', + date: 'Date', + time: 'Temps', + maxParticipants: 'Max de participants autorisés', + presenceOfSupervisor: 'Présence d\'un superviseur', + supervisor: 'Superviseur', + members: 'Membres', + participants: 'Participants', + roomCreator: 'Créateur de la salle', + inviteSupervisor: 'Inviter un superviseur', + joinSupervisor: 'Rejoindre en tant que superviseur', + inviteParticipant: 'Inviter un participant', + joinParticipant: 'Rejoindre en tant que participant', + rapport: 'Rapport', + invite: 'Inviter', + save: 'Sauvegarder la salle' }, agreementText: 'J\'ai lu et j\'accepte les dispositions de l\'Accord Utilisateur et de la', userAgreement: '', diff --git a/src/i18nKeys/it.ts b/src/i18nKeys/it.ts index 5633747..f2c17c6 100644 --- a/src/i18nKeys/it.ts +++ b/src/i18nKeys/it.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Prossime e recenti sessioni', + rooms: 'Stanze', notifications: 'Notifica', support: 'Assistenza e supporto', information: 'Informazioni legali', @@ -48,7 +49,23 @@ export default { upcoming: 'Prossime sale', requested: 'Sale richieste', recent: 'Sale recenti', - newRoom: 'Nuova sala' + newRoom: 'Nuova sala', + editRoom: 'Modifica sala', + date: 'Data', + time: 'Tempo', + maxParticipants: 'Numero massimo di partecipanti consentiti', + presenceOfSupervisor: 'Presenza di un relatore', + supervisor: 'Relatore', + members: 'Iscritti', + participants: 'Partecipanti', + roomCreator: 'Creatore sala', + inviteSupervisor: 'Invita relatore', + joinSupervisor: 'Partecipa come relatore', + inviteParticipant: 'Invita partecipante', + joinParticipant: 'Partecipa come partecipante', + rapport: 'Rapporto', + invite: 'Invita', + save: 'Salva sala' }, agreementText: 'Ho letto e accetto i termini dell\'Accordo con l\'utente,', userAgreement: '', diff --git a/src/i18nKeys/ru.ts b/src/i18nKeys/ru.ts index e8252b6..7ef3681 100644 --- a/src/i18nKeys/ru.ts +++ b/src/i18nKeys/ru.ts @@ -1,6 +1,7 @@ export default { accountMenu: { sessions: 'Предстоящие и недавние сессии', + rooms: 'Комнаты', notifications: 'Уведомления', support: 'Служба поддержки', information: 'Юридическая информация', @@ -48,7 +49,23 @@ export default { upcoming: 'Предстоящие комнаты', requested: 'Запрошенные комнаты', recent: 'Недавние комнаты', - newRoom: 'Новая комната' + newRoom: 'Новая комната', + editRoom: 'Изменить комнату', + date: 'Дата', + time: 'Время', + maxParticipants: 'Макс. кол-во участников', + presenceOfSupervisor: 'Присутствие супервизора', + supervisor: 'Супервайзер', + members: 'Участники', + participants: 'Участники', + roomCreator: 'Создатель комнаты', + inviteSupervisor: 'Пригласить супервизора', + joinSupervisor: 'Присоединиться как супервизор', + inviteParticipant: 'Пригласить участника', + joinParticipant: 'Присоединиться как участник', + rapport: 'Раппорт', + invite: 'Пригласить', + save: 'Сохранить комнату' }, agreementText: 'Я прочитал и согласен с условиями Пользовательского соглашения,', userAgreement: 'Пользовательского соглашения', diff --git a/src/styles/_default.scss b/src/styles/_default.scss index 93f42f9..c871c19 100644 --- a/src/styles/_default.scss +++ b/src/styles/_default.scss @@ -668,6 +668,7 @@ a { & > div { display: flex; gap: 4px; + padding-left: 1px; &:first-child { flex-direction: column; diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index 05be32d..f72e0a1 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -82,6 +82,13 @@ } } } + + &__users-list__content { + display: flex; + flex-direction: column; + padding: 40px; + gap: 24px; + } } .ant-modal-mask { diff --git a/src/styles/_pages.scss b/src/styles/_pages.scss index eb8eafc..4579515 100644 --- a/src/styles/_pages.scss +++ b/src/styles/_pages.scss @@ -931,6 +931,10 @@ &.chosen { color: #D93E5C; } + + &.history { + color: #c4c4c4; + } } } } diff --git a/src/styles/sessions/_agora.scss b/src/styles/sessions/_agora.scss index 4e49e64..3114ae6 100644 --- a/src/styles/sessions/_agora.scss +++ b/src/styles/sessions/_agora.scss @@ -2,9 +2,12 @@ &__wrap { width: 100%; height: 716px; - border-radius: 16px; position: relative; overflow: hidden; + + &__single { + border-radius: 16px; + } } &__container { @@ -25,6 +28,16 @@ justify-content: space-between; align-items: flex-end; z-index: 2; + + &_group { + width: 100%; + display: flex; + justify-content: center; + background: rgba(0, 59, 70, 0.4); + padding: 16px; + border-radius: 16px; + margin-top: 24px; + } } &__controls { @@ -126,6 +139,44 @@ position: absolute; display: flex; } + + &_groups { + width: 100%; + height: 100%; + display: flex; + flex-wrap: wrap; + gap: 16px; + justify-content: center; + + & > div { + border-radius: 16px; + overflow: hidden; + + video { + object-fit: contain !important; + } + } + + &.gr-1 { + & > div { + width: 100%; + } + } + + &.gr-2, &.gr-3, &.gr-4 { + & > div { + flex: calc((100% - 16px) / 2) 0; + } + } + + &.gr-5, &.gr-6, &.gr-7, &.gr-8, &.gr-9 { + flex: calc((100% - 16px) / 3) 0; + } + + &.gr-10, &.gr-11, &.gr-12, &.gr-13, &.gr-14, &.gr-15, &.gr-16 { + flex: calc((100% - 16px) / 4) 0; + } + } } &__video { diff --git a/src/styles/sessions/_details.scss b/src/styles/sessions/_details.scss index e8430da..c421f92 100644 --- a/src/styles/sessions/_details.scss +++ b/src/styles/sessions/_details.scss @@ -18,6 +18,11 @@ background: lightgray 50%; box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32); overflow: hidden; + + &_small { + width: 86px; + height: 86px; + } } &__inner { @@ -41,6 +46,17 @@ line-height: 120%; } + &__supervisor-comment { + width: 100%; + background: #E4F5FA; + padding: 8px; + border-radius: 0 8px 8px 8px; + color: #66A5AD; + @include rem(13); + font-weight: 500; + line-height: 120%; + } + &__comments { display: flex; flex-direction: column; @@ -200,6 +216,31 @@ } } + &__filled { + user-select: none; + outline: none !important; + border: none !important; + text-decoration: none; + cursor: pointer; + border-radius: 8px !important; + background: #66A5AD !important; + box-shadow: none !important; + display: flex; + height: 54px !important; + padding: 15px 24px; + justify-content: center; + align-items: center; + color: #fff !important; + @include rem(15); + font-style: normal; + font-weight: 400; + line-height: 160%; + + &:hover, &:active { + color: #fff !important; + } + } + &__header { display: flex; padding-bottom: 8px; @@ -268,6 +309,54 @@ overflow: hidden; } + &__profile { + display: flex; + flex-direction: column; + gap: 16px; + padding-top: 16px; + align-items: flex-start; + border-top: 1px solid #C4DFE6; + + &_title { + width: 100%; + gap: 16px; + display: flex; + justify-content: space-between; + + div { + @include rem(18); + font-weight: 600; + line-height: 150%; + color: #6FB98F; + + &:first-child { + color: #003B46; + } + } + } + + &_list { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; + } + + &_item { + display: flex; + gap: 16px; + justify-content: space-between; + + .card-detail__inner { + justify-content: center; + } + + .card-detail__name { + color: #2C7873; + } + } + } + &__footer { display: flex; justify-content: flex-end; diff --git a/src/styles/view/_calendar.scss b/src/styles/view/_calendar.scss index fe04d1e..3431bf4 100644 --- a/src/styles/view/_calendar.scss +++ b/src/styles/view/_calendar.scss @@ -48,7 +48,6 @@ opacity: 1 !important; background: transparent !important; } - } th, td { diff --git a/src/styles/view/_datepicker.scss b/src/styles/view/_datepicker.scss new file mode 100644 index 0000000..ede86fb --- /dev/null +++ b/src/styles/view/_datepicker.scss @@ -0,0 +1,128 @@ +.b-datepicker { + width: 100% !important; + height: 54px !important; + + &.ant-picker-filled { + background: transparent !important; + z-index: 1; + padding-top: 22px !important; + padding-left: 16px !important; + + &:hover { + border-color: #2c7873 !important; + } + + .ant-picker-input { + input { + font-size: 14px !important; + } + } + } + + .ant-picker-suffix { + margin-top: -20px; + } + + &-wrap { + position: relative; + width: 100%; + background-color: #F8F8F7; + border-radius: 8px; + + &.b-datepicker__active .b-datepicker-label { + font-size: 12px; + font-weight: 300; + line-height: 14px; + top: 8px; + } + } + + &-label { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; + color: #000; + opacity: .3; + position: absolute; + left: 16px; + top: 15px; + right: 22px; + z-index: 0; + transition: all .1s ease; + overflow: hidden; + text-overflow: ellipsis; + + span { + white-space: nowrap; + } + } + + &-popup { + padding: 16px !important; + + .ant-picker-date-panel { + padding: 16px 8px !important; + } + + .ant-picker-header-view { + color: #2c7873 !important; + } + + .ant-picker-header { + border: none !important; + + .ant-picker-header-super-prev-btn, .ant-picker-header-super-next-btn { + display: none !important; + } + } + + .ant-picker-cell { + opacity: 0 !important; + padding: 0 !important; + + &:not(.ant-picker-cell-disabled) { + color: #66A5AD !important; + + &:hover { + .ant-picker-cell-inner { + color: #6FB98F !important; + background: transparent !important; + } + } + } + + &-selected:not(.ant-picker-cell-disabled) .ant-picker-cell-inner { + color: #6FB98F !important; + background: transparent !important; + } + + &-disabled { + color: rgba(0, 0, 0, 0.25) !important; + + &::before { + background: transparent !important; + } + } + + &.ant-picker-cell-in-view { + opacity: 1 !important; + background: transparent !important; + } + + } + + .ant-picker-cell-inner::before { + border: none !important; + } + + th, td { + vertical-align: middle !important; + height: 36px !important; + } + + th { + color: #66A5AD !important; + } + } +} diff --git a/src/styles/view/_room.scss b/src/styles/view/_room.scss new file mode 100644 index 0000000..71c7087 --- /dev/null +++ b/src/styles/view/_room.scss @@ -0,0 +1,86 @@ +.card-room { + &__details { + width: 100%; + display: grid; + grid-template-columns: 120px auto; + gap: 4px 8px; + + div { + @include rem(13); + font-weight: 500; + line-height: 120%; + color: #2C7873; + + &:nth-child(2n) { + color: #6FB98F; + } + } + } +} + +.b-users-list { + width: 100%; + display: flex; + flex-direction: column; + gap: 24px; + padding: 0 16px; + + &__empty { + color: gray; + } + + &-item { + padding: 0 0 16px; + border-bottom: 1px solid #C4DFE6; + display: flex; + flex-direction: column; + gap: 16px; + + &:last-child { + border-bottom: none; + padding: 0; + } + + & > div { + display: flex; + gap: 16px; + align-items: center; + } + } +} + +.b-room-form { + &__grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; + align-items: center; + } + + .ant-form-item { + margin-bottom: 0 !important; + } + + .card-detail__apply { + align-self: flex-start; + } + + .b-room-switch { + label { + margin-right: 24px; + &:after { + display: none !important; + } + } + + & > div { + justify-content: space-between; + } + } +} + +.ant-select-item-option-content { + span { + text-transform: capitalize; + } +} \ No newline at end of file diff --git a/src/styles/view/_select.scss b/src/styles/view/_select.scss index 2bd47c1..fe853b6 100644 --- a/src/styles/view/_select.scss +++ b/src/styles/view/_select.scss @@ -8,6 +8,7 @@ border-radius: 8px !important; padding: 22px 16px 8px !important; box-shadow: none !important; + z-index: 1; .ant-select-selection-item { font-size: 15px !important; @@ -53,7 +54,7 @@ } &-label { - font-size: 15px; + font-size: 14px; font-style: normal; font-weight: 400; line-height: 24px; diff --git a/src/styles/view/_timepicker.scss b/src/styles/view/_timepicker.scss index 6d5cda3..e98e282 100644 --- a/src/styles/view/_timepicker.scss +++ b/src/styles/view/_timepicker.scss @@ -13,7 +13,7 @@ .ant-picker-input { input { - font-size: 15px !important; + font-size: 14px !important; } } } diff --git a/src/styles/view/style.scss b/src/styles/view/style.scss index 09086f3..4eb5a4d 100644 --- a/src/styles/view/style.scss +++ b/src/styles/view/style.scss @@ -9,6 +9,8 @@ @import "_practice.scss"; @import "_collapse.scss"; @import "_timepicker.scss"; +@import "_datepicker.scss"; @import "_calendar.scss"; @import "_schedule.scss"; @import "_radio.scss"; +@import "_room.scss"; diff --git a/src/types/author.ts b/src/types/author.ts index f836cc4..b36d83e 100644 --- a/src/types/author.ts +++ b/src/types/author.ts @@ -1,5 +1,4 @@ import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful' -import {BlogPostFields} from "./blogPost"; import {ContentImage} from "../lib/contentful/contentImage"; export interface AuthorFields { diff --git a/src/types/rooms.ts b/src/types/rooms.ts new file mode 100644 index 0000000..95f4bf3 --- /dev/null +++ b/src/types/rooms.ts @@ -0,0 +1,44 @@ +import { PublicUser, Session, SessionState } from './sessions'; +import { Tag } from './tags'; +import { Slot } from './experts'; + +export enum RoomsType { + UPCOMING = 'upcoming', + RECENT = 'recent', + NEW = 'new', +} + +export type Record = { + id: number; + sessionId: number; + sid?: string; + resourceId?: string; + readyForLoad?: boolean; + cname?: string; +} + +export type Room = Session & { recordings?: Record[] }; + +export type GetUsersForRooms = { + items?: PublicUser[], + isTooManyResults?: boolean; +} + +export type RoomEdit = { + id: number, + scheduledStartAtUtc?: string, + scheduledEndAtUtc?: string, + state?: SessionState, + cost?: number, + maxClients?: number, + title?: string, + description?: string, + isNeedSupervisor?: boolean, + tagIds?: number[] +}; + +export type RoomEditDTO = { + item: RoomEdit; + tags?: Tag[]; + availableSlots: Slot[]; +}; diff --git a/src/types/sessions.ts b/src/types/sessions.ts index 2c271ab..6d3a7d7 100644 --- a/src/types/sessions.ts +++ b/src/types/sessions.ts @@ -6,6 +6,8 @@ export type PublicUser = { name?: string; surname?: string; faceImageUrl?: string; + coachBotId?: number; + parentId?: number; }; // type User = { @@ -148,6 +150,7 @@ export type Session = { themesTags?: SessionTag[]; coachComments?: SessionComment[]; clientComments?: SessionComment[]; + creatorId?: number; }; export enum SessionType { diff --git a/src/utils/account.ts b/src/utils/account.ts index ca29021..aeb2ec1 100644 --- a/src/utils/account.ts +++ b/src/utils/account.ts @@ -2,7 +2,7 @@ import { message } from 'antd'; import type { UploadFile } from 'antd'; import { i18nText } from '../i18nKeys'; -const ROUTES = ['sessions', 'notifications', 'support', 'information', 'settings', 'messages', 'expert-profile']; +const ROUTES = ['sessions', 'rooms', 'notifications', 'support', 'information', 'settings', 'messages', 'expert-profile']; const COUNTS: Record = { sessions: 12, notifications: 5, diff --git a/src/utils/locale.ts b/src/utils/locale.ts new file mode 100644 index 0000000..596a35c --- /dev/null +++ b/src/utils/locale.ts @@ -0,0 +1,28 @@ +import locale_ru from 'antd/lib/calendar/locale/ru_RU'; +import locale_en from 'antd/lib/calendar/locale/en_GB'; +import locale_de from 'antd/lib/calendar/locale/de_DE'; +import locale_it from 'antd/lib/calendar/locale/it_IT'; +import locale_es from 'antd/lib/calendar/locale/es_ES'; +import locale_fr from 'antd/lib/calendar/locale/fr_FR'; + +// for calendars +export const getLocale = (locale: string) => { + if (locale) { + switch (locale) { + case 'ru': + return locale_ru; + case 'de': + return locale_de; + case 'fr': + return locale_fr; + case 'it': + return locale_it; + case 'es': + return locale_es; + default: + return locale_en; + } + } + + return locale_en; +}; \ No newline at end of file