Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/(protected)/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ProfilePage as default } from 'pages/profile/';
6 changes: 6 additions & 0 deletions public/github-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions public/google-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions public/vkontakte-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions public/yandex-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 8 additions & 3 deletions src/app/providers/AppProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { PropsWithChildren } from 'react';
import { PropsWithChildren, Suspense } from 'react';
import { QueryProvider } from './QueryProvider';
import { Toaster, TooltipProvider } from 'shared/ui';
import { FrontendObservability } from 'shared/config/';

import { QueryParamsHandler } from './QueryParamsHandler';
export function AppProviders({ children }: PropsWithChildren) {
return (
<>
<FrontendObservability />
<QueryProvider>
<TooltipProvider>{children}</TooltipProvider>
<TooltipProvider>
<Suspense>
<QueryParamsHandler />
</Suspense>
{children}
</TooltipProvider>
</QueryProvider>
<Toaster richColors />
</>
Expand Down
27 changes: 27 additions & 0 deletions src/app/providers/QueryParamsHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Как-то странно, что ты его глобально вынес, думаю лучше на уровне авторизации посадить это


import { type Route } from 'next';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { toast } from 'sonner';

export function QueryParamsHandler() {
const params = useSearchParams();
const isShowToast = useRef(false);
const router = useRouter();
useEffect(() => {
const success = params?.get('success');
const message = params?.get('message');

if (isShowToast.current || !success) return;

if (success === 'true' && message) {
toast.success(message);
} else if (success === 'false' && message) {
toast.error(message);
}
isShowToast.current = true;
router.replace(location.pathname as Route);
}, [params, router]);
return null;
}
5 changes: 3 additions & 2 deletions src/entities/auth/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,17 @@ export class AuthHttp {
signal,
});
}
static connectedOAuthProviders() {
static connectedOAuthProviders(signal: AbortSignal) {
return api<TAuth.ConnectedOAuthProvidersResponse>({
url: '/auth/oauth/providers/connected',
method: 'GET',
contracts: {
response: SAuth.ConnectedOAuthProvidersResponse,
},
signal,
});
}
static connecteOAuthProvder(provider: TAuth.OAuthProvider) {
static connectOAuthProvder(provider: TAuth.OAuthProvider) {
return api<TAuth.ConnectOAuthProviderResponse>({
url: `/auth/oauth/${provider}/connect`,
method: 'POST',
Expand Down
10 changes: 9 additions & 1 deletion src/entities/auth/api/queries.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { queryOptions } from '@tanstack/react-query';
import { AuthHttp } from './http';
import { authKeys } from '../model/const';

export class AuthQueries {
static getOAuthProviders() {
return queryOptions({
queryKey: ['oauth-providers'],
queryKey: authKeys.availableProviders(),
queryFn: async ({ signal }) => AuthHttp.oAuthProviders(signal),
staleTime: 60_000 * 360 * 24,
});
}
static getConnectedOAuthProviders() {
return queryOptions({
queryKey: authKeys.connectedProviders(),
queryFn: async ({ signal }) => AuthHttp.connectedOAuthProviders(signal),
staleTime: 60_000 * 360 * 24,
});
}
}
1 change: 1 addition & 0 deletions src/entities/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export type * as TAuth from './model/types';
export * as CAuth from './model/const';
export { AuthHttp } from './api/http';
export { AuthQueries } from './api/queries';
export { type OAuthProviderMeta } from './model/const';
38 changes: 38 additions & 0 deletions src/entities/auth/model/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
import { type TAuth } from 'entities/auth';
import YandexIcon from 'public/yandex-logo.svg';
import VkontakteIcon from 'public/vkontakte-logo.svg';
import GoogleIcon from 'public/google-logo.svg';
import GithubIcon from 'public/github-logo.svg';

export const MIN_PASS_LENGTH = 8;
export const MAX_PASS_LENGTH = 32;
export const OTP_LENGTH = 6;

export const MIN_NAME_LENGTH = 2;
export const MAX_NAME_LENGTH = 50;

export type OAuthProviderMeta = {
label: string;
iconSrc: string;
buttonClassName?: string;
};

export const OAUTH_PROVIDERS: Record<TAuth.OAuthProvider, OAuthProviderMeta> = {
yandex: {
label: 'Яндекс',
iconSrc: YandexIcon,
buttonClassName: 'text-[#fc3f1d] hover:text-[#fc3f1d]',
},
vkontakte: {
label: 'Вконтакте',
iconSrc: VkontakteIcon,
buttonClassName: 'bg-[#07f] hover:bg-[#07f]',
},
google: { label: 'Google', iconSrc: GoogleIcon },
github: {
label: 'GitHub',
iconSrc: GithubIcon,
buttonClassName: 'bg-[#24292f] hover:bg-[#24292f] text-white hover:text-white ',
},
} as const;
export const OAUTH_PROVIDERS_COUNT = Object.keys(OAUTH_PROVIDERS).length;

export const authKeys = {
all: ['auth'] as const,
availableProviders: () => [...authKeys.all, 'providers', 'available'] as const,
connectedProviders: () => [...authKeys.all, 'providers', 'connected'] as const,
};
39 changes: 0 additions & 39 deletions src/features/auth/oauth-login/model/consts.ts

This file was deleted.

49 changes: 49 additions & 0 deletions src/features/auth/oauth-login/ui/OAuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { ButtonHTMLAttributes } from 'react';
import { Button } from 'shared/ui';
import { cn } from 'shared/lib/utils';
import { CAuth } from 'entities/auth';
import type { TAuth } from 'entities/auth';
import Link from 'next/link';
import { type Route } from 'next';
import Image from 'next/image';

type OAuthButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> & {
provider: TAuth.OAuthProvider;
iconClassName?: string;
href: Route;
};

export function OAuthButton({
provider,
className,
iconClassName,
href,
...props
}: OAuthButtonProps) {
const meta = CAuth.OAUTH_PROVIDERS[provider];
if (!meta) return null;

return (
<Button
type="button"
size={meta.iconSrc ? 'icon' : 'default'}
variant={'outline'}
className={cn(meta.buttonClassName, className)}
{...props}
>
<Link href={href} className="text-base">
{meta.iconSrc ? (
<Image
src={meta.iconSrc}
alt={meta.label}
width={24}
height={24}
className={cn('size-6', iconClassName)}
/>
) : (
meta.label
)}
</Link>
</Button>
);
}
56 changes: 0 additions & 56 deletions src/features/auth/oauth-login/ui/OAuthIcons.tsx

This file was deleted.

36 changes: 20 additions & 16 deletions src/features/auth/oauth-login/ui/OAuthLoginButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
'use client';
import { Button, Skeleton } from 'shared/ui';
import { OAUTH_PROVIDERS, OAUTH_PROVIDERS_COUNT } from '../model/consts';
import { Skeleton } from 'shared/ui';
import { CAuth } from 'entities/auth';
import { cn } from 'shared/lib/utils';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { AuthQueries } from 'entities/auth';
import { type TAuth } from 'entities/auth';
import { routes } from 'shared/config';
import { StartOauthParams } from '../model/types';
import { OAuthButton } from './OAuthButton';
import { type Route } from 'next';

export const getRoute = (provider: TAuth.OAuthProvider) => {
const params = new URLSearchParams({
provider,
startOAuth: 'true',
} satisfies Record<keyof StartOauthParams, string>);

return `${routes.auth.oauth()}?${params.toString()}`;
};

export function OAuthLoginButtons({ className }: { className?: string }) {
const { data, isLoading } = useQuery(AuthQueries.getOAuthProviders());

return (
<div className={cn('flex items-center justify-center gap-2', className)}>
{isLoading &&
Array.from({ length: OAUTH_PROVIDERS_COUNT }, (_v, i) => (
Array.from({ length: CAuth.OAUTH_PROVIDERS_COUNT }, (_v, i) => (
<Skeleton className="size-8" key={i} />
))}

{data?.map((item) => {
const providerConfig = OAUTH_PROVIDERS[item.value];

return (
<Button
asChild
style={{ backgroundColor: providerConfig.color }}
<OAuthButton
key={item.value}
size={'icon'}
variant={'outline'}
>
<Link href={providerConfig.href as Route}>
<providerConfig.icon className="size-6" />
</Link>
</Button>
provider={item.value}
href={getRoute(item.value) as Route}
/>
);
})}
</div>
Expand Down
Loading
Loading