Compare commits

..

No commits in common. "master" and "v0.3.0" have entirely different histories.

73 changed files with 1655 additions and 2891 deletions

5
.env
View File

@ -1,9 +1,8 @@
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_GOOGLE_CLIENT_ID=909563069647-03rivr8k1jmirf382bcfehegamthcfg4.apps.googleusercontent.com
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LVB3LK5pVGxNPeKk4gedt5NW4cb8k7BVXvgOMPTK4x1nnbGTD8BCqDqgInboT6N72YwrTl4tOsVz8rAjbUadX1m00y4Aq5qE8 NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LVB3LK5pVGxNPeKk4gedt5NW4cb8k7BVXvgOMPTK4x1nnbGTD8BCqDqgInboT6N72YwrTl4tOsVz8rAjbUadX1m00y4Aq5qE8
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_test_51LVB3LK5pVGxNPeK6j0wCsPqYMoGfcuwf1LpwGEBsr1dUx4NngukyjYL2oMZer5EOlW3lqnVEPjNDruN0OkUohIf00fWFUHN5O STRIPE_SECRET_KEY=sk_test_51LVB3LK5pVGxNPeK6j0wCsPqYMoGfcuwf1LpwGEBsr1dUx4NngukyjYL2oMZer5EOlW3lqnVEPjNDruN0OkUohIf00fWFUHN5O
NEXT_PUBLIC_STRIPE_PAYMENT_DESCRIPTION='BBuddy services' 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

2
.gitignore vendored
View File

@ -38,5 +38,3 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
certificates

View File

@ -1,6 +1,5 @@
// @ts-check // @ts-check
const createNextIntlPlugin = require('next-intl/plugin'); const withNextIntl = require('next-intl/plugin')();
const withNextIntl = createNextIntlPlugin();
const path = require('path'); const path = require('path');
const json = require('./package.json'); const json = require('./package.json');

3735
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "bbuddy-ui", "name": "bbuddy-ui",
"version": "0.3.5", "version": "0.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 4200", "dev": "next dev -p 4200",
@ -14,7 +14,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",
"@microsoft/signalr": "^8.0.7", "@microsoft/signalr": "^8.0.7",
"@react-oauth/google": "^0.12.1",
"@stripe/react-stripe-js": "^2.7.3", "@stripe/react-stripe-js": "^2.7.3",
"@stripe/stripe-js": "^4.1.0", "@stripe/stripe-js": "^4.1.0",
"agora-rtc-react": "2.1.0", "agora-rtc-react": "2.1.0",
@ -29,7 +28,6 @@
"next": "14.0.3", "next": "14.0.3",
"next-intl": "^3.3.1", "next-intl": "^3.3.1",
"react": "^18", "react": "^18",
"react-apple-login": "^1.1.6",
"react-dom": "^18", "react-dom": "^18",
"react-signalr": "^0.2.24", "react-signalr": "^0.2.24",
"react-slick": "^0.29.0", "react-slick": "^0.29.0",

View File

@ -12,39 +12,3 @@ export const getRegister = (locale: string): Promise<{ jwtToken: string }> => ap
method: 'post', method: 'post',
locale locale
}); });
export const getRegisterByGoogle = (locale: string, accesstoken: string): Promise<{ jwtToken: string }> => apiRequest({
url: '/auth/registerexternal',
method: 'post',
data: {
platform: 0,
provider: 4,
accesstoken
},
locale
});
export const getLoginByGoogle = (locale: string, accesstoken: string): Promise<{ jwtToken: string }> => apiRequest({
url: '/auth/tryloginexternal',
method: 'post',
data: {
platform: 0,
provider: 4,
accesstoken
},
locale
});
export const getRegisterByApple = (locale: string, code: string): Promise<{ jwtToken: string }> => apiRequest({
url: '/auth/registerexternalappleweb',
method: 'post',
data: { code },
locale
});
export const getLoginByApple = (locale: string, code: string): Promise<{ jwtToken: string }> => apiRequest({
url: '/auth/tryloginexternalappleweb',
method: 'post',
data: { code },
locale
});

View File

@ -23,20 +23,20 @@ export const useSessionTracking = (locale: string, sessionId: number) => {
useEffect(() => { useEffect(() => {
return () => { return () => {
window?.clearInterval(timer.current); window.clearInterval(timer.current);
} }
}, []); }, []);
const start = () => { const start = () => {
window?.clearInterval(timer.current); window.clearInterval(timer.current);
timer.current = window?.setInterval(() => { timer.current = window.setInterval(() => {
fetchData(); fetchData();
}, DURATION); }, DURATION);
}; };
const stop = () => { const stop = () => {
window?.clearInterval(timer.current); window.clearInterval(timer.current);
}; };
return { return {

View File

@ -13,6 +13,7 @@ export async function createCheckoutSession(
const ui_mode = data.get( const ui_mode = data.get(
"uiMode", "uiMode",
) as Stripe.Checkout.SessionCreateParams.UiMode; ) as Stripe.Checkout.SessionCreateParams.UiMode;
console.log('DATA', data)
const origin: string = headers().get("origin") as string; const origin: string = headers().get("origin") as string;
const checkoutSession: Stripe.Checkout.Session = const checkoutSession: Stripe.Checkout.Session =

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
// import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
export default function Directions() { export default function Directions({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
return ( return (
<div className="main-popular"> <div className="main-popular">

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
// 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 { Experts } from '../../../../components/Experts/Experts'; import { Experts } from '../../../../components/Experts/Experts';
export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) { export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const t = useTranslations('Experts'); const t = useTranslations('Experts');
return ( return (

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
// import { useTranslations } from 'next-intl'; // import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
import { getTranslations } 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';
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})

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Link } from '../../../../../../i18n/routing'; import { Link } from '../../../../../../navigation';
import { CustomSelect } from '../../../../../../components/view/CustomSelect'; import { CustomSelect } from '../../../../../../components/view/CustomSelect';
export default function AddOffer() { export default function AddOffer() {

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Link } from '../../../../../../i18n/routing'; import { Link } from '../../../../../../navigation';
import { CustomSelect } from '../../../../../../components/view/CustomSelect'; import { CustomSelect } from '../../../../../../components/view/CustomSelect';
export default function NewTopic() { export default function NewTopic() {

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; 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 { i18nText } from '../../../../../i18nKeys'; import { i18nText } from '../../../../../i18nKeys';
@ -10,7 +10,7 @@ export const metadata: Metadata = {
}; };
export default function Information({ params: { locale } }: { params: { locale: string } }) { export default function Information({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const t = useTranslations('Account.LegalInformation'); const t = useTranslations('Account.LegalInformation');
return ( return (

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import React from 'react'; import React from 'react';
import { Link } from '../../../../../../i18n/routing'; import { Link } from '../../../../../../navigation';
import { i18nText } from '../../../../../../i18nKeys'; import { i18nText } from '../../../../../../i18nKeys';
import {ChatMessages} from "../../../../../../components/Chat/ChatMessages"; import {ChatMessages} from "../../../../../../components/Chat/ChatMessages";
/* /*

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
// import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/ru'; import 'dayjs/locale/ru';
import 'dayjs/locale/en'; import 'dayjs/locale/en';
@ -16,7 +16,7 @@ export const metadata: Metadata = {
}; };
export default function Notifications({ params: { locale } }: { params: { locale: string } }) { export default function Notifications({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const date = dayjs('2022-05-22').locale(locale); const date = dayjs('2022-05-22').locale(locale);
return ( return (

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
// import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
import { Link } from '../../../../../../i18n/routing'; import { Link } from '../../../../../../navigation';
import { i18nText } from '../../../../../../i18nKeys/'; import { i18nText } from '../../../../../../i18nKeys/';
export default function ChangePassword({ params: { locale } }: { params: { locale: string } }) { export default function ChangePassword({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
return ( return (
<> <>

View File

@ -1,10 +1,10 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
// import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
import { ProfileSettings } from '../../../../../components/Account'; import { ProfileSettings } from '../../../../../components/Account';
import { i18nText } from '../../../../../i18nKeys'; import { i18nText } from '../../../../../i18nKeys';
export default function Settings({ params: { locale } }: { params: { locale: string } }) { export default function Settings({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
return ( return (
<> <>

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
// import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { i18nText } from '../../../../../i18nKeys'; import { i18nText } from '../../../../../i18nKeys';
@ -9,7 +9,7 @@ export const metadata: Metadata = {
}; };
export default function Support({ params: { locale } }: { params: { locale: string } }) { export default function Support({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
return ( return (
<> <>

View File

@ -1,5 +1,5 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
// import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { AccountMenu, RoomDetails, RoomsTabs } from '../../../../../../components/Account'; import { AccountMenu, RoomDetails, RoomsTabs } from '../../../../../../components/Account';
import { RoomsType } from '../../../../../../types/rooms'; import { RoomsType } from '../../../../../../types/rooms';
@ -13,7 +13,7 @@ export async function generateStaticParams({
} }
export default function RoomsDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) { export default function RoomsDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const roomType: string = slug?.length > 0 && slug[0] || ''; const roomType: string = slug?.length > 0 && slug[0] || '';
const roomId: number | null = slug?.length > 1 && Number(slug[1]) || null; const roomId: number | null = slug?.length > 1 && Number(slug[1]) || null;

View File

@ -1,5 +1,5 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
// import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { AccountMenu, SessionDetails, SessionsTabs } from '../../../../../../components/Account'; import { AccountMenu, SessionDetails, SessionsTabs } from '../../../../../../components/Account';
import { SessionType } from '../../../../../../types/sessions'; import { SessionType } from '../../../../../../types/sessions';
@ -13,7 +13,7 @@ export async function generateStaticParams({
} }
export default function SessionDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) { export default function SessionDetailItem({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const sessionType: string = slug?.length > 0 && slug[0] || ''; const sessionType: string = slug?.length > 0 && slug[0] || '';
const sessionId: number | null = slug?.length > 1 && Number(slug[1]) || null; const sessionId: number | null = slug?.length > 1 && Number(slug[1]) || null;

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; 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';
@ -9,8 +9,8 @@ export const metadata: Metadata = {
description: 'Bbuddy desc Take the lead with BB' description: 'Bbuddy desc Take the lead with BB'
}; };
export default function BbClientPage() { export default function BbClientPage({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const t = useTranslations('BbClient'); const t = useTranslations('BbClient');
return ( return (

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; 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';
@ -10,8 +10,8 @@ export const metadata: Metadata = {
description: 'Bbuddy desc Become a BB expert' description: 'Bbuddy desc Become a BB expert'
}; };
export default function BbExpertPage() { export default function BbExpertPage({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const t = useTranslations('BbExpert'); const t = useTranslations('BbExpert');
return ( return (

View File

@ -50,6 +50,7 @@ function renderWidget (widget: Widget, index: number) {
export default async function BlogItem({params}: { params: BlogPostPageParams }) { export default async function BlogItem({params}: { params: BlogPostPageParams }) {
const item = await fetchBlogPost({slug: params.slug, preview: draftMode().isEnabled }) const item = await fetchBlogPost({slug: params.slug, preview: draftMode().isEnabled })
console.log('BLOG POST')
console.log(Util.inspect(item, {showHidden: false, depth: null, colors: true})) console.log(Util.inspect(item, {showHidden: false, depth: null, colors: true}))
if (!item) notFound(); if (!item) notFound();

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { draftMode } from 'next/headers' import { draftMode } from 'next/headers'
// import {unstable_setRequestLocale} from "next-intl/server"; import {unstable_setRequestLocale} from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts"; import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts";
import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories"; import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories";
@ -20,9 +20,9 @@ interface BlogPostPageProps {
params: BlogPostPageParams params: BlogPostPageParams
} }
export default async function Blog({params, searchParams}: { params: BlogPostPageParams, searchParams?: {page: number} }) { export default async function Blog({params, searchParams}: { params: BlogPostPageParams, searhParams?: {page: number} }) {
// unstable_setRequestLocale(params.locale); unstable_setRequestLocale(params.locale);
const page = searchParams?.page || undefined const page = searchParams.page || undefined
return ( return (
<BlogPosts basePath={'/'+params.locale+'/blog/'} locale={params.locale} currentCat={params.slug} page={page}/> <BlogPosts basePath={'/'+params.locale+'/blog/'} locale={params.locale} currentCat={params.slug} page={page}/>
); );

View File

@ -2,7 +2,7 @@ import React from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import * as Util from "node:util"; import * as Util from "node:util";
import {fetchBlogPosts} from "../../../lib/contentful/blogPosts"; import {fetchBlogPosts} from "../../../lib/contentful/blogPosts";
// import {unstable_setRequestLocale} from "next-intl/server"; import {unstable_setRequestLocale} from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import {fetchBlogPostCategories} from "../../../lib/contentful/blogPostsCategories"; import {fetchBlogPostCategories} from "../../../lib/contentful/blogPostsCategories";
import {CustomPagination} from "../../../components/view/CustomPagination"; import {CustomPagination} from "../../../components/view/CustomPagination";
@ -24,10 +24,10 @@ export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
} }
export default async function Blog({ params: { locale }, searchParams }: { params: { locale: string }, searchParams?: {page: number} }) { export default async function Blog({ params: { locale }, searchParams }: { params: { locale: string }, searhParams?: {page: number} }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const pageSize = DEFAULT_PAGE_SIZE const pageSize = DEFAULT_PAGE_SIZE
const page = searchParams?.page || undefined const page = searchParams.page || undefined
// BlogPosts('/'+locale+'/blog/', locale, pageSize) // BlogPosts('/'+locale+'/blog/', locale, pageSize)
return ( return (
@ -36,6 +36,7 @@ export default async function Blog({ params: { locale }, searchParams }: { param
locale={locale} locale={locale}
pageSize={pageSize} pageSize={pageSize}
page={page} page={page}
/> >
</BlogPosts>
); );
} }

View File

@ -1,6 +1,6 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
// import { unstable_setRequestLocale } from 'next-intl/server'; import { unstable_setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { getExpertById, getExpertsList } from '../../../../actions/experts'; import { getExpertById, getExpertsList } from '../../../../actions/experts';
import { import {
@ -20,7 +20,7 @@ export const metadata: Metadata = {
export async function generateStaticParams({ export async function generateStaticParams({
params: { locale }, params: { locale },
}: { params: { locale: string } }) { }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const result: { locale: string, expertId: string }[] = []; const result: { locale: string, expertId: string }[] = [];
const experts = await getExpertsList(locale, { themesTagIds: [] }); const experts = await getExpertsList(locale, { themesTagIds: [] });

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import type { Metadata } from 'next'; 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 { Experts } from '../../../components/Experts/Experts'; import { Experts } from '../../../components/Experts/Experts';
@ -10,7 +10,7 @@ export const metadata: Metadata = {
}; };
export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) { export default function ExpertsPage({ params: { locale } }: { params: { locale: string } }) {
// unstable_setRequestLocale(locale); unstable_setRequestLocale(locale);
const t = useTranslations('Experts'); const t = useTranslations('Experts');
return ( return (

View File

@ -1,15 +1,12 @@
import React, { ReactNode, Suspense } from 'react'; import React, { ReactNode, Suspense } from 'react';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { NextIntlClientProvider } from 'next-intl'; import { unstable_setRequestLocale } from 'next-intl/server';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import { AntdRegistry } from '@ant-design/nextjs-registry'; import { AntdRegistry } from '@ant-design/nextjs-registry';
import { GoogleOAuthProvider } from '@react-oauth/google';
import theme from '../../constants/theme'; import theme from '../../constants/theme';
import { ALLOWED_LOCALES } from '../../constants/locale'; import { ALLOWED_LOCALES } from '../../constants/locale';
import { Header, Footer, AppConfig } from '../../components/Page'; import { Header, Footer, AppConfig } from '../../components/Page';
import { routing } from '../../i18n/routing';
type LayoutProps = { type LayoutProps = {
children: ReactNode; children: ReactNode;
@ -24,17 +21,13 @@ export const metadata: Metadata = {
title: 'Bbuddy' title: 'Bbuddy'
}; };
export default async function LocaleLayout({ children, params: { locale } }: LayoutProps) { export default function LocaleLayout({ children, params: { locale } }: LayoutProps) {
if (!routing.locales.includes(locale as any)) { if (!ALLOWED_LOCALES.includes(locale as any)) notFound();
notFound();
}
const messages = await getMessages(); unstable_setRequestLocale(locale);
return ( return (
<NextIntlClientProvider messages={messages}>
<AntdRegistry> <AntdRegistry>
<GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''}>
<ConfigProvider theme={theme}> <ConfigProvider theme={theme}>
<div className="b-wrapper"> <div className="b-wrapper">
<Suspense fallback={null}> <Suspense fallback={null}>
@ -47,8 +40,6 @@ export default async function LocaleLayout({ children, params: { locale } }: Lay
<Footer locale={locale} /> <Footer locale={locale} />
</div> </div>
</ConfigProvider> </ConfigProvider>
</GoogleOAuthProvider>
</AntdRegistry> </AntdRegistry>
</NextIntlClientProvider>
); );
} }

View File

@ -1,50 +0,0 @@
'use client'
import React, { useEffect } from 'react';
import { notification } from 'antd';
import { useSearchParams } from 'next/navigation';
import { CustomSpin } from '../../../../components/view/CustomSpin';
import { getLoginByApple } from '../../../../actions/auth';
import { getUserData } from '../../../../actions/profile';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../../constants/common';
import { useLocalStorage } from '../../../../hooks/useLocalStorage';
import { useRouter } from '../../../../i18n/routing';
export default function AppleLoginPage({ params: { locale } }: { params: { locale: string } }) {
const params = useSearchParams();
const router = useRouter();
const [, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
useEffect(() => {
const code = params.get('code');
if (code) {
getLoginByApple(locale, code)
.then((data) => {
if (data.jwtToken) {
getUserData(locale, data.jwtToken)
.then((profile) => {
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
setToken(data.jwtToken);
})
} else {
notification.error({
message: 'Error',
description: 'Access denied'
});
}
})
.catch((error) => {
const err = error?.message ? JSON.parse(error.message) : {};
notification.error({
message: 'Error',
description: err?.details?.errMessage || undefined
});
})
.finally(() => {
router.push('/');
});
}
}, [params]);
return <CustomSpin />;
}

View File

@ -1,45 +0,0 @@
'use client'
import React, { useEffect } from 'react';
import { notification } from 'antd';
import { useSearchParams } from 'next/navigation';
import { CustomSpin } from '../../../../components/view/CustomSpin';
import { getRegisterByApple } from '../../../../actions/auth';
import { getUserData } from '../../../../actions/profile';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../../constants/common';
import { useLocalStorage } from "../../../../hooks/useLocalStorage";
import { useRouter } from '../../../../i18n/routing';
export default function AppleRegisterPage({ params: { locale } }: { params: { locale: string } }) {
const params = useSearchParams();
const router = useRouter();
const [, setToken] = useLocalStorage(AUTH_TOKEN_KEY, '');
useEffect(() => {
const code = params.get('code');
if (code) {
getRegisterByApple(locale, code)
.then((data) => {
if (data.jwtToken) {
getUserData(locale, data.jwtToken)
.then((profile) => {
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
setToken(data.jwtToken);
})
}
})
.catch((error) => {
const err = error?.message ? JSON.parse(error.message) : {};
notification.error({
message: 'Error',
description: err?.details?.errMessage || undefined
});
})
.finally(() => {
router.push('/');
});
}
}, [params]);
return <CustomSpin />;
}

View File

@ -10,10 +10,7 @@ type RootLayoutProps = {
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Bbuddy', title: 'Bbuddy',
description: 'Bbuddy', description: 'Bbuddy'
verification: {
google: 'UqmM7WbpuMetvvkMeyVRGKiSvXHEGyaaRuYbWxM-njs'
}
}; };
export default function RootLayout({ children, params: { locale } }: RootLayoutProps) { export default function RootLayout({ children, params: { locale } }: RootLayoutProps) {

27
src/app/sitemap.jsx Normal file
View File

@ -0,0 +1,27 @@
import { fetchBlogPosts } from '../lib/contentful/blogPosts';
export default async function sitemap() {
const paths = [
{
url: process.env.NEXT_PUBLIC_HOST,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1
}
]
const blogPosts = await fetchBlogPosts({ preview: false })
blogPosts.data.forEach((item) => {
paths.push({
url: `${process.env.NEXT_PUBLIC_HOST}${item.slug}`,
lastModified: item.createdAt.split('T')[0],
changeFrequency: 'daily',
priority: '1.0'
})
})
return paths
}

View File

@ -1,14 +1,14 @@
'use client'; 'use client';
import { useEffect, useState } from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import { useSelectedLayoutSegment, usePathname } from 'next/navigation'; import { useSelectedLayoutSegment, usePathname } from 'next/navigation';
import { Link } from '../../i18n/routing'; import { Link } from '../../navigation';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common'; import { AUTH_TOKEN_KEY, AUTH_USER } from '../../constants/common';
import {deleteStorageKey, useLocalStorage} from '../../hooks/useLocalStorage'; import {deleteStorageKey, useLocalStorage} from '../../hooks/useLocalStorage';
import { i18nText } from '../../i18nKeys'; import { i18nText } from '../../i18nKeys';
import { getMenuConfig } from '../../utils/account'; import { getMenuConfig } from '../../utils/account';
import { getChatList } from '../../actions/chat/groups'; import {useEffect, useState} from "react";
import {getChatList} from "../../actions/chat/groups";
export const AccountMenu = ({ locale }: { locale: string }) => { export const AccountMenu = ({ locale }: { locale: string }) => {
const selectedLayoutSegment = useSelectedLayoutSegment(); const selectedLayoutSegment = useSelectedLayoutSegment();

View File

@ -5,7 +5,7 @@ import { Form, message, Upload } from 'antd';
import type { UploadFile } from 'antd'; import type { UploadFile } from 'antd';
import ImgCrop from 'antd-img-crop'; import ImgCrop from 'antd-img-crop';
import { CameraOutlined, DeleteOutlined } from '@ant-design/icons'; import { CameraOutlined, DeleteOutlined } from '@ant-design/icons';
import { useRouter } from '../../i18n/routing'; import { useRouter } from '../../navigation';
import { i18nText } from '../../i18nKeys'; import { i18nText } from '../../i18nKeys';
import { ProfileRequest } from '../../types/profile'; import { ProfileRequest } from '../../types/profile';
import { validateImage } from '../../utils/account'; import { validateImage } from '../../utils/account';

View File

@ -5,7 +5,7 @@ import { EditRoomForm } from './EditRoomForm';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { createRoom } from '../../../actions/rooms'; import { createRoom } from '../../../actions/rooms';
import { Loader } from '../../view/Loader'; import { Loader } from '../../view/Loader';
import { useRouter } from '../../../i18n/routing'; import { useRouter } from '../../../navigation';
import { RoomsType } from '../../../types/rooms'; import { RoomsType } from '../../../types/rooms';

View File

@ -5,7 +5,7 @@ import { Button, notification, Tag } from 'antd';
import { DeleteOutlined, LeftOutlined } from '@ant-design/icons'; import { DeleteOutlined, LeftOutlined } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from '../../../i18n/routing'; import { useRouter } from '../../../navigation';
import { Report, Room, RoomsType } from '../../../types/rooms'; import { Report, Room, RoomsType } from '../../../types/rooms';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import { LinkButton } from '../../view/LinkButton'; import { LinkButton } from '../../view/LinkButton';

View File

@ -14,7 +14,7 @@ import { getRecentRooms, getUpcomingRooms } from '../../../actions/rooms';
import { Loader } from '../../view/Loader'; import { Loader } from '../../view/Loader';
import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY } from '../../../constants/common'; import { AUTH_TOKEN_KEY } from '../../../constants/common';
import { usePathname, useRouter } from '../../../i18n/routing'; import { usePathname, useRouter } from '../../../navigation';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import { CreateRoom } from './CreateRoom'; import { CreateRoom } from './CreateRoom';

View File

@ -5,7 +5,7 @@ import { Button, Empty, notification, Tag } from 'antd';
import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons'; import { LeftOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons';
import Image from 'next/image'; import Image from 'next/image';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Link, useRouter } from '../../../i18n/routing'; import { Link, useRouter } from '../../../navigation';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
import { getDuration, getPrice } from '../../../utils/expert'; import { getDuration, getPrice } from '../../../utils/expert';
import { PublicUser, Session, SessionState, SessionType } from '../../../types/sessions'; import { PublicUser, Session, SessionState, SessionType } from '../../../types/sessions';

View File

@ -14,7 +14,7 @@ import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common'; import { AUTH_TOKEN_KEY, AUTH_USER } from '../../../constants/common';
import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../../actions/sessions'; import { getRecentSessions, getRequestedSessions, getUpcomingSessions } from '../../../actions/sessions';
import { Session, Sessions, SessionType } from '../../../types/sessions'; import { Session, Sessions, SessionType } from '../../../types/sessions';
import { useRouter, usePathname } from '../../../i18n/routing'; import { useRouter, usePathname } from '../../../navigation';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
type SessionsTabsProps = { type SessionsTabsProps = {

View File

@ -10,7 +10,7 @@ type PostsProps = {
basePath: string; basePath: string;
locale: string; locale: string;
pageSize?: number; pageSize?: number;
currentCat?: string; currentCat: string;
page?: number page?: number
}; };

View File

@ -3,7 +3,7 @@ import React, {useEffect, useState} from 'react';
import {AUTH_TOKEN_KEY} from '../../constants/common'; import {AUTH_TOKEN_KEY} from '../../constants/common';
import {getChatList, getChatMessages} from "../../actions/chat/groups"; import {getChatList, getChatMessages} from "../../actions/chat/groups";
import {useLocalStorage} from "../../hooks/useLocalStorage"; import {useLocalStorage} from "../../hooks/useLocalStorage";
import { Link } from "../../i18n/routing"; import {Link} from "../../navigation";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import {message} from "antd"; import {message} from "antd";

View File

@ -19,8 +19,8 @@ type CompProps = {
}; };
export const ChatMessages = ({ locale, groupId }: CompProps) => { export const ChatMessages = ({ locale, groupId }: CompProps) => {
const { newMessage, joinChat, readMessages, addListener } = SignalrConnection();
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const { newMessage, joinChat, readMessages, addListener } = SignalrConnection({ jwt }) || {};
//const messages = await getChatMessages(locale, jwt, groupId) //const messages = await getChatMessages(locale, jwt, groupId)
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [text, setText] = useState(''); const [text, setText] = useState('');
@ -46,7 +46,6 @@ export const ChatMessages = ({ locale, groupId }: CompProps) => {
setNotMe(_group.group.members[0].user) setNotMe(_group.group.members[0].user)
} }
setMessages(_messages.messages); setMessages(_messages.messages);
joinChat(groupId)
}) })
.catch((e) => { .catch((e) => {
@ -61,7 +60,7 @@ export const ChatMessages = ({ locale, groupId }: CompProps) => {
const onConnected = (flag: boolean) =>{ const onConnected = (flag: boolean) =>{
if (flag && joinChat) { if (flag) {
joinChat(groupId) joinChat(groupId)
readUreaded() readUreaded()
} }
@ -74,7 +73,7 @@ export const ChatMessages = ({ locale, groupId }: CompProps) => {
msgs.push(message.id) msgs.push(message.id)
} }
}) })
readMessages && readMessages(msgs); readMessages(msgs)
} }
const onReceiveMessage = (payload: any) => { const onReceiveMessage = (payload: any) => {
@ -104,7 +103,7 @@ export const ChatMessages = ({ locale, groupId }: CompProps) => {
} else { } else {
setMessages([msg, ..._messages]); setMessages([msg, ..._messages]);
} }
readMessages && readMessages([msg.id]); readMessages([msg.id])
} }
@ -121,18 +120,16 @@ export const ChatMessages = ({ locale, groupId }: CompProps) => {
} }
useEffect(() => { useEffect(() => {
if (addListener) { addListener('onConnected', onConnected)
addListener('onConnected', onConnected); addListener('ReceiveMessage', onReceiveMessage)
addListener('ReceiveMessage', onReceiveMessage); addListener('MessageWasRead', onOpponentRead)
addListener('MessageWasRead', onOpponentRead);
}
}, [messages, me, setMessages]); }, [messages, me, setMessages]);
const handleSendMessages = () => { const handleSendMessages = () => {
newMessage && newMessage({ newMessage({
GroupId: groupId.toString(), GroupId: groupId.toString(),
CreatorId: me.id, CreatorId: me.id,
Content: text Content: text

View File

@ -3,7 +3,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useRouter } from '../../i18n/routing'; import { useRouter } from '../../navigation';
import { AdditionalFilter } from '../../types/experts'; import { AdditionalFilter } from '../../types/experts';
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter'; import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
import { CustomInput } from '../view/CustomInput'; import { CustomInput } from '../view/CustomInput';

View File

@ -16,7 +16,7 @@ import { AUTH_TOKEN_KEY, SESSION_DATA } from '../../constants/common';
import { ScheduleModal } from '../Modals/ScheduleModal'; import { ScheduleModal } from '../Modals/ScheduleModal';
import { ScheduleModalResult } from '../Modals/ScheduleModalResult'; import { ScheduleModalResult } from '../Modals/ScheduleModalResult';
import SignalrConnection from '../../lib/signalr-connection'; import SignalrConnection from '../../lib/signalr-connection';
import { useRouter } from '../../i18n/routing'; import { useRouter } from '../../navigation';
import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useLocalStorage } from '../../hooks/useLocalStorage';
type ExpertDetailsProps = { type ExpertDetailsProps = {
@ -38,7 +38,7 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId })
const isRus = locale === Locale.ru; const isRus = locale === Locale.ru;
const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] , id, botUserId} } = expert || {}; const { publicCoachDetails: { tags = [], sessionCost = 0, sessionDuration = 0, coachLanguages = [] , id, botUserId} } = expert || {};
const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, ''); const [jwt] = useLocalStorage(AUTH_TOKEN_KEY, '');
const { joinChatPerson, closeConnection } = SignalrConnection({ jwt }) || {}; const { joinChatPerson, closeConnection } = SignalrConnection();
const router = useRouter(); const router = useRouter();
@ -46,13 +46,13 @@ export const ExpertCard: FC<ExpertDetailsProps> = ({ expert, locale, expertId })
document?.addEventListener('show_pay_form', handleShowPayForm); document?.addEventListener('show_pay_form', handleShowPayForm);
return () => { return () => {
// if (closeConnection) closeConnection(); closeConnection();
document?.removeEventListener('show_pay_form', handleShowPayForm); document?.removeEventListener('show_pay_form', handleShowPayForm);
} }
}, []); }, []);
const handleJoinChat = (id?: number) => { const handleJoinChat = (id?: number) => {
if (id && joinChatPerson) { if (id) {
joinChatPerson(id).then((res: any) => { joinChatPerson(id).then((res: any) => {
router.push(`/account/messages/${res.id}` as string); router.push(`/account/messages/${res.id}` as string);
}) })

View File

@ -6,7 +6,7 @@ import { List, Tag } from 'antd';
import { RightOutlined } from '@ant-design/icons'; import { RightOutlined } from '@ant-design/icons';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import Image from 'next/image'; import Image from 'next/image';
import { Link, useRouter } from '../../i18n/routing'; import { Link, useRouter } from '../../navigation';
import { ExpertsData, Filter, GeneralFilter } from '../../types/experts'; import { ExpertsData, Filter, GeneralFilter } from '../../types/experts';
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter'; import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';
import { getDuration, getPrice } from '../../utils/expert'; import { getDuration, getPrice } from '../../utils/expert';

View File

@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Button, Collapse, List } from 'antd'; import { Button, Collapse, List } from 'antd';
import type { CollapseProps } from 'antd'; import type { CollapseProps } from 'antd';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useRouter } from '../../i18n/routing'; import { useRouter } from '../../navigation';
import { Filter } from '../../types/experts'; import { Filter } from '../../types/experts';
import { Languages, SearchData, Tag } from '../../types/tags'; import { Languages, SearchData, Tag } from '../../types/tags';
import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter'; import { getObjectByFilter, getObjectByAdditionalFilter, getSearchParamsString } from '../../utils/filter';

View File

@ -1,16 +1,12 @@
'use client'; 'use client';
import React, { Dispatch, FC, SetStateAction, useEffect } from 'react'; import React, { Dispatch, FC, SetStateAction, useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { Modal, Form, notification } from 'antd'; import { Modal, Form } from 'antd';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent'; import { RegisterContent, ResetContent, FinishContent, EnterContent } from './authModalContent';
import { i18nText } from '../../i18nKeys'; import { i18nText } from '../../i18nKeys';
import { useRouter } from '../../i18n/routing';
import { AUTH_USER} from '../../constants/common';
import { getRegisterByApple, getLoginByApple } from '../../actions/auth';
import { getUserData } from '../../actions/profile';
type AuthModalProps = { type AuthModalProps = {
open: boolean; open: boolean;
@ -31,47 +27,6 @@ export const AuthModal: FC<AuthModalProps> = ({
}) => { }) => {
const [form] = Form.useForm<{ login: string, password: string, confirmPassword: string }>(); const [form] = Form.useForm<{ login: string, password: string, confirmPassword: string }>();
const paths = usePathname().split('/'); const paths = usePathname().split('/');
const params = useSearchParams();
const router = useRouter();
const onUpdateToken = (token: string) => {
if (updateToken && typeof updateToken !== 'string') {
updateToken(token);
}
};
useEffect(() => {
const code = params.get('code');
const type = params.get('state');
if (code && type) {
const appleFunc = type === 'bbregister' ? getRegisterByApple : getLoginByApple;
appleFunc(locale, code)
.then((data) => {
if (data.jwtToken) {
getUserData(locale, data.jwtToken)
.then((profile) => {
localStorage.setItem(AUTH_USER, JSON.stringify(profile));
onUpdateToken(data.jwtToken);
})
} else {
notification.error({
message: 'Error',
description: 'Access denied'
});
}
})
.catch((error) => {
const err = error?.message ? JSON.parse(error.message) : {};
notification.error({
message: 'Error',
description: err?.details?.errMessage || undefined
});
})
.finally(() => {
router.push('/');
});
}
}, [params]);
const onAfterClose = () => { const onAfterClose = () => {
form.resetFields(); form.resetFields();
@ -83,6 +38,12 @@ export const AuthModal: FC<AuthModalProps> = ({
} }
}, [mode]); }, [mode]);
const onUpdateToken = (token: string) => {
if (updateToken && typeof updateToken !== 'string') {
updateToken(token);
}
};
return ( return (
<Modal <Modal
className="b-modal" className="b-modal"

View File

@ -76,6 +76,7 @@ export const ScheduleModal: FC<ScheduleModalProps> = ({
getSchedulerSession(parseData as SignupSessionData, locale || 'en', jwt) getSchedulerSession(parseData as SignupSessionData, locale || 'en', jwt)
.then((session) => { .then((session) => {
setSessionId(session?.sessionId); setSessionId(session?.sessionId);
console.log(session?.sessionId);
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err);

View File

@ -3,8 +3,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Result } from 'antd'; import { Modal, Result } from 'antd';
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { useSearchParams } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import { useRouter } from '../../i18n/routing';
import { Stripe } from 'stripe'; import { Stripe } from 'stripe';
import { getStripePaymentStatus } from '../../actions/stripe'; import { getStripePaymentStatus } from '../../actions/stripe';
import { sessionPaymentConfirm } from '../../actions/sessions'; import { sessionPaymentConfirm } from '../../actions/sessions';

View File

@ -1,11 +1,12 @@
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { Form, FormInstance, notification } from 'antd'; import { Form, FormInstance, notification } from 'antd';
import Image from 'next/image'; import Image from 'next/image';
import { useGoogleLogin } from '@react-oauth/google'; import { Social } from '../../../types/social';
import AppleLogin from 'react-apple-login';
import { AUTH_USER } from '../../../constants/common'; import { AUTH_USER } from '../../../constants/common';
import { getAuth, getLoginByGoogle } from '../../../actions/auth'; import { SocialConfig } from '../../../constants/social';
import { getUserData } from '../../../actions/profile'; import { useOauthWindow } from '../../../hooks/useOauthWindow';
import { getAuth } from '../../../actions/auth';
import {getPersonalData, getUserData} from '../../../actions/profile';
import { CustomInput } from '../../view/CustomInput'; import { CustomInput } from '../../view/CustomInput';
import { CustomInputPassword } from '../../view/CustomInputPassword'; import { CustomInputPassword } from '../../view/CustomInputPassword';
import { FilledButton } from '../../view/FilledButton'; import { FilledButton } from '../../view/FilledButton';
@ -29,6 +30,7 @@ export const EnterContent: FC<EnterProps> = ({
handleCancel handleCancel
}) => { }) => {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const { openOauthWindow } = useOauthWindow();
const onLogin = () => { const onLogin = () => {
form.validateFields().then(() => { form.validateFields().then(() => {
@ -57,44 +59,47 @@ export const EnterContent: FC<EnterProps> = ({
}); });
}; };
const onSocialEnter = (type: Social) => {
const url = SocialConfig[type].oauthUrl;
const onGoogleLogin = useGoogleLogin({ if (!url) return;
onError: (err) => {
notification.error({ openOauthWindow(url, type, async (event: MessageEvent) => {
message: err.error, const { data: socialData } = event
description: err.error_description
}); // примерная схема последующей обработки
},
onSuccess: (tokenResponse) => { // const socialErrors: string[] = [];
setIsLoading(true); // try {
getLoginByGoogle(locale, tokenResponse.access_token) // // отправляем запрос на бэк с данными из соц сети
.then((data) => { // const { data: { jwtToken } } = await query(socialData);
if (data.jwtToken) { // // обновляем токен
getUserData(locale, data.jwtToken) // updateToken(jwtToken);
.then((profile) => { // // получаем данные о пользователе
localStorage.setItem(AUTH_USER, JSON.stringify(profile)); // await getAuthUser()
updateToken(data.jwtToken); // } catch (error: any) {
handleCancel(); // if (error.httpStatus === 449) {
// // ошибка, когда отсутствует e-mail
//
// // какие-то дальнейшие действия после получения ошибки, например, закрываем окно и открываем модалку регистрации
// handleCancel();
// openSocialEmailRequestModal(socialData);
// } else if (error.httpStatus === 409) {
// // ошибка, когда по переданному email уже существует аккаунт
//
// // какие-то дальнейшие действия после получения ошибки, например, закрываем окно и открываем модалку с вводом пароля
// handleCancel();
// openSocialPasswordModal(socialData);
// } else {
// // в остальных случаях записываем ошибку в массив ошибок
// socialErrors.push(error.toString());
// }
// }
//
// // если все успешно, закрываем окно
// handleCancel();
}) })
} else { };
notification.error({
message: 'Error',
description: 'Access denied'
});
}
})
.catch((error) => {
const err = error?.message ? JSON.parse(error.message) : {};
notification.error({
message: 'Error',
description: err?.details?.errMessage || undefined
});
})
.finally(() => {
setIsLoading(false);
});
}
});
return ( return (
<> <>
@ -150,24 +155,21 @@ export const EnterContent: FC<EnterProps> = ({
{`${i18nText('forgotPass', locale)}?`} {`${i18nText('forgotPass', locale)}?`}
</LinkButton> </LinkButton>
<span>{i18nText('or', locale)}</span> <span>{i18nText('or', locale)}</span>
<AppleLogin <OutlinedButton
clientId="bbuddy.expert" icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />}
redirectURI="https://bbuddy.expert" onClick={() => onSocialEnter(Social.FACEBOOK)}
state="bblogin" >
responseType="code" {i18nText('facebook', locale)}
responseMode="query" </OutlinedButton>
render={({ onClick }) => (
<OutlinedButton <OutlinedButton
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />} icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
onClick={onClick} onClick={() => onSocialEnter(Social.APPLE)}
> >
{i18nText('apple', locale)} {i18nText('apple', locale)}
</OutlinedButton> </OutlinedButton>
)}
/>
<OutlinedButton <OutlinedButton
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />} icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
onClick={onGoogleLogin} onClick={() => onSocialEnter(Social.GOOGLE)}
> >
{i18nText('google', locale)} {i18nText('google', locale)}
</OutlinedButton> </OutlinedButton>

View File

@ -1,11 +1,12 @@
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import { Form, FormInstance, notification } from 'antd'; import { Form, FormInstance, notification } from 'antd';
import Image from 'next/image'; import Image from 'next/image';
import { useGoogleLogin } from '@react-oauth/google'; import { Social } from '../../../types/social';
import AppleLogin from 'react-apple-login';
import { AUTH_USER } from '../../../constants/common'; import { AUTH_USER } from '../../../constants/common';
import { getRegister, getRegisterByGoogle } from '../../../actions/auth'; import { SocialConfig } from '../../../constants/social';
import { getUserData, setPersonData } from '../../../actions/profile'; import { getRegister } from '../../../actions/auth';
import { setPersonData } from '../../../actions/profile';
import { useOauthWindow } from '../../../hooks/useOauthWindow';
import { CustomInput } from '../../view/CustomInput'; import { CustomInput } from '../../view/CustomInput';
import { CustomInputPassword } from '../../view/CustomInputPassword'; import { CustomInputPassword } from '../../view/CustomInputPassword';
import { FilledButton } from '../../view/FilledButton'; import { FilledButton } from '../../view/FilledButton';
@ -28,6 +29,7 @@ export const RegisterContent: FC<RegisterProps> = ({
handleCancel handleCancel
}) => { }) => {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const { openOauthWindow } = useOauthWindow();
const onRegister = () => { const onRegister = () => {
form.validateFields().then(() => { form.validateFields().then(() => {
@ -62,38 +64,47 @@ export const RegisterContent: FC<RegisterProps> = ({
}); });
}; };
const onGoogleLogin = useGoogleLogin({ const onSocialRegister = (type: Social) => {
onError: (err) => { const url = SocialConfig[type].oauthUrl;
notification.error({
message: err.error, if (!url) return;
description: err.error_description
}); openOauthWindow(url, type, async (event: MessageEvent) => {
}, const { data: socialData } = event
onSuccess: (tokenResponse) => {
setIsLoading(true); // примерная схема последующей обработки
getRegisterByGoogle(locale, tokenResponse.access_token)
.then((data) => { // const socialErrors: string[] = [];
if (data.jwtToken) { // try {
getUserData(locale, data.jwtToken) // // отправляем запрос на бэк с данными из соц сети
.then((profile) => { // const { data: { jwtToken } } = await query(socialData);
localStorage.setItem(AUTH_USER, JSON.stringify(profile)); // // обновляем токен
updateToken(data.jwtToken); // updateToken(jwtToken);
handleCancel(); // // получаем данные о пользователе
// await getAuthUser()
// } catch (error: any) {
// if (error.httpStatus === 449) {
// // ошибка, когда отсутствует e-mail
//
// // какие-то дальнейшие действия после получения ошибки, например, закрываем окно и открываем модалку регистрации
// handleCancel();
// openSocialEmailRequestModal(socialData);
// } else if (error.httpStatus === 409) {
// // ошибка, когда по переданному email уже существует аккаунт
//
// // какие-то дальнейшие действия после получения ошибки, например, закрываем окно и открываем модалку с вводом пароля
// handleCancel();
// openSocialPasswordModal(socialData);
// } else {
// // в остальных случаях записываем ошибку в массив ошибок
// socialErrors.push(error.toString());
// }
// }
//
// // если все успешно, закрываем окно
// handleCancel();
}) })
} };
})
.catch((error) => {
const err = error?.message ? JSON.parse(error.message) : {};
notification.error({
message: 'Error',
description: err?.details?.errMessage || undefined
});
})
.finally(() => {
setIsLoading(false);
});
}
});
return ( return (
<> <>
@ -166,24 +177,21 @@ export const RegisterContent: FC<RegisterProps> = ({
</FilledButton> </FilledButton>
<OutlinedButton onClick={() => updateMode('enter')}>{i18nText('enter', locale)}</OutlinedButton> <OutlinedButton onClick={() => updateMode('enter')}>{i18nText('enter', locale)}</OutlinedButton>
<span>{i18nText('or', locale)}</span> <span>{i18nText('or', locale)}</span>
<AppleLogin <OutlinedButton
clientId="bbuddy.expert" icon={<Image src="/images/facebook-logo.png" height={20} width={20} alt="" />}
redirectURI="https://bbuddy.expert" onClick={() => onSocialRegister(Social.FACEBOOK)}
state="bbregister" >
responseType="code" {i18nText('facebook', locale)}
responseMode="query" </OutlinedButton>
render={({ onClick }) => (
<OutlinedButton <OutlinedButton
icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />} icon={<Image src="/images/apple-logo.png" height={22} width={22} alt="" />}
onClick={onClick} onClick={() => onSocialRegister(Social.APPLE)}
> >
{i18nText('apple', locale)} {i18nText('apple', locale)}
</OutlinedButton> </OutlinedButton>
)}
/>
<OutlinedButton <OutlinedButton
icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />} icon={<Image src="/images/google-logo.png" height={20} width={20} alt="" />}
onClick={onGoogleLogin} onClick={() => onSocialRegister(Social.GOOGLE)}
> >
{i18nText('google', locale)} {i18nText('google', locale)}
</OutlinedButton> </OutlinedButton>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Link as IntlLink } from '../../../i18n/routing'; import { Link as IntlLink } from '../../../navigation';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
export const Footer = ({ locale }: { locale: string }) => { export const Footer = ({ locale }: { locale: string }) => {

View File

@ -1,9 +1,9 @@
'use client' 'use client'
import React, { useState, useEffect } from 'react'; import React, { FC, useState, useEffect } from 'react';
import { Button } from 'antd'; import { Button } from 'antd';
import { useSelectedLayoutSegment } from 'next/navigation'; import { useSelectedLayoutSegment } from 'next/navigation';
import { Link } from '../../../i18n/routing'; import { Link } from '../../../navigation';
import { AUTH_TOKEN_KEY } from '../../../constants/common'; import { AUTH_TOKEN_KEY } from '../../../constants/common';
import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { AuthModal } from '../../Modals/AuthModal'; import { AuthModal } from '../../Modals/AuthModal';

View File

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import { useSelectedLayoutSegment } from 'next/navigation'; import { useSelectedLayoutSegment } from 'next/navigation';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { Link } from '../../../i18n/routing'; import { Link } from '../../../navigation';
type HeaderMenuProps = { type HeaderMenuProps = {
locale: string; locale: string;

View File

@ -3,7 +3,7 @@
import React, { FC, useState } from 'react'; import React, { FC, useState } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useSelectedLayoutSegment } from 'next/navigation'; import { useSelectedLayoutSegment } from 'next/navigation';
import { Link } from '../../../i18n/routing'; import { Link } from '../../../navigation';
type HeaderMenuMobileProps = { type HeaderMenuMobileProps = {
locale: string; locale: string;

View File

@ -4,7 +4,7 @@ import React, { useTransition, useState } from 'react';
import { Dropdown } from 'antd'; import { Dropdown } from 'antd';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons'; import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
import { useRouter, usePathname } from '../../../i18n/routing'; import { useRouter, usePathname } from '../../../navigation';
import { LOCALES } from '../../../constants/locale'; import { LOCALES } from '../../../constants/locale';
import { Locale } from '../../../types/locale'; import { Locale } from '../../../types/locale';

View File

@ -3,7 +3,7 @@ import { HeaderMenu } from './HeaderMenu';
import { LanguageSwitcher } from './LanguageSwitcher'; import { LanguageSwitcher } from './LanguageSwitcher';
import { HeaderMobileMenu } from './HeaderMobileMenu'; import { HeaderMobileMenu } from './HeaderMobileMenu';
import { HEAD_ROUTES } from '../../../constants/routes'; import { HEAD_ROUTES } from '../../../constants/routes';
import { Link } from '../../../i18n/routing'; import { Link } from '../../../navigation';
import { i18nText } from '../../../i18nKeys'; import { i18nText } from '../../../i18nKeys';
type HeaderProps = { type HeaderProps = {

View File

@ -81,7 +81,7 @@ export const CheckoutForm: FC<PaymentFormProps> = ({ amount, sessionId, locale }
elements, elements,
clientSecret, clientSecret,
confirmParams: { confirmParams: {
return_url: window?.location?.href || '', return_url: window.location.href,
payment_method_data: { payment_method_data: {
allow_redisplay: 'limited', allow_redisplay: 'limited',
// billing_details: { // billing_details: {

View File

@ -2,6 +2,7 @@ import { Locale } from '../types/locale';
export const DEFAULT_LOCALE = Locale.en; export const DEFAULT_LOCALE = Locale.en;
export const ALLOWED_LOCALES = [Locale.en, Locale.ru, Locale.de, Locale.it, Locale.es, Locale.fr] as const; export const ALLOWED_LOCALES = [Locale.en, Locale.ru, Locale.de, Locale.it, Locale.es, Locale.fr] as const;
export const LOCALE_PREFIX = undefined;
export const LOCALES = { export const LOCALES = {
[Locale.en]: 'En', [Locale.en]: 'En',

View File

@ -2,14 +2,14 @@ import { useState, useEffect } from 'react';
export function getStorageValue (key: string, defaultValue: any) { export 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;
} }
}; };
export function deleteStorageKey (key: string) { export function deleteStorageKey (key: string) {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
localStorage?.removeItem(key); localStorage.removeItem(key);
} }
}; };
@ -19,7 +19,7 @@ export const useLocalStorage = (key: string, defaultValue: any) => {
}); });
useEffect(() => { useEffect(() => {
localStorage?.setItem(key, value); localStorage.setItem(key, value);
}, [key, value]); }, [key, value]);
return [value, setValue]; return [value, setValue];

View File

@ -14,21 +14,21 @@ export const useOauthWindow = () => {
if (data.messageType === 'oAuth') { if (data.messageType === 'oAuth') {
messageHandler(event); messageHandler(event);
window?.removeEventListener('message', handler); window.removeEventListener('message', handler);
} }
} }
window?.removeEventListener('message', handler); window.removeEventListener('message', handler);
if (!oauthWindow || oauthWindow.closed) { if (!oauthWindow || oauthWindow.closed) {
// окно ещё не существует, либо было закрыто // окно ещё не существует, либо было закрыто
oauthWindow = window?.open(url, name, params)!; oauthWindow = window.open(url, name, params)!;
} else { } else {
// окно уже существует // окно уже существует
oauthWindow!.focus(); oauthWindow!.focus();
} }
window?.addEventListener('message', handler); window.addEventListener('message', handler);
} }
return { return {

10
src/i18n.ts Normal file
View File

@ -0,0 +1,10 @@
import { getRequestConfig } from 'next-intl/server';
import { Locale } from './types/locale';
export default getRequestConfig(async ({ locale }) => ({
messages: (
await (locale === Locale.en
? import('../messages/en.json')
: import(`../messages/${locale}.json`))
).default
}));

View File

@ -1,15 +0,0 @@
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});

View File

@ -1,10 +0,0 @@
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';
import { ALLOWED_LOCALES, DEFAULT_LOCALE } from '../constants/locale';
export const routing = defineRouting({
locales: ALLOWED_LOCALES,
defaultLocale: DEFAULT_LOCALE
});
export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing);

View File

@ -20,27 +20,6 @@ export const onSuccessRequestCallback = (config: InternalAxiosRequestConfig) =>
return newConfig; return newConfig;
}; };
export const onSuccessRequestJwtCallback = (config: InternalAxiosRequestConfig) => {
const newConfig = { ...config };
if (typeof window !== 'undefined') {
var jwt = localStorage.getItem('bbuddy_token_test');
if(jwt) {
newConfig.headers.set('Authorization', `Bearer ${jwt}`);
}
}
return newConfig;
};
export const onSuccessResponseJwtCallback = (response: AxiosResponse) => {
var header = response.headers['x-new-token'];
if(header) {
localStorage.setItem('bbuddy_token_test', header);
}
return response;
};
export const onSuccessResponseCallback = (response: AxiosResponse) => response; export const onSuccessResponseCallback = (response: AxiosResponse) => response;
export const onErrorResponseCallback = (error: any) => Promise.reject(error); export const onErrorResponseCallback = (error: any) => Promise.reject(error);
@ -55,6 +34,3 @@ apiClient.interceptors.response.use(
onSuccessResponseCallback, onSuccessResponseCallback,
onErrorResponseCallback onErrorResponseCallback
); );
apiClient.interceptors.response.use(onSuccessResponseJwtCallback);
apiClient.interceptors.request.use(onSuccessRequestJwtCallback);

View File

@ -1,6 +1,6 @@
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
import { IHttpConnectionOptions } from "@microsoft/signalr/src/IHttpConnectionOptions"; import { IHttpConnectionOptions } from "@microsoft/signalr/src/IHttpConnectionOptions";
import { BASE_URL } from '../constants/common'; import { AUTH_TOKEN_KEY, BASE_URL } from '../constants/common';
import { IChatMessage } from '../types/chat'; import { IChatMessage } from '../types/chat';
const chatMessageMethodName = 'ReceiveMessage'; const chatMessageMethodName = 'ReceiveMessage';
@ -25,11 +25,10 @@ const chatsReceiveMessageMethodName = 'ChatsMessageCreated';
class SignalConnector { class SignalConnector {
private connection: HubConnection; private connection: HubConnection;
private events ={} as any; private events ={} as any;
static instance?: SignalConnector; static instance: SignalConnector;
constructor({ jwt }: { jwt?: string}) { constructor() {
console.log('here')
const options = { const options = {
accessTokenFactory: () => jwt accessTokenFactory: () => localStorage.getItem(AUTH_TOKEN_KEY)
} as IHttpConnectionOptions; } as IHttpConnectionOptions;
this.connection = new HubConnectionBuilder() this.connection = new HubConnectionBuilder()
.withUrl(`${BASE_URL}/hubs/chat`, options) .withUrl(`${BASE_URL}/hubs/chat`, options)
@ -70,17 +69,10 @@ class SignalConnector {
this.connection.invoke(sendChatMessagesSeenMethodName, messagesId).then(x => console.log(sendChatMessagesSeenMethodName, x)) this.connection.invoke(sendChatMessagesSeenMethodName, messagesId).then(x => console.log(sendChatMessagesSeenMethodName, x))
} }
public static getInstance({ jwt }: { jwt?: string }): SignalConnector | undefined { public static getInstance(): SignalConnector {
if (!SignalConnector.instance) { if (!SignalConnector.instance)
if (jwt) { SignalConnector.instance = new SignalConnector();
SignalConnector.instance = new SignalConnector({ jwt });
return SignalConnector.instance; return SignalConnector.instance;
} else {
return undefined;
}
} else {
return SignalConnector.instance;
}
} }
} }

View File

@ -2,7 +2,7 @@ import "server-only";
import Stripe from "stripe"; import Stripe from "stripe";
export const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY as string, { export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
apiVersion: "2024-06-20", apiVersion: "2024-06-20",
appInfo: { appInfo: {
name: "bbuddy-ui", name: "bbuddy-ui",

View File

@ -1,7 +1,11 @@
import createMiddleware from 'next-intl/middleware'; import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing'; import { ALLOWED_LOCALES, DEFAULT_LOCALE, LOCALE_PREFIX } from './constants/locale';
export default createMiddleware(routing); export default createMiddleware({
locales: ALLOWED_LOCALES,
localePrefix: LOCALE_PREFIX,
defaultLocale: DEFAULT_LOCALE
});
export const config = { export const config = {
matcher: ['/', '/(en|ru|de|it|es|fr)/:path*'] matcher: ['/', '/(en|ru|de|it|es|fr)/:path*']

17
src/navigation.ts Normal file
View File

@ -0,0 +1,17 @@
import { createLocalizedPathnamesNavigation, Pathnames } from 'next-intl/navigation';
import { ALLOWED_LOCALES, LOCALE_PREFIX } from './constants/locale';
export const { Link, redirect, usePathname, useRouter } =
createLocalizedPathnamesNavigation({
locales: ALLOWED_LOCALES,
pathnames: {
'/': '/',
// '/experts': '/experts',
// '/experts/[expertId]': '/experts/[expertId]',
// '/news': '/news',
// '/privacy-policy': '/privacy-policy',
// '/[userId]': '/[userId]',
// '/[userId]/[...slug]': '/[userId]/[...slug]',
} satisfies Pathnames<typeof ALLOWED_LOCALES>,
localePrefix: LOCALE_PREFIX
});

View File

@ -1,5 +1,5 @@
.b-slider { .b-slider {
//margin-top: -105px; margin-top: -105px;
h2 { h2 {
margin: 40px 0 20px; margin: 40px 0 20px;

View File

@ -30,7 +30,6 @@ export type ProfileRequest = {
faceImage?: any; faceImage?: any;
isFaceImageKeepExisting?: boolean; isFaceImageKeepExisting?: boolean;
phone?: string; phone?: string;
role?: string;
}; };
export type PayInfo = { export type PayInfo = {