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 (
+ <>
+
+
+ >
+ );
+ }
+
+ 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