Compare commits
2 Commits
a39f53c57d
...
5712cbcf56
Author | SHA1 | Date |
---|---|---|
SD | 5712cbcf56 | |
SD | b31d2cf700 |
|
@ -28,6 +28,7 @@
|
|||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-slick": "^0.29.0",
|
||||
"react-stripe-js": "^1.1.5",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"stripe": "^16.2.0",
|
||||
"styled-components": "^6.1.1"
|
||||
|
@ -5718,6 +5719,68 @@
|
|||
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-stripe-js": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/react-stripe-js/-/react-stripe-js-1.1.5.tgz",
|
||||
"integrity": "sha512-4lIucgf/FZj6Uxvf/TH+QQa/Qi3FXigwN/QY6H7naPyoEfw9LOuTzdgPAmm7aeSXj8nZJXVoigiGzzFZchXjew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stripe/react-stripe-js": "1.7.2",
|
||||
"@stripe/stripe-js": "1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-stripe-js/node_modules/@stripe/react-stripe-js": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.7.2.tgz",
|
||||
"integrity": "sha512-IAVg2nPUPoSwI//XDRCO7D8mGeK4+N3Xg63fYZHmlfEWAuFVcuaqJKTT67uzIdKYZhHZ/NMdZw/ttz+GOjP/rQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": "^1.26.0",
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-stripe-js/node_modules/@stripe/stripe-js": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.29.0.tgz",
|
||||
"integrity": "sha512-OsUxk0VLlum8E2d6onlEdKuQcvLMs7qTrOXCnl/BGV3fAm65qr6h3e1IZ5AX4lgUlPRrzRcddSOA5DvkKKYLvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-stripe-js/node_modules/react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"scheduler": "^0.20.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-stripe-js/node_modules/scheduler": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
|
||||
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-slick": "^0.29.0",
|
||||
"react-stripe-js": "^1.1.5",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"stripe": "^16.2.0",
|
||||
"styled-components": "^6.1.1"
|
||||
|
|
|
@ -91,3 +91,11 @@ export const finishSession = (locale: string, token: string, sessionId: number):
|
|||
locale,
|
||||
token
|
||||
});
|
||||
|
||||
export const sessionPaymentConfirm = (locale: string, token: string, sessionId: number): Promise<Session> => apiRequest({
|
||||
url: '/home/session_pay_confirm',
|
||||
method: 'post',
|
||||
data: { id: sessionId },
|
||||
locale,
|
||||
token
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use server";
|
||||
|
||||
import {PaymentIntentCreateParams, Stripe} from "stripe";
|
||||
import { Stripe } from "stripe";
|
||||
|
||||
import { headers } from "next/headers";
|
||||
|
||||
|
@ -52,20 +52,19 @@ export async function createCheckoutSession(
|
|||
}
|
||||
|
||||
export async function createPaymentIntent(
|
||||
data: any,
|
||||
data: { amount: number, sessionId?: string },
|
||||
): Promise<{ client_secret: string }> {
|
||||
|
||||
const params = {
|
||||
amount: formatAmountForStripe(
|
||||
Number(data['amount'] as string),
|
||||
data.amount,
|
||||
'eur',
|
||||
),
|
||||
automatic_payment_methods: { enabled: true },
|
||||
currency: 'eur',
|
||||
} as PaymentIntentCreateParams;
|
||||
} as Stripe.PaymentIntentCreateParams;
|
||||
|
||||
// additional params
|
||||
if (data.sessionId){
|
||||
if (data?.sessionId){
|
||||
params.metadata = {
|
||||
sessionId : data.sessionId
|
||||
}
|
||||
|
@ -76,3 +75,5 @@ export async function createPaymentIntent(
|
|||
|
||||
return { client_secret: paymentIntent.client_secret as string };
|
||||
}
|
||||
|
||||
export const getStripePaymentStatus = async (payment_intent: string): Promise<Stripe.PaymentIntent> => await stripe.paymentIntents.retrieve(payment_intent);
|
|
@ -1,14 +1,14 @@
|
|||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {getTranslations, unstable_setRequestLocale} from 'next-intl/server';
|
||||
// import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { getTranslations, unstable_setRequestLocale } from 'next-intl/server';
|
||||
import { i18nText } from '../../../../i18nKeys';
|
||||
import {fetchBlogPosts} from "../../../../lib/contentful/blogPosts";
|
||||
import Link from "next/link";
|
||||
import { fetchBlogPosts } from '../../../../lib/contentful/blogPosts';
|
||||
|
||||
export default async function News({params: {locale}}: { params: { locale: string } }) {
|
||||
unstable_setRequestLocale(locale);
|
||||
const t = await getTranslations('Main');
|
||||
const {data, total} = await fetchBlogPosts({preview: false, sticky: true})
|
||||
const { data, total } = await fetchBlogPosts({preview: false, sticky: true})
|
||||
|
||||
return (
|
||||
<div className="main-articles">
|
||||
|
|
|
@ -57,7 +57,7 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
|||
}
|
||||
}, [jwt]);
|
||||
|
||||
return (
|
||||
return data ? (
|
||||
<Loader isLoading={loading}>
|
||||
<ExpertProfile
|
||||
isFull={isFull}
|
||||
|
@ -66,5 +66,5 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
|||
updateData={setData}
|
||||
/>
|
||||
</Loader>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function SessionDetailItem({ params: { locale, slug } }: { params
|
|||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<SessionDetails
|
||||
locale={locale}
|
||||
sessionId={sessionId}
|
||||
sessionId={sessionId as number}
|
||||
activeType={sessionType as SessionType}
|
||||
/>
|
||||
</Suspense>
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { Metadata } from 'next';
|
|||
import { unstable_setRequestLocale } from 'next-intl/server';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { GeneralTopSection } from '../../../components/Page';
|
||||
import { ScreenCarousel } from '../../../components/Page/ScreenCarousel/index';
|
||||
import { ScreenCarousel } from '../../../components/Page/ScreenCarousel';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Bbuddy - Become a BB expert',
|
||||
|
|
|
@ -9,7 +9,6 @@ import {CustomPagination} from "../../../components/view/CustomPagination";
|
|||
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
|
||||
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
|
||||
|
||||
|
||||
interface BlogPostPageParams {
|
||||
slug: string
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
import {ElementsForm} from "../../../../components/stripe/ElementsForm";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Payment",
|
||||
};
|
||||
|
||||
export default function PaymentElementPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams?: { payment_intent_client_secret?: string };
|
||||
}) {
|
||||
return (
|
||||
<div className="page-container">
|
||||
<h1>Pay</h1>
|
||||
<ElementsForm />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import React from 'react';
|
||||
import type { Metadata } from 'next';
|
||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { GeneralTopSection } from '../../../components/Page';
|
||||
import PaymentElementPage from "./@payment/page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Bbuddy - Take the lead with BB',
|
||||
description: 'Bbuddy desc Take the lead with BB'
|
||||
};
|
||||
|
||||
export default function BbClientPage({ params: { locale } }: { params: { locale: string } }) {
|
||||
unstable_setRequestLocale(locale);
|
||||
const t = useTranslations('BbClient');
|
||||
|
||||
return (
|
||||
<>
|
||||
<GeneralTopSection
|
||||
title={t('header')}
|
||||
mainImage="banner-phone.png"
|
||||
/>
|
||||
<div className="main-articles bb-client">
|
||||
<PaymentElementPage />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
import type { Metadata } from "next";
|
||||
import React from "react";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Payment Intent Result",
|
||||
};
|
||||
|
||||
export default function ResultLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="page-container">
|
||||
<h1>Payment Intent Result</h1>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import type { Stripe } from "stripe";
|
||||
|
||||
import { stripe} from "../../../../lib/stripe";
|
||||
import PrintObject from "../../../../components/stripe/PrintObject";
|
||||
|
||||
export default async function ResultPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { payment_intent: string };
|
||||
}) {
|
||||
if (!searchParams.payment_intent)
|
||||
throw new Error("Please provide a valid payment_intent (`pi_...`)");
|
||||
|
||||
const paymentIntent: Stripe.PaymentIntent =
|
||||
await stripe.paymentIntents.retrieve(searchParams.payment_intent);
|
||||
|
||||
// Тут под идее тыкнуться в бек на тему того - прошла ли оплата. в зависимости от этого показать что все ок или нет
|
||||
// также стоит расшить ссылкой КУДА переходить после того как показали что все ок.
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Status: {paymentIntent.status}</h2>
|
||||
<h3>Payment Intent response:</h3>
|
||||
<PrintObject content={paymentIntent} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,21 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, useState } from 'react';
|
||||
import React, { FC, useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Tag, Image as AntdImage, Space, Button } from 'antd';
|
||||
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
|
||||
import { ExpertScheduler } from '../../types/experts';
|
||||
import { SignupSessionData } from '../../types/experts';
|
||||
import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts';
|
||||
import { ExpertDocument } from '../../types/file';
|
||||
import { Locale } from '../../types/locale';
|
||||
import { CustomRate } from '../view/CustomRate';
|
||||
import { i18nText } from '../../i18nKeys';
|
||||
import { FilledYellowButton } from '../view/FilledButton';
|
||||
import {getSchedulerByExpertId} from "../../actions/experts";
|
||||
import {useLocalStorage} from "../../hooks/useLocalStorage";
|
||||
import {AUTH_TOKEN_KEY} from "../../constants/common";
|
||||
import dayjs from "dayjs";
|
||||
import {ScheduleModal} from "../Modals/ScheduleModal";
|
||||
import { getStorageValue } from '../../hooks/useLocalStorage';
|
||||
import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common';
|
||||
import { ScheduleModal } from '../Modals/ScheduleModal';
|
||||
import { ScheduleModalResult } from '../Modals/ScheduleModalResult';
|
||||
|
||||
type ExpertDetailsProps = {
|
||||
expert: ExpertDetails;
|
||||
|
@ -36,9 +35,35 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId })
|
|||
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] } } = expert || {};
|
||||
const isRus = locale === Locale.ru;
|
||||
|
||||
const checkSession = (data?: SignupSessionData) => {
|
||||
if (data?.startAtUtc && data?.tagId) {
|
||||
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
|
||||
sessionStorage?.setItem(SESSION_DATA, JSON.stringify(data));
|
||||
if (jwt) {
|
||||
setMode('pay');
|
||||
} else {
|
||||
setShowSchedulerModal(false);
|
||||
const showAuth = new Event('show_auth_enter');
|
||||
document.dispatchEvent(showAuth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowPayForm = () => {
|
||||
setShowSchedulerModal(true);
|
||||
setMode('pay');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('show_pay_form', handleShowPayForm);
|
||||
return () => {
|
||||
document.removeEventListener('show_pay_form', handleShowPayForm);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSchedulerHandle = () => {
|
||||
setMode('data');
|
||||
setShowSchedulerModal(true)
|
||||
setShowSchedulerModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -112,7 +137,9 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId })
|
|||
expertId={expertId as string}
|
||||
locale={locale as string}
|
||||
sessionCost={sessionCost}
|
||||
checkSession={checkSession}
|
||||
/>
|
||||
<ScheduleModalResult locale={locale as string} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
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';
|
||||
|
@ -12,7 +12,6 @@ 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 { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import 'dayjs/locale/ru';
|
||||
import 'dayjs/locale/en';
|
||||
|
@ -20,14 +19,15 @@ import 'dayjs/locale/de';
|
|||
import 'dayjs/locale/it';
|
||||
import 'dayjs/locale/fr';
|
||||
import 'dayjs/locale/es';
|
||||
import { ExpertScheduler, SignupSessionData } from "../../types/experts";
|
||||
import { Tag } from "../../types/tags";
|
||||
import { useLocalStorage } from "../../hooks/useLocalStorage";
|
||||
import { AUTH_TOKEN_KEY } from "../../constants/common";
|
||||
import { getSchedulerByExpertId, getSchedulerSession } from "../../actions/experts";
|
||||
import { ElementsForm } from "../stripe/ElementsForm";
|
||||
import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common';
|
||||
import { ExpertScheduler, SignupSessionData } from '../../types/experts';
|
||||
import { Tag } from '../../types/tags';
|
||||
import { getSchedulerByExpertId, getSchedulerSession } from '../../actions/experts';
|
||||
import { StripeElementsForm } from '../stripe/StripeElementsForm';
|
||||
import { i18nText } from '../../i18nKeys';
|
||||
import { CustomSelect } from '../../components/view/CustomSelect';
|
||||
import { Loader } from '../view/Loader';
|
||||
import { getStorageValue } from '../../hooks/useLocalStorage';
|
||||
|
||||
type ScheduleModalProps = {
|
||||
open: boolean;
|
||||
|
@ -37,6 +37,7 @@ type ScheduleModalProps = {
|
|||
sessionCost: number;
|
||||
expertId: string;
|
||||
locale: string;
|
||||
checkSession: (data?: SignupSessionData) => void;
|
||||
};
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
|
@ -56,7 +57,7 @@ const getLocale = (locale: string) => {
|
|||
return locale_es;
|
||||
default:
|
||||
return locale_en;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return locale_en;
|
||||
|
@ -79,20 +80,43 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
|
|||
sessionCost,
|
||||
locale,
|
||||
expertId,
|
||||
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, setSesssionData] = useState<SignupSessionData>({ coachId: +expertId });
|
||||
const [sessionId, setSessionId] = useState<number>(-1);
|
||||
const [rawScheduler, setRawScheduler] = useState<ExpertScheduler | null>(null);
|
||||
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||
const [isPayLoading, setIsPayLoading] = useState<boolean>(false);
|
||||
const [sessionId, setSessionId] = useState<string>('');
|
||||
const [form] = Form.useForm<{ clientComment?: string, startAtUtc?: string, tagId?: number }>();
|
||||
|
||||
dayjs.locale(locale);
|
||||
|
||||
const signupSession = () => {
|
||||
const data = sessionStorage?.getItem(SESSION_DATA);
|
||||
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
|
||||
|
||||
if (jwt && data) {
|
||||
const parseData = JSON.parse(data);
|
||||
setIsPayLoading(true);
|
||||
getSchedulerSession(parseData as SignupSessionData, locale || 'en', jwt)
|
||||
.then((session) => {
|
||||
setSessionId(session?.sessionId);
|
||||
console.log(session?.sessionId);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
message.error('Не удалось провести оплату')
|
||||
})
|
||||
.finally(() => {
|
||||
sessionStorage?.removeItem(SESSION_DATA);
|
||||
setIsPayLoading(false);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(()=> {
|
||||
if (open) {
|
||||
if (open && mode !== 'pay') {
|
||||
getSchedulerByExpertId(expertId as string, locale as string)
|
||||
.then((data) => {
|
||||
setRawScheduler(data);
|
||||
|
@ -101,8 +125,18 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
|
|||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (!open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && mode === 'pay') {
|
||||
signupSession();
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
useEffect(() => {
|
||||
const map = {} as any
|
||||
rawScheduler?.availableSlots.forEach((el) => {
|
||||
|
@ -127,35 +161,6 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
|
|||
|
||||
const disabledDate = (currentDate: Dayjs) => !dates || !dates[currentDate.format('YYYY-MM-DD')];
|
||||
|
||||
const onChangeTimeSlot = (e: RadioChangeEvent) => setSesssionData({ ...sessionData, startAtUtc: e.target.value.startTime });
|
||||
|
||||
const onChangeTag = (tagId: number) => setSesssionData({ ...sessionData, tagId });
|
||||
|
||||
const singupSession = () => {
|
||||
console.log(sessionData);
|
||||
if (sessionData?.startAtUtc && sessionData?.tagId) {
|
||||
if (jwt) {
|
||||
setIsPayLoading(true);
|
||||
getSchedulerSession(sessionData, locale, jwt)
|
||||
.then((session) => {
|
||||
console.log(session);
|
||||
// тут должна быть проверка все ли с регистрацией сессии
|
||||
setSessionId(+session?.sessionId);
|
||||
updateMode('pay');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
message.error('Не удалось провести оплату')
|
||||
})
|
||||
.finally(() => {
|
||||
setIsPayLoading(false);
|
||||
})
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
|
||||
const isWeekend = date.day() === 6 || date.day() === 0;
|
||||
return React.cloneElement(info.originNode, {
|
||||
|
@ -175,6 +180,13 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const onValidate = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
checkSession({ coachId: +expertId, ...values });
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="b-modal"
|
||||
|
@ -231,43 +243,69 @@ 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) => {
|
||||
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={1}
|
||||
value={sessionData?.clientComment}
|
||||
rows={2}
|
||||
placeholder={i18nText('sessionWishes', locale)}
|
||||
onChange={(e) => setSesssionData({ ...sessionData, clientComment: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Button
|
||||
className="btn-apply"
|
||||
onClick={singupSession}
|
||||
loading={isPayLoading}
|
||||
onClick={onValidate}
|
||||
>
|
||||
{i18nText('pay', locale)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'pay' && (
|
||||
<ElementsForm amount={sessionCost}/>
|
||||
<div className="b-schedule-payment">
|
||||
<Loader isLoading={isPayLoading}>
|
||||
<StripeElementsForm
|
||||
amount={sessionCost}
|
||||
locale={locale}
|
||||
sessionId={sessionId}
|
||||
/>
|
||||
</Loader>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, Result } from 'antd';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { Stripe } from 'stripe';
|
||||
import { getStripePaymentStatus } from '../../actions/stripe';
|
||||
import { sessionPaymentConfirm } from '../../actions/sessions';
|
||||
import { getStorageValue } from '../../hooks/useLocalStorage';
|
||||
import { AUTH_TOKEN_KEY } from '../../constants/common';
|
||||
import { Session, SessionState } from '../../types/sessions';
|
||||
import { i18nText } from '../../i18nKeys';
|
||||
|
||||
export const ScheduleModalResult = ({ locale }: { locale: string }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const [paymentStatus, setPaymentStatus] = useState<Stripe.PaymentIntent.Status | undefined>();
|
||||
const [session, setSession] = useState<Session | undefined>();
|
||||
const [error, setError] = useState<any>();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setError(undefined);
|
||||
const payment_intent = searchParams.get('payment_intent') || false;
|
||||
if (payment_intent) {
|
||||
getStripePaymentStatus(payment_intent)
|
||||
.then((result) => {
|
||||
setPaymentStatus(result?.status);
|
||||
if (result?.status === 'succeeded' && result?.metadata?.sessionId) {
|
||||
const jwt = getStorageValue(AUTH_TOKEN_KEY, '');
|
||||
sessionPaymentConfirm(locale, jwt, +result.metadata.sessionId)
|
||||
.then((session) => {
|
||||
setSession(session);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
setError(err);
|
||||
})
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const onClose = () => {
|
||||
const { origin, pathname } = window?.location || {};
|
||||
|
||||
router.push(`${origin}${pathname}`);
|
||||
setPaymentStatus(undefined);
|
||||
setSession(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="b-modal"
|
||||
open={paymentStatus === 'succeeded' && session?.state === SessionState.PAID}
|
||||
title={undefined}
|
||||
onOk={undefined}
|
||||
onCancel={onClose}
|
||||
footer={false}
|
||||
width={498}
|
||||
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
|
||||
>
|
||||
<div className="b-schedule-payment-result">
|
||||
<Result
|
||||
status="success"
|
||||
title={i18nText('successPayment', locale)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -23,6 +23,31 @@ function HeaderAuthLinks ({
|
|||
const selectedLayoutSegment = useSelectedLayoutSegment();
|
||||
const pathname = selectedLayoutSegment || '';
|
||||
const [token, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
||||
const [isPayPath, setIsPayPath] = useState<boolean>(false);
|
||||
|
||||
const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => {
|
||||
setMode(mode);
|
||||
setIsOpenModal(true);
|
||||
};
|
||||
|
||||
const handleAuthRegister = () => {
|
||||
setIsPayPath(true);
|
||||
onOpen('register');
|
||||
};
|
||||
|
||||
const handleAuthEnter = () => {
|
||||
setIsPayPath(true);
|
||||
onOpen('enter');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('show_auth_register', handleAuthRegister);
|
||||
document.addEventListener('show_auth_enter', handleAuthEnter);
|
||||
return () => {
|
||||
document.removeEventListener('show_auth_register', handleAuthRegister);
|
||||
document.removeEventListener('show_auth_enter', handleAuthEnter);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpenModal) {
|
||||
|
@ -30,9 +55,16 @@ function HeaderAuthLinks ({
|
|||
}
|
||||
}, [isOpenModal]);
|
||||
|
||||
const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => {
|
||||
setMode(mode);
|
||||
setIsOpenModal(true);
|
||||
useEffect(() => {
|
||||
if (token && isPayPath) {
|
||||
const showPayForm = new Event('show_pay_form');
|
||||
document.dispatchEvent(showPayForm);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const addNewEvent = (name: 'show_auth_register' | 'show_auth_enter') => {
|
||||
const evt = new Event(name);
|
||||
document.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
return token
|
||||
|
@ -49,7 +81,7 @@ function HeaderAuthLinks ({
|
|||
<Button
|
||||
className="b-header__auth"
|
||||
type="link"
|
||||
onClick={() => onOpen('register')}
|
||||
onClick={() => addNewEvent('show_auth_register')}
|
||||
>
|
||||
{i18nText('registration', locale)}
|
||||
</Button>
|
||||
|
@ -61,7 +93,7 @@ function HeaderAuthLinks ({
|
|||
<Button
|
||||
className="b-header__auth"
|
||||
type="link"
|
||||
onClick={() => onOpen('enter')}
|
||||
onClick={() => addNewEvent('show_auth_enter')}
|
||||
>
|
||||
{i18nText('enter', locale)}
|
||||
</Button>
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import type { StripeError } from "@stripe/stripe-js";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
useStripe,
|
||||
useElements,
|
||||
PaymentElement,
|
||||
Elements,
|
||||
} from "@stripe/react-stripe-js";
|
||||
|
||||
import StripeTestCards from "./StripeTestCards";
|
||||
|
||||
import getStripe from "../../utils/get-stripe";
|
||||
import { createPaymentIntent} from "../../actions/stripe";
|
||||
import {Form} from "antd";
|
||||
import {Payment} from "../../types/payment";
|
||||
import {CustomInput} from "../view/CustomInput";
|
||||
import {i18nText} from "../../i18nKeys";
|
||||
import {FC, useEffect} from "react";
|
||||
import {getPersonalData} from "../../actions/profile";
|
||||
|
||||
type PaymentFormProps = {
|
||||
amount: number,
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
|
||||
export const CheckoutForm: FC<PaymentFormProps> = ({amount, sessionId}) => {
|
||||
const [input, setInput] = React.useState<{
|
||||
paySumm: number;
|
||||
cardholderName: string;
|
||||
}>({
|
||||
paySumm: 1,
|
||||
cardholderName: "",
|
||||
});
|
||||
const [form, ] = Form.useForm<Payment>();
|
||||
const formAmount = Form.useWatch('amount', form);
|
||||
const [paymentType, setPaymentType] = React.useState<string>("");
|
||||
const [payment, setPayment] = React.useState<{
|
||||
status: "initial" | "processing" | "error";
|
||||
}>({ status: "initial" });
|
||||
const [errorMessage, setErrorMessage] = React.useState<string>("");
|
||||
|
||||
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":
|
||||
return (
|
||||
<>
|
||||
<h2>Error 😭</h2>
|
||||
<p className="error-message">{errorMessage}</p>
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
elements?.update({ amount: formAmount * 100 });
|
||||
}, [formAmount]);
|
||||
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setInput({
|
||||
...input,
|
||||
[e.currentTarget.name]: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
if (!elements || !stripe) return;
|
||||
|
||||
setPayment({ status: "processing" });
|
||||
|
||||
const { error: submitError } = await elements.submit();
|
||||
|
||||
if (submitError) {
|
||||
setPayment({ status: "error" });
|
||||
setErrorMessage(submitError.message ?? "An unknown error occurred");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a PaymentIntent with the specified amount.
|
||||
console.log('DATA', data);
|
||||
const { client_secret: clientSecret } = await createPaymentIntent(
|
||||
{amount: amount},
|
||||
);
|
||||
|
||||
// Use your card Element with other Stripe.js APIs
|
||||
const { error: confirmError } = await stripe!.confirmPayment({
|
||||
elements,
|
||||
clientSecret,
|
||||
confirmParams: {
|
||||
return_url: `${window.location.origin}/ru/payment/result`,
|
||||
payment_method_data: {
|
||||
allow_redisplay: 'limited',
|
||||
billing_details: {
|
||||
name: input.cardholderName,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (confirmError) {
|
||||
setPayment({ status: "error" });
|
||||
setErrorMessage(confirmError.message ?? "An unknown error occurred");
|
||||
}
|
||||
} catch (err) {
|
||||
const { message } = err as StripeError;
|
||||
|
||||
setPayment({ status: "error" });
|
||||
setErrorMessage(message ?? "An unknown error occurred");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form} onFinish={onSubmit}>
|
||||
<fieldset className="elements-style">
|
||||
<StripeTestCards/>
|
||||
<legend>Your payment details:</legend>
|
||||
{paymentType === "card" ? (
|
||||
<input
|
||||
placeholder="Cardholder name"
|
||||
className="elements-style"
|
||||
type="Text"
|
||||
name="cardholderName"
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
) : null}
|
||||
<div className="FormRow elements-style">
|
||||
<PaymentElement
|
||||
onChange={(e) => {
|
||||
setPaymentType(e.value.type);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<button
|
||||
className="elements-style-background"
|
||||
type="submit"
|
||||
disabled={
|
||||
!["initial", "succeeded", "error"].includes(payment.status) ||
|
||||
!stripe
|
||||
}
|
||||
>
|
||||
Pay
|
||||
</button>
|
||||
</Form>
|
||||
<PaymentStatus status={payment.status}/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const ElementsForm: FC<PaymentFormProps> = ({amount, sessionId}) => {
|
||||
return (
|
||||
<Elements
|
||||
stripe={getStripe()}
|
||||
options={{
|
||||
appearance: {
|
||||
variables: {
|
||||
colorIcon: "#6772e5",
|
||||
fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif",
|
||||
},
|
||||
},
|
||||
currency: 'eur',
|
||||
mode: "payment",
|
||||
amount: amount*100,
|
||||
}}
|
||||
>
|
||||
<CheckoutForm amount={amount} sessionId={sessionId}/>
|
||||
</Elements>
|
||||
)
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import type { Stripe } from "stripe";
|
||||
|
||||
export default function PrintObject({
|
||||
content,
|
||||
}: {
|
||||
content: Stripe.PaymentIntent | Stripe.Checkout.Session;
|
||||
}): JSX.Element {
|
||||
const formattedContent: string = JSON.stringify(content, null, 2);
|
||||
return <pre>{formattedContent}</pre>;
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
'use client';
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import type { StripeError } from '@stripe/stripe-js';
|
||||
import {
|
||||
useStripe,
|
||||
useElements,
|
||||
PaymentElement,
|
||||
Elements,
|
||||
} from '@stripe/react-stripe-js';
|
||||
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,
|
||||
sessionId?: 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 }) => {
|
||||
const [form] = Form.useForm<Payment>();
|
||||
const formAmount = Form.useWatch('amount', form);
|
||||
const [paymentType, setPaymentType] = useState<string>('');
|
||||
const [payment, setPayment] = useState<{
|
||||
status: PaymentInfo
|
||||
}>({ status: 'initial' });
|
||||
const [errorData, setErrorData] = useState<any>();
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
|
||||
useEffect(() => {
|
||||
elements?.update({ amount: formAmount * 100 });
|
||||
}, [formAmount]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
if (!elements || !stripe) return;
|
||||
|
||||
setErrorData(undefined);
|
||||
setPayment({ status: "processing" });
|
||||
|
||||
const { error: submitError } = await elements.submit();
|
||||
|
||||
if (submitError) {
|
||||
if (submitError.message) {
|
||||
message.error(submitError.message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { client_secret: clientSecret } = await createPaymentIntent(
|
||||
{ amount, sessionId }
|
||||
);
|
||||
|
||||
const { error: confirmError } = await stripe!.confirmPayment({
|
||||
elements,
|
||||
clientSecret,
|
||||
confirmParams: {
|
||||
return_url: window.location.href,
|
||||
payment_method_data: {
|
||||
allow_redisplay: 'limited',
|
||||
// billing_details: {
|
||||
// name: input.cardholderName,
|
||||
// },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (confirmError) {
|
||||
setErrorData({
|
||||
title: i18nText('errorPayment', locale),
|
||||
message: confirmError.message ?? 'An unknown error occurred'
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const { message } = err as StripeError;
|
||||
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
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export const StripeElementsForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }) => {
|
||||
return (
|
||||
<Elements
|
||||
stripe={getStripe()}
|
||||
options={{
|
||||
fonts: [{
|
||||
cssSrc: 'https://fonts.googleapis.com/css2?family=Comfortaa&display=swap',
|
||||
}],
|
||||
appearance: {
|
||||
variables: {
|
||||
colorIcon: '#2c7873',
|
||||
fontSizeBase: '16px',
|
||||
colorPrimary: '#66A5AD',
|
||||
colorBackground: '#F8F8F7',
|
||||
colorText: '#000',
|
||||
colorDanger: '#ff4d4f',
|
||||
focusBoxShadow: 'none',
|
||||
borderRadius: '8px'
|
||||
},
|
||||
},
|
||||
currency: 'eur',
|
||||
mode: "payment",
|
||||
amount: amount*100,
|
||||
}}
|
||||
>
|
||||
<CheckoutForm amount={amount} sessionId={sessionId} locale={locale} />
|
||||
</Elements>
|
||||
);
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
export default function StripeTestCards(): JSX.Element {
|
||||
return (
|
||||
<div className="test-card-notice">
|
||||
Use any of the{" "}
|
||||
<a
|
||||
href="https://stripe.com/docs/testing#cards"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Stripe test cards
|
||||
</a>{" "}
|
||||
for demo, e.g.{" "}
|
||||
<div className="card-number">
|
||||
4242<span></span>4242<span></span>4242<span></span>4242
|
||||
</div>
|
||||
.
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export const BASE_URL = process.env.NEXT_PUBLIC_SERVER_BASE_URL || 'https://api.bbuddy.expert/api';
|
||||
export const AUTH_TOKEN_KEY = 'bbuddy_token';
|
||||
export const AUTH_USER = 'bbuddy_auth_user';
|
||||
export const SESSION_DATA = 'bbuddy_session_data';
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 5;
|
||||
export const DEFAULT_PAGE = 1;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
function getStorageValue (key: string, defaultValue: any) {
|
||||
export function getStorageValue (key: string, defaultValue: any) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem(key);
|
||||
return saved || defaultValue;
|
||||
|
|
|
@ -146,8 +146,10 @@ export default {
|
|||
saturday: 'Sa',
|
||||
addNew: 'Neu hinzufügen',
|
||||
mExperiences: 'Führungserfahrung',
|
||||
pay: 'Pay',
|
||||
sessionWishes: 'Write your wishes about the session',
|
||||
pay: 'Zahlung',
|
||||
sessionWishes: 'Schreiben Sie Ihre Wünsche zur Sitzung',
|
||||
successPayment: 'Erfolgreiche Zahlung',
|
||||
errorPayment: 'Zahlungsfehler',
|
||||
errors: {
|
||||
invalidEmail: 'Die E-Mail-Adresse ist ungültig',
|
||||
emptyEmail: 'Bitte geben Sie Ihre E-Mail ein',
|
||||
|
|
|
@ -148,6 +148,8 @@ export default {
|
|||
mExperiences: 'Managerial Experience',
|
||||
pay: 'Pay',
|
||||
sessionWishes: 'Write your wishes about the session',
|
||||
successPayment: 'Successful Payment',
|
||||
errorPayment: 'Payment Error',
|
||||
errors: {
|
||||
invalidEmail: 'The email address is not valid',
|
||||
emptyEmail: 'Please enter your E-mail',
|
||||
|
|
|
@ -146,8 +146,10 @@ export default {
|
|||
saturday: 'S',
|
||||
addNew: 'Añadir nuevo',
|
||||
mExperiences: 'Experiencia de dirección',
|
||||
pay: 'Pay',
|
||||
sessionWishes: 'Write your wishes about the session',
|
||||
pay: 'Pago',
|
||||
sessionWishes: 'Escribe tus deseos sobre la sesión',
|
||||
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',
|
||||
|
|
|
@ -146,8 +146,10 @@ export default {
|
|||
saturday: 'Sa',
|
||||
addNew: 'Ajouter un nouveau',
|
||||
mExperiences: 'Expérience en gestion',
|
||||
pay: 'Pay',
|
||||
sessionWishes: 'Write your wishes about the session',
|
||||
pay: 'Paiement',
|
||||
sessionWishes: 'Écrivez vos souhaits concernant la session',
|
||||
successPayment: 'Paiement Réussi',
|
||||
errorPayment: 'Erreur de Paiement',
|
||||
errors: {
|
||||
invalidEmail: 'L\'adresse e-mail n\'est pas valide',
|
||||
emptyEmail: 'Veuillez saisir votre e-mail',
|
||||
|
|
|
@ -146,8 +146,10 @@ export default {
|
|||
saturday: 'Sa',
|
||||
addNew: 'Aggiungi nuovo',
|
||||
mExperiences: 'Esperienza manageriale',
|
||||
pay: 'Pay',
|
||||
sessionWishes: 'Write your wishes about the session',
|
||||
pay: 'Pagamento',
|
||||
sessionWishes: 'Scrivi i tuoi desideri riguardo alla sessione',
|
||||
successPayment: 'Pagamento Riuscito',
|
||||
errorPayment: 'Errore di Pagamento',
|
||||
errors: {
|
||||
invalidEmail: 'L\'indirizzo e-mail non è valido',
|
||||
emptyEmail: 'Inserisci l\'e-mail',
|
||||
|
|
|
@ -146,8 +146,10 @@ export default {
|
|||
saturday: 'Сб',
|
||||
addNew: 'Добавить',
|
||||
mExperiences: 'Управленческий опыт',
|
||||
pay: 'Pay',
|
||||
sessionWishes: 'Write your wishes about the session',
|
||||
pay: 'Оплата',
|
||||
sessionWishes: 'Напишите свои пожелания по поводу сессии',
|
||||
successPayment: 'Успешная оплата',
|
||||
errorPayment: 'Ошибка оплаты',
|
||||
errors: {
|
||||
invalidEmail: 'Адрес электронной почты недействителен',
|
||||
emptyEmail: 'Пожалуйста, введите ваш E-mail',
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.ant-form-item-has-error .ant-radio-inner {
|
||||
border-color: #ff4d4f !important;
|
||||
}
|
|
@ -23,4 +23,9 @@
|
|||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-payment {
|
||||
padding: 44px 40px;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
height: 54px !important;
|
||||
|
||||
.ant-select-selector {
|
||||
background-color: #F8F8F7 !important;
|
||||
background-color: transparent !important;
|
||||
border-color: #F8F8F7 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 22px 16px 8px !important;
|
||||
|
@ -17,6 +17,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.ant-select-status-error {
|
||||
.ant-select-selector {
|
||||
border-color: #ff4d4f !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selection-overflow-item {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
@ -35,6 +41,9 @@
|
|||
&-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #F8F8F7;
|
||||
border-radius: 8px;
|
||||
|
||||
&.b-multiselect__active .b-multiselect-label {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
|
@ -49,7 +58,7 @@
|
|||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #000;
|
||||
opacity: .3;
|
||||
opacity: .4;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 15px;
|
||||
|
@ -70,11 +79,12 @@
|
|||
height: 54px !important;
|
||||
|
||||
.ant-select-selector {
|
||||
background-color: #F8F8F7 !important;
|
||||
background-color: transparent !important;
|
||||
border-color: #F8F8F7 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 22px 16px 8px !important;
|
||||
box-shadow: none !important;
|
||||
z-index: 1;
|
||||
|
||||
.ant-select-selection-item {
|
||||
font-size: 15px !important;
|
||||
|
@ -84,6 +94,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.ant-select-status-error {
|
||||
.ant-select-selector {
|
||||
border-color: #ff4d4f !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: #2c7873 !important;
|
||||
}
|
||||
|
@ -98,6 +114,8 @@
|
|||
&-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #F8F8F7;
|
||||
border-radius: 8px;
|
||||
|
||||
&.b-select__active .b-select-label {
|
||||
font-size: 12px;
|
||||
|
@ -113,7 +131,7 @@
|
|||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #000;
|
||||
opacity: .3;
|
||||
opacity: .4;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 15px;
|
||||
|
|
|
@ -11,3 +11,4 @@
|
|||
@import "_timepicker.scss";
|
||||
@import "_calendar.scss";
|
||||
@import "_schedule.scss";
|
||||
@import "_radio.scss";
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
/**
|
||||
* This is a singleton to ensure we only instantiate Stripe once.
|
||||
*/
|
||||
import { Stripe, loadStripe } from "@stripe/stripe-js";
|
||||
import { Stripe, loadStripe } from '@stripe/stripe-js';
|
||||
|
||||
let stripePromise: Promise<Stripe | null>;
|
||||
|
||||
|
|
Loading…
Reference in New Issue