feat: add errors
This commit is contained in:
parent
b31d2cf700
commit
5712cbcf56
|
@ -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>
|
||||
<div className="b-schedule-select-tag">
|
||||
{tags && (
|
||||
<CustomSelect
|
||||
label={i18nText('selectTopic', locale)}
|
||||
value={sessionData?.tagId}
|
||||
options={tags?.map(({ id, name }) => ({ value: id, label: name }))}
|
||||
onChange={onChangeTag}
|
||||
<Form form={form}>
|
||||
<div className="b-schedule-select-tag">
|
||||
{tags && (
|
||||
<Form.Item
|
||||
name="tagId"
|
||||
rules={[{
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
</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>)
|
||||
})}
|
||||
</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>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Button
|
||||
className="btn-apply"
|
||||
onClick={() => checkSession(sessionData)}
|
||||
onClick={onValidate}
|
||||
>
|
||||
{i18nText('pay', locale)}
|
||||
</Button>
|
||||
|
|
|
@ -45,7 +45,7 @@ export const ScheduleModalResult = ({ locale }: { locale: string }) => {
|
|||
|
||||
const onClose = () => {
|
||||
const { origin, pathname } = window?.location || {};
|
||||
|
||||
|
||||
router.push(`${origin}${pathname}`);
|
||||
setPaymentStatus(undefined);
|
||||
setSession(undefined);
|
||||
|
|
|
@ -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,40 +92,45 @@ 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 (
|
||||
<Form form={form} onFinish={onSubmit} style={{ display: 'flex', overflow: 'hidden', flexDirection: 'column', gap: 16, justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PaymentElement
|
||||
onChange={(e) => {
|
||||
setPaymentType(e.value.type);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PaymentStatus status={payment.status}/>
|
||||
</div>
|
||||
<Button
|
||||
className="btn-apply"
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!["initial", "succeeded", "error"].includes(payment.status) ||
|
||||
!stripe
|
||||
}
|
||||
>
|
||||
{`${i18nText('pay', locale)} ${amount}€`}
|
||||
</Button>
|
||||
</Form>
|
||||
<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
|
||||
onChange={(e) => {
|
||||
setPaymentType(e.value.type);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<PaymentStatus status={payment.status}/>
|
||||
</div>
|
||||
<Button
|
||||
className="btn-apply"
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!["initial", "succeeded", "error"].includes(payment.status) ||
|
||||
!stripe
|
||||
}
|
||||
>
|
||||
{`${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'
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -148,8 +148,8 @@ export default {
|
|||
mExperiences: 'Управленческий опыт',
|
||||
pay: 'Оплата',
|
||||
sessionWishes: 'Напишите свои пожелания по поводу сессии',
|
||||
successPayment: 'Success',
|
||||
errorPayment: 'Error',
|
||||
successPayment: 'Успешная оплата',
|
||||
errorPayment: 'Ошибка оплаты',
|
||||
errors: {
|
||||
invalidEmail: 'Адрес электронной почты недействителен',
|
||||
emptyEmail: 'Пожалуйста, введите ваш E-mail',
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.ant-form-item-has-error .ant-radio-inner {
|
||||
border-color: #ff4d4f !important;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -11,3 +11,4 @@
|
|||
@import "_timepicker.scss";
|
||||
@import "_calendar.scss";
|
||||
@import "_schedule.scss";
|
||||
@import "_radio.scss";
|
||||
|
|
Loading…
Reference in New Issue