Merge pull request 'feat/auth' (#9) from feat/auth into dev

Reviewed-on: #9
This commit is contained in:
adilovcode 2025-05-01 20:56:48 +05:00
commit 885d17a15c
17 changed files with 379 additions and 136 deletions

View File

@ -23,8 +23,10 @@
"@radix-ui/react-toast": "^1.2.11", "@radix-ui/react-toast": "^1.2.11",
"@reduxjs/toolkit": "^2.7.0", "@reduxjs/toolkit": "^2.7.0",
"aos": "^2.3.4", "aos": "^2.3.4",
"axios": "^1.9.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookies-next": "^5.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-autoplay": "^8.6.0", "embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",

95
pnpm-lock.yaml generated
View File

@ -47,12 +47,18 @@ importers:
aos: aos:
specifier: ^2.3.4 specifier: ^2.3.4
version: 2.3.4 version: 2.3.4
axios:
specifier: ^1.9.0
version: 1.9.0
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
cookies-next:
specifier: ^5.1.0
version: 5.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
date-fns: date-fns:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
@ -1306,6 +1312,9 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1314,6 +1323,9 @@ packages:
resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
engines: {node: '>=4'} engines: {node: '>=4'}
axios@1.9.0:
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
axobject-query@4.1.0: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1385,9 +1397,23 @@ packages:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'} engines: {node: '>=12.5.0'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
cookies-next@5.1.0:
resolution: {integrity: sha512-9Ekne+q8hfziJtnT9c1yDUBqT0eDMGgPrfPl4bpR3xwQHLTd/8gbSf6+IEkP/pjGsDZt1TGbC6emYmFYRbIXwQ==}
peerDependencies:
next: '>=15.0.0'
react: '>= 16.8.0'
cross-spawn@7.0.6: cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1441,6 +1467,10 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.0.3: detect-libc@2.0.3:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1711,10 +1741,23 @@ packages:
flatted@3.3.3: flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
for-each@0.3.5: for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
form-data@4.0.2:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@ -2090,6 +2133,14 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
minimatch@10.0.1: minimatch@10.0.1:
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@ -2307,6 +2358,9 @@ packages:
prop-types@15.8.1: prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3792,12 +3846,22 @@ snapshots:
async-function@1.0.0: {} async-function@1.0.0: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
axe-core@4.10.3: {} axe-core@4.10.3: {}
axios@1.9.0:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
@ -3873,8 +3937,20 @@ snapshots:
color-string: 1.9.1 color-string: 1.9.1
optional: true optional: true
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
concat-map@0.0.1: {} concat-map@0.0.1: {}
cookie@1.0.2: {}
cookies-next@5.1.0(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
dependencies:
cookie: 1.0.2
next: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
cross-spawn@7.0.6: cross-spawn@7.0.6:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1
@ -3927,6 +4003,8 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
delayed-stream@1.0.0: {}
detect-libc@2.0.3: {} detect-libc@2.0.3: {}
detect-node-es@1.1.0: {} detect-node-es@1.1.0: {}
@ -4352,10 +4430,19 @@ snapshots:
flatted@3.3.3: {} flatted@3.3.3: {}
follow-redirects@1.15.9: {}
for-each@0.3.5: for-each@0.3.5:
dependencies: dependencies:
is-callable: 1.2.7 is-callable: 1.2.7
form-data@4.0.2:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
function-bind@1.1.2: {} function-bind@1.1.2: {}
function.prototype.name@1.1.8: function.prototype.name@1.1.8:
@ -4708,6 +4795,12 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
minimatch@10.0.1: minimatch@10.0.1:
dependencies: dependencies:
brace-expansion: 2.0.1 brace-expansion: 2.0.1
@ -4877,6 +4970,8 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
react-is: 16.13.1 react-is: 16.13.1
proxy-from-env@1.1.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}

View File

@ -0,0 +1,10 @@
import { Axios } from 'axios';
const oriyoClient = new Axios({
baseURL: process.env.ORIOYO_API_ENDPOINT || '',
headers: {
'Content-type': 'application/json',
},
});
export default oriyoClient;

View File

@ -1,55 +1,44 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { LoginData } from '@/entities/auth/model/types'; import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
export const GET = async (req: NextRequest) => { import { loginFormSchema } from '@/entities/auth/model/validation/login-form.schema';
if (req.method !== 'GET') {
return NextResponse.json(
{ error: 'Method is not supported' },
{ status: 405 },
);
}
const { searchParams } = req.nextUrl; import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
const phoneNumber = searchParams.get('phoneNumber'); const routeHandler = async (req: NextRequest) => {
const cardNumber = searchParams.get('cardNumber'); const body = await req.json();
const type = searchParams.get('type');
if (!phoneNumber || !cardNumber || !type) { const validatedBody = loginFormSchema
return NextResponse.json({ error: 'Bad request' }, { status: 400 }); .merge(z.object({ type: z.enum(['bonus', 'corporate']) }))
} .parse(body);
try { try {
const loginRes = await fetch( const oriyoResponse = await oriyoClient.get('/client/login', {
`https://test.oriyo.tj/api/client/login?type=${type}&phone=${phoneNumber}&uid=${cardNumber}`, params: {
{ type: validatedBody.type,
method: 'GET', phone: validatedBody.phoneNumber,
uid: validatedBody.cardNumber,
}, },
); });
if (!loginRes.ok) { const { token, card_id } = JSON.parse(oriyoResponse.data);
return NextResponse.json(
{ error: 'Error during login' },
{ status: 400 },
);
}
const data = (await loginRes.json()) as LoginData;
const token = data.token;
if (!token) { if (!token) {
return NextResponse.json({ error: 'No auth token' }, { status: 401 }); return NextResponse.json({ error: 'Credentials error' }, { status: 401 });
} }
const response = NextResponse.json({ success: true }); const response = NextResponse.json({ success: true });
response.cookies.set('token', token, { response.cookies.set(
httpOnly: true, `${validatedBody.type}__token`,
path: '/', JSON.stringify({ token, card_id }),
maxAge: 2 * 60 * 60, {
secure: process.env.NODE_ENV === 'production', path: '/',
}); maxAge: 2 * 60 * 60,
},
);
return response; return response;
} catch (error) { } catch (error) {
@ -57,3 +46,5 @@ export const GET = async (req: NextRequest) => {
return NextResponse.json({ error: 'Server error' }, { status: 500 }); return NextResponse.json({ error: 'Server error' }, { status: 500 });
} }
}; };
export const POST = validationErrorHandler(routeHandler);

View File

@ -0,0 +1,31 @@
import { NextRequest, NextResponse } from 'next/server';
import oriyoClient from '@/app/api-utlities/utilities/oriyo.client';
import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
export const routeHandler = async (req: NextRequest) => {
const bonusTokenData = req.cookies.get('bonus__token');
if (!bonusTokenData) {
return NextResponse.json(
{ error: 'User does not have access' },
{ status: 401 },
);
}
const { card_id, token } = JSON.parse(bonusTokenData.value);
const oriyoResponse = await oriyoClient.get('/client/info', {
params: {
card_id,
token,
},
});
return new Response(oriyoResponse.data, {
headers: { 'Content-Type': 'application/json' },
});
};
export const GET = validationErrorHandler(routeHandler);

View File

@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';
import { ZodError } from 'zod';
export const validationErrorHandler =
(handler: Function) => async (req: NextRequest, res: NextResponse) => {
try {
return await handler(req, res);
} catch (error) {
if (error instanceof ZodError)
return NextResponse.json({ message: error.format() }, { status: 400 });
console.error(error);
return NextResponse.json(
{ message: 'Server died for some reason' },
{ status: 500 },
);
}
};

View File

@ -7,7 +7,9 @@ import {
import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request'; import { mainPageRequest } from '@/app/api-utlities/requests/main-page.request';
import { requestTaylor } from '@/app/api-utlities/utilities/taylor.client'; import { requestTaylor } from '@/app/api-utlities/utilities/taylor.client';
export async function GET(request: Request) { import { validationErrorHandler } from '../../middlewares/error-handler.middleware';
const routeHandler = async (request: Request) => {
const response = await requestTaylor(mainPageRequest); const response = await requestTaylor(mainPageRequest);
return new Response( return new Response(
@ -21,4 +23,6 @@ export async function GET(request: Request) {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}, },
); );
} };
export const GET = validationErrorHandler(routeHandler);

View File

@ -1,24 +1,23 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { baseAPI } from '@/shared/api/base-api';
import { LoginParams, LoginResponse } from '../model/contracts/login.contract'; import { LoginParams, LoginResponse } from '../model/contracts/login.contract';
export const loginAPI = createApi({ export const authenticationApi = baseAPI.injectEndpoints({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }), endpoints: (builder) => ({
endpoints: (build) => ({ login: builder.query<LoginResponse, LoginParams>({
login: build.query<LoginResponse, LoginParams>({
query: (data) => { query: (data) => {
const params = new URLSearchParams({
type: data.type,
phoneNumber: data.phoneNumber,
cardNumber: data.cardNumber,
}).toString();
return { return {
url: `/auth/login?${params}`, method: 'POST',
body: {
type: data.type,
phoneNumber: data.phoneNumber,
cardNumber: data.cardNumber,
},
url: '/auth/login',
}; };
}, },
}), }),
}), }),
}); });
export const { useLazyLoginQuery } = loginAPI; export const { useLazyLoginQuery } = authenticationApi;

View File

@ -1,4 +1,4 @@
import { LoginFormData } from '@/features/auth/login-form/model/login-form.schema'; import { LoginFormData } from '@/entities/auth/model/validation/login-form.schema';
export interface LoginResponse { export interface LoginResponse {
success: boolean; success: boolean;

View File

@ -0,0 +1,17 @@
import { baseAPI } from '@/shared/api/base-api';
import { ClientInfo } from '../model/types/bonus-client-info.type';
export const bonusApi = baseAPI.injectEndpoints({
endpoints: (builder) => ({
fetchMyBonusInfo: builder.query<ClientInfo, any>({
query: () => {
return {
url: '/bonus/info',
};
},
}),
}),
});
export const { useFetchMyBonusInfoQuery } = bonusApi;

View File

@ -0,0 +1,8 @@
export interface ClientInfo {
card_id: number;
fullname: string;
cardno: string;
reg_date: string;
end_date: string;
bonuses: string;
}

View File

@ -19,7 +19,10 @@ import {
} from '@/shared/shadcn-ui/form'; } from '@/shared/shadcn-ui/form';
import { Input } from '@/shared/shadcn-ui/input'; import { Input } from '@/shared/shadcn-ui/input';
import { LoginFormData, loginFormSchema } from '../model/login-form.schema'; import {
LoginFormData,
loginFormSchema,
} from '../../../../entities/auth/model/validation/login-form.schema';
interface LoginFormProps { interface LoginFormProps {
type: 'bonus' | 'corporate'; type: 'bonus' | 'corporate';
@ -40,16 +43,12 @@ export const LoginForm = ({ type }: LoginFormProps) => {
}); });
const onSubmit = async (data: LoginFormData) => { const onSubmit = async (data: LoginFormData) => {
try { await login({ ...data, type }).unwrap();
await login({ ...data, type }).unwrap();
toast.success('Logged in successfully!'); toast.success('Logged in successfully!');
router.push( router.push(
type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard', type === 'bonus' ? '/customer-dashboard' : '/corporate-dashboard',
); );
} catch (error) {
toast.error('An error occured during login');
}
}; };
return ( return (

View File

@ -2,6 +2,8 @@
import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react'; import { ArrowUpRight, Clock, CreditCard, LogOut, User } from 'lucide-react';
import { useFetchMyBonusInfoQuery } from '@/entities/bonus/api/bonus.api';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button'; import { Button } from '@/shared/shadcn-ui/button';
import { import {
@ -14,20 +16,11 @@ import {
import { TransactionsTable } from '@/widgets/transactions-table'; import { TransactionsTable } from '@/widgets/transactions-table';
// Sample customer data
const customerData = {
firstName: 'Алишер',
lastName: 'Рахмонов',
passportNumber: 'A12345678',
bonusPoints: 1250,
cardNumber: '5678-9012-3456-7890',
expiryDate: '12/2025',
registrationDate: '15.06.2020',
};
export function CustomerDashboard() { export function CustomerDashboard() {
const { t } = useTextController(); const { t } = useTextController();
const { data, isLoading } = useFetchMyBonusInfoQuery({});
return ( return (
<div className='flex min-h-screen flex-col'> <div className='flex min-h-screen flex-col'>
<main className='flex-1 py-10'> <main className='flex-1 py-10'>
@ -41,35 +34,44 @@ export function CustomerDashboard() {
</div> </div>
<div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'> <div className='mb-10 grid gap-3 md:grid-cols-3 md:gap-6'>
{/* Bonus Card */}
<Card className='bg-gradient-to-br from-red-600 to-red-800 text-white'> <Card className='bg-gradient-to-br from-red-600 to-red-800 text-white'>
<CardHeader> {!data || isLoading ? (
<CardTitle className='flex items-center gap-2'> // TODO: Bunyod please add loader here
<CreditCard className='h-5 w-5' /> <>Loader here</>
{t('customer.bonusCard.title')} ) : (
</CardTitle> <>
<CardDescription className='text-white/80'> <CardHeader>
{t('customer.bonusCard.description')} <CardTitle className='flex items-center gap-2'>
</CardDescription> <CreditCard className='h-5 w-5' />
</CardHeader> {t('customer.bonusCard.title')}
<CardContent> </CardTitle>
<div className='text-center'> <CardDescription className='text-white/80'>
<p className='mb-2 text-4xl font-bold'> {t('customer.bonusCard.description')}
{customerData.bonusPoints} </CardDescription>
</p> </CardHeader>
<p className='text-white/80'> <CardContent>
{t('customer.bonusCard.points')} <div className='text-center'>
</p> <p className='mb-2 text-4xl font-bold'>{data.bonuses}</p>
</div> <p className='text-white/80'>
<div className='mt-6 flex items-center justify-between'> {t('customer.bonusCard.points')}
<div className='flex items-center gap-1 text-sm text-white/80'> </p>
<Clock className='h-4 w-4' /> </div>
<span>{t('customer.bonusCard.validUntil')}</span> <div className='mt-6 flex items-center justify-between'>
</div> <div className='flex items-center gap-1 text-sm text-white/80'>
<ArrowUpRight className='h-5 w-5 text-white/60' /> <Clock className='h-4 w-4' />
</div> <span>
</CardContent> {t('customer.bonusCard.validUntil')}{' '}
{new Date(data.end_date).toLocaleDateString('en-GB')}
</span>
</div>
<ArrowUpRight className='h-5 w-5 text-white/60' />
</div>
</CardContent>
</>
)}
</Card> </Card>
{/* Bonus Card */}
{/* Customer Card */} {/* Customer Card */}
<Card className='md:col-span-2'> <Card className='md:col-span-2'>
<CardHeader className='pb-2'> <CardHeader className='pb-2'>
@ -79,40 +81,37 @@ export function CustomerDashboard() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='grid gap-6 md:grid-cols-2'> {!data || isLoading ? (
<div> // TODO: Bunyod please add loader here
<div className='mb-4 space-y-1'> <>Loader here</>
<p className='text-sm text-gray-500'> ) : (
{t('customer.infoCard.regDateLabel')} <div className='grid gap-6 md:grid-cols-2'>
</p> <div>
<p className='font-medium'> <div className='mb-4 space-y-1'>
{customerData.firstName} {customerData.lastName} <p className='text-sm text-gray-500'>
</p> {t('customer.infoCard.regDateLabel')}
</p>
<p className='font-medium'>{data.fullname}</p>
</div>
<div className='space-y-1'>
<p className='text-sm text-gray-500'>
{t('customer.infoCard.regDateLabel')}
</p>
<p className='font-medium'>
{new Date(data.reg_date).toLocaleDateString('en-GB')}
</p>
</div>
</div> </div>
<div className='space-y-1'> <div>
<p className='text-sm text-gray-500'> <div className='mb-4 space-y-1'>
{t('customer.infoCard.regDateLabel')} <p className='text-sm text-gray-500'>
</p> {t('customer.infoCard.cardNumberLabel')}
<p className='font-medium'> </p>
{customerData.registrationDate} <p className='font-medium'>{data.cardno}</p>
</p> </div>
</div> </div>
</div> </div>
<div> )}
<div className='mb-4 space-y-1'>
<p className='text-sm text-gray-500'>
{t('customer.infoCard.cardNumberLabel')}
</p>
<p className='font-medium'>{customerData.cardNumber}</p>
</div>
<div className='mb-4 space-y-1'>
<p className='text-sm text-gray-500'>
{t('customer.infoCard.expiryDateLabel')}
</p>
<p className='font-medium'>{customerData.expiryDate}</p>
</div>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -1,11 +1,14 @@
'use client'; 'use client';
import { deleteCookie, getCookie } from 'cookies-next';
import { Building2, Fuel, User } from 'lucide-react'; import { Building2, Fuel, User } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { LoginForm } from '@/features/auth/login-form'; import { LoginForm } from '@/features/auth/login-form';
import { useTextController } from '@/shared/language/hooks/use-text-controller'; import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { Button } from '@/shared/shadcn-ui/button';
import { import {
Card, Card,
CardContent, CardContent,
@ -39,6 +42,13 @@ const tabs = [
export default function LoginPage() { export default function LoginPage() {
const { t } = useTextController(); const { t } = useTextController();
const router = useRouter();
const searchParams = useSearchParams();
const defaultTab = searchParams.get('tab') || 'bonus';
const handleTabChange = (tabType: string) => {
router.push(`?tab=${tabType}`, undefined, { shallow: true });
};
return ( return (
<div className='flex min-h-screen flex-col items-center justify-center'> <div className='flex min-h-screen flex-col items-center justify-center'>
@ -55,7 +65,12 @@ export default function LoginPage() {
</div> </div>
<div className='mx-auto max-w-lg'> <div className='mx-auto max-w-lg'>
<Tabs defaultValue='bonus' className='w-full'> <Tabs
defaultValue={defaultTab}
value={defaultTab}
onValueChange={handleTabChange}
className='w-full'
>
<TabsList className='mb-8 flex h-fit w-full flex-col sm:flex-row'> <TabsList className='mb-8 flex h-fit w-full flex-col sm:flex-row'>
{tabs.map((tab) => { {tabs.map((tab) => {
return ( return (
@ -71,6 +86,45 @@ export default function LoginPage() {
</TabsList> </TabsList>
{tabs.map((tab) => { {tabs.map((tab) => {
const tabCookieName = `${tab.type}__token`;
const authenticationCookie = getCookie(tabCookieName);
if (authenticationCookie) {
return (
<TabsContent key={tab.label} value={tab.type}>
<Card>
<CardHeader>
<CardTitle>{t(tab.title)}</CardTitle>
</CardHeader>
<CardContent className='flex justify-center gap-2 space-y-4'>
<Link
href={
tab.type === 'bonus'
? '/customer-dashboard'
: '/corporate-dashboard'
}
>
<Button className='flex items-center'>
Открыть
</Button>
</Link>
<Button
variant='outline'
className='flex items-center gap-2'
onClick={() => {
deleteCookie(tabCookieName);
window.location.reload();
}}
>
Выйти
</Button>
</CardContent>
</Card>
</TabsContent>
);
}
return ( return (
<TabsContent key={tab.label} value={tab.type}> <TabsContent key={tab.label} value={tab.type}>
<Card> <Card>

View File

@ -1,4 +1,5 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { toast } from 'sonner';
const baseQuery = fetchBaseQuery({ const baseQuery = fetchBaseQuery({
baseUrl: process.env.NEXT_PUBLIC_API_URL, baseUrl: process.env.NEXT_PUBLIC_API_URL,
@ -15,7 +16,25 @@ export const TAGS = {
export const baseAPI = createApi({ export const baseAPI = createApi({
reducerPath: 'baseAPI', reducerPath: 'baseAPI',
baseQuery, baseQuery: async (args, api, extraOptions) => {
const result = await baseQuery(args, api, extraOptions);
if (result.error) {
switch (result.error.status) {
case 401:
toast.error('Login credentials error');
break;
case 500:
toast.error('Server error, please try later');
break;
default:
break;
}
}
return result;
},
tagTypes: Object.values(TAGS), tagTypes: Object.values(TAGS),
endpoints: () => ({}), endpoints: () => ({}),
}); });

View File

@ -1,10 +1,7 @@
import { combineReducers } from '@reduxjs/toolkit'; import { combineReducers } from '@reduxjs/toolkit';
import { loginAPI } from '@/entities/auth/api/login.api';
import { baseAPI } from '@/shared/api/base-api'; import { baseAPI } from '@/shared/api/base-api';
export const rootReducer = combineReducers({ export const rootReducer = combineReducers({
[baseAPI.reducerPath]: baseAPI.reducer, [baseAPI.reducerPath]: baseAPI.reducer,
[loginAPI.reducerPath]: loginAPI.reducer,
}); });