Compare commits
No commits in common. "5712cbcf565236060178076720808638d763c0d4" and "4ac2740942de9e76bb98ee10a83ee1871035dc27" have entirely different histories.
5712cbcf56
...
4ac2740942
3
.env
3
.env
|
@ -1,8 +1,5 @@
|
||||||
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
|
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
|
||||||
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
|
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LVB3LK5pVGxNPeKk4gedt5NW4cb8k7BVXvgOMPTK4x1nnbGTD8BCqDqgInboT6N72YwrTl4tOsVz8rAjbUadX1m00y4Aq5qE8
|
|
||||||
STRIPE_SECRET_KEY=sk_test_51LVB3LK5pVGxNPeK6j0wCsPqYMoGfcuwf1LpwGEBsr1dUx4NngukyjYL2oMZer5EOlW3lqnVEPjNDruN0OkUohIf00fWFUHN5O
|
|
||||||
STRIPE_PAYMENT_DESCRIPTION='BBuddy services'
|
|
||||||
|
|
||||||
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
|
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
|
||||||
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
|
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -13,13 +13,10 @@
|
||||||
"@ant-design/icons": "^5.2.6",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@ant-design/nextjs-registry": "^1.0.0",
|
"@ant-design/nextjs-registry": "^1.0.0",
|
||||||
"@contentful/rich-text-react-renderer": "^15.22.9",
|
"@contentful/rich-text-react-renderer": "^15.22.9",
|
||||||
"@stripe/react-stripe-js": "^2.7.3",
|
|
||||||
"@stripe/stripe-js": "^4.1.0",
|
|
||||||
"agora-rtc-react": "^2.1.0",
|
"agora-rtc-react": "^2.1.0",
|
||||||
"agora-rtc-sdk-ng": "^4.20.2",
|
"agora-rtc-sdk-ng": "^4.20.2",
|
||||||
"antd": "^5.12.1",
|
"antd": "^5.12.1",
|
||||||
"antd-img-crop": "^4.21.0",
|
"antd-img-crop": "^4.21.0",
|
||||||
"antd-style": "^3.6.2",
|
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
"contentful": "^10.13.3",
|
"contentful": "^10.13.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
@ -29,9 +26,7 @@
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-slick": "^0.29.0",
|
"react-slick": "^0.29.0",
|
||||||
"react-stripe-js": "^1.1.5",
|
|
||||||
"slick-carousel": "^1.8.1",
|
"slick-carousel": "^1.8.1",
|
||||||
"stripe": "^16.2.0",
|
|
||||||
"styled-components": "^6.1.1"
|
"styled-components": "^6.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { GeneralFilter, ExpertsData, ExpertDetails } from '../types/experts';
|
||||||
import { apiRequest } from './helpers';
|
import { apiRequest } from './helpers';
|
||||||
import { GeneralFilter, ExpertsData, ExpertDetails, ExpertScheduler, ExpertSchedulerSession, SignupSessionData } from '../types/experts';
|
|
||||||
|
|
||||||
export const getExpertsList = (locale: string, filter?: GeneralFilter): Promise<ExpertsData> => apiRequest({
|
export const getExpertsList = (locale: string, filter?: GeneralFilter): Promise<ExpertsData> => apiRequest({
|
||||||
url: '/home/coachsearch1',
|
url: '/home/coachsearch1',
|
||||||
|
@ -14,18 +14,3 @@ export const getExpertById = (id: string, locale: string): Promise<ExpertDetails
|
||||||
data: { id },
|
data: { id },
|
||||||
locale
|
locale
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getSchedulerByExpertId = (id: string, locale: string): Promise<ExpertScheduler> => apiRequest({
|
|
||||||
url: '/home/sessionsignupdata',
|
|
||||||
method: 'post',
|
|
||||||
data: { id },
|
|
||||||
locale
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getSchedulerSession = (data: SignupSessionData, locale: string, token: string): Promise<ExpertSchedulerSession> => apiRequest({
|
|
||||||
url: '/home/sessionsignupsubmit',
|
|
||||||
method: 'post',
|
|
||||||
data,
|
|
||||||
locale,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
|
|
|
@ -91,11 +91,3 @@ export const finishSession = (locale: string, token: string, sessionId: number):
|
||||||
locale,
|
locale,
|
||||||
token
|
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,79 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { Stripe } from "stripe";
|
|
||||||
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
import { formatAmountForStripe } from "../utils/stripe-helpers";
|
|
||||||
import { stripe } from "../lib/stripe";
|
|
||||||
|
|
||||||
export async function createCheckoutSession(
|
|
||||||
data: FormData,
|
|
||||||
): Promise<{ client_secret: string | null; url: string | null }> {
|
|
||||||
const ui_mode = data.get(
|
|
||||||
"uiMode",
|
|
||||||
) as Stripe.Checkout.SessionCreateParams.UiMode;
|
|
||||||
console.log('DATA', data)
|
|
||||||
const origin: string = headers().get("origin") as string;
|
|
||||||
|
|
||||||
const checkoutSession: Stripe.Checkout.Session =
|
|
||||||
await stripe.checkout.sessions.create({
|
|
||||||
mode: "payment",
|
|
||||||
submit_type: "donate",
|
|
||||||
line_items: [
|
|
||||||
{
|
|
||||||
quantity: 1,
|
|
||||||
price_data: {
|
|
||||||
currency: 'eur',
|
|
||||||
product_data: {
|
|
||||||
name: "Custom amount donation",
|
|
||||||
},
|
|
||||||
unit_amount: formatAmountForStripe(
|
|
||||||
Number(data.get("customDonation") as string),
|
|
||||||
'eur',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
...(ui_mode === "hosted" && {
|
|
||||||
success_url: `${origin}/payment/with-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
|
|
||||||
cancel_url: `${origin}/with-checkout`,
|
|
||||||
}),
|
|
||||||
...(ui_mode === "embedded" && {
|
|
||||||
return_url: `${origin}/payment/with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
|
|
||||||
}),
|
|
||||||
ui_mode,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
client_secret: checkoutSession.client_secret,
|
|
||||||
url: checkoutSession.url,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createPaymentIntent(
|
|
||||||
data: { amount: number, sessionId?: string },
|
|
||||||
): Promise<{ client_secret: string }> {
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
amount: formatAmountForStripe(
|
|
||||||
data.amount,
|
|
||||||
'eur',
|
|
||||||
),
|
|
||||||
automatic_payment_methods: { enabled: true },
|
|
||||||
currency: 'eur',
|
|
||||||
} as Stripe.PaymentIntentCreateParams;
|
|
||||||
|
|
||||||
if (data?.sessionId){
|
|
||||||
params.metadata = {
|
|
||||||
sessionId : data.sessionId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const paymentIntent: Stripe.PaymentIntent =
|
|
||||||
await stripe.paymentIntents.create(params);
|
|
||||||
|
|
||||||
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 React from 'react';
|
||||||
// import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Link from 'next/link';
|
import {getTranslations, unstable_setRequestLocale} from 'next-intl/server';
|
||||||
import { getTranslations, unstable_setRequestLocale } from 'next-intl/server';
|
|
||||||
import { i18nText } from '../../../../i18nKeys';
|
import { i18nText } from '../../../../i18nKeys';
|
||||||
import { fetchBlogPosts } from '../../../../lib/contentful/blogPosts';
|
import {fetchBlogPosts} from "../../../../lib/contentful/blogPosts";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function News({params: {locale}}: { params: { locale: string } }) {
|
export default async function News({params: {locale}}: { params: { locale: string } }) {
|
||||||
unstable_setRequestLocale(locale);
|
unstable_setRequestLocale(locale);
|
||||||
const t = await getTranslations('Main');
|
const t = await getTranslations('Main');
|
||||||
const { data, total } = await fetchBlogPosts({preview: false, sticky: true})
|
const {data, total} = await fetchBlogPosts({preview: false, sticky: true})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="main-articles">
|
<div className="main-articles">
|
||||||
|
|
|
@ -15,8 +15,7 @@ import React, { ReactNode } from 'react';
|
||||||
export default function MainLayout({ children, news, experts }: {
|
export default function MainLayout({ children, news, experts }: {
|
||||||
children: ReactNode,
|
children: ReactNode,
|
||||||
news: ReactNode,
|
news: ReactNode,
|
||||||
experts: ReactNode,
|
experts: ReactNode
|
||||||
payment: ReactNode
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -25,4 +24,4 @@ export default function MainLayout({ children, news, experts }: {
|
||||||
{experts}
|
{experts}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
||||||
}
|
}
|
||||||
}, [jwt]);
|
}, [jwt]);
|
||||||
|
|
||||||
return data ? (
|
return (
|
||||||
<Loader isLoading={loading}>
|
<Loader isLoading={loading}>
|
||||||
<ExpertProfile
|
<ExpertProfile
|
||||||
isFull={isFull}
|
isFull={isFull}
|
||||||
|
@ -66,5 +66,5 @@ export default function ExpertProfilePage({ params: { locale } }: { params: { lo
|
||||||
updateData={setData}
|
updateData={setData}
|
||||||
/>
|
/>
|
||||||
</Loader>
|
</Loader>
|
||||||
) : null;
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function SessionDetailItem({ params: { locale, slug } }: { params
|
||||||
<Suspense fallback={<p>Loading...</p>}>
|
<Suspense fallback={<p>Loading...</p>}>
|
||||||
<SessionDetails
|
<SessionDetails
|
||||||
locale={locale}
|
locale={locale}
|
||||||
sessionId={sessionId as number}
|
sessionId={sessionId}
|
||||||
activeType={sessionType as SessionType}
|
activeType={sessionType as SessionType}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { Metadata } from 'next';
|
||||||
import { unstable_setRequestLocale } from 'next-intl/server';
|
import { unstable_setRequestLocale } from 'next-intl/server';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { GeneralTopSection } from '../../../components/Page';
|
import { GeneralTopSection } from '../../../components/Page';
|
||||||
import { ScreenCarousel } from '../../../components/Page/ScreenCarousel';
|
import { ScreenCarousel } from '../../../components/Page/ScreenCarousel/index';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Bbuddy - Become a BB expert',
|
title: 'Bbuddy - Become a BB expert',
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {CustomPagination} from "../../../components/view/CustomPagination";
|
||||||
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
|
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
|
||||||
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
|
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
|
||||||
|
|
||||||
|
|
||||||
interface BlogPostPageParams {
|
interface BlogPostPageParams {
|
||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { getExpertById, getExpertsList } from '../../../../actions/experts';
|
||||||
import {
|
import {
|
||||||
ExpertCard,
|
ExpertCard,
|
||||||
ExpertCertificate,
|
ExpertCertificate,
|
||||||
|
ExpertInformation,
|
||||||
ExpertPractice
|
ExpertPractice
|
||||||
} from '../../../../components/Experts/ExpertDetails';
|
} from '../../../../components/Experts/ExpertDetails';
|
||||||
import { Details } from '../../../../types/education';
|
import { Details } from '../../../../types/education';
|
||||||
|
@ -81,7 +82,8 @@ export default async function ExpertItem({ params: { expertId = '', locale } }:
|
||||||
</BackButton>
|
</BackButton>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<ExpertCard expert={expert} locale={locale} expertId={expertId}/>
|
<ExpertCard expert={expert} locale={locale} />
|
||||||
|
<ExpertInformation expert={expert} locale={locale} />
|
||||||
|
|
||||||
<h2 className="title-h2">{i18nText('expertBackground', locale)}</h2>
|
<h2 className="title-h2">{i18nText('expertBackground', locale)}</h2>
|
||||||
<p className="base-text">
|
<p className="base-text">
|
||||||
|
|
|
@ -42,4 +42,4 @@ export default function LocaleLayout({ children, params: { locale } }: LayoutPro
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</AntdRegistry>
|
</AntdRegistry>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
import type { Stripe } from "stripe";
|
|
||||||
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
import { stripe } from "../../../lib/stripe";
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
|
||||||
let event: Stripe.Event;
|
|
||||||
|
|
||||||
try {
|
|
||||||
event = stripe.webhooks.constructEvent(
|
|
||||||
await (await req.blob()).text(),
|
|
||||||
req.headers.get("stripe-signature") as string,
|
|
||||||
process.env.STRIPE_WEBHOOK_SECRET as string,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
||||||
// On error, log and return the error message.
|
|
||||||
if (err! instanceof Error) console.log(err);
|
|
||||||
console.log(`❌ Error message: ${errorMessage}`);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: `Webhook Error: ${errorMessage}` },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Successfully constructed event.
|
|
||||||
console.log("✅ Success:", event.id);
|
|
||||||
|
|
||||||
const permittedEvents: string[] = [
|
|
||||||
"checkout.session.completed",
|
|
||||||
"payment_intent.succeeded",
|
|
||||||
"payment_intent.payment_failed",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (permittedEvents.includes(event.type)) {
|
|
||||||
let data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (event.type) {
|
|
||||||
case "checkout.session.completed":
|
|
||||||
data = event.data.object as Stripe.Checkout.Session;
|
|
||||||
console.log(`💰 CheckoutSession status: ${data.payment_status}`);
|
|
||||||
break;
|
|
||||||
case "payment_intent.payment_failed":
|
|
||||||
data = event.data.object as Stripe.PaymentIntent;
|
|
||||||
console.log(`❌ Payment failed: ${data.last_payment_error?.message}`);
|
|
||||||
break;
|
|
||||||
case "payment_intent.succeeded":
|
|
||||||
data = event.data.object as Stripe.PaymentIntent;
|
|
||||||
console.log(`💰 PaymentIntent status: ${data.status}`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unhandled event: ${event.type}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: "Webhook handler failed" },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return a response to acknowledge receipt of the event.
|
|
||||||
return NextResponse.json({ message: "Received" }, { status: 200 });
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Alert, message } from 'antd';
|
import {Alert, message} from 'antd';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
import { ExpertData, PayInfo, ProfileData } from '../../types/profile';
|
import { ExpertData, PayInfo, ProfileData } from '../../types/profile';
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { FC, useState, useEffect } from 'react';
|
import React, { FC } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Tag, Image as AntdImage, Space, Button } from 'antd';
|
import { Tag, Image as AntdImage, Space } from 'antd';
|
||||||
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
|
import { ZoomInOutlined, ZoomOutOutlined, StarFilled } from '@ant-design/icons';
|
||||||
import { SignupSessionData } from '../../types/experts';
|
|
||||||
import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts';
|
import { ExpertDetails, Practice, ThemeGroup } from '../../types/experts';
|
||||||
import { ExpertDocument } from '../../types/file';
|
import { ExpertDocument } from '../../types/file';
|
||||||
import { Locale } from '../../types/locale';
|
import { Locale } from '../../types/locale';
|
||||||
import { CustomRate } from '../view/CustomRate';
|
import { CustomRate } from '../view/CustomRate';
|
||||||
import { i18nText } from '../../i18nKeys';
|
import { i18nText } from '../../i18nKeys';
|
||||||
import { FilledYellowButton } from '../view/FilledButton';
|
import { FilledYellowButton } from '../view/FilledButton';
|
||||||
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 = {
|
type ExpertDetailsProps = {
|
||||||
expert: ExpertDetails;
|
expert: ExpertDetails;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
expertId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExpertPracticeProps = {
|
type ExpertPracticeProps = {
|
||||||
|
@ -28,77 +22,50 @@ type ExpertPracticeProps = {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId }) => {
|
export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
||||||
const { publicCoachDetails } = expert || {};
|
const { publicCoachDetails } = expert || {};
|
||||||
const [showSchedulerModal, setShowSchedulerModal] = useState<boolean>(false);
|
|
||||||
const [mode, setMode] = useState<'data' | 'time' | 'pay' | 'finish'>('data');
|
return (
|
||||||
|
<div className="expert-card">
|
||||||
|
<div className="expert-card__wrap">
|
||||||
|
<div className="expert-card__avatar">
|
||||||
|
<Image src={publicCoachDetails?.faceImageUrl || '/images/person.png'} width={216} height={216} alt="" />
|
||||||
|
</div>
|
||||||
|
<div className="expert-card__inner">
|
||||||
|
<h1 className="expert-card__title">{`${publicCoachDetails?.name} ${publicCoachDetails?.surname || ''}`}</h1>
|
||||||
|
<div className="expert-card__info">
|
||||||
|
<span>{`${publicCoachDetails?.practiceHours} ${i18nText('practiceHours', locale)}`}</span>
|
||||||
|
<i>|</i>
|
||||||
|
<span>{`${publicCoachDetails?.supervisionPerYearId} ${i18nText('supervisionCount', locale)}`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="expert-card__rating">
|
||||||
|
<CustomRate defaultValue={4} character={<StarFilled style={{ fontSize: 32 }} />} disabled />
|
||||||
|
<span>{`4/5 (${i18nText('outOf', locale)} 345)`}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="expert-card__wrap-btn">
|
||||||
|
<a href="#" className="btn-apply">
|
||||||
|
<img src="/images/calendar-outline.svg" className="" alt="" />
|
||||||
|
{i18nText('schedule', locale)}
|
||||||
|
</a>
|
||||||
|
{/*
|
||||||
|
<a href="#" className="btn-video">
|
||||||
|
<img src="/images/videocam-outline.svg" className="" alt=""/>
|
||||||
|
Video
|
||||||
|
</a>
|
||||||
|
*/}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExpertInformation: FC<ExpertDetailsProps> = ({ expert, locale }) => {
|
||||||
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] } } = expert || {};
|
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] } } = expert || {};
|
||||||
const isRus = locale === Locale.ru;
|
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="expert-card">
|
|
||||||
<div className="expert-card__wrap">
|
|
||||||
<div className="expert-card__avatar">
|
|
||||||
<Image src={publicCoachDetails?.faceImageUrl || '/images/person.png'} width={216} height={216} alt="" />
|
|
||||||
</div>
|
|
||||||
<div className="expert-card__inner">
|
|
||||||
<h1 className="expert-card__title">{`${publicCoachDetails?.name} ${publicCoachDetails?.surname || ''}`}</h1>
|
|
||||||
<div className="expert-card__info">
|
|
||||||
<span>{`${publicCoachDetails?.practiceHours} ${i18nText('practiceHours', locale)}`}</span>
|
|
||||||
<i>|</i>
|
|
||||||
<span>{`${publicCoachDetails?.supervisionPerYearId} ${i18nText('supervisionCount', locale)}`}</span>
|
|
||||||
</div>
|
|
||||||
<div className="expert-card__rating">
|
|
||||||
<CustomRate defaultValue={4} character={<StarFilled style={{ fontSize: 32 }} />} disabled />
|
|
||||||
<span>{`4/5 (${i18nText('outOf', locale)} 345)`}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="expert-card__wrap-btn">
|
|
||||||
<Button className="btn-apply" onClick={onSchedulerHandle}>
|
|
||||||
<img src="/images/calendar-outline.svg" className="" alt="" />
|
|
||||||
{i18nText('schedule', locale)}
|
|
||||||
</Button>
|
|
||||||
{/*
|
|
||||||
<a href="#" className="btn-video">
|
|
||||||
<img src="/images/videocam-outline.svg" className="" alt=""/>
|
|
||||||
Video
|
|
||||||
</a>
|
|
||||||
*/}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="expert-info">
|
<div className="expert-info">
|
||||||
{/* <h2 className="title-h2">{}</h2> */}
|
{/* <h2 className="title-h2">{}</h2> */}
|
||||||
<div className="skills__list">
|
<div className="skills__list">
|
||||||
|
@ -124,22 +91,11 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId })
|
||||||
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
{tags?.map((skill) => <Tag key={skill?.id} className="skills__list__item">{skill?.name}</Tag>)}
|
||||||
</div>
|
</div>
|
||||||
<div className="wrap-btn-prise">
|
<div className="wrap-btn-prise">
|
||||||
<FilledYellowButton onClick={onSchedulerHandle}>{i18nText('signUp', locale)}</FilledYellowButton>
|
<FilledYellowButton onClick={() => console.log('schedule')}>{i18nText('signUp', locale)}</FilledYellowButton>
|
||||||
<div className="wrap-btn-prise__text">
|
<div className="wrap-btn-prise__text">
|
||||||
{`${sessionCost}€`} <span>/ {`${sessionDuration}${isRus ? 'мин' : 'min'}`}</span>
|
{`${sessionCost}€`} <span>/ {`${sessionDuration}${isRus ? 'мин' : 'min'}`}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScheduleModal
|
|
||||||
open={showSchedulerModal}
|
|
||||||
handleCancel={() => setShowSchedulerModal(false)}
|
|
||||||
updateMode={setMode}
|
|
||||||
mode={mode}
|
|
||||||
expertId={expertId as string}
|
|
||||||
locale={locale as string}
|
|
||||||
sessionCost={sessionCost}
|
|
||||||
checkSession={checkSession}
|
|
||||||
/>
|
|
||||||
<ScheduleModalResult locale={locale as string} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -114,6 +114,7 @@ export const ExpertsFilter = ({
|
||||||
...getObjectByAdditionalFilter(searchParams)
|
...getObjectByAdditionalFilter(searchParams)
|
||||||
};
|
};
|
||||||
const search = getSearchParamsString(newFilter);
|
const search = getSearchParamsString(newFilter);
|
||||||
|
console.log('basePath', basePath);
|
||||||
|
|
||||||
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
router.push(search ? `${basePath}?${search}#filter` : `${basePath}#filter`);
|
||||||
|
|
||||||
|
|
|
@ -1,312 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import React, {FC, useEffect, useState} from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
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';
|
|
||||||
import locale_en from 'antd/lib/calendar/locale/en_GB';
|
|
||||||
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 dayjs, { Dayjs } from 'dayjs';
|
|
||||||
import 'dayjs/locale/ru';
|
|
||||||
import 'dayjs/locale/en';
|
|
||||||
import 'dayjs/locale/de';
|
|
||||||
import 'dayjs/locale/it';
|
|
||||||
import 'dayjs/locale/fr';
|
|
||||||
import 'dayjs/locale/es';
|
|
||||||
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;
|
|
||||||
handleCancel: () => void;
|
|
||||||
mode: 'data' | 'time' | 'pay' | 'finish';
|
|
||||||
updateMode: (mode: 'data' | 'time' | 'pay' | 'finish') => void;
|
|
||||||
sessionCost: number;
|
|
||||||
expertId: string;
|
|
||||||
locale: string;
|
|
||||||
checkSession: (data?: SignupSessionData) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MenuItem = Required<MenuProps>['items'][number];
|
|
||||||
|
|
||||||
const getLocale = (locale: string) => {
|
|
||||||
if (locale) {
|
|
||||||
switch (locale) {
|
|
||||||
case 'ru':
|
|
||||||
return locale_ru;
|
|
||||||
case 'de':
|
|
||||||
return locale_de;
|
|
||||||
case 'fr':
|
|
||||||
return locale_fr;
|
|
||||||
case 'it':
|
|
||||||
return locale_it;
|
|
||||||
case 'es':
|
|
||||||
return locale_es;
|
|
||||||
default:
|
|
||||||
return locale_en;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return locale_en;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCalendarMenu = (start: Dayjs): MenuItem[] => Array.from({ length: 3 })
|
|
||||||
.map((_: unknown, index: number) => {
|
|
||||||
const date = index ? start.add(index, 'M') : start.clone();
|
|
||||||
return {
|
|
||||||
label: <span className="b-calendar-month">{date.format('MMMM')}</span>,
|
|
||||||
key: date.format('YYYY-MM-DD')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ScheduleModal: FC<ScheduleModalProps> = ({
|
|
||||||
open,
|
|
||||||
handleCancel,
|
|
||||||
mode,
|
|
||||||
updateMode,
|
|
||||||
sessionCost,
|
|
||||||
locale,
|
|
||||||
expertId,
|
|
||||||
checkSession,
|
|
||||||
}) => {
|
|
||||||
const [selectDate, setSelectDate] = useState<Dayjs>(dayjs());
|
|
||||||
const [dates, setDates] = useState<Record<string, { startTime: string, endTime: string }[]> | undefined>();
|
|
||||||
const [tags, setTags] = useState<Tag[] | undefined>();
|
|
||||||
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);
|
|
||||||
|
|
||||||
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 && mode !== 'pay') {
|
|
||||||
getSchedulerByExpertId(expertId as string, locale as string)
|
|
||||||
.then((data) => {
|
|
||||||
setRawScheduler(data);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!open) {
|
|
||||||
form.resetFields();
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open && mode === 'pay') {
|
|
||||||
signupSession();
|
|
||||||
}
|
|
||||||
}, [mode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const map = {} as any
|
|
||||||
rawScheduler?.availableSlots.forEach((el) => {
|
|
||||||
const key = dayjs(el.startTime).format('YYYY-MM-DD');
|
|
||||||
if (!map[key]){
|
|
||||||
map[key] = []
|
|
||||||
}
|
|
||||||
map[key].push(el);
|
|
||||||
})
|
|
||||||
setDates(map);
|
|
||||||
setTags(rawScheduler?.tags)
|
|
||||||
}, [rawScheduler]);
|
|
||||||
|
|
||||||
const onPanelChange = (value: Dayjs) => setSelectDate(value);
|
|
||||||
|
|
||||||
const onDateChange: CalendarProps<Dayjs>['onSelect'] = (value, selectInfo) => {
|
|
||||||
if (selectInfo.source === 'date') {
|
|
||||||
setSelectDate(value);
|
|
||||||
updateMode('time');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disabledDate = (currentDate: Dayjs) => !dates || !dates[currentDate.format('YYYY-MM-DD')];
|
|
||||||
|
|
||||||
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
|
|
||||||
const isWeekend = date.day() === 6 || date.day() === 0;
|
|
||||||
return React.cloneElement(info.originNode, {
|
|
||||||
...info.originNode.props,
|
|
||||||
className: classNames('b-calendar-cell', {
|
|
||||||
['b-calendar-cell__select']: selectDate.isSame(date, 'date'),
|
|
||||||
['b-calendar-cell__today']: date.isSame(dayjs(), 'date'),
|
|
||||||
['b-calendar-cell__weekend']: isWeekend,
|
|
||||||
}),
|
|
||||||
children: (
|
|
||||||
<div>
|
|
||||||
<span>
|
|
||||||
{date.get('date')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onValidate = () => {
|
|
||||||
form.validateFields()
|
|
||||||
.then((values) => {
|
|
||||||
checkSession({ coachId: +expertId, ...values });
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="b-modal"
|
|
||||||
open={open}
|
|
||||||
title={undefined}
|
|
||||||
onOk={undefined}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
footer={false}
|
|
||||||
width={498}
|
|
||||||
closeIcon={<CloseOutlined style={{ fontSize: 20, color: '#000' }}/>}
|
|
||||||
>
|
|
||||||
{mode === 'data' && (
|
|
||||||
<Calendar
|
|
||||||
className="b-calendar"
|
|
||||||
fullscreen={false}
|
|
||||||
onPanelChange={onPanelChange}
|
|
||||||
fullCellRender={cellRender}
|
|
||||||
onSelect={onDateChange}
|
|
||||||
value={selectDate}
|
|
||||||
disabledDate={disabledDate}
|
|
||||||
locale={getLocale(locale)}
|
|
||||||
validRange={[selectDate.startOf('M'), selectDate.endOf('M')]}
|
|
||||||
headerRender={({ onChange }) => {
|
|
||||||
const start = dayjs().startOf('M');
|
|
||||||
const [activeMonth, setActiveMonth] = useState<string>(start.format('YYYY-MM-DD'));
|
|
||||||
|
|
||||||
const onClick: MenuProps['onClick'] = (e) => {
|
|
||||||
setActiveMonth(e.key);
|
|
||||||
onChange(dayjs(e.key));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu
|
|
||||||
className="b-calendar-header"
|
|
||||||
onClick={onClick}
|
|
||||||
selectedKeys={[activeMonth]}
|
|
||||||
mode="horizontal"
|
|
||||||
items={getCalendarMenu(start)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{mode === 'time' && (
|
|
||||||
<div className="b-schedule-time">
|
|
||||||
<div className="b-schedule-time-header">
|
|
||||||
<Button
|
|
||||||
className="b-button-link-big"
|
|
||||||
type="link"
|
|
||||||
onClick={() => updateMode('data')}
|
|
||||||
icon={<ArrowLeftOutlined />}
|
|
||||||
iconPosition="start"
|
|
||||||
>
|
|
||||||
{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)}
|
|
||||||
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>
|
|
||||||
</Form>
|
|
||||||
<Button
|
|
||||||
className="btn-apply"
|
|
||||||
onClick={onValidate}
|
|
||||||
>
|
|
||||||
{i18nText('pay', locale)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{mode === 'pay' && (
|
|
||||||
<div className="b-schedule-payment">
|
|
||||||
<Loader isLoading={isPayLoading}>
|
|
||||||
<StripeElementsForm
|
|
||||||
amount={sessionCost}
|
|
||||||
locale={locale}
|
|
||||||
sessionId={sessionId}
|
|
||||||
/>
|
|
||||||
</Loader>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,73 +0,0 @@
|
||||||
'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,31 +23,6 @@ function HeaderAuthLinks ({
|
||||||
const selectedLayoutSegment = useSelectedLayoutSegment();
|
const selectedLayoutSegment = useSelectedLayoutSegment();
|
||||||
const pathname = selectedLayoutSegment || '';
|
const pathname = selectedLayoutSegment || '';
|
||||||
const [token, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isOpenModal) {
|
if (!isOpenModal) {
|
||||||
|
@ -55,16 +30,9 @@ function HeaderAuthLinks ({
|
||||||
}
|
}
|
||||||
}, [isOpenModal]);
|
}, [isOpenModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
const onOpen = (mode: 'enter' | 'register' | 'reset' | 'finish') => {
|
||||||
if (token && isPayPath) {
|
setMode(mode);
|
||||||
const showPayForm = new Event('show_pay_form');
|
setIsOpenModal(true);
|
||||||
document.dispatchEvent(showPayForm);
|
|
||||||
}
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
const addNewEvent = (name: 'show_auth_register' | 'show_auth_enter') => {
|
|
||||||
const evt = new Event(name);
|
|
||||||
document.dispatchEvent(evt);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
@ -81,7 +49,7 @@ function HeaderAuthLinks ({
|
||||||
<Button
|
<Button
|
||||||
className="b-header__auth"
|
className="b-header__auth"
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => addNewEvent('show_auth_register')}
|
onClick={() => onOpen('register')}
|
||||||
>
|
>
|
||||||
{i18nText('registration', locale)}
|
{i18nText('registration', locale)}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -93,7 +61,7 @@ function HeaderAuthLinks ({
|
||||||
<Button
|
<Button
|
||||||
className="b-header__auth"
|
className="b-header__auth"
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => addNewEvent('show_auth_enter')}
|
onClick={() => onOpen('enter')}
|
||||||
>
|
>
|
||||||
{i18nText('enter', locale)}
|
{i18nText('enter', locale)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,165 +0,0 @@
|
||||||
'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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -16,8 +16,8 @@ export const WithError: FC<WithErrorProps> = ({
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
status="error"
|
status="error"
|
||||||
title={errorData?.title}
|
title="Submission Failed"
|
||||||
subTitle={errorData?.message}
|
subTitle="Please check and modify the following information before resubmitting."
|
||||||
extra={refresh ? (
|
extra={refresh ? (
|
||||||
<Button type="primary" onClick={refresh}>
|
<Button type="primary" onClick={refresh}>
|
||||||
Refresh page
|
Refresh page
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
export const BASE_URL = process.env.NEXT_PUBLIC_SERVER_BASE_URL || 'https://api.bbuddy.expert/api';
|
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_TOKEN_KEY = 'bbuddy_token';
|
||||||
export const AUTH_USER = 'bbuddy_auth_user';
|
export const AUTH_USER = 'bbuddy_auth_user';
|
||||||
export const SESSION_DATA = 'bbuddy_session_data';
|
|
||||||
|
|
||||||
export const DEFAULT_PAGE_SIZE = 5;
|
export const DEFAULT_PAGE_SIZE = 5;
|
||||||
export const DEFAULT_PAGE = 1;
|
export const DEFAULT_PAGE = 1;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
export function getStorageValue (key: string, defaultValue: any) {
|
function getStorageValue (key: string, defaultValue: any) {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const saved = localStorage.getItem(key);
|
const saved = localStorage.getItem(key);
|
||||||
return saved || defaultValue;
|
return saved || defaultValue;
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
addComment: 'Neuen Kommentar hinzufügen',
|
addComment: 'Neuen Kommentar hinzufügen',
|
||||||
commentPlaceholder: 'Ihr Kommentar',
|
commentPlaceholder: 'Ihr Kommentar',
|
||||||
clientComments: 'Kundenkommentare',
|
clientComments: 'Kundenkommentare',
|
||||||
coachComments: 'Expertenkommentare'
|
coachComments: 'Trainerkommentare'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Zukünftige Räume',
|
upcoming: 'Zukünftige Räume',
|
||||||
|
@ -110,9 +110,9 @@ export default {
|
||||||
seminars: 'Seminare',
|
seminars: 'Seminare',
|
||||||
courses: 'Kurse',
|
courses: 'Kurse',
|
||||||
mba: 'MBA-Information',
|
mba: 'MBA-Information',
|
||||||
aboutCoach: 'Über den Experten',
|
aboutCoach: 'Über Coach',
|
||||||
education: 'Bildung',
|
education: 'Bildung',
|
||||||
coaching: 'Expertenprofil',
|
coaching: 'Coaching',
|
||||||
experiences: 'Praktische Erfahrung',
|
experiences: 'Praktische Erfahrung',
|
||||||
payInfo: 'Zahlungsdaten',
|
payInfo: 'Zahlungsdaten',
|
||||||
sessionDuration: 'Sitzungsdauer',
|
sessionDuration: 'Sitzungsdauer',
|
||||||
|
@ -146,10 +146,6 @@ export default {
|
||||||
saturday: 'Sa',
|
saturday: 'Sa',
|
||||||
addNew: 'Neu hinzufügen',
|
addNew: 'Neu hinzufügen',
|
||||||
mExperiences: 'Führungserfahrung',
|
mExperiences: 'Führungserfahrung',
|
||||||
pay: 'Zahlung',
|
|
||||||
sessionWishes: 'Schreiben Sie Ihre Wünsche zur Sitzung',
|
|
||||||
successPayment: 'Erfolgreiche Zahlung',
|
|
||||||
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',
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
addComment: 'Add new',
|
addComment: 'Add new',
|
||||||
commentPlaceholder: 'Your comment',
|
commentPlaceholder: 'Your comment',
|
||||||
clientComments: 'Client Comments',
|
clientComments: 'Client Comments',
|
||||||
coachComments: 'Expert Comments'
|
coachComments: 'Coach Comments'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Upcoming Rooms',
|
upcoming: 'Upcoming Rooms',
|
||||||
|
@ -109,10 +109,10 @@ export default {
|
||||||
seminars: 'Seminars',
|
seminars: 'Seminars',
|
||||||
courses: 'Courses',
|
courses: 'Courses',
|
||||||
mba: 'MBA Information',
|
mba: 'MBA Information',
|
||||||
aboutCoach: 'About Expert',
|
aboutCoach: 'About Coach',
|
||||||
skillsInfo: 'Skills Info',
|
skillsInfo: 'Skills Info',
|
||||||
education: 'Education',
|
education: 'Education',
|
||||||
coaching: 'Expert profile',
|
coaching: 'Coaching',
|
||||||
experiences: 'Practical experience',
|
experiences: 'Practical experience',
|
||||||
payInfo: 'Payment Info',
|
payInfo: 'Payment Info',
|
||||||
sessionDuration: 'Session duration',
|
sessionDuration: 'Session duration',
|
||||||
|
@ -146,10 +146,6 @@ export default {
|
||||||
saturday: 'Sa',
|
saturday: 'Sa',
|
||||||
addNew: 'Add New',
|
addNew: 'Add New',
|
||||||
mExperiences: 'Managerial Experience',
|
mExperiences: 'Managerial Experience',
|
||||||
pay: 'Pay',
|
|
||||||
sessionWishes: 'Write your wishes about the session',
|
|
||||||
successPayment: 'Successful Payment',
|
|
||||||
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',
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
addComment: 'Añadir nuevo comentario',
|
addComment: 'Añadir nuevo comentario',
|
||||||
commentPlaceholder: 'Tu comentario',
|
commentPlaceholder: 'Tu comentario',
|
||||||
clientComments: 'Comentarios del cliente',
|
clientComments: 'Comentarios del cliente',
|
||||||
coachComments: 'Comentarios del experto'
|
coachComments: 'Comentarios del entrenador'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Próximas salas',
|
upcoming: 'Próximas salas',
|
||||||
|
@ -110,9 +110,9 @@ export default {
|
||||||
seminars: 'Seminarios',
|
seminars: 'Seminarios',
|
||||||
courses: 'Cursos',
|
courses: 'Cursos',
|
||||||
mba: 'Información sobre máster en ADE (MBA)',
|
mba: 'Información sobre máster en ADE (MBA)',
|
||||||
aboutCoach: 'Acerca del experto',
|
aboutCoach: 'Sobre el coach',
|
||||||
education: 'Educación',
|
education: 'Educación',
|
||||||
coaching: 'Perfil del experto',
|
coaching: 'Coaching',
|
||||||
experiences: 'Experiencia práctica',
|
experiences: 'Experiencia práctica',
|
||||||
payInfo: 'Información de pago',
|
payInfo: 'Información de pago',
|
||||||
sessionDuration: 'Duración de la sesión',
|
sessionDuration: 'Duración de la sesión',
|
||||||
|
@ -146,10 +146,6 @@ export default {
|
||||||
saturday: 'S',
|
saturday: 'S',
|
||||||
addNew: 'Añadir nuevo',
|
addNew: 'Añadir nuevo',
|
||||||
mExperiences: 'Experiencia de dirección',
|
mExperiences: 'Experiencia de dirección',
|
||||||
pay: 'Pago',
|
|
||||||
sessionWishes: 'Escribe tus deseos sobre la sesión',
|
|
||||||
successPayment: 'Pago Exitoso',
|
|
||||||
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',
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
addComment: 'Ajouter un nouveau commentaire',
|
addComment: 'Ajouter un nouveau commentaire',
|
||||||
commentPlaceholder: 'Votre commentaire',
|
commentPlaceholder: 'Votre commentaire',
|
||||||
clientComments: 'Commentaires du client',
|
clientComments: 'Commentaires du client',
|
||||||
coachComments: 'Commentaires de l\'expert'
|
coachComments: 'Commentaires du coach'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Salles futures',
|
upcoming: 'Salles futures',
|
||||||
|
@ -110,9 +110,9 @@ export default {
|
||||||
seminars: 'Séminaires',
|
seminars: 'Séminaires',
|
||||||
courses: 'Cours',
|
courses: 'Cours',
|
||||||
mba: 'Infos Maîtrise en gestion',
|
mba: 'Infos Maîtrise en gestion',
|
||||||
aboutCoach: 'À propos de l\'expert',
|
aboutCoach: 'À propos du coach',
|
||||||
education: 'Éducation',
|
education: 'Éducation',
|
||||||
coaching: 'Profil de l\'expert',
|
coaching: 'Coaching',
|
||||||
experiences: 'Expérience pratique',
|
experiences: 'Expérience pratique',
|
||||||
payInfo: 'Infos sur le paiement',
|
payInfo: 'Infos sur le paiement',
|
||||||
sessionDuration: 'Durée de la session',
|
sessionDuration: 'Durée de la session',
|
||||||
|
@ -146,10 +146,6 @@ export default {
|
||||||
saturday: 'Sa',
|
saturday: 'Sa',
|
||||||
addNew: 'Ajouter un nouveau',
|
addNew: 'Ajouter un nouveau',
|
||||||
mExperiences: 'Expérience en gestion',
|
mExperiences: 'Expérience en gestion',
|
||||||
pay: 'Paiement',
|
|
||||||
sessionWishes: 'Écrivez vos souhaits concernant la session',
|
|
||||||
successPayment: 'Paiement Réussi',
|
|
||||||
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',
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
addComment: 'Aggiungi nuovo commento',
|
addComment: 'Aggiungi nuovo commento',
|
||||||
commentPlaceholder: 'Il tuo commento',
|
commentPlaceholder: 'Il tuo commento',
|
||||||
clientComments: 'Commenti del cliente',
|
clientComments: 'Commenti del cliente',
|
||||||
coachComments: 'Commenti dell\'esperto'
|
coachComments: 'Commenti dell\'allenatore'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Prossime sale',
|
upcoming: 'Prossime sale',
|
||||||
|
@ -110,9 +110,9 @@ export default {
|
||||||
seminars: 'Seminari',
|
seminars: 'Seminari',
|
||||||
courses: 'Corsi',
|
courses: 'Corsi',
|
||||||
mba: 'Info sull\'MBA',
|
mba: 'Info sull\'MBA',
|
||||||
aboutCoach: 'Informazioni sull\'esperto',
|
aboutCoach: 'Informazioni sul coach',
|
||||||
education: 'Istruzione',
|
education: 'Istruzione',
|
||||||
coaching: 'Profilo dell\'esperto',
|
coaching: 'Coaching',
|
||||||
experiences: 'Esperienza pratica',
|
experiences: 'Esperienza pratica',
|
||||||
payInfo: 'Info pagamento',
|
payInfo: 'Info pagamento',
|
||||||
sessionDuration: 'Durata della sessione',
|
sessionDuration: 'Durata della sessione',
|
||||||
|
@ -146,10 +146,6 @@ export default {
|
||||||
saturday: 'Sa',
|
saturday: 'Sa',
|
||||||
addNew: 'Aggiungi nuovo',
|
addNew: 'Aggiungi nuovo',
|
||||||
mExperiences: 'Esperienza manageriale',
|
mExperiences: 'Esperienza manageriale',
|
||||||
pay: 'Pagamento',
|
|
||||||
sessionWishes: 'Scrivi i tuoi desideri riguardo alla sessione',
|
|
||||||
successPayment: 'Pagamento Riuscito',
|
|
||||||
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',
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
addComment: 'Добавить новый',
|
addComment: 'Добавить новый',
|
||||||
commentPlaceholder: 'Ваш комментарий',
|
commentPlaceholder: 'Ваш комментарий',
|
||||||
clientComments: 'Комментарии клиента',
|
clientComments: 'Комментарии клиента',
|
||||||
coachComments: 'Комментарии эксперта'
|
coachComments: 'Комментарии коуча'
|
||||||
},
|
},
|
||||||
room: {
|
room: {
|
||||||
upcoming: 'Предстоящие комнаты',
|
upcoming: 'Предстоящие комнаты',
|
||||||
|
@ -111,9 +111,9 @@ export default {
|
||||||
courses: 'Курсы',
|
courses: 'Курсы',
|
||||||
mba: 'Информация о MBA',
|
mba: 'Информация о MBA',
|
||||||
experiences: 'Практический опыт',
|
experiences: 'Практический опыт',
|
||||||
aboutCoach: 'Информация об эксперте',
|
aboutCoach: 'О коуче',
|
||||||
education: 'Образование',
|
education: 'Образование',
|
||||||
coaching: 'Профиль эксперта',
|
coaching: 'Коучинг',
|
||||||
payInfo: 'Платежная информация',
|
payInfo: 'Платежная информация',
|
||||||
sessionDuration: 'Продолжительность сессии',
|
sessionDuration: 'Продолжительность сессии',
|
||||||
experienceHours: 'Общее количество часов практического опыта',
|
experienceHours: 'Общее количество часов практического опыта',
|
||||||
|
@ -146,10 +146,6 @@ export default {
|
||||||
saturday: 'Сб',
|
saturday: 'Сб',
|
||||||
addNew: 'Добавить',
|
addNew: 'Добавить',
|
||||||
mExperiences: 'Управленческий опыт',
|
mExperiences: 'Управленческий опыт',
|
||||||
pay: 'Оплата',
|
|
||||||
sessionWishes: 'Напишите свои пожелания по поводу сессии',
|
|
||||||
successPayment: 'Успешная оплата',
|
|
||||||
errorPayment: 'Ошибка оплаты',
|
|
||||||
errors: {
|
errors: {
|
||||||
invalidEmail: 'Адрес электронной почты недействителен',
|
invalidEmail: 'Адрес электронной почты недействителен',
|
||||||
emptyEmail: 'Пожалуйста, введите ваш E-mail',
|
emptyEmail: 'Пожалуйста, введите ваш E-mail',
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import "server-only";
|
|
||||||
|
|
||||||
import Stripe from "stripe";
|
|
||||||
|
|
||||||
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
|
|
||||||
apiVersion: "2024-06-20",
|
|
||||||
appInfo: {
|
|
||||||
name: "bbuddy-ui",
|
|
||||||
url: "",
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,62 +0,0 @@
|
||||||
.b-calendar {
|
|
||||||
padding: 44px 40px !important;
|
|
||||||
|
|
||||||
&-month {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-header {
|
|
||||||
justify-content: center;
|
|
||||||
border-bottom: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-cell {
|
|
||||||
span {
|
|
||||||
color: #66A5AD;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__weekend {
|
|
||||||
span {
|
|
||||||
color: #FFBD00;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-picker-body {
|
|
||||||
margin-bottom: -42px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-picker-panel {
|
|
||||||
border-top: none !important;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-picker-cell {
|
|
||||||
opacity: 0 !important;
|
|
||||||
|
|
||||||
&-disabled {
|
|
||||||
&::before {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: rgba(0, 0, 0, 0.25) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ant-picker-cell-in-view {
|
|
||||||
opacity: 1 !important;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
vertical-align: middle !important;
|
|
||||||
height: 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
color: #66A5AD !important;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
.ant-form-item-has-error .ant-radio-inner {
|
|
||||||
border-color: #ff4d4f !important;
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
.b-schedule {
|
|
||||||
&-time {
|
|
||||||
padding: 44px 40px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
|
|
||||||
.b-button-link-big {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 32px;
|
|
||||||
color: #6FB98F !important;
|
|
||||||
font-family: var(--font-comfortaa);
|
|
||||||
padding: 0 !important;
|
|
||||||
border: none !important;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-radio-list {
|
|
||||||
.ant-radio-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-payment {
|
|
||||||
padding: 44px 40px;
|
|
||||||
min-height: 300px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
height: 54px !important;
|
height: 54px !important;
|
||||||
|
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
background-color: transparent !important;
|
background-color: #F8F8F7 !important;
|
||||||
border-color: #F8F8F7 !important;
|
border-color: #F8F8F7 !important;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
padding: 22px 16px 8px !important;
|
padding: 22px 16px 8px !important;
|
||||||
|
@ -17,12 +17,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.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;
|
||||||
}
|
}
|
||||||
|
@ -41,9 +35,6 @@
|
||||||
&-wrap {
|
&-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #F8F8F7;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
&.b-multiselect__active .b-multiselect-label {
|
&.b-multiselect__active .b-multiselect-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
@ -58,7 +49,7 @@
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: #000;
|
color: #000;
|
||||||
opacity: .4;
|
opacity: .3;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
|
@ -79,12 +70,11 @@
|
||||||
height: 54px !important;
|
height: 54px !important;
|
||||||
|
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
background-color: transparent !important;
|
background-color: #F8F8F7 !important;
|
||||||
border-color: #F8F8F7 !important;
|
border-color: #F8F8F7 !important;
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
padding: 22px 16px 8px !important;
|
padding: 22px 16px 8px !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.ant-select-selection-item {
|
.ant-select-selection-item {
|
||||||
font-size: 15px !important;
|
font-size: 15px !important;
|
||||||
|
@ -94,12 +84,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ant-select-status-error {
|
|
||||||
.ant-select-selector {
|
|
||||||
border-color: #ff4d4f !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-select-arrow {
|
.ant-select-arrow {
|
||||||
color: #2c7873 !important;
|
color: #2c7873 !important;
|
||||||
}
|
}
|
||||||
|
@ -114,8 +98,6 @@
|
||||||
&-wrap {
|
&-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #F8F8F7;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
&.b-select__active .b-select-label {
|
&.b-select__active .b-select-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
@ -131,7 +113,7 @@
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: #000;
|
color: #000;
|
||||||
opacity: .4;
|
opacity: .3;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
top: 15px;
|
top: 15px;
|
||||||
|
|
|
@ -9,6 +9,3 @@
|
||||||
@import "_practice.scss";
|
@import "_practice.scss";
|
||||||
@import "_collapse.scss";
|
@import "_collapse.scss";
|
||||||
@import "_timepicker.scss";
|
@import "_timepicker.scss";
|
||||||
@import "_calendar.scss";
|
|
||||||
@import "_schedule.scss";
|
|
||||||
@import "_radio.scss";
|
|
||||||
|
|
|
@ -70,24 +70,3 @@ export type ExpertDetails = {
|
||||||
associations?: Association[];
|
associations?: Association[];
|
||||||
associationLevels?: AssociationLevel[];
|
associationLevels?: AssociationLevel[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Slot = {
|
|
||||||
startTime: string;
|
|
||||||
endTime: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExpertScheduler = {
|
|
||||||
tags: Tag[],
|
|
||||||
availableSlots: Slot[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ExpertSchedulerSession = {
|
|
||||||
sessionId: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SignupSessionData = {
|
|
||||||
coachId: number,
|
|
||||||
tagId?: number,
|
|
||||||
startAtUtc?: string,
|
|
||||||
clientComment?: string
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
export type Payment = {
|
|
||||||
amount: number;
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Stripe, loadStripe } from '@stripe/stripe-js';
|
|
||||||
|
|
||||||
let stripePromise: Promise<Stripe | null>;
|
|
||||||
|
|
||||||
export default function getStripe(): Promise<Stripe | null> {
|
|
||||||
if (!stripePromise)
|
|
||||||
stripePromise = loadStripe(
|
|
||||||
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string,
|
|
||||||
);
|
|
||||||
|
|
||||||
return stripePromise;
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
export function formatAmountForDisplay(
|
|
||||||
amount: number,
|
|
||||||
currency: string,
|
|
||||||
): string {
|
|
||||||
let numberFormat = new Intl.NumberFormat(["en-US"], {
|
|
||||||
style: "currency",
|
|
||||||
currency: currency,
|
|
||||||
currencyDisplay: "symbol",
|
|
||||||
});
|
|
||||||
return numberFormat.format(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatAmountForStripe(
|
|
||||||
amount: number,
|
|
||||||
currency: string,
|
|
||||||
): number {
|
|
||||||
let numberFormat = new Intl.NumberFormat(["en-US"], {
|
|
||||||
style: "currency",
|
|
||||||
currency: currency,
|
|
||||||
currencyDisplay: "symbol",
|
|
||||||
});
|
|
||||||
const parts = numberFormat.formatToParts(amount);
|
|
||||||
let zeroDecimalCurrency: boolean = true;
|
|
||||||
for (let part of parts) {
|
|
||||||
if (part.type === "decimal") {
|
|
||||||
zeroDecimalCurrency = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return zeroDecimalCurrency ? amount : Math.round(amount * 100);
|
|
||||||
}
|
|
Loading…
Reference in New Issue