diff --git a/app/(protected)/profile/page.tsx b/app/(protected)/profile/page.tsx new file mode 100644 index 0000000..d071d18 --- /dev/null +++ b/app/(protected)/profile/page.tsx @@ -0,0 +1 @@ +export { ProfilePage as default } from 'pages/profile/'; diff --git a/public/github-logo.svg b/public/github-logo.svg new file mode 100644 index 0000000..d4c9ed9 --- /dev/null +++ b/public/github-logo.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/public/google-logo.svg b/public/google-logo.svg new file mode 100644 index 0000000..7a6dba8 --- /dev/null +++ b/public/google-logo.svg @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/public/vkontakte-logo.svg b/public/vkontakte-logo.svg new file mode 100644 index 0000000..4f105a1 --- /dev/null +++ b/public/vkontakte-logo.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/public/yandex-logo.svg b/public/yandex-logo.svg new file mode 100644 index 0000000..27e8a20 --- /dev/null +++ b/public/yandex-logo.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/app/providers/AppProviders.tsx b/src/app/providers/AppProviders.tsx index 76e167b..a633adf 100644 --- a/src/app/providers/AppProviders.tsx +++ b/src/app/providers/AppProviders.tsx @@ -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 ( <> - {children} + + + + + {children} + diff --git a/src/app/providers/QueryParamsHandler.tsx b/src/app/providers/QueryParamsHandler.tsx new file mode 100644 index 0000000..720fc51 --- /dev/null +++ b/src/app/providers/QueryParamsHandler.tsx @@ -0,0 +1,27 @@ +'use client'; + +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; +} diff --git a/src/entities/auth/api/http.ts b/src/entities/auth/api/http.ts index 3b5a245..959f5a1 100644 --- a/src/entities/auth/api/http.ts +++ b/src/entities/auth/api/http.ts @@ -96,16 +96,17 @@ export class AuthHttp { signal, }); } - static connectedOAuthProviders() { + static connectedOAuthProviders(signal: AbortSignal) { return api({ url: '/auth/oauth/providers/connected', method: 'GET', contracts: { response: SAuth.ConnectedOAuthProvidersResponse, }, + signal, }); } - static connecteOAuthProvder(provider: TAuth.OAuthProvider) { + static connectOAuthProvder(provider: TAuth.OAuthProvider) { return api({ url: `/auth/oauth/${provider}/connect`, method: 'POST', diff --git a/src/entities/auth/api/queries.ts b/src/entities/auth/api/queries.ts index f1c3a2c..5ade73e 100644 --- a/src/entities/auth/api/queries.ts +++ b/src/entities/auth/api/queries.ts @@ -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, + }); + } } diff --git a/src/entities/auth/index.ts b/src/entities/auth/index.ts index 5287e31..146ba7e 100644 --- a/src/entities/auth/index.ts +++ b/src/entities/auth/index.ts @@ -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'; diff --git a/src/entities/auth/model/const.ts b/src/entities/auth/model/const.ts index 2d40b1f..0a44380 100644 --- a/src/entities/auth/model/const.ts +++ b/src/entities/auth/model/const.ts @@ -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 = { + 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, +}; diff --git a/src/features/auth/oauth-login/model/consts.ts b/src/features/auth/oauth-login/model/consts.ts deleted file mode 100644 index d611103..0000000 --- a/src/features/auth/oauth-login/model/consts.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { type TAuth } from 'entities/auth'; -import { ComponentType, SVGProps } from 'react'; -import { GithubIcon, GoogleIcon, VkontakteIcon, YandexIcon } from '../ui/OAuthIcons'; -import { routes } from 'shared/config'; -import { StartOauthParams } from './types'; - -const getRoute = (provider: TAuth.OAuthProvider) => { - const params = new URLSearchParams({ - provider, - startOAuth: 'true', - } satisfies Record); - - return `${routes.auth.oauth()}?${params.toString()}`; -}; - -export type OAuthProviderConfig = { - label: string; - icon: ComponentType>; - href: string; - color?: string; -}; - -export const OAUTH_PROVIDERS: Record = { - yandex: { label: 'Яндекс', icon: YandexIcon, href: getRoute('yandex') }, - vkontakte: { - label: 'Вконтакте', - icon: VkontakteIcon, - href: getRoute('vkontakte'), - color: '#07f', - }, - google: { label: 'Google', icon: GoogleIcon, href: getRoute('google') }, - github: { - label: 'GitHub', - icon: GithubIcon, - href: getRoute('github'), - color: '#24292f', - }, -}; -export const OAUTH_PROVIDERS_COUNT = Object.keys(OAUTH_PROVIDERS).length; diff --git a/src/features/auth/oauth-login/ui/OAuthButton.tsx b/src/features/auth/oauth-login/ui/OAuthButton.tsx new file mode 100644 index 0000000..b35d8d0 --- /dev/null +++ b/src/features/auth/oauth-login/ui/OAuthButton.tsx @@ -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, '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 ( + + ); +} diff --git a/src/features/auth/oauth-login/ui/OAuthIcons.tsx b/src/features/auth/oauth-login/ui/OAuthIcons.tsx deleted file mode 100644 index 9332584..0000000 --- a/src/features/auth/oauth-login/ui/OAuthIcons.tsx +++ /dev/null @@ -1,56 +0,0 @@ -type IconProps = { className?: string }; - -export function GoogleIcon({ className }: IconProps) { - return ( - - - - - - - - ); -} - -export function YandexIcon({ className }: IconProps) { - return ( - - - - ); -} -export function GithubIcon({ className }: IconProps) { - return ( - - - - ); -} -export function VkontakteIcon({ className }: IconProps) { - return ( - - - - ); -} diff --git a/src/features/auth/oauth-login/ui/OAuthLoginButtons.tsx b/src/features/auth/oauth-login/ui/OAuthLoginButtons.tsx index 9de4cd7..2ba8d5c 100644 --- a/src/features/auth/oauth-login/ui/OAuthLoginButtons.tsx +++ b/src/features/auth/oauth-login/ui/OAuthLoginButtons.tsx @@ -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); + + return `${routes.auth.oauth()}?${params.toString()}`; +}; + export function OAuthLoginButtons({ className }: { className?: string }) { const { data, isLoading } = useQuery(AuthQueries.getOAuthProviders()); return (
{isLoading && - Array.from({ length: OAUTH_PROVIDERS_COUNT }, (_v, i) => ( + Array.from({ length: CAuth.OAUTH_PROVIDERS_COUNT }, (_v, i) => ( ))} {data?.map((item) => { - const providerConfig = OAUTH_PROVIDERS[item.value]; - return ( - + provider={item.value} + href={getRoute(item.value) as Route} + /> ); })}
diff --git a/src/pages/auth/oauth/ui/OAuthPage.tsx b/src/pages/auth/oauth/ui/OAuthPage.tsx index 960533b..16613be 100644 --- a/src/pages/auth/oauth/ui/OAuthPage.tsx +++ b/src/pages/auth/oauth/ui/OAuthPage.tsx @@ -21,6 +21,8 @@ interface Props { export async function OAuthPage({ searchParams }: Props) { const { success, message, provider, startOAuth } = await searchParams; + // TODO: страница знает API + if (provider && startOAuth === 'true') { redirect(`${env.NEXT_PUBLIC_API_BASE_URL}/auth/oauth/${provider}` as Route); } @@ -30,11 +32,13 @@ export async function OAuthPage({ searchParams }: Props) { } if (success === 'true') { - redirect(routes.user.profile()); + redirect( + `${routes.user.profile()}?success=true&message=${encodeURIComponent(message || 'Вход успешен')}` + ); } const errorUrl = message - ? `${routes.auth.signin()}?oauth_error=1&message=${encodeURIComponent(message)}` + ? `${routes.auth.signin()}?success=true&message=${encodeURIComponent(message)}` : routes.auth.signin(); redirect(errorUrl as Route); diff --git a/src/pages/auth/signin/model/useAuthRedirectMessage.ts b/src/pages/auth/signin/model/useAuthRedirectMessage.ts deleted file mode 100644 index 8d64438..0000000 --- a/src/pages/auth/signin/model/useAuthRedirectMessage.ts +++ /dev/null @@ -1,19 +0,0 @@ -'use client'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { useEffect } from 'react'; -import { routes } from 'shared/config'; -import { toast } from 'sonner'; - -export function useAuthRedirectMessage() { - const params = useSearchParams(); - const router = useRouter(); - - useEffect(() => { - const error = params?.get('oauth_error'); - const message = params?.get('message'); - - if (!error) return; - toast.error(message ?? 'Authorization failed'); - router.replace(routes.auth.signin()); - }, [params, router]); -} diff --git a/src/pages/auth/signin/ui/AuthRedirectHandler.tsx b/src/pages/auth/signin/ui/AuthRedirectHandler.tsx deleted file mode 100644 index 2b5a8ee..0000000 --- a/src/pages/auth/signin/ui/AuthRedirectHandler.tsx +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; -import { useAuthRedirectMessage } from '../model/useAuthRedirectMessage'; - -export function AuthRedirectHandler() { - useAuthRedirectMessage(); - return null; -} diff --git a/src/pages/auth/signin/ui/SigninPage.tsx b/src/pages/auth/signin/ui/SigninPage.tsx index 70cbad5..fd6c09b 100644 --- a/src/pages/auth/signin/ui/SigninPage.tsx +++ b/src/pages/auth/signin/ui/SigninPage.tsx @@ -6,16 +6,11 @@ import { routes } from 'shared/config'; import { AccessToken } from 'shared/api'; import { toast } from 'sonner'; import { useRouter } from 'next/navigation'; -import { Suspense } from 'react'; -import { AuthRedirectHandler } from './AuthRedirectHandler'; function SigninPage() { const router = useRouter(); return ( <> - - -
diff --git a/src/pages/auth/signup/ui/SignupForm.tsx b/src/pages/auth/signup/ui/SignupForm.tsx index 7d7b61b..dc74434 100644 --- a/src/pages/auth/signup/ui/SignupForm.tsx +++ b/src/pages/auth/signup/ui/SignupForm.tsx @@ -30,6 +30,7 @@ import { prepareFullName } from '../model/utils/prepare-fullname'; import { extractValidationIssues } from 'shared/api'; import { TAuth } from 'entities/auth'; import { useSignup, type UseSignupOptions } from '../model/useSignup'; +import { OAuthLoginButtons, OAuthSeparator } from 'features/auth/oauth-login'; interface SignupFormProps extends Omit, 'children' | 'onSubmit'> { mutateOptions?: UseSignupOptions; @@ -183,6 +184,8 @@ export function SignupForm({ className, mutateOptions = {}, ...props }: SignupFo + + ); diff --git a/src/pages/profile/api/useConnectOauthProvider.ts b/src/pages/profile/api/useConnectOauthProvider.ts new file mode 100644 index 0000000..dbf46dc --- /dev/null +++ b/src/pages/profile/api/useConnectOauthProvider.ts @@ -0,0 +1,21 @@ +import { type DefaultError, useMutation, UseMutationOptions } from '@tanstack/react-query'; +import { AuthHttp, type TAuth } from 'entities/auth'; + +export type UseConnectOAuthProviderOptions = Omit< + UseMutationOptions, + 'mutationFn' +>; + +export function useConnectOAuthProvider({ ...rest }: UseConnectOAuthProviderOptions = {}) { + return useMutation< + Awaited, + DefaultError, + TAuth.OAuthProvider + >({ + ...rest, + mutationFn: AuthHttp.connectOAuthProvder, + meta: { + skipGlobalValidationToast: true, + }, + }); +} diff --git a/src/pages/profile/api/useConnectedAccounts.ts b/src/pages/profile/api/useConnectedAccounts.ts new file mode 100644 index 0000000..8518a9d --- /dev/null +++ b/src/pages/profile/api/useConnectedAccounts.ts @@ -0,0 +1,22 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; +import { AuthQueries } from 'entities/auth'; +import { useMemo } from 'react'; + +export function useConnectedAccounts() { + const available = useQuery(AuthQueries.getOAuthProviders()); + const connected = useQuery(AuthQueries.getConnectedOAuthProviders()); + + const providers = useMemo(() => { + if (!available.data) return []; + + const connectedSet = new Set(connected.data?.map((v) => v.provider)); + + return available.data.map((item) => ({ + ...item, + isConnected: connectedSet.has(item.value), + })); + }, [available.data, connected.data]); + + return { providers }; +} diff --git a/src/pages/profile/api/useDisconnectOauthProvider.ts b/src/pages/profile/api/useDisconnectOauthProvider.ts new file mode 100644 index 0000000..ca7464c --- /dev/null +++ b/src/pages/profile/api/useDisconnectOauthProvider.ts @@ -0,0 +1,19 @@ +import { type DefaultError, useMutation, UseMutationOptions } from '@tanstack/react-query'; +import { AuthHttp, type TAuth } from 'entities/auth'; + +export type UseDisconnectOAuthProviderOptions = Omit< + UseMutationOptions, + 'mutationFn' +>; + +export function useDisconnectOAuthProvider({ ...rest }: UseDisconnectOAuthProviderOptions = {}) { + return useMutation, DefaultError, TAuth.OAuthProvider>( + { + ...rest, + mutationFn: AuthHttp.removeOAuthProvder, + meta: { + skipGlobalValidationToast: true, + }, + } + ); +} diff --git a/src/pages/profile/index.ts b/src/pages/profile/index.ts index 8922138..76b7a15 100644 --- a/src/pages/profile/index.ts +++ b/src/pages/profile/index.ts @@ -2,3 +2,4 @@ export { profileTabs } from './config/tabs'; export { MePage } from './ui/me-page/MePage'; export { NotificationsPage } from './ui/notifications-page/NotificationsPage'; export { SecurityPage } from './ui/security-page/SecurityPage'; +export { ProfilePage } from './ui/profile-page/ProfilePage'; diff --git a/src/pages/profile/ui/me-page/MePage.tsx b/src/pages/profile/ui/me-page/MePage.tsx index 838e1ef..ac4c875 100644 --- a/src/pages/profile/ui/me-page/MePage.tsx +++ b/src/pages/profile/ui/me-page/MePage.tsx @@ -12,6 +12,7 @@ import { import { IdentityItem } from './IdentityItem'; import { ProfileForm } from './ProfileForm'; import { useMePage } from '../../model/useMePage'; +import { AccountSection } from './account-section/AccountsSection'; function MePage() { const { form, profile, email, isDirty, isPending, onSubmit, onDiscard } = useMePage(); @@ -28,7 +29,7 @@ function MePage() { } return ( - <> +
+ - +
); } diff --git a/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx b/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx new file mode 100644 index 0000000..e505e4f --- /dev/null +++ b/src/pages/profile/ui/me-page/account-section/AccountsSection.tsx @@ -0,0 +1,26 @@ +import { OAuthManageButton } from './OAuthManageButton'; +import { useConnectedAccounts } from 'pages/profile/api/useConnectedAccounts'; +import { CardSection } from 'shared/ui'; + +export function AccountSection() { + const { providers } = useConnectedAccounts(); + + return ( + + {providers?.map((provider) => { + return ( + + ); + })} + + ); +} diff --git a/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx b/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx new file mode 100644 index 0000000..7ac92bf --- /dev/null +++ b/src/pages/profile/ui/me-page/account-section/OAuthManageButton.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { TAuth } from 'entities/auth'; +import { CAuth } from 'entities/auth'; +import { useCallback, type ComponentProps } from 'react'; +import { Button } from 'shared/ui'; +import { useConnectOAuthProvider } from '../../../api/useConnectOauthProvider'; +import { useDisconnectOAuthProvider } from '../../../api/useDisconnectOauthProvider'; +import { env } from 'shared/config'; +import { toast } from 'sonner'; +import Image from 'next/image'; + +type OAuthManageButtonProps = ComponentProps & { + provider: TAuth.OAuthProvider; + label: string; + isLinked: boolean; +}; + +export function OAuthManageButton({ provider, label, isLinked, ...props }: OAuthManageButtonProps) { + const connect = useConnectOAuthProvider(); + const disconnect = useDisconnectOAuthProvider(); + + const isLoading = connect.isPending || disconnect.isPending; + + const handleToggleConnect = useCallback(() => { + if (isLinked) { + disconnect.mutate(provider, { + onSuccess: (data, _v, _m, context) => { + context.client.invalidateQueries({ queryKey: CAuth.authKeys.connectedProviders() }); + toast.success(data.message); + }, + }); + } else { + connect.mutate(provider, { + onSuccess: (data) => { + const url = data.url.startsWith('http') + ? data.url + : new URL(data.url, env.NEXT_PUBLIC_API_BASE_URL).toString(); + window.location.href = url; + }, + }); + } + }, [connect, disconnect, isLinked, provider]); + + const meta = CAuth.OAUTH_PROVIDERS[provider]; + + return ( + + ); +} diff --git a/src/pages/profile/ui/profile-page/ProfilePage.tsx b/src/pages/profile/ui/profile-page/ProfilePage.tsx new file mode 100644 index 0000000..cf35eaa --- /dev/null +++ b/src/pages/profile/ui/profile-page/ProfilePage.tsx @@ -0,0 +1,28 @@ +import { type Route } from 'next'; +import { redirect } from 'next/navigation'; +import { routes } from 'shared/config'; + +type BooleanRaw = 'false' | 'true'; +type PageParams = { + success: BooleanRaw; + message: string; +}; + +interface Props { + searchParams: Promise>; +} +export async function ProfilePage({ searchParams }: Props) { + const { success, message } = await searchParams; + + if (success === 'true') { + redirect( + `${routes.user.profile()}?success=true&message=${encodeURIComponent(message || 'Провайдер успешно привязан')}` + ); + } + + const errorUrl = message + ? `${routes.user.profile()}?success=false&message=${encodeURIComponent(message)}` + : routes.user.profile(); + + redirect(errorUrl as Route); +} diff --git a/src/shared/config/routes.ts b/src/shared/config/routes.ts index 36081e4..d226fea 100644 --- a/src/shared/config/routes.ts +++ b/src/shared/config/routes.ts @@ -2,6 +2,7 @@ import type { Route } from 'next'; export const routes = { home: (): Route => '/', + profile: (): Route => '/profile', user: { root: (): Route => '/user', profile: (): Route => '/user/profile', diff --git a/src/shared/ui/icon/auth/GithubIcon.tsx b/src/shared/ui/icon/auth/GithubIcon.tsx new file mode 100644 index 0000000..991dff8 --- /dev/null +++ b/src/shared/ui/icon/auth/GithubIcon.tsx @@ -0,0 +1,12 @@ +import { IconProps } from '../types'; + +export function GithubIcon({ className }: IconProps) { + return ( + + + + ); +} diff --git a/src/shared/ui/icon/auth/GoogleIcon.tsx b/src/shared/ui/icon/auth/GoogleIcon.tsx new file mode 100644 index 0000000..c1c7276 --- /dev/null +++ b/src/shared/ui/icon/auth/GoogleIcon.tsx @@ -0,0 +1,25 @@ +import { IconProps } from '../types'; + +export function GoogleIcon({ className }: IconProps) { + return ( + + + + + + + + ); +} diff --git a/src/shared/ui/icon/auth/VkontakteIcon.tsx b/src/shared/ui/icon/auth/VkontakteIcon.tsx new file mode 100644 index 0000000..0bba3c3 --- /dev/null +++ b/src/shared/ui/icon/auth/VkontakteIcon.tsx @@ -0,0 +1,12 @@ +import { IconProps } from '../types'; + +export function VkontakteIcon({ className }: IconProps) { + return ( + + + + ); +} diff --git a/src/shared/ui/icon/auth/YandexIcon.tsx b/src/shared/ui/icon/auth/YandexIcon.tsx new file mode 100644 index 0000000..8b61981 --- /dev/null +++ b/src/shared/ui/icon/auth/YandexIcon.tsx @@ -0,0 +1,12 @@ +import { IconProps } from '../types'; + +export function YandexIcon({ className }: IconProps) { + return ( + + + + ); +} diff --git a/src/shared/ui/icon/index.ts b/src/shared/ui/icon/index.ts new file mode 100644 index 0000000..54097b6 --- /dev/null +++ b/src/shared/ui/icon/index.ts @@ -0,0 +1,5 @@ +export { type IconProps } from './types'; +export { GithubIcon } from './auth/GithubIcon'; +export { GoogleIcon } from './auth/GoogleIcon'; +export { VkontakteIcon } from './auth/VkontakteIcon'; +export { YandexIcon } from './auth/YandexIcon'; diff --git a/src/shared/ui/icon/types.ts b/src/shared/ui/icon/types.ts new file mode 100644 index 0000000..fae7fa0 --- /dev/null +++ b/src/shared/ui/icon/types.ts @@ -0,0 +1 @@ +export type IconProps = { className?: string }; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 8a9ae79..df97fc1 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -39,3 +39,4 @@ export * from './Select'; export * from './Empty'; export * from './ScrollArea'; export * from './Kanban'; +export * from './icon'; diff --git a/src/widgets/app-sidebar/ui/AppSidebar.tsx b/src/widgets/app-sidebar/ui/AppSidebar.tsx index 18ebf7c..dc4144c 100644 --- a/src/widgets/app-sidebar/ui/AppSidebar.tsx +++ b/src/widgets/app-sidebar/ui/AppSidebar.tsx @@ -10,6 +10,7 @@ import { MyTeams } from './MyTeams'; import { Projects } from './projects/Projects'; import { Team } from './teams/Team'; import { TeamsDropdown } from './teams/TeamsDropdown'; +import Link from 'next/link'; export function AppSidebar({ ...props }: Omit, 'children'>) { return ( @@ -32,6 +33,7 @@ export function AppSidebar({ ...props }: Omit + тест