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 classNames from 'classnames';
import { Modal, Menu, Calendar, Radio, Button, Input, message } from 'antd';
import type { CalendarProps, RadioChangeEvent, MenuProps } from 'antd';
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';
@ -57,7 +57,7 @@ const getLocale = (locale: string) => {
return locale_es;
default:
return locale_en;
};
}
}
return locale_en;
@ -83,12 +83,12 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
checkSession,
}) => {
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 [sessionData, setSessionData] = useState<SignupSessionData>({ coachId: +expertId });
const [rawScheduler, setRawScheduler] = useState<ExpertScheduler | null>(null);
const [isPayLoading, setIsPayLoading] = useState<boolean>(false);
const [sessionId, setSessionId] = useState<string>('');
const [form] = Form.useForm<{ clientComment?: string, startAtUtc?: string, tagId?: number }>();
dayjs.locale(locale);
@ -117,7 +117,6 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
useEffect(()=> {
if (open && mode !== 'pay') {
setSessionData({ coachId: +expertId });
getSchedulerByExpertId(expertId as string, locale as string)
.then((data) => {
setRawScheduler(data);
@ -126,6 +125,10 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
console.log(err);
});
}
if (!open) {
form.resetFields();
}
}, [open]);
useEffect(() => {
@ -158,10 +161,6 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
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 isWeekend = date.day() === 6 || date.day() === 0;
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 (
<Modal
className="b-modal"
@ -237,35 +243,54 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
{selectDate.locale(locale).format('DD MMMM YYYY')}
</Button>
</div>
<Form form={form}>
<div className="b-schedule-select-tag">
{tags && (
<Form.Item
name="tagId"
rules={[{
required: true,
message: ''
}]}
>
<CustomSelect
label={i18nText('selectTopic', locale)}
value={sessionData?.tagId}
options={tags?.map(({ id, name }) => ({ value: id, label: name }))}
onChange={onChangeTag}
options={tags?.map(({id, name}) => ({value: id, label: name}))}
/>
</Form.Item>
)}
</div>
<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>)
})}
<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>
<div>
<Form.Item name="clientComment">
<Input.TextArea
className="b-textarea"
rows={2}
value={sessionData?.clientComment}
placeholder={i18nText('sessionWishes', locale)}
onChange={(e) => setSessionData({ ...sessionData, clientComment: e.target.value })}
/>
</div>
</Form.Item>
</Form>
<Button
className="btn-apply"
onClick={() => checkSession(sessionData)}
onClick={onValidate}
>
{i18nText('pay', locale)}
</Button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -148,8 +148,8 @@ export default {
mExperiences: 'Управленческий опыт',
pay: 'Оплата',
sessionWishes: 'Напишите свои пожелания по поводу сессии',
successPayment: 'Success',
errorPayment: 'Error',
successPayment: 'Успешная оплата',
errorPayment: 'Ошибка оплаты',
errors: {
invalidEmail: 'Адрес электронной почты недействителен',
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 {
margin-right: 4px;
}
@ -88,6 +94,12 @@
}
}
&.ant-select-status-error {
.ant-select-selector {
border-color: #ff4d4f !important;
}
}
.ant-select-arrow {
color: #2c7873 !important;
}

View File

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