diff --git a/package.json b/package.json index 53ef16a..0edbf83 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,11 @@ "@mantine/core": "8.1.2", "@mantine/form": "^8.1.3", "@mantine/hooks": "8.1.2", + "@mantine/notifications": "^8.2.1", "@next/bundle-analyzer": "^15.3.3", "@reduxjs/toolkit": "^2.8.2", "@tabler/icons-react": "^3.34.0", + "axios": "^1.11.0", "classnames": "^2.5.1", "framer-motion": "^12.23.7", "i18n-iso-countries": "^7.14.0", diff --git a/src/app/consent/components/ConsentButton/ConsentButton.tsx b/src/app/consent/components/ConsentButton/ConsentButton.tsx index ef7e8cd..87d1e07 100644 --- a/src/app/consent/components/ConsentButton/ConsentButton.tsx +++ b/src/app/consent/components/ConsentButton/ConsentButton.tsx @@ -1,28 +1,63 @@ "use client"; import { FC, useEffect, useState } from "react"; -import { redirect } from "next/navigation"; import { useSelector } from "react-redux"; import { Button, Text } from "@mantine/core"; -import SERVICES from "@/constants/services"; -import { ServiceCode } from "@/enums/ServiceCode"; +import SCOPES from "@/constants/scopes"; +import { Scopes } from "@/enums/Scopes"; +import { notifications } from "@/lib/notifications"; import { RootState } from "@/lib/store"; -import ServiceData from "@/types/ServiceData"; +import { AuthService } from "@/mocks/authService"; const ConsentButton: FC = () => { - const serviceCode = useSelector( - (state: RootState) => state.targetService.serviceCode - ); - const [serviceData, setServiceData] = useState(); + const auth = useSelector((state: RootState) => state.auth); + const [clientName, setClientName] = useState(Scopes.UNDEFINED); + const [serviceRequiredAccess, setServiceRequiredAccess] = + useState(""); + + const setAccessesForScope = () => { + const accesses: string[] = []; + (auth.scope ?? []).forEach((scopeItem: Scopes) => { + const access = SCOPES[scopeItem]; + if (access) accesses.push(access); + }); + setServiceRequiredAccess(accesses.join("\n")); + }; + + const requestConsent = () => { + if (!auth.loginChallenge || auth.scope.length === 0) return; + + new AuthService() + .requestConsent(auth.loginChallenge) + .then(response => response.data) + .then(({ clientName }) => { + setClientName(clientName); + }) + .catch(error => { + console.error(error); + notifications.error({ message: error.toString() }); + }); + }; useEffect(() => { - if (serviceCode === ServiceCode.UNDEFINED) { - redirect("services"); - } - setServiceData(SERVICES[serviceCode]); - }, [serviceCode]); + setAccessesForScope(); + requestConsent(); + }, []); - const confirmAccess = () => {}; + const confirmAccess = () => { + if (!auth.loginChallenge) return; + + new AuthService() + .approveConsent(auth.loginChallenge) + .then(response => response.data) + .then(({ redirectUrl }) => { + window.location.href = redirectUrl; + }) + .catch(error => { + console.error(error); + notifications.error({ message: error.toString() }); + }); + }; return ( <> @@ -34,8 +69,7 @@ const ConsentButton: FC = () => { - Сервис {serviceData?.name} получит{" "} - {serviceData?.requiredAccesses} + Сервис {clientName} получит {serviceRequiredAccess} ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..cd301f4 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,19 @@ +import LoginForm from "@/components/LoginForm/LoginForm"; +import Logo from "@/components/Logo/Logo"; +import PageItem from "@/components/PageBlock/PageItem"; +import PageContainer from "@/components/PageContainer/PageContainer"; + +interface LoginPageProps { + searchParams: { login_challenge?: string }; +} + +export default function LoginPage({ searchParams }: LoginPageProps) { + return ( + + + + + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 25af879..21505a8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,15 +1,5 @@ -import LoginForm from "@/components/LoginForm/LoginForm"; -import Logo from "@/components/Logo/Logo"; -import PageItem from "@/components/PageBlock/PageItem"; -import PageContainer from "@/components/PageContainer/PageContainer"; +import { redirect } from "next/navigation"; export default function MainPage() { - return ( - - - - - - - ); + redirect("/login"); } diff --git a/src/app/services/components/ServicesList/ServicesList.tsx b/src/app/services/components/ServicesList/ServicesList.tsx index d5e15b3..96413fd 100644 --- a/src/app/services/components/ServicesList/ServicesList.tsx +++ b/src/app/services/components/ServicesList/ServicesList.tsx @@ -5,8 +5,8 @@ import { redirect } from "next/navigation"; import { Button, Stack, Title } from "@mantine/core"; import styles from "@/app/services/components/ServicesList/ServicesList.module.css"; import TitleWithLines from "@/components/TitleWithLines/TitleWithLines"; -import SERVICES from "@/constants/services"; -import { ServiceCode } from "@/enums/ServiceCode"; +import SCOPES from "@/constants/scopes"; +import { Scopes } from "@/enums/Scopes"; import { setTargetService } from "@/lib/features/targetService/targetServiceSlice"; import { useAppDispatch } from "@/lib/store"; import ServiceData from "@/types/ServiceData"; @@ -15,10 +15,10 @@ const ServicesList = () => { const dispatch = useAppDispatch(); const services = useMemo( () => - Object.entries(SERVICES) - .filter(([key]) => key !== ServiceCode.UNDEFINED) + Object.entries(SCOPES) + .filter(([key]) => key !== Scopes.UNDEFINED) .map(([, value]) => value), - [SERVICES] + [SCOPES] ); const onServiceClick = (service: ServiceData) => { diff --git a/src/app/verify-phone/components/VerifyPhoneForm/VerifyPhoneForm.tsx b/src/app/verify-phone/components/VerifyPhoneForm/VerifyPhoneForm.tsx index 9f318d0..72c849b 100644 --- a/src/app/verify-phone/components/VerifyPhoneForm/VerifyPhoneForm.tsx +++ b/src/app/verify-phone/components/VerifyPhoneForm/VerifyPhoneForm.tsx @@ -2,10 +2,15 @@ import { FC } from "react"; import { redirect } from "next/navigation"; +import { useSelector } from "react-redux"; import { Button, PinInput, Stack } from "@mantine/core"; import { useForm } from "@mantine/form"; import ResendVerificationCode from "@/app/verify-phone/components/ResendVerificationCode/ResendVerificationCode"; import style from "@/app/verify-phone/components/VerifyPhoneForm/VerifyPhone.module.css"; +import { setScope } from "@/lib/features/auth/authSlice"; +import { notifications } from "@/lib/notifications"; +import { RootState, useAppDispatch } from "@/lib/store"; +import { AuthService } from "@/mocks/authService"; type VerifyNumberForm = { code: string; @@ -20,11 +25,27 @@ const VerifyPhoneForm: FC = () => { code: code => code.length !== 6 && "Введите весь код", }, }); + const authState = useSelector((state: RootState) => state.auth); + const dispatch = useAppDispatch(); const handleSubmit = (values: VerifyNumberForm) => { - console.log(values); + if (!authState.phoneNumber || !authState.loginChallenge) return; - redirect("/services"); + new AuthService() + .approveLogin( + authState.phoneNumber, + values.code, + authState.loginChallenge + ) + .then(response => response.data) + .then(({ redirectUrl, scope }) => { + dispatch(setScope(scope)); + window.location.href = redirectUrl; + }) + .catch(error => { + console.error(error); + notifications.error({ message: error.toString() }); + }); }; const navigateToLogin = () => redirect("/"); diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/LoginForm.tsx index 779d980..37869ee 100644 --- a/src/components/LoginForm/LoginForm.tsx +++ b/src/components/LoginForm/LoginForm.tsx @@ -1,21 +1,27 @@ "use client"; -import { FC, useState } from "react"; -import { redirect } from "next/navigation"; +import { FC, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; import { Button, Stack } from "@mantine/core"; import { useForm } from "@mantine/form"; import PhoneInput from "@/components/PhoneInput/PhoneInput"; +import { useAppDispatch } from "@/lib/store"; +import { setLoginChallenge, setPhoneNumber } from "@/lib/features/auth/authSlice"; +import { AuthService } from "@/mocks/authService"; +import { notifications } from "@/lib/notifications"; type LoginForm = { phoneNumber: string; }; type Props = { + loginChallenge?: string; isCreatingId?: boolean; }; -const LoginForm: FC = ({ isCreatingId = false }) => { +const LoginForm: FC = ({ loginChallenge, isCreatingId = false }) => { const [phoneMask, setPhoneMask] = useState(""); + const router = useRouter(); const form = useForm({ initialValues: { phoneNumber: "", @@ -26,17 +32,33 @@ const LoginForm: FC = ({ isCreatingId = false }) => { "Введите корректный номер", }, }); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setLoginChallenge(loginChallenge ?? null)); + }, [loginChallenge]); const handleSubmit = (values: LoginForm) => { - console.log(values); - console.log(phoneMask); + dispatch(setPhoneNumber(values.phoneNumber)); - redirect("/verify-phone"); + new AuthService().requestLogin(values.phoneNumber) + .then(response => response.data) + .then(({ ok, message }) => { + if (!ok) { + notifications.error({ message }); + } else { + router.push("/verify-phone"); + } + }) + .catch(error => { + console.error(error); + notifications.error({ message: error.toString() }); + }) }; - const navigateToCreateId = () => redirect("/create-id"); + const navigateToCreateId = () => router.push("/create-id"); - const navigateToLogin = () => redirect("/"); + const navigateToLogin = () => router.push("/"); return (
diff --git a/src/constants/scopes.ts b/src/constants/scopes.ts new file mode 100644 index 0000000..2a2be4e --- /dev/null +++ b/src/constants/scopes.ts @@ -0,0 +1,8 @@ +import { Scopes } from "@/enums/Scopes"; + +const SCOPES = { + [Scopes.UNDEFINED]: "", + [Scopes.OPENID]: "доступ к учетной записи и номеру телефона", +}; + +export default SCOPES; diff --git a/src/constants/services.ts b/src/constants/services.ts deleted file mode 100644 index 248040f..0000000 --- a/src/constants/services.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ServiceCode } from "@/enums/ServiceCode"; -import ServiceData from "@/types/ServiceData"; - -const SERVICES = { - [ServiceCode.UNDEFINED]: { - code: ServiceCode.UNDEFINED, - name: "undefined", - link: "", - requiredAccesses: "", - }, - [ServiceCode.CRM]: { - code: ServiceCode.CRM, - name: "LogiDex CRM", - link: "https://skirbo.ru", - requiredAccesses: - "доступ к учетной записи и сделкам по фулфиллменту", - } as ServiceData, -}; - -export default SERVICES; diff --git a/src/enums/Scopes.ts b/src/enums/Scopes.ts new file mode 100644 index 0000000..9194fc2 --- /dev/null +++ b/src/enums/Scopes.ts @@ -0,0 +1,4 @@ +export enum Scopes { + OPENID = "openid", + UNDEFINED = "", +} diff --git a/src/enums/ServiceCode.ts b/src/enums/ServiceCode.ts deleted file mode 100644 index eeaedc2..0000000 --- a/src/enums/ServiceCode.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ServiceCode { - CRM = "crm", - UNDEFINED = "", -} diff --git a/src/lib/features/auth/authSlice.ts b/src/lib/features/auth/authSlice.ts new file mode 100644 index 0000000..9e189dd --- /dev/null +++ b/src/lib/features/auth/authSlice.ts @@ -0,0 +1,34 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { Scopes } from "@/enums/Scopes"; + +interface AuthState { + loginChallenge: string | null; + phoneNumber: string | null; + scope: Scopes[]; +} + +const initialState: AuthState = { + loginChallenge: null, + phoneNumber: null, + scope: [], +}; + +export const authSlice = createSlice({ + name: "authentication", + initialState, + reducers: { + setLoginChallenge: (state, action: PayloadAction) => { + state.loginChallenge = action.payload; + }, + setPhoneNumber: (state, action: PayloadAction) => { + state.phoneNumber = action.payload; + }, + setScope: (state, action: PayloadAction) => { + state.scope = action.payload; + } + }, +}); + +export const { setLoginChallenge, setPhoneNumber, setScope } = authSlice.actions; + +export default authSlice.reducer; diff --git a/src/lib/features/rootReducer.ts b/src/lib/features/rootReducer.ts index f36bf13..53e895a 100644 --- a/src/lib/features/rootReducer.ts +++ b/src/lib/features/rootReducer.ts @@ -1,10 +1,12 @@ import { combineReducers } from "@reduxjs/toolkit"; import targetServiceReducer from "@/lib/features/targetService/targetServiceSlice"; import verificationReducer from "@/lib/features/verification/verificationSlice"; +import authReducer from "@/lib/features/auth/authSlice"; const rootReducer = combineReducers({ targetService: targetServiceReducer, verification: verificationReducer, + auth: authReducer, }); export default rootReducer; diff --git a/src/lib/features/targetService/targetServiceSlice.tsx b/src/lib/features/targetService/targetServiceSlice.tsx index c7f8ae5..231515b 100644 --- a/src/lib/features/targetService/targetServiceSlice.tsx +++ b/src/lib/features/targetService/targetServiceSlice.tsx @@ -1,19 +1,19 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { ServiceCode } from "@/enums/ServiceCode"; +import { Scopes } from "@/enums/Scopes"; interface TargetServiceState { - serviceCode: ServiceCode; + serviceCode: Scopes; } const initialState: TargetServiceState = { - serviceCode: ServiceCode.UNDEFINED, + serviceCode: Scopes.UNDEFINED, }; export const targetServiceSlice = createSlice({ name: "targetService", initialState, reducers: { - setTargetService: (state, action: PayloadAction) => { + setTargetService: (state, action: PayloadAction) => { state.serviceCode = action.payload; }, }, diff --git a/src/lib/notifications/index.ts b/src/lib/notifications/index.ts new file mode 100644 index 0000000..adcb2c9 --- /dev/null +++ b/src/lib/notifications/index.ts @@ -0,0 +1 @@ +export * from "./notifications"; diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts new file mode 100644 index 0000000..20ca858 --- /dev/null +++ b/src/lib/notifications/notifications.ts @@ -0,0 +1,46 @@ +import { notifications } from "@mantine/notifications"; + +type CustomNotifications = { + notify: (...params: Parameters) => void; + success: (...params: Parameters) => void; + warn: (...params: Parameters) => void; + error: (...params: Parameters) => void; + guess: ( + ok: boolean, + ...params: Parameters + ) => void; +} & typeof notifications; + +const customNotifications: CustomNotifications = { + ...notifications, + notify: params => { + return notifications.show({ + ...params, + color: "blue", + }); + }, + success: params => { + return notifications.show({ + ...params, + color: "green", + }); + }, + warn: params => { + return notifications.show({ + ...params, + color: "yellow", + }); + }, + error: params => { + return notifications.show({ + ...params, + color: "red", + }); + }, + guess: (ok: boolean, params) => { + if (ok) return customNotifications.success(params); + return customNotifications.error(params); + }, +}; + +export { customNotifications as notifications }; diff --git a/src/lib/store.ts b/src/lib/store.ts index 22e0215..4f20ae3 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -7,7 +7,7 @@ import rootReducer from "@/lib/features/rootReducer"; const persistConfig = { key: "root", storage, - whitelist: ["targetService", "verification"], + whitelist: ["targetService", "verification", "auth"], }; const persistedReducer = persistReducer(persistConfig, rootReducer); diff --git a/src/mocks/authService.ts b/src/mocks/authService.ts new file mode 100644 index 0000000..b22d0cc --- /dev/null +++ b/src/mocks/authService.ts @@ -0,0 +1,75 @@ +import { AxiosResponse } from "axios"; +import { Scopes } from "@/enums/Scopes"; + +type MockOkMessage = { + ok: boolean; + message: string; +} + +type MockApproveLoginResponse = { + redirectUrl: string; + scope: Scopes[]; +} + +type MockRequestConsentResponse = { + clientName: string; +} + +type MockApproveConsentResponse = { + redirectUrl: string; +} + +export class AuthService { + async requestLogin(phoneNumber: string): Promise> { + return Promise.resolve({ + data: { ok: true, message: "Mock response" } as MockOkMessage, + status: 200, + statusText: "OK", + headers: {}, + config: {} as any, + }); + } + + async approveLogin( + phoneNumber: string, + code: string, + loginChallenge: string + ): Promise> { + return Promise.resolve({ + data: { + redirectUrl: + "http://oauth2.logidex.ru/oauth2/auth?client_id=crm-client&login_verifier=c-4HxjyA0rUMMOYQG-kHStU5pxFeOdViKOyk-eT_qv7vNPHu3xdysdYj3Jwq2gh6vRSHdO6xA7NQgneUho5I_LyENNTFqRkjWwhAsy7Ad9DFhcAWVRCU1rI3ksy38-UTWlto7vKVVhGgRzvlk-coa273uLz-BEo64oUl9B_gcojLnLjO1Q1W6Hu8_nJxqyQBhLPcFOZ1vDVnNcQF5UbB9bld3Wr_1nn3r7sQMBQR54Vphcku6a37GYbPVMVGHo0nBGf6rps6Xg4L6IsD5hlsHzsw5OX3W0MDJ6o--VxHr_HveAH8K7R21Q59JtHa-26pO4KGjetGfgr8rPOTRtsZiKApjZ8qjM9pEgusPj39ysmqYo1wmJo1ZXz8wXh-t3UO0pGIZogmzEOLs5bPCCbXrjG7VaQ78jufSAG2pmhMJxR9AmWsaJms16lev1dBFn-IBdrr4LqYrsVQMqpZstF18ENSURAKejHc8l8m4xocy2-D-ZaeZX0k0vnuGsNTbxT79D1u_-ALm2n0LRwcsK5VCF8v5oO_aMFxcSO87QHU8wy8Wj3um3IBb0sCQXDCpsYIlwqczWNWmxaGXDsmqqUYvZdvWKEXMI5BbbVc44h4_sOaa6BKAaNSceC2GHqN94GWz8dmsX7xyfXMsHYR8_hUFsztN8OstrQkRddJ8een4mdW777W3PYe_U4UL2S2az4L7tC5DTifDCHTfYknb4baQ3UT7x4N8eCd2_Xlkl4gQKU5Mm8njZWucWsLjdW7NG8Q_aDQEl45VunmaQ8iKOTrn1BiNzRHnYdOm15C8nnxHyZ9pO8IKELxUGIKnwP8eF6-A-Rj_bLWWIBquLTRgBrR153gu5Srh2Xl8-LU4ffxM6ipO1nHJMPg3_5493yS1ua_ZWUuht_d4C4Y6j6xuFJHx-bKrCIffiGiSUnpKkepzGnTHCS02wNDVItheAUnlO18zbxsHBFM1tjQQrLIB3cxQRrK12NmIgOheiQMzkSwpQ2CmdRnVpJBGy8Nzp7X_YP-nVC9ctzFR1YrTEw-ZH0wVYjPu_vsijwUtqq3mABD8lw%3D&redirect_uri=http%3A%2F%2Fcrm.logidex.ru%2Fcallback&response_type=code&scope=openid&state=csrf_token", + scope: ["openid"] as Scopes[], + }, + status: 200, + statusText: "OK", + headers: {}, + config: {} as any, + }); + } + + async requestConsent(consentChallenge: string): Promise> { + return Promise.resolve({ + data: { + clientName: "crm", + }, + status: 200, + statusText: "OK", + headers: {}, + config: {} as any, + }); + } + + async approveConsent(consentChallenge: string): Promise> { + return Promise.resolve({ + data: { + redirectUrl: + "http://oauth2.logidex.ru/oauth2/auth?client_id=crm-client&consent_verifier=hU0HetHSHoqc4ZMvwsjMaFUsVckZ3B6ztXyQim4vwcptp4Dp9cIvSByiFHvThLkaIVl2f7uDxB8hUcUoG1-DvNDC3qcGCskLlNn0tDlNcxb41LZtS28D8iZJAUiZedqDdGCfhkuH4TioErId5m-8-y5Y-PYrosfcrqsVfK88vZ5kgViMIjROe68Vc_O5kxpPUymt5I_-oUeFMdrDnjpVcTipwTJIG-WutbtUBHp6tA3FXIfo-0ai-o8yr2Lv2bQiBSegYKA4GmfrQ25xn7_yQGLyGVBVsKPCNRQAyRvdeqFEVGm-3SUxvIJCeyCXaZrHxENSUbxo6xd1m_oVHqye8hXcZSWmFVOa4eo4Rw6OWsnN3AWl75XLt_maKcL_LZftkQERtJBgV2-8C1QYJXwoPS0uTFANq39s2778KIP0XbufiB3UW1QvmUdzKKH43K4MnB-F9ah26nzaw8HwEBTbDGclvkq7TFAozKddwnumgrqRkbElwC3eqr5LnpMfGR1vCVBP81sPjx26LoiKOpmuamfT-O37EsVHdooeP8ry2IjCx0KrUe5wI93XiUc4RIMn_MsO8zaifyNrzFfvVQ7VPNj3QasM4O4drDyGLict1fWiNZP_KVFeAnojOp258nPqDn76VSzROweummzSD-lC56zviX9pZjmGb0RpXb8eeB2Nc4uWCy3dYw4kxEFQSqwU7liqI9paZLv0Vl1PrvS4GL_vv3zh4YKpp8h1yT0IWDnEL75dmIeSBXB7eTkZy8ING1HdwvfH1TdYImCrmLTi93JWtSJvsZWklUBQsFQ900hYPYGK4WVdxRQOTsHrJhJwOex5so7mrnowHpXQuUU6eDi9p2Yj0_YN2XuPIs1I9iS2F2S6t1_kNRmJupzo3g09bY8AGNsSDeEwp2riqXQ_o3xgATaIUycbve2qcOIr89kHomYSCc0YiQnka3zBb5RRTlYhDuQlgeHuEnIBWU2oLYcnyP-zlbeSxRbYuYu4uxXtLHDT9E6tDxqXxSYvw4AzUw-EwQX8v0LamJnQLHCSwD7F8S0M5COSm_Pv56DhtBevnU7PqrpZ-FKOXm89A2HXx9XO8qQMG-tg3y5TZ5vVfTwJ6WkzZNkZSWAZGiwQX4wiQmjep4wlKgP3ZQ8VV0CVS6R9f1DMwY8tCvuJFumd_OORK7-q_bgg3Xp3Njc%3D&redirect_uri=http%3A%2F%2Fcrm.logidex.ru%2Fcallback&response_type=code&scope=openid&state=csrf_token", + }, + status: 200, + statusText: "OK", + headers: {}, + config: {} as any, + }); + } +} diff --git a/src/types/ServiceData.ts b/src/types/ServiceData.ts deleted file mode 100644 index e7858ce..0000000 --- a/src/types/ServiceData.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ServiceCode } from "@/enums/ServiceCode"; - -type ServiceData = { - code: ServiceCode; - name: string; - link: string; - requiredAccesses: string; -} - -export default ServiceData; diff --git a/yarn.lock b/yarn.lock index b026173..134d4d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2025,7 +2025,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.28.2 resolution: "@babel/runtime@npm:7.28.2" checksum: 10c0/c20afe253629d53a405a610b12a62ac74d341a2c1e0fb202bbef0c118f6b5c84f94bf16039f58fd0483dd256901259930a43976845bdeb180cab1f882c21b6e0 @@ -3797,6 +3797,30 @@ __metadata: languageName: node linkType: hard +"@mantine/notifications@npm:^8.2.1": + version: 8.2.1 + resolution: "@mantine/notifications@npm:8.2.1" + dependencies: + "@mantine/store": "npm:8.2.1" + react-transition-group: "npm:4.4.5" + peerDependencies: + "@mantine/core": 8.2.1 + "@mantine/hooks": 8.2.1 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + checksum: 10c0/879b6e286ba28a111f0a765e5e0e56574c1e2521bcbb753ef3a25be7bf42df77a3168e0b99d7c093a250324e6bbd0cd6ab9a40e3ed1acce5ee18d5d2919dd118 + languageName: node + linkType: hard + +"@mantine/store@npm:8.2.1": + version: 8.2.1 + resolution: "@mantine/store@npm:8.2.1" + peerDependencies: + react: ^18.x || ^19.x + checksum: 10c0/9897fd48d4c9d1f5cebd641881df8334604d844f0fbc630b80f18807ad27152d10491041cfc7ed75ca12ddc0fc059e7728a94d0bfda4ac479b56ba0f953f2e91 + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.11 resolution: "@napi-rs/wasm-runtime@npm:0.2.11" @@ -6330,6 +6354,13 @@ __metadata: languageName: node linkType: hard +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d + languageName: node + linkType: hard + "available-typed-arrays@npm:^1.0.5": version: 1.0.5 resolution: "available-typed-arrays@npm:1.0.5" @@ -6353,6 +6384,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.11.0": + version: 1.11.0 + resolution: "axios@npm:1.11.0" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10c0/5de273d33d43058610e4d252f0963cc4f10714da0bfe872e8ef2cbc23c2c999acc300fd357b6bce0fc84a2ca9bd45740fa6bb28199ce2c1266c8b1a393f2b36e + languageName: node + linkType: hard + "axobject-query@npm:^4.1.0": version: 4.1.0 resolution: "axobject-query@npm:4.1.0" @@ -7216,6 +7258,15 @@ __metadata: languageName: node linkType: hard +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -7715,6 +7766,13 @@ __metadata: languageName: node linkType: hard +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 + languageName: node + linkType: hard + "dequal@npm:^2.0.2, dequal@npm:^2.0.3": version: 2.0.3 resolution: "dequal@npm:2.0.3" @@ -7835,6 +7893,16 @@ __metadata: languageName: node linkType: hard +"dom-helpers@npm:^5.0.1": + version: 5.2.1 + resolution: "dom-helpers@npm:5.2.1" + dependencies: + "@babel/runtime": "npm:^7.8.7" + csstype: "npm:^3.0.2" + checksum: 10c0/f735074d66dd759b36b158fa26e9d00c9388ee0e8c9b16af941c38f014a37fc80782de83afefd621681b19ac0501034b4f1c4a3bff5caa1b8667f0212b5e124c + languageName: node + linkType: hard + "dom-serializer@npm:^1.0.1": version: 1.4.1 resolution: "dom-serializer@npm:1.4.1" @@ -9149,6 +9217,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.6": + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" + peerDependenciesMeta: + debug: + optional: true + checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -9191,6 +9269,19 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.4": + version: 4.0.4 + resolution: "form-data@npm:4.0.4" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695 + languageName: node + linkType: hard + "framer-motion@npm:^12.23.7": version: 12.23.9 resolution: "framer-motion@npm:12.23.9" @@ -11633,6 +11724,7 @@ __metadata: "@mantine/core": "npm:8.1.2" "@mantine/form": "npm:^8.1.3" "@mantine/hooks": "npm:8.1.2" + "@mantine/notifications": "npm:^8.2.1" "@next/bundle-analyzer": "npm:^15.3.3" "@reduxjs/toolkit": "npm:^2.8.2" "@storybook/nextjs": "npm:^8.6.8" @@ -11648,6 +11740,7 @@ __metadata: "@types/react": "npm:19.1.8" "@types/react-redux": "npm:^7.1.34" "@types/redux-persist": "npm:^4.3.1" + axios: "npm:^1.11.0" babel-loader: "npm:^10.0.0" classnames: "npm:^2.5.1" eslint: "npm:^9.29.0" @@ -11934,7 +12027,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.27, mime-types@npm:^2.1.31": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -13243,7 +13336,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -13254,6 +13347,13 @@ __metadata: languageName: node linkType: hard +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b + languageName: node + linkType: hard + "public-encrypt@npm:^4.0.0": version: 4.0.3 resolution: "public-encrypt@npm:4.0.3" @@ -13591,6 +13691,21 @@ __metadata: languageName: node linkType: hard +"react-transition-group@npm:4.4.5": + version: 4.4.5 + resolution: "react-transition-group@npm:4.4.5" + dependencies: + "@babel/runtime": "npm:^7.5.5" + dom-helpers: "npm:^5.0.1" + loose-envify: "npm:^1.4.0" + prop-types: "npm:^15.6.2" + peerDependencies: + react: ">=16.6.0" + react-dom: ">=16.6.0" + checksum: 10c0/2ba754ba748faefa15f87c96dfa700d5525054a0141de8c75763aae6734af0740e77e11261a1e8f4ffc08fd9ab78510122e05c21c2d79066c38bb6861a886c82 + languageName: node + linkType: hard + "react@npm:19.1.0": version: 19.1.0 resolution: "react@npm:19.1.0"