feat: add sessions page, add delete modal, refactoring

This commit is contained in:
SD 2024-05-21 01:18:08 +04:00
parent ffbbeadb40
commit c2e29cbef3
21 changed files with 548 additions and 53 deletions

18
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@ant-design/cssinjs": "^1.18.1",
"@ant-design/icons": "^5.2.6",
"@ant-design/nextjs-registry": "^1.0.0",
"agora-rtc-react": "^2.1.0",
"antd": "^5.12.1",
"antd-img-crop": "^4.21.0",
"axios": "^1.6.5",
@ -873,6 +874,17 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agora-rtc-react": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/agora-rtc-react/-/agora-rtc-react-2.1.0.tgz",
"integrity": "sha512-3FGteA7FG51oK5MusbYNgAcKZaAQK+4sbEz4F0DPzcpDxqNANpocJDqOsmXoUAj5yDBsBZelmagU3abd++6RGA==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -5947,6 +5959,12 @@
"dev": true,
"requires": {}
},
"agora-rtc-react": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/agora-rtc-react/-/agora-rtc-react-2.1.0.tgz",
"integrity": "sha512-3FGteA7FG51oK5MusbYNgAcKZaAQK+4sbEz4F0DPzcpDxqNANpocJDqOsmXoUAj5yDBsBZelmagU3abd++6RGA==",
"requires": {}
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",

View File

@ -12,6 +12,7 @@
"@ant-design/cssinjs": "^1.18.1",
"@ant-design/icons": "^5.2.6",
"@ant-design/nextjs-registry": "^1.0.0",
"agora-rtc-react": "^2.1.0",
"antd": "^5.12.1",
"antd-img-crop": "^4.21.0",
"axios": "^1.6.5",

View File

@ -73,3 +73,16 @@ export const getRecentSessions = (locale: string, jwt: string, filter?: Sessions
}
)
);
export const getSessionDetails = (locale: string, jwt: string, id: number): Promise<AxiosResponse<Session>> => (
apiClient.post(
'/home/session',
{ id },
{
headers: {
'X-User-Language': locale,
Authorization: `Bearer ${jwt}`
}
}
)
);

View File

@ -1,7 +1,7 @@
import React, { Suspense } from 'react';
import type { Metadata } from 'next';
import { useTranslations } from 'next-intl';
import { SessionsTabs } from '../../../../../components/Account';
import { SessionsAll } from '../../../../../components/Account';
export const metadata: Metadata = {
title: 'Bbuddy - Account - Sessions',
@ -13,9 +13,7 @@ export default function Sessions({ params: { locale } }: { params: { locale: str
return (
<Suspense fallback={<p>Loading...</p>}>
<SessionsTabs
locale={locale}
/>
<SessionsAll locale={locale} />
</Suspense>
);
}

View File

@ -1,6 +1,6 @@
'use client';
import React from 'react';
import React, { useState } from 'react';
import styled from 'styled-components';
import { Button } from 'antd';
import { useSelectedLayoutSegment, usePathname } from 'next/navigation';
@ -8,6 +8,7 @@ import { Link } from '../../navigation';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common';
import { deleteStorageKey } from '../../hooks/useLocalStorage';
import { i18nText } from '../../i18nKeys';
import { DeleteAccountModal } from '../Modals/DeleteAccountModal';
const Logout = styled(Button)`
width: 100%;
@ -24,6 +25,7 @@ export const AccountMenu = ({ menu, locale }: { menu: { path: string, title: str
const selectedLayoutSegment = useSelectedLayoutSegment();
const pathname = selectedLayoutSegment || '';
const paths = usePathname();
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
const onLogout = () => {
deleteStorageKey(AUTH_TOKEN_KEY);
@ -31,9 +33,7 @@ export const AccountMenu = ({ menu, locale }: { menu: { path: string, title: str
window?.location?.replace(`/${paths.split('/')[1]}/`);
};
const onDeleteAccount = () => {
console.log('delete');
};
const onDeleteAccount = () => setShowDeleteModal(true);
return (
<ul className="list-sidebar">
@ -62,6 +62,10 @@ export const AccountMenu = ({ menu, locale }: { menu: { path: string, title: str
>
{i18nText('deleteAcc', locale)}
</Logout>
<DeleteAccountModal
open={showDeleteModal}
handleCancel={() => setShowDeleteModal(false)}
/>
</li>
</ul>
);

View File

@ -1,5 +1,5 @@
'use client'
export { AccountMenu } from './AccountMenu';
export { SessionsTabs } from './SessionsTabs';
export { ProfileSettings } from './ProfileSettings';
export * from './sessions';

View File

@ -0,0 +1,137 @@
'use client'
import React, { useCallback, useEffect, useState } from 'react';
import { Tag } from 'antd';
import { RightOutlined } from '@ant-design/icons';
import Image from 'next/image';
import dayjs from 'dayjs';
import { Link } from '../../../navigation';
import { i18nText } from '../../../i18nKeys';
import { getDuration, getPrice } from '../../../utils/expert';
import { Session } from '../../../types/sessions';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common';
import { getSessionDetails } from '../../../actions/profile';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { Loader } from '../../view/Loader';
type SessionDetailsProps = {
locale: string;
sessionId: number;
goBack: () => void;
};
export const SessionDetails = ({ sessionId, locale, goBack }: SessionDetailsProps) => {
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const [userId] = useLocalStorage(AUTH_USER, '');
const [loading, setLoading] = useState<boolean>(false);
const [errorData, setErrorData] = useState<any>();
const [session, setSession] = useState<Session>();
const fetchData = useCallback(() => {
setLoading(true);
setErrorData(undefined);
setSession(undefined);
getSessionDetails(locale, jwt, sessionId)
.then(({ data }) => {
console.log(data);
setSession(data);
})
.catch((err) => {
setErrorData(err);
})
.finally(() => {
setLoading(false);
})
}, []);
useEffect(() => {
fetchData();
}, [sessionId]);
const client = session?.clients?.length ? session?.clients[0] : null;
const current = +userId !== client?.id ? client : session?.coach;
const startDate = session?.scheduledStartAtUtc ? dayjs(session?.scheduledStartAtUtc).locale(locale) : null;
const endDate = session?.scheduledEndAtUtc ? dayjs(session?.scheduledEndAtUtc).locale(locale) : null;
const today = startDate ? dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD') : false;
return (
<Loader
isLoading={loading}
errorData={errorData}
refresh={fetchData}
>
<div className="card-detail">
<div>
<button onClick={goBack}>back</button>
</div>
<div className="card-detail__expert">
<div className="card-detail__portrait">
<Image src={current?.faceImageUrl || '/images/person.png'} width={140} height={140} alt="" />
</div>
<div className="card-detail__inner">
<Link href={`/experts/${current?.id}` as any} target="_blank">
<div className="card-detail__name">{`${current?.name} ${current?.surname || ''}`}</div>
</Link>
<div className="card-detail__info">
<div className="card-detail__lang">
{/* current?.coachLanguages?.map((lang) => (
<Tag key={lang} className="skills__list__item">{lang}</Tag>
)) */}
</div>
</div>
{/* <div className="card-profile__title">{current?.speciality}</div> */}
<div className="card-detail__coast">
{getPrice(session?.cost)} <span>/ {getDuration(locale, session?.totalDuration)}</span>
</div>
<div className={`card-detail__date${today ? ' chosen': ''}`}>
{today
? `${i18nText('today', locale)} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}`
: `${startDate.format('D MMMM')} ${startDate.format('HH:mm')} - ${endDate.format('HH:mm')}`}
</div>
<div className="card-detail__skills">
<div className="skills__list">
{session?.themesTags?.slice(0, 2).map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
{session?.themesTags?.length > 2
? (
<Tag className="skills__list__more">
<Link href={`/experts/${current?.id}` as any} target="_blank">
{`+${session?.themesTags?.length - 2}`}
</Link>
</Tag>
) : null }
</div>
</div>
{/* <div className="card-profile__subtitle">{current?.specialityDesc}</div>
<div className="card-profile__desc">{current?.description}</div> */}
<Link href={`/experts/${current?.id}` as any} target="_blank">
{i18nText('details', locale)}
<RightOutlined style={{ fontSize: '10px', padding: '0 7px' }}/>
</Link>
</div>
</div>
<div className="card-detail__actions">
<button>Start Session</button>
<button>Decline Session</button>
</div>
<div className="card-detail__comments">
<div className="card-detail__comments_header">
<div className="card-detail__comments_title">
My Comments
</div>
<button>Add new</button>
</div>
<div className="card-detail__comments_item">
Sed tincidunt finibus eros nec feugiat. Nulla facilisi. Nunc maximus magna et egestas tincidunt. Integer lobortis laoreet neque at sodales. Aenean eget risus pharetra, efficitur dolor ut, commodo lacus. Sed vitae nunc odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis et velit et dolor rutrum euismod a pretium est.
</div>
<div className="card-detail__comments_title">
Coach Comments
</div>
<div className="card-detail__comments_item">
Sed tincidunt finibus eros nec feugiat. Nulla facilisi. Nunc maximus magna et egestas tincidunt. Integer lobortis laoreet neque at sodales. Aenean eget risus pharetra, efficitur dolor ut, commodo lacus. Sed vitae nunc odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis et velit et dolor rutrum euismod a pretium est.
</div>
</div>
</div>
</Loader>
);
};

View File

@ -1,6 +1,6 @@
'use client';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState, MouseEvent } from 'react';
import { Empty, Space } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/ru';
@ -9,14 +9,19 @@ import 'dayjs/locale/de';
import 'dayjs/locale/it';
import 'dayjs/locale/fr';
import 'dayjs/locale/es';
import { Loader } from '../view/Loader';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common';
import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../actions/profile';
import { Session, Sessions, SessionType } from '../../types/sessions';
import { i18nText } from '../../i18nKeys';
import { Loader } from '../../view/Loader';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common';
import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../../actions/profile';
import { Session, Sessions, SessionType } from '../../../types/sessions';
import { i18nText } from '../../../i18nKeys';
export const SessionsTabs = ({ locale }: { locale: string }) => {
type SessionsTabsProps = {
locale: string;
updateSession: (val: number) => void;
};
export const SessionsTabs = ({ locale, updateSession }: SessionsTabsProps) => {
const [activeTab, setActiveTab] = useState<number>(0);
const [sort, setSort] = useState<string>();
const [sessions, setSessions] = useState<Sessions>();
@ -56,6 +61,12 @@ export const SessionsTabs = ({ locale }: { locale: string }) => {
setSort(value);
}, [sort]);
const onClickSession = (event: MouseEvent<HTMLDivElement>, id: number) => {
event.stopPropagation();
event.preventDefault();
updateSession(id);
};
const getChildren = (list?: Session[]) => (
<>
{/* <div className="filter-session">
@ -82,7 +93,7 @@ export const SessionsTabs = ({ locale }: { locale: string }) => {
const today = dayjs().format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD');
return (
<div key={id} className="card-profile session__item">
<div key={id} className="card-profile session__item" onClick={(e: MouseEvent<HTMLDivElement>) => onClickSession(e, id)}>
<div className="card-profile__header">
<div className="card-profile__header__portrait">
<img src={current?.faceImageUrl || '/images/person.png'} className="" alt="" />

View File

@ -0,0 +1,22 @@
'use client'
import React, { useState } from 'react';
import { SessionDetails } from './SessionDetails';
import { SessionsTabs } from './SessionsTabs';
export const SessionsAll = ({ locale }: { locale: string }) => {
const [customSession, setCustomSession] = useState<number | undefined>();
return customSession ? (
<SessionDetails
locale={locale}
sessionId={customSession}
goBack={() => setCustomSession(undefined)}
/>
) : (
<SessionsTabs
locale={locale}
updateSession={setCustomSession}
/>
);
};

View File

@ -9,6 +9,7 @@ import Image from 'next/image';
import { Link, useRouter } from '../../navigation';
import { ExpertsData, Filter, GeneralFilter } from '../../types/experts';
import { getObjectByFilter, getObjectByAdditionalFilter } from '../../utils/filter';
import { getDuration, getPrice } from '../../utils/expert';
import { getExpertsList } from '../../actions/experts';
import { CustomPagination } from '../view/CustomPagination';
import { CustomSpin } from '../view/CustomSpin';
@ -31,8 +32,6 @@ export const ExpertsList = ({
}: ExpertListProps) => {
const searchParams = useSearchParams();
const router = useRouter();
const getDuration = (value?: any): string => `${value || 0}${locale === 'ru' ? 'мин' : 'min'}`;
const getPrice = (value?: any): string => `${value || 0}`;
const [experts, setExperts] = useState<ExpertsData | undefined>();
const [loading, setLoading] = useState<boolean>(true);
@ -102,7 +101,7 @@ export const ExpertsList = ({
<div className="card-profile__header__name">{`${item.name} ${item?.surname || ''}`}</div>
</Link>
<div className="card-profile__header__price">
{getPrice(item?.sessionCost)} <span>/ {getDuration(item?.sessionDuration)}</span>
{getPrice(item?.sessionCost)} <span>/ {getDuration(locale, item?.sessionDuration)}</span>
</div>
</div>
<div className="card-profile__header__lang">

View File

@ -3,9 +3,8 @@
import React, { Dispatch, FC, SetStateAction, useEffect } from 'react';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { Modal as AntdModal, Form } from 'antd';
import { Modal, Form } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { styled } from 'styled-components';
import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
type AuthModalProps = {
@ -16,25 +15,6 @@ type AuthModalProps = {
updateToken: string | Dispatch<SetStateAction<string | undefined>> | undefined;
};
const Modal = styled(AntdModal)`
.ant-modal-content {
border-radius: 24px !important;
}
.ant-modal-close {
height: 64px !important;
width: 64px !important;
border-radius: 50% !important;
background: #fff !important;
top: -32px !important;
inset-inline-end: -32px !important;
&:active, &:hover {
background: #fff !important;
}
}
`;
export const AuthModal: FC<AuthModalProps> = ({
open,
handleCancel,
@ -63,6 +43,7 @@ export const AuthModal: FC<AuthModalProps> = ({
return (
<Modal
className="b-modal"
open={open}
title={undefined}
onOk={undefined}

View File

@ -0,0 +1,37 @@
'use client';
import React, { FC } from 'react';
import { Modal } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import Link from 'next/link';
type DeleteAccountModalProps = {
open: boolean;
handleCancel: () => void;
};
export const DeleteAccountModal: FC<DeleteAccountModalProps> = ({
open,
handleCancel
}) => (
<Modal
className="b-modal"
open={open}
title="Account deletion instructions"
onOk={undefined}
onCancel={handleCancel}
footer={false}
width={498}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
>
<div className="b-modal__content">
<p>
To delete your BBUDDY account, please send an email requesting account deletion to the following email address:
</p>
<Link href="mailto:info@bbuddy.expert">info@bbuddy.expert</Link>
<p>
Upon receiving the request, we will delete all existing data associated with your account within 24 hours.
</p>
</div>
</Modal>
);

View File

@ -586,7 +586,6 @@ a {
line-height: 160%;
}
//
.card-profile {
display: flex !important;
flex-direction: column;
@ -598,6 +597,7 @@ a {
&.session {
&__item {
margin: 0 !important;
cursor: pointer;
}
}

24
src/styles/_modal.scss Normal file
View File

@ -0,0 +1,24 @@
.b-modal {
.ant-modal-content {
border-radius: 24px !important;
}
.ant-modal-close {
height: 64px !important;
width: 64px !important;
border-radius: 50% !important;
background: #fff !important;
top: -32px !important;
inset-inline-end: -32px !important;
&:active, &:hover {
background: #fff !important;
}
}
&__content {
p {
margin: 16px 0;
}
}
}

View File

@ -767,9 +767,9 @@
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 8px 4px 8px;
padding: 6px 8px 4px;
border-radius: 16px;
background: #66A5AD;
background: #FFBD00;
color: $white;
@include rem(12);
font-style: normal;

View File

@ -0,0 +1,149 @@
.card-detail {
display: flex;
flex-direction: column;
gap: 16px;
&__expert {
border-block-end: 1px solid #C4DFE6;
display: flex;
gap: 16px;
padding: 0 0 16px;
}
&__portrait {
width: 140px;
height: 140px;
border-radius: 16px;
border: 2px solid #FFF;
background: lightgray 50%;
box-shadow: 0 8px 16px 0 rgba(102, 165, 173, 0.32);
overflow: hidden;
}
&__inner {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1 0 0;
}
&__actions {
display: flex;
gap: 16px;
justify-content: flex-start;
}
&__comments {
display: flex;
flex-direction: column;
gap: 16px;
&_header {
display: flex;
justify-content: space-between;
align-items: center;
}
&_title {
color: #6FB98F;
@include rem(18);
font-weight: 600;
line-height: 150%;
}
&_item {
padding: 16px;
background: #E4F5FA;
border-radius: 0 16px 16px 16px;
color: #66A5AD;
@include rem(13);
font-weight: 500;
line-height: 133.333%;
}
}
&__header {
display: flex;
padding-bottom: 8px;
align-items: center !important;
gap: 16px;
align-self: stretch;
margin-block-end: 0 !important;
img {
object-fit: cover;
width: 100%;
height: 100%;
display: block;
border-radius: 16px;
}
&__name {
overflow: hidden;
color: #003B46;
text-overflow: ellipsis;
white-space: nowrap;
@include rem(18);
font-style: normal;
font-weight: 600;
line-height: 133.333%;
}
&__price {
color: #6FB98F;
@include rem(15);
font-style: normal;
font-weight: 600;
line-height: 120%;
span {
color: #B7B7B7;
}
}
}
&__title {
color: #003B46;
@include rem(18);
font-style: normal;
font-weight: 600;
line-height: 133.333%;
}
&__subtitle {
color: #003B46;
@include rem(15);
font-style: normal;
font-weight: 600;
line-height: 120%;
}
&__desc {
color: #66A5AD;
@include rem(13);
font-style: normal;
font-weight: 500;
line-height: 123.077%;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
&__footer {
display: flex;
justify-content: flex-end;
align-items: center;
a {
text-decoration: none;
color: #FF8A00 !important;
@include rem(15);
font-style: normal;
font-weight: 400;
line-height: 160%;
display: inline-flex;
align-items: center;
}
}
}

View File

@ -0,0 +1 @@
@import "_details.scss";

View File

@ -14,8 +14,10 @@
@import "_form.scss";
@import "_message.scss";
@import "_auth-modal.scss";
@import "_modal.scss";
@import "./view/style.scss";
@import "./sessions/style.scss";

View File

@ -1,4 +1,6 @@
type User = {
import { Language } from './tags';
type PublicUser = {
id: number;
login?: string;
name?: string;
@ -6,6 +8,88 @@ type User = {
faceImageUrl?: string;
};
// type User = {
// "id": 0,
// "login": "string",
// "password": "string",
// "role": "coach",
// "name": "string",
// "surname": "string",
// "phone": "string",
// "faceImageId": 0,
// "createdAtUtc": "2024-05-15T14:26:19.310Z",
// "practiceHours": 0,
// "supervisionPerYearId": 0,
// "sessionDuration": 0,
// "sessionCost": 0,
// "adminRoles": "admin",
// "isCoachApproved": true,
// "isBanned": true,
// "faceImage": {
// "id": 0,
// "userId": 0,
// "descriptor": "string",
// "fileType": "string",
// "contentLength": 0,
// "state": "active",
// "createdAtUtc": "2024-05-15T14:26:19.310Z",
// "unusedSinceUtc": "2024-05-15T14:26:19.310Z"
// },
// "coachRating": 0,
// "isTestMode": true,
// "beneficiaryName": "string",
// "beneficiaryIban": "string",
// "beneficiaryBicOrSwift": "string",
// "userThemesTags": [
// {
// "userId": 0,
// "themesTagId": 0,
// "themesTag": {
// "id": 0,
// "groupId": 0,
// "name": "string",
// "isActive": true,
// "userId": 0,
// "group": {
// "id": 0,
// "name": "string",
// "isActive": true,
// "userId": 0,
// "tags": [
// "string"
// ],
// "multilangs": [
// {
// "parentId": 0,
// "languageCode": "string",
// "name": "string"
// }
// ]
// },
// "userThemesTags": [
// "string"
// ],
// "multilangs": [
// {
// "parentId": 0,
// "languageCode": "string",
// "name": "string"
// }
// ]
// },
// "user": "string"
// }
// ],
// "languagesLinks": [
// {
// "userId": 0,
// "languageId": 0,
// "user": "string",
// "language": Language
// }
// ]
// };
type SessionTag = {
id: number;
groupId?: number;
@ -21,6 +105,15 @@ export type SessionsFilter = {
endDate?: string;
};
export type SessionComment = {
id: number;
createdAtUtc: string;
comment?: string;
author?: PublicUser;
authorId?: number;
sessionId?: number;
};
export type Session = {
id: number;
scheduledStartAtUtc?: string;
@ -37,11 +130,13 @@ export type Session = {
description?: string;
isNeedSupervisor?: boolean;
supervisorComment?: string;
user?: User;
coach?: User;
supervisor?: User;
clients?: User[];
themesTags?: SessionTag[]
user?: PublicUser;
coach?: PublicUser;
supervisor?: PublicUser;
clients?: PublicUser[];
themesTags?: SessionTag[];
coachComments?: SessionComment[];
clientComments?: SessionComment[];
};
export enum SessionType {

3
src/utils/expert.ts Normal file
View File

@ -0,0 +1,3 @@
export const getDuration = (locale: string, value?: any): string => `${value || 0}${locale === 'ru' ? 'мин' : 'min'}`;
export const getPrice = (value?: any): string => `${value || 0}`;