feat: add errors

This commit is contained in:
SD 2024-10-28 15:28:14 +04:00
parent b31d2cf700
commit 5712cbcf56
13 changed files with 155 additions and 108 deletions

View File

@ -2,8 +2,8 @@
import React, {FC, useEffect, useState} from 'react'; import React, {FC, useEffect, useState} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Modal, Menu, Calendar, Radio, Button, Input, message } from 'antd'; import { Modal, Menu, Calendar, Radio, Button, Input, message, Form } from 'antd';
import type { CalendarProps, RadioChangeEvent, MenuProps } from 'antd'; import type { CalendarProps, MenuProps } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons'; import { ArrowLeftOutlined } from '@ant-design/icons';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import locale_ru from 'antd/lib/calendar/locale/ru_RU'; import locale_ru from 'antd/lib/calendar/locale/ru_RU';
@ -57,7 +57,7 @@ const getLocale = (locale: string) => {
return locale_es; return locale_es;
default: default:
return locale_en; return locale_en;
}; }
} }
return locale_en; return locale_en;
@ -83,12 +83,12 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
checkSession, checkSession,
}) => { }) => {
const [selectDate, setSelectDate] = useState<Dayjs>(dayjs()); const [selectDate, setSelectDate] = useState<Dayjs>(dayjs());
const [dates, setDates] = useState<any>(); const [dates, setDates] = useState<Record<string, { startTime: string, endTime: string }[]> | undefined>();
const [tags, setTags] = useState<Tag[] | undefined>(); const [tags, setTags] = useState<Tag[] | undefined>();
const [sessionData, setSessionData] = useState<SignupSessionData>({ coachId: +expertId });
const [rawScheduler, setRawScheduler] = useState<ExpertScheduler | null>(null); const [rawScheduler, setRawScheduler] = useState<ExpertScheduler | null>(null);
const [isPayLoading, setIsPayLoading] = useState<boolean>(false); const [isPayLoading, setIsPayLoading] = useState<boolean>(false);
const [sessionId, setSessionId] = useState<string>(''); const [sessionId, setSessionId] = useState<string>('');
const [form] = Form.useForm<{ clientComment?: string, startAtUtc?: string, tagId?: number }>();
dayjs.locale(locale); dayjs.locale(locale);
@ -117,7 +117,6 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
useEffect(()=> { useEffect(()=> {
if (open && mode !== 'pay') { if (open && mode !== 'pay') {
setSessionData({ coachId: +expertId });
getSchedulerByExpertId(expertId as string, locale as string) getSchedulerByExpertId(expertId as string, locale as string)
.then((data) => { .then((data) => {
setRawScheduler(data); setRawScheduler(data);
@ -126,6 +125,10 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
console.log(err); console.log(err);
}); });
} }
if (!open) {
form.resetFields();
}
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
@ -158,10 +161,6 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
const disabledDate = (currentDate: Dayjs) => !dates || !dates[currentDate.format('YYYY-MM-DD')]; const disabledDate = (currentDate: Dayjs) => !dates || !dates[currentDate.format('YYYY-MM-DD')];
const onChangeTimeSlot = (e: RadioChangeEvent) => setSessionData({ ...sessionData, startAtUtc: e.target.value.startTime });
const onChangeTag = (tagId: number) => setSessionData({ ...sessionData, tagId });
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => { const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
const isWeekend = date.day() === 6 || date.day() === 0; const isWeekend = date.day() === 6 || date.day() === 0;
return React.cloneElement(info.originNode, { return React.cloneElement(info.originNode, {
@ -181,6 +180,13 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
}); });
}; };
const onValidate = () => {
form.validateFields()
.then((values) => {
checkSession({ coachId: +expertId, ...values });
})
}
return ( return (
<Modal <Modal
className="b-modal" className="b-modal"
@ -237,35 +243,54 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
{selectDate.locale(locale).format('DD MMMM YYYY')} {selectDate.locale(locale).format('DD MMMM YYYY')}
</Button> </Button>
</div> </div>
<div className="b-schedule-select-tag"> <Form form={form}>
{tags && ( <div className="b-schedule-select-tag">
<CustomSelect {tags && (
label={i18nText('selectTopic', locale)} <Form.Item
value={sessionData?.tagId} name="tagId"
options={tags?.map(({ id, name }) => ({ value: id, label: name }))} rules={[{
onChange={onChangeTag} required: true,
message: ''
}]}
>
<CustomSelect
label={i18nText('selectTopic', locale)}
options={tags?.map(({id, name}) => ({value: id, label: name}))}
/>
</Form.Item>
)}
</div>
<div className="b-schedule-radio-list">
<Form.Item
name="startAtUtc"
rules={[{
required: true,
message: ''
}]}
>
<Radio.Group>
{dates && dates[selectDate.format('YYYY-MM-DD')].map((el: any) => (
<Radio
key={el.startTime}
value={el.startTime}
>
{dayjs(el.startTime).format('HH:mm')} - {dayjs(el.endTime).format('HH:mm')}
</Radio>)
)}
</Radio.Group>
</Form.Item>
</div>
<Form.Item name="clientComment">
<Input.TextArea
className="b-textarea"
rows={2}
placeholder={i18nText('sessionWishes', locale)}
/> />
)} </Form.Item>
</div> </Form>
<div className="b-schedule-radio-list">
<Radio.Group name="radiogroupSlots" onChange={onChangeTimeSlot}>
{dates[selectDate.format('YYYY-MM-DD')].map((el: any) => {
return (<Radio key={dayjs(el.startTime).format()} value={el}>{dayjs(el.startTime).format('HH:mm')} - {dayjs(el.endTime).format('HH:mm')}</Radio>)
})}
</Radio.Group>
</div>
<div>
<Input.TextArea
className="b-textarea"
rows={2}
value={sessionData?.clientComment}
placeholder={i18nText('sessionWishes', locale)}
onChange={(e) => setSessionData({ ...sessionData, clientComment: e.target.value })}
/>
</div>
<Button <Button
className="btn-apply" className="btn-apply"
onClick={() => checkSession(sessionData)} onClick={onValidate}
> >
{i18nText('pay', locale)} {i18nText('pay', locale)}
</Button> </Button>

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { FC, useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import type { StripeError } from '@stripe/stripe-js'; import type { StripeError } from '@stripe/stripe-js';
import { import {
useStripe, useStripe,
@ -8,11 +8,12 @@ import {
PaymentElement, PaymentElement,
Elements, Elements,
} from '@stripe/react-stripe-js'; } from '@stripe/react-stripe-js';
import { Form, Button } from 'antd'; import { Form, Button, message } from 'antd';
import getStripe from '../../utils/get-stripe'; import getStripe from '../../utils/get-stripe';
import { createPaymentIntent} from '../../actions/stripe'; import { createPaymentIntent} from '../../actions/stripe';
import { Payment } from '../../types/payment'; import { Payment } from '../../types/payment';
import { i18nText } from '../../i18nKeys'; import { i18nText } from '../../i18nKeys';
import { WithError } from '../view/WithError';
type PaymentFormProps = { type PaymentFormProps = {
amount: number, amount: number,
@ -20,39 +21,37 @@ type PaymentFormProps = {
locale: string locale: string
} }
type PaymentInfo = 'initial' | 'error' | 'processing' | 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'succeeded';
const PaymentStatus = ({ status }: { status?: PaymentInfo }) => {
switch (status) {
case 'processing':
case 'requires_payment_method':
case 'requires_confirmation':
return <h2>Processing...</h2>;
case 'requires_action':
return <h2>Authenticating...</h2>;
case 'succeeded':
return <h2>Payment Succeeded</h2>;
default:
return null;
}
};
export const CheckoutForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }) => { export const CheckoutForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }) => {
const [form] = Form.useForm<Payment>(); const [form] = Form.useForm<Payment>();
const formAmount = Form.useWatch('amount', form); const formAmount = Form.useWatch('amount', form);
const [paymentType, setPaymentType] = useState<string>(''); const [paymentType, setPaymentType] = useState<string>('');
const [payment, setPayment] = useState<{ const [payment, setPayment] = useState<{
status: "initial" | "processing" | "error"; status: PaymentInfo
}>({ status: "initial" }); }>({ status: 'initial' });
const [errorMessage, setErrorMessage] = useState<string>(''); const [errorData, setErrorData] = useState<any>();
const stripe = useStripe(); const stripe = useStripe();
const elements = useElements(); const elements = useElements();
const PaymentStatus = ({ status }: { status: string }) => {
switch (status) {
case "processing":
case "requires_payment_method":
case "requires_confirmation":
return <h2>Processing...</h2>;
case "requires_action":
return <h2>Authenticating...</h2>;
case "succeeded":
return <h2>Payment Succeeded</h2>;
case "error":
console.log('errorMessage', errorMessage);
return null;
default:
return null;
}
};
useEffect(() => { useEffect(() => {
elements?.update({ amount: formAmount * 100 }); elements?.update({ amount: formAmount * 100 });
}, [formAmount]); }, [formAmount]);
@ -61,13 +60,15 @@ export const CheckoutForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }
try { try {
if (!elements || !stripe) return; if (!elements || !stripe) return;
setErrorData(undefined);
setPayment({ status: "processing" }); setPayment({ status: "processing" });
const { error: submitError } = await elements.submit(); const { error: submitError } = await elements.submit();
if (submitError) { if (submitError) {
setPayment({ status: "error" }); if (submitError.message) {
setErrorMessage(submitError.message ?? "An unknown error occurred"); message.error(submitError.message);
}
return; return;
} }
@ -91,40 +92,45 @@ export const CheckoutForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }
}); });
if (confirmError) { if (confirmError) {
setPayment({ status: "error" }); setErrorData({
setErrorMessage(confirmError.message ?? "An unknown error occurred"); title: i18nText('errorPayment', locale),
message: confirmError.message ?? 'An unknown error occurred'
});
} }
} catch (err) { } catch (err) {
const { message } = err as StripeError; const { message } = err as StripeError;
setErrorData({
setPayment({ status: "error" }); title: i18nText('errorPayment', locale),
setErrorMessage(message ?? "An unknown error occurred"); message: message ?? 'An unknown error occurred'
});
} }
}; };
return ( return (
<Form form={form} onFinish={onSubmit} style={{ display: 'flex', overflow: 'hidden', flexDirection: 'column', gap: 16, justifyContent: 'space-between', alignItems: 'center' }}> <WithError errorData={errorData}>
<div style={{ width: '100%' }}> <Form form={form} onFinish={onSubmit} style={{ display: 'flex', overflow: 'hidden', flexDirection: 'column', gap: 16, justifyContent: 'space-between', alignItems: 'center' }}>
<PaymentElement <div style={{ width: '100%' }}>
onChange={(e) => { <PaymentElement
setPaymentType(e.value.type); onChange={(e) => {
}} setPaymentType(e.value.type);
/> }}
</div> />
<div> </div>
<PaymentStatus status={payment.status}/> <div>
</div> <PaymentStatus status={payment.status}/>
<Button </div>
className="btn-apply" <Button
htmlType="submit" className="btn-apply"
disabled={ htmlType="submit"
!["initial", "succeeded", "error"].includes(payment.status) || disabled={
!stripe !["initial", "succeeded", "error"].includes(payment.status) ||
} !stripe
> }
{`${i18nText('pay', locale)} ${amount}`} >
</Button> {`${i18nText('pay', locale)} ${amount}`}
</Form> </Button>
</Form>
</WithError>
); );
} }
@ -143,7 +149,7 @@ export const StripeElementsForm: FC<PaymentFormProps> = ({ amount, sessionId, lo
colorPrimary: '#66A5AD', colorPrimary: '#66A5AD',
colorBackground: '#F8F8F7', colorBackground: '#F8F8F7',
colorText: '#000', colorText: '#000',
colorDanger: '#D93E5C', colorDanger: '#ff4d4f',
focusBoxShadow: 'none', focusBoxShadow: 'none',
borderRadius: '8px' borderRadius: '8px'
}, },

View File

@ -16,8 +16,8 @@ export const WithError: FC<WithErrorProps> = ({
return ( return (
<Result <Result
status="error" status="error"
title="Submission Failed" title={errorData?.title}
subTitle="Please check and modify the following information before resubmitting." subTitle={errorData?.message}
extra={refresh ? ( extra={refresh ? (
<Button type="primary" onClick={refresh}> <Button type="primary" onClick={refresh}>
Refresh page Refresh page

View File

@ -148,8 +148,8 @@ export default {
mExperiences: 'Führungserfahrung', mExperiences: 'Führungserfahrung',
pay: 'Zahlung', pay: 'Zahlung',
sessionWishes: 'Schreiben Sie Ihre Wünsche zur Sitzung', sessionWishes: 'Schreiben Sie Ihre Wünsche zur Sitzung',
successPayment: 'Success', successPayment: 'Erfolgreiche Zahlung',
errorPayment: 'Error', errorPayment: 'Zahlungsfehler',
errors: { errors: {
invalidEmail: 'Die E-Mail-Adresse ist ungültig', invalidEmail: 'Die E-Mail-Adresse ist ungültig',
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein', emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',

View File

@ -148,8 +148,8 @@ export default {
mExperiences: 'Managerial Experience', mExperiences: 'Managerial Experience',
pay: 'Pay', pay: 'Pay',
sessionWishes: 'Write your wishes about the session', sessionWishes: 'Write your wishes about the session',
successPayment: 'Success', successPayment: 'Successful Payment',
errorPayment: 'Error', errorPayment: 'Payment Error',
errors: { errors: {
invalidEmail: 'The email address is not valid', invalidEmail: 'The email address is not valid',
emptyEmail: 'Please enter your E-mail', emptyEmail: 'Please enter your E-mail',

View File

@ -148,8 +148,8 @@ export default {
mExperiences: 'Experiencia de dirección', mExperiences: 'Experiencia de dirección',
pay: 'Pago', pay: 'Pago',
sessionWishes: 'Escribe tus deseos sobre la sesión', sessionWishes: 'Escribe tus deseos sobre la sesión',
successPayment: 'Success', successPayment: 'Pago Exitoso',
errorPayment: 'Error', errorPayment: 'Error de Pago',
errors: { errors: {
invalidEmail: 'La dirección de correo electrónico no es válida', invalidEmail: 'La dirección de correo electrónico no es válida',
emptyEmail: 'Introduce tu correo electrónico', emptyEmail: 'Introduce tu correo electrónico',

View File

@ -148,8 +148,8 @@ export default {
mExperiences: 'Expérience en gestion', mExperiences: 'Expérience en gestion',
pay: 'Paiement', pay: 'Paiement',
sessionWishes: 'Écrivez vos souhaits concernant la session', sessionWishes: 'Écrivez vos souhaits concernant la session',
successPayment: 'Success', successPayment: 'Paiement Réussi',
errorPayment: 'Error', errorPayment: 'Erreur de Paiement',
errors: { errors: {
invalidEmail: 'L\'adresse e-mail n\'est pas valide', invalidEmail: 'L\'adresse e-mail n\'est pas valide',
emptyEmail: 'Veuillez saisir votre e-mail', emptyEmail: 'Veuillez saisir votre e-mail',

View File

@ -148,8 +148,8 @@ export default {
mExperiences: 'Esperienza manageriale', mExperiences: 'Esperienza manageriale',
pay: 'Pagamento', pay: 'Pagamento',
sessionWishes: 'Scrivi i tuoi desideri riguardo alla sessione', sessionWishes: 'Scrivi i tuoi desideri riguardo alla sessione',
successPayment: 'Success', successPayment: 'Pagamento Riuscito',
errorPayment: 'Error', errorPayment: 'Errore di Pagamento',
errors: { errors: {
invalidEmail: 'L\'indirizzo e-mail non è valido', invalidEmail: 'L\'indirizzo e-mail non è valido',
emptyEmail: 'Inserisci l\'e-mail', emptyEmail: 'Inserisci l\'e-mail',

View File

@ -148,8 +148,8 @@ export default {
mExperiences: 'Управленческий опыт', mExperiences: 'Управленческий опыт',
pay: 'Оплата', pay: 'Оплата',
sessionWishes: 'Напишите свои пожелания по поводу сессии', sessionWishes: 'Напишите свои пожелания по поводу сессии',
successPayment: 'Success', successPayment: 'Успешная оплата',
errorPayment: 'Error', errorPayment: 'Ошибка оплаты',
errors: { errors: {
invalidEmail: 'Адрес электронной почты недействителен', invalidEmail: 'Адрес электронной почты недействителен',
emptyEmail: 'Пожалуйста, введите ваш E-mail', emptyEmail: 'Пожалуйста, введите ваш E-mail',

View File

@ -0,0 +1,3 @@
.ant-form-item-has-error .ant-radio-inner {
border-color: #ff4d4f !important;
}

View File

@ -17,6 +17,12 @@
} }
} }
&.ant-select-status-error {
.ant-select-selector {
border-color: #ff4d4f !important;
}
}
.ant-select-selection-overflow-item { .ant-select-selection-overflow-item {
margin-right: 4px; margin-right: 4px;
} }
@ -88,6 +94,12 @@
} }
} }
&.ant-select-status-error {
.ant-select-selector {
border-color: #ff4d4f !important;
}
}
.ant-select-arrow { .ant-select-arrow {
color: #2c7873 !important; color: #2c7873 !important;
} }

View File

@ -11,3 +11,4 @@
@import "_timepicker.scss"; @import "_timepicker.scss";
@import "_calendar.scss"; @import "_calendar.scss";
@import "_schedule.scss"; @import "_schedule.scss";
@import "_radio.scss";