Compare commits
25 Commits
a4d0d8b746
...
13c3399a6e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
13c3399a6e | ||
![]() |
80a2636f90 | ||
![]() |
9c4637528c | ||
![]() |
661de6e8d7 | ||
![]() |
4a0ae590b0 | ||
![]() |
53162ed333 | ||
![]() |
79c292d943 | ||
![]() |
10a6375347 | ||
![]() |
f950617372 | ||
![]() |
99fafda624 | ||
![]() |
3ed3f2fecb | ||
![]() |
b80939c78d | ||
![]() |
779984f65c | ||
![]() |
8bb6c9e992 | ||
![]() |
60ecca48b4 | ||
![]() |
7a6014a115 | ||
![]() |
37ace98e60 | ||
![]() |
44091e0432 | ||
![]() |
279496ea38 | ||
![]() |
a450747670 | ||
![]() |
215476cfaa | ||
![]() |
c93b31b2f6 | ||
![]() |
4cf93f431e | ||
![]() |
2b09261f0a | ||
![]() |
1325a361dc |
@@ -18,3 +18,4 @@ T.B.A.
|
|||||||
|
|
||||||
develop/requirements/REQ0188
|
develop/requirements/REQ0188
|
||||||
develop/frontend/party-user/trunk
|
develop/frontend/party-user/trunk
|
||||||
|
develop/frontend/party-user-auth/trunk
|
||||||
|
20
01_Requirements/REQ0189/index.md
Normal file
20
01_Requirements/REQ0189/index.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
tags: mobile, payment
|
||||||
|
---
|
||||||
|
|
||||||
|
# REQ0189 party payment flow
|
||||||
|
|
||||||
|
frontend page to handle party-user pay join event
|
||||||
|
|
||||||
|
edit page T.B.A.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
## sources
|
||||||
|
|
||||||
|
T.B.A.
|
||||||
|
|
||||||
|
## branch
|
||||||
|
|
||||||
|
develop/requirements/REQ0189
|
||||||
|
develop/mobile/DummyPayPage/trunk
|
@@ -1,3 +1,5 @@
|
|||||||
|
// src/cms_backend/eslint.config.mjs
|
||||||
|
//
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
import eslintJs from '@eslint/js';
|
import eslintJs from '@eslint/js';
|
||||||
import eslintTs from 'typescript-eslint';
|
import eslintTs from 'typescript-eslint';
|
||||||
@@ -69,10 +71,7 @@ const importRules = () => ({
|
|||||||
*/
|
*/
|
||||||
const unusedImportsRules = () => ({
|
const unusedImportsRules = () => ({
|
||||||
'unused-imports/no-unused-imports': 1,
|
'unused-imports/no-unused-imports': 1,
|
||||||
'unused-imports/no-unused-vars': [
|
'unused-imports/no-unused-vars': [0, { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }],
|
||||||
0,
|
|
||||||
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,15 +92,17 @@ const sortImportsRules = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'perfectionist/sort-named-imports': [1, { type: 'line-length', order: 'asc' }],
|
'perfectionist/sort-named-imports': [1, { type: 'line-length', order: 'asc' }],
|
||||||
'perfectionist/sort-named-exports': [1, { type: 'line-length', order: 'asc' }],
|
|
||||||
'perfectionist/sort-exports': [
|
// disable sorting of export, i manage the export ordering
|
||||||
1,
|
// 'perfectionist/sort-named-exports': [1, { type: 'line-length', order: 'asc' }],
|
||||||
{
|
// 'perfectionist/sort-exports': [
|
||||||
order: 'asc',
|
// 1,
|
||||||
type: 'line-length',
|
// {
|
||||||
groupKind: 'values-first',
|
// order: 'asc',
|
||||||
},
|
// type: 'line-length',
|
||||||
],
|
// groupKind: 'values-first',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
'perfectionist/sort-imports': [
|
'perfectionist/sort-imports': [
|
||||||
2,
|
2,
|
||||||
{
|
{
|
||||||
|
@@ -16,11 +16,11 @@ model Helloworld {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String @map("user_id")
|
userId String @map("user_id")
|
||||||
type String
|
type String
|
||||||
provider String
|
provider String
|
||||||
providerAccountId String @map("provider_account_id")
|
providerAccountId String @map("provider_account_id")
|
||||||
refresh_token String?
|
refresh_token String?
|
||||||
access_token String?
|
access_token String?
|
||||||
expires_at Int?
|
expires_at Int?
|
||||||
@@ -30,10 +30,9 @@ model Account {
|
|||||||
session_state String?
|
session_state String?
|
||||||
oauth_token_secret String?
|
oauth_token_secret String?
|
||||||
oauth_token String?
|
oauth_token String?
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
PartyUser PartyUser? @relation(fields: [partyUserId], references: [id])
|
||||||
PartyUser PartyUser? @relation(fields: [partyUserId], references: [id])
|
partyUserId String?
|
||||||
partyUserId String?
|
|
||||||
|
|
||||||
@@unique([provider, providerAccountId])
|
@@unique([provider, providerAccountId])
|
||||||
}
|
}
|
||||||
@@ -1290,4 +1289,7 @@ model PartyUser {
|
|||||||
city String @default("")
|
city String @default("")
|
||||||
address String @default("")
|
address String @default("")
|
||||||
zipCode String @default("")
|
zipCode String @default("")
|
||||||
|
//
|
||||||
|
rank String @default("user")
|
||||||
|
sex String @default("")
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Party User seed data generator
|
||||||
|
* Creates initial user accounts for development and testing
|
||||||
|
* Includes:
|
||||||
|
* - Fixed demo accounts (alice, demo)
|
||||||
|
* - Randomly generated test accounts with CJK locale data
|
||||||
|
*/
|
||||||
import { faker as enFaker } from '@faker-js/faker/locale/en_US';
|
import { faker as enFaker } from '@faker-js/faker/locale/en_US';
|
||||||
import { faker as zhFaker } from '@faker-js/faker/locale/zh_CN';
|
import { faker as zhFaker } from '@faker-js/faker/locale/zh_CN';
|
||||||
import { faker as jaFaker } from '@faker-js/faker/locale/ja';
|
import { faker as jaFaker } from '@faker-js/faker/locale/ja';
|
||||||
@@ -51,6 +58,7 @@ async function partyUser() {
|
|||||||
status: STATUS[0],
|
status: STATUS[0],
|
||||||
role: ROLE[0],
|
role: ROLE[0],
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
|
sex: 'F',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,15 +67,19 @@ async function partyUser() {
|
|||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
email: 'demo@minimals.cc',
|
email: 'demo@minimals.cc',
|
||||||
name: 'Demo',
|
|
||||||
username: 'pudemo',
|
|
||||||
password: '@2Minimal',
|
password: '@2Minimal',
|
||||||
|
//
|
||||||
|
username: 'pudemo',
|
||||||
|
name: 'Demo',
|
||||||
emailVerified: new Date(),
|
emailVerified: new Date(),
|
||||||
phoneNumber: '+85291234568',
|
phoneNumber: '+85291234568',
|
||||||
company: 'helloworld company',
|
company: 'helloworld company',
|
||||||
status: STATUS[1],
|
status: STATUS[1],
|
||||||
role: ROLE[1],
|
role: ROLE[1],
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
|
avatarUrl: 'https://images.unsplash.com/photo-1619970096024-c7b438a3b82a',
|
||||||
|
rank: 'user',
|
||||||
|
sex: 'M',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,6 +114,7 @@ async function partyUser() {
|
|||||||
role: ROLE[Math.floor(Math.random() * ROLE.length)],
|
role: ROLE[Math.floor(Math.random() * ROLE.length)],
|
||||||
status: STATUS[Math.floor(Math.random() * STATUS.length)],
|
status: STATUS[Math.floor(Math.random() * STATUS.length)],
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
|
sex: i % 2 ? 'F' : 'M',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,13 @@
|
|||||||
import type { User } from '@prisma/client';
|
// src/app/api/auth/me/route.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// - T.B.A.
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// - T.B.A.
|
||||||
|
//
|
||||||
|
|
||||||
|
import type { PartyUser, User } from '@prisma/client';
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
import { headers } from 'next/headers';
|
import { headers } from 'next/headers';
|
||||||
@@ -11,9 +20,11 @@ import { getUserById } from 'src/app/services/user.service';
|
|||||||
import { createAccessLog } from 'src/app/services/access-log.service';
|
import { createAccessLog } from 'src/app/services/access-log.service';
|
||||||
|
|
||||||
import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
|
import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
|
||||||
|
import { getPartyUserById } from 'src/app/services/party-user.service';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// NOTE: keep this comment to let prisma running on nextjs
|
||||||
// export const runtime = 'edge';
|
// export const runtime = 'edge';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +40,7 @@ const INVALID_AUTH_TOKEN = 'Invalid authorization token';
|
|||||||
const USER_ID_NOT_FOUND = 'userId not found';
|
const USER_ID_NOT_FOUND = 'userId not found';
|
||||||
const USER_TOKEN_OK = 'user token check ok';
|
const USER_TOKEN_OK = 'user token check ok';
|
||||||
const AUTHORIZATION_TOKEN_MISSING_OR_INVALID = 'Authorization token missing or invalid';
|
const AUTHORIZATION_TOKEN_MISSING_OR_INVALID = 'Authorization token missing or invalid';
|
||||||
|
const USER_BANNED = 'user banned';
|
||||||
|
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||||
@@ -43,12 +55,17 @@ export async function GET(req: NextRequest) {
|
|||||||
|
|
||||||
const accessToken = `${authorization}`.split(' ')[1];
|
const accessToken = `${authorization}`.split(' ')[1];
|
||||||
const data = await verify(accessToken, JWT_SECRET);
|
const data = await verify(accessToken, JWT_SECRET);
|
||||||
console.log(data.userId);
|
|
||||||
|
|
||||||
if (data.userId) {
|
if (data.userId) {
|
||||||
// TODO: remove me
|
const { userId } = data;
|
||||||
// const currentUser = _users.find((user) => user.id === data.userId);
|
|
||||||
const currentUser: User | null = await getUserById(data.userId);
|
let currentUser: User | PartyUser | null = null;
|
||||||
|
|
||||||
|
currentUser = await getPartyUserById(userId);
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
currentUser = await getUserById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
createAccessLog('', USER_TOKEN_CHECK_FAILED, debug);
|
createAccessLog('', USER_TOKEN_CHECK_FAILED, debug);
|
||||||
|
@@ -1,11 +1,26 @@
|
|||||||
###
|
###
|
||||||
|
|
||||||
# username and password ok
|
# username and password ok
|
||||||
GET http://localhost:7272/api/auth/me
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWJnbnUyengwMDBjaHEzaGZ3dmtjejlvIiwiaWF0IjoxNzQ4OTY0ODkyLCJleHAiOjE3NTAxNzQ0OTJ9.lo04laCxtm0IVeYaETEV3hXKyDmXPEn7SyWtY2VR4dI
|
|
||||||
|
|
||||||
|
GET http://localhost:7272/api/auth/me
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWMwdWo4aXgwMDBqM2Y1eWhxc29xMW9wIiwiaWF0IjoxNzUwMjE5NTYyLCJleHAiOjE3NTE0MjkxNjJ9.8gKM2oMquccM_HDEfBAgtapCGf3M1eIp6SZ_knx7d1g
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
|
# username and password ok
|
||||||
|
|
||||||
|
POST http://localhost:7272/api/auth/sign-in
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "demo@minimals.cc",
|
||||||
|
"password": "@2Minimal"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
# There is no user corresponding to the email address.
|
# There is no user corresponding to the email address.
|
||||||
|
|
||||||
POST http://localhost:7272/api/auth/sign-in
|
POST http://localhost:7272/api/auth/sign-in
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
|
|
||||||
@@ -15,7 +30,9 @@ content-type: application/json
|
|||||||
}
|
}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
# Wrong password
|
# Wrong password
|
||||||
|
|
||||||
POST http://localhost:7272/api/auth/sign-in
|
POST http://localhost:7272/api/auth/sign-in
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
|
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
###
|
###
|
||||||
|
|
||||||
# username and password ok
|
# username and password ok
|
||||||
|
|
||||||
POST http://localhost:7272/api/auth/sign-in
|
POST http://localhost:7272/api/auth/sign-in
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
|
|
||||||
@@ -9,7 +11,9 @@ content-type: application/json
|
|||||||
}
|
}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
# There is no user corresponding to the email address.
|
# There is no user corresponding to the email address.
|
||||||
|
|
||||||
POST http://localhost:7272/api/auth/sign-in
|
POST http://localhost:7272/api/auth/sign-in
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
|
|
||||||
@@ -19,7 +23,9 @@ content-type: application/json
|
|||||||
}
|
}
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
# Wrong password
|
# Wrong password
|
||||||
|
|
||||||
POST http://localhost:7272/api/auth/sign-in
|
POST http://localhost:7272/api/auth/sign-in
|
||||||
content-type: application/json
|
content-type: application/json
|
||||||
|
|
||||||
|
21
03_source/cms_backend/src/app/api/event/helloworld/route.ts
Normal file
21
03_source/cms_backend/src/app/api/event/helloworld/route.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { logger } from 'src/utils/logger';
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { countTotalEvents } from 'src/app/services/eventItem.service';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* GET - Events, obsoleted
|
||||||
|
*************************************** */
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const numOfEvent = await countTotalEvents();
|
||||||
|
|
||||||
|
logger('[Event] list', numOfEvent);
|
||||||
|
|
||||||
|
return response({ numOfEvent }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('Event - Get list', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:7272/api/event/helloworld
|
22
03_source/cms_backend/src/app/api/event/numOfEvent/route.ts
Normal file
22
03_source/cms_backend/src/app/api/event/numOfEvent/route.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { logger } from 'src/utils/logger';
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
// src/app/api/event/list/route.ts
|
||||||
|
import { countTotalEvents, listEvents } from 'src/app/services/eventItem.service';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* GET - Events, obsoleted
|
||||||
|
*************************************** */
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const numOfEvents = await countTotalEvents();
|
||||||
|
|
||||||
|
// logger('[Event] list', numOfEvents.length);
|
||||||
|
|
||||||
|
return response({ numOfEvents }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('Event - Get list', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:7272/api/event/numOfEvent
|
@@ -0,0 +1,58 @@
|
|||||||
|
// src/app/api/product/createEvent/route.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// create product to db
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// T.B.A.
|
||||||
|
//
|
||||||
|
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { getEventItemById } from 'src/app/services/eventItem.service';
|
||||||
|
import { getPartyUserByEmail } from 'src/app/services/party-user.service';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
***************************************
|
||||||
|
* POST - Events
|
||||||
|
***************************************
|
||||||
|
*/
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
// logger('[Event] list', events.length);
|
||||||
|
const { data } = await req.json();
|
||||||
|
const { eventItemId, email } = data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eventItem = await getEventItemById(eventItemId);
|
||||||
|
const partyUser = await getPartyUserByEmail(email);
|
||||||
|
|
||||||
|
if (partyUser) {
|
||||||
|
if (eventItem && eventItem?.joinMembers) {
|
||||||
|
const foundJoined = _.find(eventItem.joinMembers, { email });
|
||||||
|
|
||||||
|
if (foundJoined) {
|
||||||
|
console.log('user joined event already, skipping');
|
||||||
|
} else {
|
||||||
|
const { sex } = partyUser;
|
||||||
|
eventItem.joinMembers.push({ email, sex });
|
||||||
|
|
||||||
|
await prisma?.eventItem.update({
|
||||||
|
where: { id: eventItem.id },
|
||||||
|
data: { joinMembers: JSON.parse(JSON.stringify(eventItem.joinMembers)) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response({ result: 'joined' }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ hello: 'world', data });
|
||||||
|
return handleError('Event - Create', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
# username and password ok
|
||||||
|
|
||||||
|
POST http://localhost:7272/api/event/partyUserJoinEvent
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"eventItemId": "e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01",
|
||||||
|
"email": "alice@prisma.io"
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
//
|
||||||
|
import { logger } from 'src/utils/logger';
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { countTotalEvents } from 'src/app/services/eventItem.service';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* GET - Events, obsoleted
|
||||||
|
*************************************** */
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const numOfEvent = await countTotalEvents();
|
||||||
|
|
||||||
|
// logger('[Event] list', numOfEvent);
|
||||||
|
|
||||||
|
return response({ numOfEvent }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('Event - Get list', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,3 @@
|
|||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:7272/api/party-event/numOfEvent
|
@@ -0,0 +1,76 @@
|
|||||||
|
// src/app/api/party-user-auth/me/route.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// - Handle authentication for party users via JWT
|
||||||
|
// - Verify and decode JWT tokens
|
||||||
|
// - Return current authenticated party user details
|
||||||
|
// - Log all access attempts (success/failure)
|
||||||
|
// - Validate token structure and user existence
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// - Must validate Bearer token format before processing
|
||||||
|
// - All errors must be logged via access-log service
|
||||||
|
// - User existence must be verified after token validation
|
||||||
|
// - Sensitive data must be filtered from responses
|
||||||
|
// - Mock JWT_SECRET should be replaced in production
|
||||||
|
// - Debug info should be included in error logs
|
||||||
|
//
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import type { PartyUser } from '@prisma/client';
|
||||||
|
|
||||||
|
import { headers } from 'next/headers';
|
||||||
|
|
||||||
|
import { verify } from 'src/utils/jwt';
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { JWT_SECRET } from 'src/_mock/_auth';
|
||||||
|
import { createAccessLog } from 'src/app/services/access-log.service';
|
||||||
|
import { getPartyUserById } from 'src/app/services/party-user.service';
|
||||||
|
|
||||||
|
import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const ERR_USER_TOKEN_CHECK_FAILED = 'user token check failed';
|
||||||
|
const ERR_INVALID_AUTH_TOKEN = 'Invalid authorization token';
|
||||||
|
const ERR_USER_ID_NOT_FOUND = 'userId not found';
|
||||||
|
const ERR_AUTHORIZATION_TOKEN_MISSING_OR_INVALID = 'Authorization token missing or invalid';
|
||||||
|
|
||||||
|
const USER_TOKEN_OK = 'user token check ok';
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headersList = headers();
|
||||||
|
const authorization = headersList.get('authorization');
|
||||||
|
|
||||||
|
if (!authorization || !authorization.startsWith('Bearer ')) {
|
||||||
|
return response({ message: ERR_AUTHORIZATION_TOKEN_MISSING_OR_INVALID }, STATUS.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = `${authorization}`.split(' ')[1];
|
||||||
|
const data = await verify(accessToken, JWT_SECRET);
|
||||||
|
|
||||||
|
if (data.userId) {
|
||||||
|
// TODO: remove me
|
||||||
|
// const currentUser = _users.find((user) => user.id === data.userId);
|
||||||
|
// const currentUser: User | null = await getUserById(data.userId);
|
||||||
|
const currentUser: PartyUser | null = await getPartyUserById(data.userId);
|
||||||
|
|
||||||
|
if (!currentUser) {
|
||||||
|
createAccessLog('', ERR_USER_TOKEN_CHECK_FAILED, debug);
|
||||||
|
|
||||||
|
return response({ message: ERR_INVALID_AUTH_TOKEN }, STATUS.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
createAccessLog(currentUser.id, USER_TOKEN_OK, debug);
|
||||||
|
|
||||||
|
return response({ user: currentUser }, STATUS.OK);
|
||||||
|
} else {
|
||||||
|
return response({ message: ERR_USER_ID_NOT_FOUND }, STATUS.ERROR);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('[Auth] - Me', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
// Test cases for Party User Authentication endpoints
|
||||||
|
// Tests both successful and error scenarios
|
||||||
|
// Environment: http://localhost:7272
|
||||||
|
// Expected responses:
|
||||||
|
// - 200 OK with user data for valid tokens
|
||||||
|
// - 401 Unauthorized for invalid/missing tokens
|
||||||
|
// - 400 Bad Request for invalid credentials
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# username and password ok
|
||||||
|
|
||||||
|
GET http://localhost:7272/api/party-user-auth/me
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWMwdWo4azIwMDBxM2Y1eTZlNXJzejRxIiwiaWF0IjoxNzUwMjEzOTkwLCJleHAiOjE3NTE0MjM1OTB9.MoKv3Nmrp_blE0jQ1rG1WyQ_TrJeF7kSe5xfHrF8b64
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# There is no user corresponding to the email address.
|
||||||
|
|
||||||
|
POST http://localhost:7272/api/party-user-auth/sign-in
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "party_user0@prisma.io",
|
||||||
|
"password": "Aa12345678"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# Wrong password
|
||||||
|
|
||||||
|
POST http://localhost:7272/api/party-user-auth/sign-in
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "demo@minimals.cc",
|
||||||
|
"password": "@2Min111imal"
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
export const ERR_USER_NOT_FOUND = 'There is no user corresponding to the email address.';
|
||||||
|
export const ERR_WRONG_PASSWORD = 'Wrong password';
|
||||||
|
export const LOG_USER_TRIED_LOGIN_WITH_EMAIL = `user tried login with email`;
|
||||||
|
export const LOG_USER_LOGGED_WITH_WRONG_PASSWORD = 'user logged with wrong password';
|
||||||
|
export const LOG_ACCESS_GRANTED = 'access granted';
|
||||||
|
export const LOG_ATTEMPTED_LOGIN_BUT_FAILED = 'attempted login but failed';
|
@@ -0,0 +1,10 @@
|
|||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens a Next.js request object into a plain object of headers
|
||||||
|
* @param {NextRequest} req - The Next.js request object
|
||||||
|
* @returns {Record<string, string>} An object containing all request headers
|
||||||
|
*/
|
||||||
|
export function flattenNextjsRequest(req: NextRequest) {
|
||||||
|
return Object.fromEntries(req.headers.entries());
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
// src/app/api/party-user-auth/sign-in/route1.ts
|
||||||
|
//
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { sign } from 'src/utils/jwt';
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { JWT_SECRET, JWT_EXPIRES_IN } from 'src/_mock/_auth';
|
||||||
|
import { createAccessLog } from 'src/app/services/access-log.service';
|
||||||
|
|
||||||
|
import prisma from '../../../lib/prisma';
|
||||||
|
import { flattenNextjsRequest } from './flattenNextjsRequest';
|
||||||
|
import {
|
||||||
|
LOG_USER_TRIED_LOGIN_WITH_EMAIL,
|
||||||
|
ERR_USER_NOT_FOUND,
|
||||||
|
LOG_USER_LOGGED_WITH_WRONG_PASSWORD,
|
||||||
|
ERR_WRONG_PASSWORD,
|
||||||
|
LOG_ACCESS_GRANTED,
|
||||||
|
LOG_ATTEMPTED_LOGIN_BUT_FAILED,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
/**
|
||||||
|
* This API is used for demo purpose only
|
||||||
|
* You should use a real database
|
||||||
|
* You should hash the password before saving to database
|
||||||
|
* You should not save the password in the database
|
||||||
|
* You should not expose the JWT_SECRET in the client side
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { email, password } = await req.json();
|
||||||
|
|
||||||
|
const currentUser = await prisma.partyUser.findFirst({ where: { email } });
|
||||||
|
if (!currentUser) {
|
||||||
|
await createAccessLog('', LOG_USER_TRIED_LOGIN_WITH_EMAIL, { email, debug });
|
||||||
|
return response({ message: ERR_USER_NOT_FOUND }, STATUS.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser?.password !== password) {
|
||||||
|
await createAccessLog(currentUser.id, LOG_USER_LOGGED_WITH_WRONG_PASSWORD, { debug });
|
||||||
|
return response({ message: ERR_WRONG_PASSWORD }, STATUS.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = await sign({ userId: currentUser?.id }, JWT_SECRET, {
|
||||||
|
expiresIn: JWT_EXPIRES_IN,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createAccessLog(currentUser.id, LOG_ACCESS_GRANTED, { debug });
|
||||||
|
|
||||||
|
return response({ user: currentUser, accessToken }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
await createAccessLog('', LOG_ATTEMPTED_LOGIN_BUT_FAILED, { debug, error });
|
||||||
|
|
||||||
|
return handleError('Auth - Sign in', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
# REQ0188 frontend party-user
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:7272/api/auth/me
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWMwdWo4bGwwMDByM2Y1eXhob3JuMW1hIiwiaWF0IjoxNzUwMjE5NTgwLCJleHAiOjE3NTE0MjkxODB9.7BtuIKEvwDcHc5j9JYX0Eb1uB37kFH1Ksx4MTDTtEWQ
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# username and password ok
|
||||||
|
|
||||||
|
POST http://localhost:7272/api/party-user-auth/sign-in
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "party_user0@prisma.io",
|
||||||
|
"password": "Aa12345678"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# There is no user corresponding to the email address.
|
||||||
|
|
||||||
|
POST http://localhost:7272/api/party-user-auth/sign-in
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "demo@minimals1.cc",
|
||||||
|
"password": "@2Minimal"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# Wrong password
|
||||||
|
|
||||||
|
POST http://localhost:7272/api/party-user-auth/sign-in
|
||||||
|
content-type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "demo@minimals.cc",
|
||||||
|
"password": "@2Min111imal"
|
||||||
|
}
|
@@ -0,0 +1,58 @@
|
|||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
import { sign } from 'src/utils/jwt';
|
||||||
|
import { STATUS, response, handleError } from 'src/utils/response';
|
||||||
|
|
||||||
|
import { _users, JWT_SECRET, JWT_EXPIRES_IN } from 'src/_mock/_auth';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This API is used for demo purpose only
|
||||||
|
* You should use a real database
|
||||||
|
* You should hash the password before saving to database
|
||||||
|
* You should not save the password in the database
|
||||||
|
* You should not expose the JWT_SECRET in the client side
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { email, password, firstName, lastName } = await req.json();
|
||||||
|
|
||||||
|
const userExists = _users.find((user) => user.email === email);
|
||||||
|
|
||||||
|
if (userExists) {
|
||||||
|
return response({ message: 'There already exists an account with the given email address.' }, STATUS.CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = {
|
||||||
|
id: _users[0].id,
|
||||||
|
displayName: `${firstName} ${lastName}`,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
photoURL: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
country: '',
|
||||||
|
address: '',
|
||||||
|
state: '',
|
||||||
|
city: '',
|
||||||
|
zipCode: '',
|
||||||
|
about: '',
|
||||||
|
role: 'user',
|
||||||
|
isPublic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const accessToken = await sign({ userId: newUser.id }, JWT_SECRET, {
|
||||||
|
expiresIn: JWT_EXPIRES_IN,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push new user to database
|
||||||
|
_users.push(newUser);
|
||||||
|
|
||||||
|
return response({ user: newUser, accessToken }, STATUS.OK);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError('Auth - Sign up', error);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,28 +1,23 @@
|
|||||||
// src/app/services/AccessLog.service.ts
|
// src/app/services/access-log.service.ts
|
||||||
//
|
//
|
||||||
// PURPOSE:
|
// PURPOSE:
|
||||||
// Service for handling AccessLog records
|
// - Core service for audit logging and access tracking
|
||||||
|
// - Records all authentication attempts and system access
|
||||||
|
// - Provides query capabilities for audit trails
|
||||||
|
// - Integrates with Prisma ORM for database operations
|
||||||
//
|
//
|
||||||
// RULES:
|
// RULES:
|
||||||
// - All methods return Promises
|
// - All methods return Promises for async operations
|
||||||
// - Input validation should be done at controller level
|
// - Input validation must be done at controller level
|
||||||
// - Errors should be propagated to caller
|
// - Errors should be propagated to caller with context
|
||||||
|
// - Audit records should never be modified after creation
|
||||||
|
// - Sensitive data should be hashed before logging
|
||||||
|
// - Metadata should be stored as JSON for flexibility
|
||||||
|
|
||||||
import type { AccessLog } from '@prisma/client';
|
import type { AccessLog } from '@prisma/client';
|
||||||
|
|
||||||
import prisma from '../lib/prisma';
|
import prisma from '../lib/prisma';
|
||||||
|
|
||||||
// type CreateAccessLog = {
|
|
||||||
// userId?: string;
|
|
||||||
// message?: string;
|
|
||||||
// metadata?: Record<string, any>;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// type UpdateAccessLog = {
|
|
||||||
// status?: number;
|
|
||||||
// metadata?: object;
|
|
||||||
// };
|
|
||||||
|
|
||||||
async function listAccessLogs(): Promise<AccessLog[]> {
|
async function listAccessLogs(): Promise<AccessLog[]> {
|
||||||
return prisma.accessLog.findMany({
|
return prisma.accessLog.findMany({
|
||||||
orderBy: { timestamp: 'desc' },
|
orderBy: { timestamp: 'desc' },
|
||||||
|
@@ -47,6 +47,21 @@ async function getEvent(eventId: string): Promise<EventItem | null> {
|
|||||||
return prisma.eventItem.findFirst({ where: { id: eventId } });
|
return prisma.eventItem.findFirst({ where: { id: eventId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getEventItemById(eventId: string): Promise<EventItem | null> {
|
||||||
|
return prisma.eventItem.findFirst({ where: { id: eventId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function countTotalEvents(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const result = await prisma.eventItem.findMany();
|
||||||
|
console.log({ result });
|
||||||
|
return result.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// async function createNewEvent(createForm: CreateEvent) {
|
// async function createNewEvent(createForm: CreateEvent) {
|
||||||
// return prisma.event.create({ data: createForm });
|
// return prisma.event.create({ data: createForm });
|
||||||
// }
|
// }
|
||||||
@@ -68,4 +83,6 @@ export {
|
|||||||
// updateEvent,
|
// updateEvent,
|
||||||
// deleteEvent,
|
// deleteEvent,
|
||||||
// createNewEvent,
|
// createNewEvent,
|
||||||
|
getEventItemById,
|
||||||
|
countTotalEvents,
|
||||||
};
|
};
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
// src/app/services/user.service.ts
|
// src/app/services/party-user.service.ts
|
||||||
//
|
//
|
||||||
// PURPOSE:
|
// PURPOSE:
|
||||||
// - Handle User Record CRUD operations
|
// - Handle Party User Record CRUD operations
|
||||||
|
// - Manage party member data and permissions
|
||||||
|
// - Interface between controllers and database
|
||||||
//
|
//
|
||||||
// RULES:
|
// RULES:
|
||||||
// - Follow Prisma best practices for database operations
|
// - Follow Prisma best practices for database operations
|
||||||
// - Validate input data before processing
|
// - Validate all party user data before processing
|
||||||
|
// - Enforce party-specific business rules
|
||||||
|
// - Maintain audit trail for sensitive operations
|
||||||
//
|
//
|
||||||
|
|
||||||
import type { User, PartyUser } from '@prisma/client';
|
import type { PartyUser } from '@prisma/client';
|
||||||
|
|
||||||
import prisma from '../lib/prisma';
|
import prisma from '../lib/prisma';
|
||||||
|
|
||||||
@@ -41,8 +45,14 @@ async function getPartyUser(partyUserId: string): Promise<PartyUser | null> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserById(id: string): Promise<User | null> {
|
async function getPartyUserByEmail(email: string): Promise<PartyUser | null> {
|
||||||
return prisma.user.findFirst({ where: { id } });
|
return prisma.partyUser.findUnique({
|
||||||
|
where: { email },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPartyUserById(id: string): Promise<PartyUser | null> {
|
||||||
|
return prisma.partyUser.findFirst({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPartyUser(partyUserData: any): Promise<PartyUser> {
|
async function createPartyUser(partyUserData: any): Promise<PartyUser> {
|
||||||
@@ -63,13 +73,15 @@ async function deletePartyUser(partyUserId: string): Promise<PartyUser | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getUserById,
|
getPartyUserById,
|
||||||
getPartyUser,
|
getPartyUser,
|
||||||
listPartyUsers,
|
listPartyUsers,
|
||||||
createPartyUser,
|
createPartyUser,
|
||||||
updatePartyUser,
|
updatePartyUser,
|
||||||
deletePartyUser,
|
deletePartyUser,
|
||||||
//
|
//
|
||||||
|
getPartyUserByEmail,
|
||||||
|
//
|
||||||
type CreateUser,
|
type CreateUser,
|
||||||
type UpdateUser,
|
type UpdateUser,
|
||||||
//
|
//
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
"re:build-npm": "npm run clean && npm install && npm run build",
|
"re:build-npm": "npm run clean && npm install && npm run build",
|
||||||
"tsc:dev": "yarn dev & yarn tsc:watch",
|
"tsc:dev": "yarn dev & yarn tsc:watch",
|
||||||
"tsc:print": "npx tsc --showConfig",
|
"tsc:print": "npx tsc --showConfig",
|
||||||
"tsc:w": "npx nodemon --delay 1 --ext ts,tsx --exec \"yarn tsc\"",
|
"tsc:w": "npx nodemon --delay 1 --ext ts,tsx --exec \"yarn tsc || (sleep 10; touch src/app.tsx)\"",
|
||||||
"tsc:watch": "tsc --noEmit --watch",
|
"tsc:watch": "tsc --noEmit --watch",
|
||||||
"tsc": "tsc --noEmit"
|
"tsc": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { endpoints, fetcher } from 'src/lib/axios';
|
import { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IPostItem } from 'src/types/blog';
|
import type { IPostItem } from 'src/types/blog';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axios, { endpoints, fetcher } from 'src/lib/axios';
|
import axios, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { ICalendarEvent } from 'src/types/calendar';
|
import type { ICalendarEvent } from 'src/types/calendar';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { keyBy } from 'es-toolkit';
|
import { keyBy } from 'es-toolkit';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axios, { endpoints, fetcher } from 'src/lib/axios';
|
import axios, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IChatConversation, IChatMessage, IChatParticipant } from 'src/types/chat';
|
import type { IChatConversation, IChatMessage, IChatParticipant } from 'src/types/chat';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
// src/actions/invoice.ts
|
// src/actions/invoice.ts
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
import axiosInstance, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IInvoiceItem, SaveInvoiceData } from 'src/types/invoice';
|
import type { IInvoiceItem, SaveInvoiceData } from 'src/types/invoice';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import type { UniqueIdentifier } from '@dnd-kit/core';
|
import type { UniqueIdentifier } from '@dnd-kit/core';
|
||||||
import { startTransition, useMemo } from 'react';
|
import { startTransition, useMemo } from 'react';
|
||||||
import axios, { endpoints, fetcher } from 'src/lib/axios';
|
import axios, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IKanban, IKanbanColumn, IKanbanTask } from 'src/types/kanban';
|
import type { IKanban, IKanbanColumn, IKanbanTask } from 'src/types/kanban';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { keyBy } from 'es-toolkit';
|
import { keyBy } from 'es-toolkit';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { endpoints, fetcher } from 'src/lib/axios';
|
import { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IMail, IMailLabel } from 'src/types/mail';
|
import type { IMail, IMailLabel } from 'src/types/mail';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
// src/actions/order.ts
|
// src/actions/order.ts
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
import axiosInstance, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IOrderItem } from 'src/types/order';
|
import type { IOrderItem } from 'src/types/order';
|
||||||
import type { IProductItem } from 'src/types/product';
|
import type { IProductItem } from 'src/types/product';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
// src/actions/party-event.ts
|
// src/actions/party-event.ts
|
||||||
//
|
//
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
import axiosInstance, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IPartyEventItem } from 'src/types/party-event';
|
import type { IPartyEventItem } from 'src/types/party-event';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
// src/actions/party-order.ts
|
// src/actions/party-order.ts
|
||||||
//
|
//
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
import axiosInstance, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IPartyOrderItem } from 'src/types/party-order';
|
import type { IPartyOrderItem } from 'src/types/party-order';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
// src/actions/party-user.ts
|
// src/actions/party-user1.ts
|
||||||
//
|
//
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
import axiosInstance, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IPartyUserItem } from 'src/types/party-user';
|
import type { IPartyUserItem } from 'src/types/party-user';
|
||||||
import type { IProductItem } from 'src/types/product';
|
import type { IProductItem } from 'src/types/product';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
@@ -21,7 +22,15 @@ type PartyUsersData = {
|
|||||||
partyUsers: IPartyUserItem[];
|
partyUsers: IPartyUserItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: i want to refactor / tidy here
|
/**
|
||||||
|
* Fetches list of party users with SWR caching
|
||||||
|
* @returns {Object} Contains:
|
||||||
|
* - partyUsers: Array of party user items
|
||||||
|
* - partyUsersLoading: Loading state
|
||||||
|
* - partyUsersError: Error object if any
|
||||||
|
* - partyUsersValidating: Validation state
|
||||||
|
* - partyUsersEmpty: Boolean if no users found
|
||||||
|
*/
|
||||||
export function useGetPartyUsers() {
|
export function useGetPartyUsers() {
|
||||||
const url = endpoints.partyUser.list;
|
const url = endpoints.partyUser.list;
|
||||||
|
|
||||||
@@ -73,7 +82,16 @@ type SearchResultsData = {
|
|||||||
results: IProductItem[];
|
results: IProductItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: update useSearchProducts
|
/**
|
||||||
|
* Searches products by query with SWR caching
|
||||||
|
* @param {string} query - Search term
|
||||||
|
* @returns {Object} Contains:
|
||||||
|
* - searchResults: Array of matching products
|
||||||
|
* - searchLoading: Loading state
|
||||||
|
* - searchError: Error object if any
|
||||||
|
* - searchValidating: Validation state
|
||||||
|
* - searchEmpty: Boolean if no results found
|
||||||
|
*/
|
||||||
export function useSearchProducts(query: string) {
|
export function useSearchProducts(query: string) {
|
||||||
const url = query ? [endpoints.product.search, { params: { query } }] : '';
|
const url = query ? [endpoints.product.search, { params: { query } }] : '';
|
||||||
|
|
||||||
@@ -117,6 +135,15 @@ type SaveUserData = {
|
|||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new party user with optimistic UI updates
|
||||||
|
* @param {CreateUserData} partyUserData - Data for the new party user
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @sideeffects
|
||||||
|
* - Makes POST request to create user on server
|
||||||
|
* - Updates local SWR cache optimistically
|
||||||
|
* - Triggers revalidation of party user list
|
||||||
|
*/
|
||||||
export async function createPartyUser(partyUserData: CreateUserData) {
|
export async function createPartyUser(partyUserData: CreateUserData) {
|
||||||
/**
|
/**
|
||||||
* Work on server
|
* Work on server
|
||||||
@@ -144,6 +171,15 @@ export async function createPartyUser(partyUserData: CreateUserData) {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates party user data with optimistic UI updates
|
||||||
|
* @param {Partial<IPartyUserItem>} partyUserData - Partial user data containing at least the ID
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @sideeffects
|
||||||
|
* - Makes PUT request to update user on server
|
||||||
|
* - Updates both list and detail views in local SWR cache
|
||||||
|
* - Preserves unchanged fields while updating modified ones
|
||||||
|
*/
|
||||||
export async function updatePartyUser(partyUserData: Partial<IPartyUserItem>) {
|
export async function updatePartyUser(partyUserData: Partial<IPartyUserItem>) {
|
||||||
/**
|
/**
|
||||||
* Work on server
|
* Work on server
|
||||||
@@ -183,6 +219,14 @@ export async function updatePartyUser(partyUserData: Partial<IPartyUserItem>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests connection to product API endpoint
|
||||||
|
* @param {SaveUserData} saveUserData - User data object (currently unused)
|
||||||
|
* @returns {Promise<AxiosResponse>} Response from test endpoint
|
||||||
|
* @deprecated This function should be renamed to better reflect its purpose
|
||||||
|
* TODO: Rename to testProductApiConnection() since it tests product API connection
|
||||||
|
* TODO: Or implement actual image upload functionality if needed
|
||||||
|
*/
|
||||||
export async function uploadUserImage(saveUserData: SaveUserData) {
|
export async function uploadUserImage(saveUserData: SaveUserData) {
|
||||||
console.log('uploadUserImage ?');
|
console.log('uploadUserImage ?');
|
||||||
// const url = userId ? [endpoints.user.details, { params: { userId } }] : '';
|
// const url = userId ? [endpoints.user.details, { params: { userId } }] : '';
|
||||||
@@ -213,6 +257,15 @@ type CreateUserData = {
|
|||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a party user with optimistic UI updates
|
||||||
|
* @param {string} partyUserId - ID of user to delete
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @sideeffects
|
||||||
|
* - Makes PATCH request to mark user as deleted on server
|
||||||
|
* - Removes user from local SWR cache
|
||||||
|
* - Triggers revalidation of party user list
|
||||||
|
*/
|
||||||
export async function deletePartyUser(partyUserId: string) {
|
export async function deletePartyUser(partyUserId: string) {
|
||||||
/**
|
/**
|
||||||
* Work on server
|
* Work on server
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
// src/actions/product.ts
|
// src/actions/product.ts
|
||||||
//
|
//
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
import axiosInstance, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IProductItem } from 'src/types/product';
|
import type { IProductItem } from 'src/types/product';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
import axiosInstance, { fetcher } from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { IProductItem } from 'src/types/product';
|
import type { IProductItem } from 'src/types/product';
|
||||||
import { IUserItem } from 'src/types/user';
|
import { IUserItem } from 'src/types/user';
|
||||||
import type { SWRConfiguration } from 'swr';
|
import type { SWRConfiguration } from 'swr';
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import axios, { endpoints } from 'src/lib/axios';
|
import axios from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import { JWT_STORAGE_KEY } from './constant';
|
import { JWT_STORAGE_KEY } from './constant';
|
||||||
import { setSession } from './utils';
|
import { setSession } from './utils';
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { useSetState } from 'minimal-shared/hooks';
|
import { useSetState } from 'minimal-shared/hooks';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import axios, { endpoints } from 'src/lib/axios';
|
import axios from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import type { AuthState } from '../../types';
|
import type { AuthState } from '../../types';
|
||||||
import { AuthContext } from '../auth-context';
|
import { AuthContext } from '../auth-context';
|
||||||
import { JWT_STORAGE_KEY } from './constant';
|
import { JWT_STORAGE_KEY } from './constant';
|
||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
export const ERR_ACCESS_TOKEN_NOT_FOUND = `Access token not found in response`;
|
||||||
|
export const ACCESS_TOKEN_NOT_FOUND_IN_RESPONSE = 'Access token not found in response';
|
85
03_source/frontend/src/auth/context/party-user-jwt/action.ts
Normal file
85
03_source/frontend/src/auth/context/party-user-jwt/action.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import axios from 'src/lib/axios';
|
||||||
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
|
import { JWT_STORAGE_KEY } from './constant';
|
||||||
|
import { ACCESS_TOKEN_NOT_FOUND_IN_RESPONSE, ERR_ACCESS_TOKEN_NOT_FOUND } from './ERRORS';
|
||||||
|
import { setSession } from './utils';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type SignInParams = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SignUpParams = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* Sign in
|
||||||
|
*************************************** */
|
||||||
|
export const signInWithPassword = async ({ email, password }: SignInParams): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const params = { email, password };
|
||||||
|
|
||||||
|
const res = await axios.post(endpoints.partyUserAuth.signIn, params);
|
||||||
|
|
||||||
|
const { accessToken } = res.data;
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error(ERR_ACCESS_TOKEN_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSession(accessToken);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during sign in:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* Sign up
|
||||||
|
*************************************** */
|
||||||
|
export const signUp = async ({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
}: SignUpParams): Promise<void> => {
|
||||||
|
const params = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.post(endpoints.auth.signUp, params);
|
||||||
|
|
||||||
|
const { accessToken } = res.data;
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
throw new Error(ACCESS_TOKEN_NOT_FOUND_IN_RESPONSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during sign up:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* Sign out
|
||||||
|
*************************************** */
|
||||||
|
export const signOut = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await setSession(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during sign out:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
@@ -0,0 +1 @@
|
|||||||
|
export const JWT_STORAGE_KEY = 'jwt_access_token';
|
@@ -0,0 +1,7 @@
|
|||||||
|
export * from './utils';
|
||||||
|
|
||||||
|
export * from './action';
|
||||||
|
|
||||||
|
export * from './constant';
|
||||||
|
|
||||||
|
// export * from './auth-provider';
|
94
03_source/frontend/src/auth/context/party-user-jwt/utils.ts
Normal file
94
03_source/frontend/src/auth/context/party-user-jwt/utils.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import axios from 'src/lib/axios';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import { JWT_STORAGE_KEY } from './constant';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function jwtDecode(token: string) {
|
||||||
|
try {
|
||||||
|
if (!token) return null;
|
||||||
|
|
||||||
|
const parts = token.split('.');
|
||||||
|
if (parts.length < 2) {
|
||||||
|
throw new Error('Invalid token!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64Url = parts[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const decoded = JSON.parse(atob(base64));
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decoding token:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function isValidToken(accessToken: string) {
|
||||||
|
if (!accessToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwtDecode(accessToken);
|
||||||
|
|
||||||
|
if (!decoded || !('exp' in decoded)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = Date.now() / 1000;
|
||||||
|
|
||||||
|
return decoded.exp > currentTime;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during token validation:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function tokenExpired(exp: number) {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const timeLeft = exp * 1000 - currentTime;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
alert('Token expired!');
|
||||||
|
sessionStorage.removeItem(JWT_STORAGE_KEY);
|
||||||
|
window.location.href = paths.auth.jwt.signIn;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during token expiration:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, timeLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const INVALID_ACCESS_TOKEN = 'Invalid access token!';
|
||||||
|
|
||||||
|
export async function setSession(accessToken: string | null) {
|
||||||
|
try {
|
||||||
|
if (accessToken) {
|
||||||
|
sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
|
||||||
|
|
||||||
|
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
|
||||||
|
|
||||||
|
const decodedToken = jwtDecode(accessToken); // ~3 days by minimals server
|
||||||
|
|
||||||
|
if (decodedToken && 'exp' in decodedToken) {
|
||||||
|
tokenExpired(decodedToken.exp);
|
||||||
|
} else {
|
||||||
|
throw new Error(INVALID_ACCESS_TOKEN);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem(JWT_STORAGE_KEY);
|
||||||
|
delete axios.defaults.headers.common.Authorization;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during set session:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
3
03_source/frontend/src/auth/view/party-user-jwt/index.ts
Normal file
3
03_source/frontend/src/auth/view/party-user-jwt/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './jwt-sign-in-view';
|
||||||
|
|
||||||
|
export * from './jwt-sign-up-view';
|
@@ -0,0 +1,167 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import { useBoolean } from 'minimal-shared/hooks';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Field, Form } from 'src/components/hook-form';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import { useRouter } from 'src/routes/hooks';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import { z as zod } from 'zod';
|
||||||
|
import { FormHead } from '../../components/form-head';
|
||||||
|
import { signInWithPassword } from '../../context/party-user-jwt';
|
||||||
|
import { useAuthContext } from '../../hooks';
|
||||||
|
import { getErrorMessage } from '../../utils';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function JwtSignInView(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const showPassword = useBoolean();
|
||||||
|
|
||||||
|
const { checkUserSession } = useAuthContext();
|
||||||
|
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
type SignInSchemaType = zod.infer<typeof SignInSchema>;
|
||||||
|
|
||||||
|
const EMAIL_IS_REQUIRED = 'Email is required!';
|
||||||
|
const EMAIL_MUST_BE_A_VALID_EMAIL_ADDRESS = 'Email must be a valid email address!';
|
||||||
|
const PASSWORD_IS_REQUIRED = 'Password is required!';
|
||||||
|
const PASSWORD_MUST_BE_AT_LEAST_6_CHARACTERS = 'Password must be at least 6 characters!';
|
||||||
|
|
||||||
|
const SignInSchema = zod.object({
|
||||||
|
email: zod
|
||||||
|
.string()
|
||||||
|
.min(1, { message: EMAIL_IS_REQUIRED })
|
||||||
|
.email({ message: EMAIL_MUST_BE_A_VALID_EMAIL_ADDRESS }),
|
||||||
|
password: zod
|
||||||
|
.string()
|
||||||
|
.min(1, { message: PASSWORD_IS_REQUIRED })
|
||||||
|
.min(6, { message: PASSWORD_MUST_BE_AT_LEAST_6_CHARACTERS }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultValues: SignInSchemaType = {
|
||||||
|
email: 'party_user0@prisma.io',
|
||||||
|
password: 'Aa12345678',
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = useForm<SignInSchemaType>({
|
||||||
|
resolver: zodResolver(SignInSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
try {
|
||||||
|
await signInWithPassword({ email: data.email, password: data.password });
|
||||||
|
await checkUserSession?.();
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
const feedbackMessage = getErrorMessage(error);
|
||||||
|
setErrorMessage(feedbackMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderForm = () => (
|
||||||
|
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||||
|
|
||||||
|
<Box sx={{ gap: 1.5, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
href="#"
|
||||||
|
variant="body2"
|
||||||
|
color="inherit"
|
||||||
|
sx={{ alignSelf: 'flex-end' }}
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Field.Text
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="6+ characters"
|
||||||
|
type={showPassword.value ? 'text' : 'password'}
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: { shrink: true },
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||||
|
<Iconify
|
||||||
|
icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
color="inherit"
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingIndicator="Sign in..."
|
||||||
|
>
|
||||||
|
{t('sign-in')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormHead
|
||||||
|
title="Sign in to your account"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{`Don’t have an account? `}
|
||||||
|
<Link component={RouterLink} href={paths.partyUserAuth.jwt.signUp} variant="subtitle2">
|
||||||
|
{t('get-started')}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Alert severity="info" sx={{ mb: 3 }}>
|
||||||
|
Use <strong>{defaultValues.email}</strong>
|
||||||
|
{' with password '}
|
||||||
|
<strong>{defaultValues.password}</strong>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{!!errorMessage && (
|
||||||
|
<Alert severity="error" sx={{ mb: 3 }}>
|
||||||
|
{errorMessage}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form methods={methods} onSubmit={onSubmit}>
|
||||||
|
{renderForm()}
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,166 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import { useBoolean } from 'minimal-shared/hooks';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { Field, Form } from 'src/components/hook-form';
|
||||||
|
import { Iconify } from 'src/components/iconify';
|
||||||
|
import { RouterLink } from 'src/routes/components';
|
||||||
|
import { useRouter } from 'src/routes/hooks';
|
||||||
|
import { paths } from 'src/routes/paths';
|
||||||
|
import { z as zod } from 'zod';
|
||||||
|
import { FormHead } from '../../components/form-head';
|
||||||
|
import { SignUpTerms } from '../../components/sign-up-terms';
|
||||||
|
import { signUp } from '../../context/jwt';
|
||||||
|
import { useAuthContext } from '../../hooks';
|
||||||
|
import { getErrorMessage } from '../../utils';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type SignUpSchemaType = zod.infer<typeof SignUpSchema>;
|
||||||
|
|
||||||
|
export const SignUpSchema = zod.object({
|
||||||
|
firstName: zod.string().min(1, { message: 'First name is required!' }),
|
||||||
|
lastName: zod.string().min(1, { message: 'Last name is required!' }),
|
||||||
|
email: zod
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Email is required!' })
|
||||||
|
.email({ message: 'Email must be a valid email address!' }),
|
||||||
|
password: zod
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Password is required!' })
|
||||||
|
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function JwtSignUpView() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const showPassword = useBoolean();
|
||||||
|
|
||||||
|
const { checkUserSession } = useAuthContext();
|
||||||
|
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const defaultValues: SignUpSchemaType = {
|
||||||
|
firstName: 'Hello',
|
||||||
|
lastName: 'Friend',
|
||||||
|
email: 'hello@gmail.com',
|
||||||
|
password: '@2Minimal',
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = useForm<SignUpSchemaType>({
|
||||||
|
resolver: zodResolver(SignUpSchema),
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit(async (data) => {
|
||||||
|
try {
|
||||||
|
await signUp({
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
});
|
||||||
|
await checkUserSession?.();
|
||||||
|
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
const feedbackMessage = getErrorMessage(error);
|
||||||
|
setErrorMessage(feedbackMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderForm = () => (
|
||||||
|
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', gap: { xs: 3, sm: 2 }, flexDirection: { xs: 'column', sm: 'row' } }}
|
||||||
|
>
|
||||||
|
<Field.Text
|
||||||
|
name="firstName"
|
||||||
|
label="First name"
|
||||||
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
|
/>
|
||||||
|
<Field.Text
|
||||||
|
name="lastName"
|
||||||
|
label="Last name"
|
||||||
|
slotProps={{ inputLabel: { shrink: true } }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||||
|
|
||||||
|
<Field.Text
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="6+ characters"
|
||||||
|
type={showPassword.value ? 'text' : 'password'}
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: { shrink: true },
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||||
|
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
color="inherit"
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
loading={isSubmitting}
|
||||||
|
loadingIndicator="Create account..."
|
||||||
|
>
|
||||||
|
Create account
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormHead
|
||||||
|
title="Hi New party user, Get started absolutely free "
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{`Already have an account? `}
|
||||||
|
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
|
||||||
|
Get started
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!!errorMessage && (
|
||||||
|
<Alert severity="error" sx={{ mb: 3 }}>
|
||||||
|
{errorMessage}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form methods={methods} onSubmit={onSubmit}>
|
||||||
|
{renderForm()}
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<SignUpTerms />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,3 +1,18 @@
|
|||||||
|
// src/lib/axios.ts
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// - Centralized Axios instance configuration
|
||||||
|
// - Global response error handling
|
||||||
|
// - Standardized API endpoint definitions
|
||||||
|
// - Reusable fetcher utility
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// - All API calls must use this axiosInstance
|
||||||
|
// - Custom error handling in interceptor
|
||||||
|
// - Endpoints should be defined here for consistency
|
||||||
|
// - Fetcher should be used for simple GET requests
|
||||||
|
//
|
||||||
|
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { CONFIG } from 'src/global-config';
|
import { CONFIG } from 'src/global-config';
|
||||||
@@ -27,94 +42,3 @@ export const fetcher = async (args: string | [string, AxiosRequestConfig]) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
export const endpoints = {
|
|
||||||
chat: '/api/chat',
|
|
||||||
kanban: '/api/kanban',
|
|
||||||
calendar: '/api/calendar',
|
|
||||||
auth: {
|
|
||||||
me: '/api/auth/me',
|
|
||||||
signIn: '/api/auth/sign-in',
|
|
||||||
signUp: '/api/auth/sign-up',
|
|
||||||
},
|
|
||||||
mail: {
|
|
||||||
list: '/api/mail/list',
|
|
||||||
details: '/api/mail/details',
|
|
||||||
labels: '/api/mail/labels',
|
|
||||||
},
|
|
||||||
post: {
|
|
||||||
list: '/api/post/list',
|
|
||||||
details: '/api/post/details',
|
|
||||||
latest: '/api/post/latest',
|
|
||||||
search: '/api/post/search',
|
|
||||||
},
|
|
||||||
product: {
|
|
||||||
list: '/api/product/list',
|
|
||||||
details: '/api/product/details',
|
|
||||||
search: '/api/product/search',
|
|
||||||
save: '/api/product/saveProduct',
|
|
||||||
create: '/api/product/create',
|
|
||||||
update: '/api/product/update',
|
|
||||||
delete: '/api/product/delete',
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
list: '/api/user/list',
|
|
||||||
profile: '/api/user/profile',
|
|
||||||
update: '/api/user/update',
|
|
||||||
settings: '/api/user/settings',
|
|
||||||
details: '/api/user/details',
|
|
||||||
},
|
|
||||||
order: {
|
|
||||||
list: '/api/order/list',
|
|
||||||
profile: '/api/order/profile',
|
|
||||||
update: '/api/order/update',
|
|
||||||
settings: '/api/order/settings',
|
|
||||||
details: '/api/order/details',
|
|
||||||
changeStatus: (orderId: string) => `/api/order/changeStatus?orderId=${orderId}`,
|
|
||||||
},
|
|
||||||
invoice: {
|
|
||||||
list: '/api/invoice/list',
|
|
||||||
profile: '/api/invoice/profile',
|
|
||||||
update: '/api/invoice/update',
|
|
||||||
saveInvoice: (invoiceId: string) => `/api/invoice/saveInvoice?invoiceId=${invoiceId}`,
|
|
||||||
settings: '/api/invoice/settings',
|
|
||||||
details: '/api/invoice/details',
|
|
||||||
changeStatus: (invoiceId: string) => `/api/invoice/changeStatus?invoiceId=${invoiceId}`,
|
|
||||||
search: '/api/invoice/search',
|
|
||||||
},
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
partyEvent: {
|
|
||||||
list: '/api/party-event/list',
|
|
||||||
details: '/api/party-event/details',
|
|
||||||
search: '/api/party-event/search',
|
|
||||||
create: '/api/party-event/create',
|
|
||||||
update: '/api/party-event/update',
|
|
||||||
delete: '/api/party-event/delete',
|
|
||||||
},
|
|
||||||
partyOrder: {
|
|
||||||
create: '/api/party-order/create',
|
|
||||||
delete: '/api/party-order/delete',
|
|
||||||
list: '/api/party-order/list',
|
|
||||||
profile: '/api/party-order/profile',
|
|
||||||
update: '/api/party-order/update',
|
|
||||||
settings: '/api/party-order/settings',
|
|
||||||
details: '/api/party-order/details',
|
|
||||||
changeStatus: (partyOrderId: string) =>
|
|
||||||
`/api/party-order/changeStatus?partyOrderId=${partyOrderId}`,
|
|
||||||
},
|
|
||||||
partyUser: {
|
|
||||||
list: '/api/party-user/list',
|
|
||||||
details: '/api/party-user/details',
|
|
||||||
search: '/api/party-user/search',
|
|
||||||
create: '/api/party-user/create',
|
|
||||||
update: '/api/party-user/update',
|
|
||||||
delete: '/api/party-user/delete',
|
|
||||||
//
|
|
||||||
detailsByPartyUserId: (partyUserId: string) =>
|
|
||||||
`/api/party-user/details?partyUserId=${partyUserId}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
96
03_source/frontend/src/lib/endpoints.ts
Normal file
96
03_source/frontend/src/lib/endpoints.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const endpoints = {
|
||||||
|
chat: '/api/chat',
|
||||||
|
kanban: '/api/kanban',
|
||||||
|
calendar: '/api/calendar',
|
||||||
|
auth: {
|
||||||
|
me: '/api/auth/me',
|
||||||
|
signIn: '/api/auth/sign-in',
|
||||||
|
signUp: '/api/auth/sign-up',
|
||||||
|
},
|
||||||
|
mail: {
|
||||||
|
list: '/api/mail/list',
|
||||||
|
details: '/api/mail/details',
|
||||||
|
labels: '/api/mail/labels',
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
list: '/api/post/list',
|
||||||
|
details: '/api/post/details',
|
||||||
|
latest: '/api/post/latest',
|
||||||
|
search: '/api/post/search',
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
list: '/api/product/list',
|
||||||
|
details: '/api/product/details',
|
||||||
|
search: '/api/product/search',
|
||||||
|
save: '/api/product/saveProduct',
|
||||||
|
create: '/api/product/create',
|
||||||
|
update: '/api/product/update',
|
||||||
|
delete: '/api/product/delete',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
list: '/api/user/list',
|
||||||
|
profile: '/api/user/profile',
|
||||||
|
update: '/api/user/update',
|
||||||
|
settings: '/api/user/settings',
|
||||||
|
details: '/api/user/details',
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
list: '/api/order/list',
|
||||||
|
profile: '/api/order/profile',
|
||||||
|
update: '/api/order/update',
|
||||||
|
settings: '/api/order/settings',
|
||||||
|
details: '/api/order/details',
|
||||||
|
changeStatus: (orderId: string) => `/api/order/changeStatus?orderId=${orderId}`,
|
||||||
|
},
|
||||||
|
invoice: {
|
||||||
|
list: '/api/invoice/list',
|
||||||
|
profile: '/api/invoice/profile',
|
||||||
|
update: '/api/invoice/update',
|
||||||
|
saveInvoice: (invoiceId: string) => `/api/invoice/saveInvoice?invoiceId=${invoiceId}`,
|
||||||
|
settings: '/api/invoice/settings',
|
||||||
|
details: '/api/invoice/details',
|
||||||
|
changeStatus: (invoiceId: string) => `/api/invoice/changeStatus?invoiceId=${invoiceId}`,
|
||||||
|
search: '/api/invoice/search',
|
||||||
|
},
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
partyEvent: {
|
||||||
|
list: '/api/party-event/list',
|
||||||
|
details: '/api/party-event/details',
|
||||||
|
search: '/api/party-event/search',
|
||||||
|
create: '/api/party-event/create',
|
||||||
|
update: '/api/party-event/update',
|
||||||
|
delete: '/api/party-event/delete',
|
||||||
|
numOfEvent: '/api/party-event/numOfEvent',
|
||||||
|
},
|
||||||
|
partyOrder: {
|
||||||
|
create: '/api/party-order/create',
|
||||||
|
delete: '/api/party-order/delete',
|
||||||
|
list: '/api/party-order/list',
|
||||||
|
profile: '/api/party-order/profile',
|
||||||
|
update: '/api/party-order/update',
|
||||||
|
settings: '/api/party-order/settings',
|
||||||
|
details: '/api/party-order/details',
|
||||||
|
changeStatus: (partyOrderId: string) =>
|
||||||
|
`/api/party-order/changeStatus?partyOrderId=${partyOrderId}`,
|
||||||
|
},
|
||||||
|
partyUser: {
|
||||||
|
list: '/api/party-user/list',
|
||||||
|
details: '/api/party-user/details',
|
||||||
|
search: '/api/party-user/search',
|
||||||
|
create: '/api/party-user/create',
|
||||||
|
update: '/api/party-user/update',
|
||||||
|
delete: '/api/party-user/delete',
|
||||||
|
//
|
||||||
|
detailsByPartyUserId: (partyUserId: string) =>
|
||||||
|
`/api/party-user/details?partyUserId=${partyUserId}`,
|
||||||
|
},
|
||||||
|
partyUserAuth: {
|
||||||
|
me: '/api/party-user-auth/me',
|
||||||
|
signIn: '/api/party-user-auth/sign-in',
|
||||||
|
signUp: '/api/party-user-auth/sign-up',
|
||||||
|
},
|
||||||
|
};
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { AmplifyResetPasswordView } from 'src/auth/view/amplify';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Reset password | Amplify - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<AmplifyResetPasswordView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { AmplifySignInView } from 'src/auth/view/amplify';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign in | Amplify - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<AmplifySignInView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { AmplifySignUpView } from 'src/auth/view/amplify';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign up | Amplify - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<AmplifySignUpView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { AmplifyUpdatePasswordView } from 'src/auth/view/amplify';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Update password | Amplify - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<AmplifyUpdatePasswordView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { AmplifyVerifyView } from 'src/auth/view/amplify';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Verify | Amplify - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<AmplifyVerifyView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
import { SplashScreen } from 'src/components/loading-screen';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export default function CallbackPage() {
|
||||||
|
return <SplashScreen />;
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { Auth0SignInView } from 'src/auth/view/auth0';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign in | Auth0 - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<Auth0SignInView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { FirebaseResetPasswordView } from 'src/auth/view/firebase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Reset password | Firebase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<FirebaseResetPasswordView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { FirebaseSignInView } from 'src/auth/view/firebase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign in | Firebase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<FirebaseSignInView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { FirebaseSignUpView } from 'src/auth/view/firebase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign up | Firebase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<FirebaseSignUpView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { FirebaseVerifyView } from 'src/auth/view/firebase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Verify | Firebase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<FirebaseVerifyView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
16
03_source/frontend/src/pages/party-user-auth/jwt/sign-in.tsx
Normal file
16
03_source/frontend/src/pages/party-user-auth/jwt/sign-in.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { JwtSignInView } from 'src/auth/view/party-user-jwt';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign in | Jwt - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<JwtSignInView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
16
03_source/frontend/src/pages/party-user-auth/jwt/sign-up.tsx
Normal file
16
03_source/frontend/src/pages/party-user-auth/jwt/sign-up.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { JwtSignUpView } from 'src/auth/view/party-user-jwt';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign up | Jwt - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<JwtSignUpView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { SupabaseResetPasswordView } from 'src/auth/view/supabase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Reset password | Supabase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<SupabaseResetPasswordView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { SupabaseSignInView } from 'src/auth/view/supabase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign in | Supabase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<SupabaseSignInView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { SupabaseSignUpView } from 'src/auth/view/supabase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Sign up | Supabase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<SupabaseSignUpView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { SupabaseUpdatePasswordView } from 'src/auth/view/supabase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Update password | Supabase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<SupabaseUpdatePasswordView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { SupabaseVerifyView } from 'src/auth/view/supabase';
|
||||||
|
import { CONFIG } from 'src/global-config';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const metadata = { title: `Verify | Supabase - ${CONFIG.appName}` };
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<title>{metadata.title}</title>
|
||||||
|
|
||||||
|
<SupabaseVerifyView />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -10,6 +10,8 @@ const ROOTS = {
|
|||||||
AUTH: '/auth',
|
AUTH: '/auth',
|
||||||
AUTH_DEMO: '/auth-demo',
|
AUTH_DEMO: '/auth-demo',
|
||||||
DASHBOARD: '/dashboard',
|
DASHBOARD: '/dashboard',
|
||||||
|
//
|
||||||
|
PARTY_USER_AUTH: '/party-user-auth',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@@ -207,4 +209,32 @@ export const paths = {
|
|||||||
demo: { edit: `${ROOTS.DASHBOARD}/party-user/${MOCK_ID}/edit` },
|
demo: { edit: `${ROOTS.DASHBOARD}/party-user/${MOCK_ID}/edit` },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
partyUserAuth: {
|
||||||
|
jwt: {
|
||||||
|
signIn: `${ROOTS.PARTY_USER_AUTH}/jwt/sign-in`,
|
||||||
|
signUp: `${ROOTS.PARTY_USER_AUTH}/jwt/sign-up`,
|
||||||
|
},
|
||||||
|
//
|
||||||
|
// amplify: {
|
||||||
|
// signIn: `${ROOTS.PARTY_USER_AUTH}/amplify/sign-in`,
|
||||||
|
// verify: `${ROOTS.PARTY_USER_AUTH}/amplify/verify`,
|
||||||
|
// signUp: `${ROOTS.PARTY_USER_AUTH}/amplify/sign-up`,
|
||||||
|
// updatePassword: `${ROOTS.PARTY_USER_AUTH}/amplify/update-password`,
|
||||||
|
// resetPassword: `${ROOTS.PARTY_USER_AUTH}/amplify/reset-password`,
|
||||||
|
// },
|
||||||
|
// firebase: {
|
||||||
|
// signIn: `${ROOTS.PARTY_USER_AUTH}/firebase/sign-in`,
|
||||||
|
// verify: `${ROOTS.PARTY_USER_AUTH}/firebase/verify`,
|
||||||
|
// signUp: `${ROOTS.PARTY_USER_AUTH}/firebase/sign-up`,
|
||||||
|
// resetPassword: `${ROOTS.PARTY_USER_AUTH}/firebase/reset-password`,
|
||||||
|
// },
|
||||||
|
// auth0: { signIn: `${ROOTS.PARTY_USER_AUTH}/auth0/sign-in` },
|
||||||
|
// supabase: {
|
||||||
|
// signIn: `${ROOTS.PARTY_USER_AUTH}/supabase/sign-in`,
|
||||||
|
// verify: `${ROOTS.PARTY_USER_AUTH}/supabase/verify`,
|
||||||
|
// signUp: `${ROOTS.PARTY_USER_AUTH}/supabase/sign-up`,
|
||||||
|
// updatePassword: `${ROOTS.PARTY_USER_AUTH}/supabase/update-password`,
|
||||||
|
// resetPassword: `${ROOTS.PARTY_USER_AUTH}/supabase/reset-password`,
|
||||||
|
// },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@@ -9,6 +9,7 @@ import { authDemoRoutes } from './auth-demo';
|
|||||||
import { componentsRoutes } from './components';
|
import { componentsRoutes } from './components';
|
||||||
import { dashboardRoutes } from './dashboard';
|
import { dashboardRoutes } from './dashboard';
|
||||||
import { mainRoutes } from './main';
|
import { mainRoutes } from './main';
|
||||||
|
import { partyUserAuthRoutes } from './party-user-auth';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -48,6 +49,9 @@ export const routesSection: RouteObject[] = [
|
|||||||
// Components
|
// Components
|
||||||
...componentsRoutes,
|
...componentsRoutes,
|
||||||
|
|
||||||
|
// party-user-auth
|
||||||
|
...partyUserAuthRoutes,
|
||||||
|
|
||||||
// No match
|
// No match
|
||||||
{ path: '*', element: <Page404 /> },
|
{ path: '*', element: <Page404 /> },
|
||||||
];
|
];
|
||||||
|
282
03_source/frontend/src/routes/sections/party-user-auth.tsx
Normal file
282
03_source/frontend/src/routes/sections/party-user-auth.tsx
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import { lazy, Suspense } from 'react';
|
||||||
|
import type { RouteObject } from 'react-router';
|
||||||
|
import { Outlet } from 'react-router';
|
||||||
|
import { GuestGuard } from 'src/auth/guard';
|
||||||
|
import { SplashScreen } from 'src/components/loading-screen';
|
||||||
|
import { AuthSplitLayout } from 'src/layouts/auth-split';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* Jwt
|
||||||
|
*************************************** */
|
||||||
|
const Jwt = {
|
||||||
|
SignInPage: lazy(() => import('src/pages/party-user-auth/jwt/sign-in')),
|
||||||
|
SignUpPage: lazy(() => import('src/pages/party-user-auth/jwt/sign-up')),
|
||||||
|
};
|
||||||
|
|
||||||
|
const authJwt = {
|
||||||
|
path: 'jwt',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'sign-in',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout
|
||||||
|
slotProps={{
|
||||||
|
section: { title: 'Hi, Welcome back' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Jwt.SignInPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sign-up',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Jwt.SignUpPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* Amplify
|
||||||
|
*************************************** */
|
||||||
|
const Amplify = {
|
||||||
|
SignInPage: lazy(() => import('src/pages/auth/amplify/sign-in')),
|
||||||
|
SignUpPage: lazy(() => import('src/pages/auth/amplify/sign-up')),
|
||||||
|
VerifyPage: lazy(() => import('src/pages/auth/amplify/verify')),
|
||||||
|
UpdatePasswordPage: lazy(() => import('src/pages/auth/amplify/update-password')),
|
||||||
|
ResetPasswordPage: lazy(() => import('src/pages/auth/amplify/reset-password')),
|
||||||
|
};
|
||||||
|
|
||||||
|
const authAmplify = {
|
||||||
|
path: 'amplify',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'sign-in',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout
|
||||||
|
slotProps={{
|
||||||
|
section: { title: 'Hi, Welcome back' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Amplify.SignInPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sign-up',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Amplify.SignUpPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'verify',
|
||||||
|
element: (
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Amplify.VerifyPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reset-password',
|
||||||
|
element: (
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Amplify.ResetPasswordPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'update-password',
|
||||||
|
element: (
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Amplify.UpdatePasswordPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* Firebase
|
||||||
|
*************************************** */
|
||||||
|
const Firebase = {
|
||||||
|
SignInPage: lazy(() => import('src/pages/auth/firebase/sign-in')),
|
||||||
|
SignUpPage: lazy(() => import('src/pages/auth/firebase/sign-up')),
|
||||||
|
VerifyPage: lazy(() => import('src/pages/auth/firebase/verify')),
|
||||||
|
ResetPasswordPage: lazy(() => import('src/pages/auth/firebase/reset-password')),
|
||||||
|
};
|
||||||
|
|
||||||
|
const authFirebase = {
|
||||||
|
path: 'firebase',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'sign-in',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout
|
||||||
|
slotProps={{
|
||||||
|
section: { title: 'Hi, Welcome back' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Firebase.SignInPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sign-up',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Firebase.SignUpPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'verify',
|
||||||
|
element: (
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Firebase.VerifyPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reset-password',
|
||||||
|
element: (
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Firebase.ResetPasswordPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* Auth0
|
||||||
|
*************************************** */
|
||||||
|
const Auth0 = {
|
||||||
|
SignInPage: lazy(() => import('src/pages/auth/auth0/sign-in')),
|
||||||
|
CallbackPage: lazy(() => import('src/pages/auth/auth0/callback')),
|
||||||
|
};
|
||||||
|
|
||||||
|
const authAuth0 = {
|
||||||
|
path: 'auth0',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'sign-in',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout
|
||||||
|
slotProps={{
|
||||||
|
section: { title: 'Hi, Welcome back' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Auth0.SignInPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'callback',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<Auth0.CallbackPage />
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** **************************************
|
||||||
|
* Supabase
|
||||||
|
*************************************** */
|
||||||
|
const Supabase = {
|
||||||
|
SignInPage: lazy(() => import('src/pages/auth/supabase/sign-in')),
|
||||||
|
SignUpPage: lazy(() => import('src/pages/auth/supabase/sign-up')),
|
||||||
|
VerifyPage: lazy(() => import('src/pages/auth/supabase/verify')),
|
||||||
|
UpdatePasswordPage: lazy(() => import('src/pages/auth/supabase/update-password')),
|
||||||
|
ResetPasswordPage: lazy(() => import('src/pages/auth/supabase/reset-password')),
|
||||||
|
};
|
||||||
|
|
||||||
|
const authSupabase = {
|
||||||
|
path: 'supabase',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'sign-in',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout
|
||||||
|
slotProps={{
|
||||||
|
section: { title: 'Hi, Welcome back' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Supabase.SignInPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sign-up',
|
||||||
|
element: (
|
||||||
|
<GuestGuard>
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Supabase.SignUpPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
</GuestGuard>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'verify',
|
||||||
|
element: (
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Supabase.VerifyPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reset-password',
|
||||||
|
element: (
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Supabase.ResetPasswordPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'update-password',
|
||||||
|
element: (
|
||||||
|
<AuthSplitLayout>
|
||||||
|
<Supabase.UpdatePasswordPage />
|
||||||
|
</AuthSplitLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const partyUserAuthRoutes: RouteObject[] = [
|
||||||
|
{
|
||||||
|
path: 'party-user-auth',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<SplashScreen />}>
|
||||||
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
|
children: [authJwt, authAmplify, authFirebase, authAuth0, authSupabase],
|
||||||
|
},
|
||||||
|
];
|
@@ -83,6 +83,44 @@ export function OverviewAppView() {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 4 }}>
|
||||||
|
<AppWidgetSummary
|
||||||
|
title="Total party-events"
|
||||||
|
percent={-0.1}
|
||||||
|
total={4}
|
||||||
|
chart={{
|
||||||
|
colors: [theme.palette.error.main],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
|
||||||
|
series: [],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 4 }}>
|
||||||
|
<AppWidgetSummary
|
||||||
|
title="Total party-events"
|
||||||
|
percent={-0.1}
|
||||||
|
total={5}
|
||||||
|
chart={{
|
||||||
|
colors: [theme.palette.error.main],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
|
||||||
|
series: [],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ xs: 12, md: 4 }}>
|
||||||
|
<AppWidgetSummary
|
||||||
|
title="Total party-events"
|
||||||
|
percent={-0.1}
|
||||||
|
total={6}
|
||||||
|
chart={{
|
||||||
|
colors: [theme.palette.error.main],
|
||||||
|
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
|
||||||
|
series: [],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
<Grid size={{ xs: 12, md: 6, lg: 4 }}>
|
||||||
<AppCurrentDownload
|
<AppCurrentDownload
|
||||||
title="Current download"
|
title="Current download"
|
||||||
|
@@ -36,7 +36,7 @@ import { EmptyContent } from 'src/components/empty-content';
|
|||||||
import { Iconify } from 'src/components/iconify';
|
import { Iconify } from 'src/components/iconify';
|
||||||
import { toast } from 'src/components/snackbar';
|
import { toast } from 'src/components/snackbar';
|
||||||
import { DashboardContent } from 'src/layouts/dashboard';
|
import { DashboardContent } from 'src/layouts/dashboard';
|
||||||
import { endpoints } from 'src/lib/axios';
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import { RouterLink } from 'src/routes/components';
|
import { RouterLink } from 'src/routes/components';
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
import type { IPartyEventItem, IProductTableFilters } from 'src/types/party-event';
|
import type { IPartyEventItem, IProductTableFilters } from 'src/types/party-event';
|
||||||
|
@@ -35,7 +35,7 @@ import { EmptyContent } from 'src/components/empty-content';
|
|||||||
import { Iconify } from 'src/components/iconify';
|
import { Iconify } from 'src/components/iconify';
|
||||||
import { toast } from 'src/components/snackbar';
|
import { toast } from 'src/components/snackbar';
|
||||||
import { DashboardContent } from 'src/layouts/dashboard';
|
import { DashboardContent } from 'src/layouts/dashboard';
|
||||||
import { endpoints } from 'src/lib/axios';
|
import { endpoints } from 'src/lib/endpoints';
|
||||||
import { RouterLink } from 'src/routes/components';
|
import { RouterLink } from 'src/routes/components';
|
||||||
import { paths } from 'src/routes/paths';
|
import { paths } from 'src/routes/paths';
|
||||||
import type { IProductItem, IProductTableFilters } from 'src/types/product';
|
import type { IProductItem, IProductTableFilters } from 'src/types/product';
|
||||||
|
@@ -48,16 +48,26 @@ import { setIsLoggedIn, setUsername, loadUserData } from './data/user/user.actio
|
|||||||
import Account from './pages/Account';
|
import Account from './pages/Account';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import MyLogin from './pages/MyLogin';
|
import MyLogin from './pages/MyLogin';
|
||||||
|
import PartyUserLogin from './pages/PartyUserLogin';
|
||||||
import Signup from './pages/Signup';
|
import Signup from './pages/Signup';
|
||||||
import Support from './pages/Support';
|
import Support from './pages/Support';
|
||||||
import Tutorial from './pages/Tutorial';
|
import Tutorial from './pages/Tutorial';
|
||||||
import HomeOrTutorial from './components/HomeOrTutorial';
|
import HomeOrTutorial from './components/HomeOrTutorial';
|
||||||
import { Schedule } from './models/Schedule';
|
import { Schedule } from './models/Schedule';
|
||||||
import RedirectToLogin from './components/RedirectToLogin';
|
import RedirectToLogin from './components/RedirectToLogin';
|
||||||
import AppRoute from './AppRoute';
|
|
||||||
|
|
||||||
import AppDemoRoute from './routes/DemoRoute';
|
import AppDemoRoute from './routes/DemoRoute';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
|
import PATHS from './PATHS';
|
||||||
|
import NotImplemented from './pages/NotImplemented';
|
||||||
|
import ChangeLanguage from './pages/ChangeLanguage';
|
||||||
|
import ServiceAgreement from './pages/ServiceAgreement';
|
||||||
|
import PrivacyAgreement from './pages/PrivacyAgreement';
|
||||||
|
import EventDetail from './pages/EventDetail';
|
||||||
|
import MemberProfile from './pages/MemberProfile';
|
||||||
|
import OrderDetail from './pages/OrderDetail';
|
||||||
|
import DummyPayPage from './pages/DummyEventPayPage';
|
||||||
|
import DummyEventPayPage from './pages/DummyEventPayPage';
|
||||||
|
|
||||||
setupIonicReact();
|
setupIonicReact();
|
||||||
|
|
||||||
@@ -84,6 +94,7 @@ interface DispatchProps {
|
|||||||
interface IonicAppProps extends StateProps, DispatchProps {}
|
interface IonicAppProps extends StateProps, DispatchProps {}
|
||||||
|
|
||||||
const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn, setUsername, loadConfData, loadUserData }) => {
|
const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn, setUsername, loadConfData, loadUserData }) => {
|
||||||
|
// Load initial user and conference data when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUserData();
|
loadUserData();
|
||||||
loadConfData();
|
loadConfData();
|
||||||
@@ -104,7 +115,6 @@ const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn,
|
|||||||
which makes transitions between tabs and non tab pages smooth
|
which makes transitions between tabs and non tab pages smooth
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
<AppRoute />
|
|
||||||
<AppDemoRoute />
|
<AppDemoRoute />
|
||||||
|
|
||||||
<Route path="/tabs" render={() => <MainTabs />} />
|
<Route path="/tabs" render={() => <MainTabs />} />
|
||||||
@@ -116,7 +126,24 @@ const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn,
|
|||||||
<Route path="/signup" component={Signup} />
|
<Route path="/signup" component={Signup} />
|
||||||
<Route path="/support" component={Support} />
|
<Route path="/support" component={Support} />
|
||||||
<Route path="/tutorial" component={Tutorial} />
|
<Route path="/tutorial" component={Tutorial} />
|
||||||
<Route path="/settings" component={Settings} />
|
{/* */}
|
||||||
|
<Route exact={true} path={PATHS.SETTINGS} component={Settings} />
|
||||||
|
<Route exact={true} path={PATHS.CHANGE_LANGUAGE} component={ChangeLanguage} />
|
||||||
|
<Route exact={true} path={PATHS.SERVICE_AGREEMENT} component={ServiceAgreement} />
|
||||||
|
<Route exact={true} path={PATHS.PRIVACY_AGREEMENT} component={PrivacyAgreement} />
|
||||||
|
|
||||||
|
{/* Event and profile detail pages */}
|
||||||
|
<Route exact={true} path="/dummy_pay_page" component={DummyPayPage} />
|
||||||
|
|
||||||
|
<Route exact={true} path="/event_detail/:id" component={EventDetail} />
|
||||||
|
<Route exact={true} path="/profile/:id" component={MemberProfile} />
|
||||||
|
|
||||||
|
{/* component make the ":id" available in the "OrderDetail" */}
|
||||||
|
<Route exact={true} path="/order_detail/:id" component={OrderDetail} />
|
||||||
|
|
||||||
|
<Route exact={true} path="/helloworld" component={Helloworld} />
|
||||||
|
|
||||||
|
<Route exact={true} path={PATHS.DUMMY_EVENT_PAY_PAGE} component={DummyEventPayPage} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/logout"
|
path="/logout"
|
||||||
@@ -124,6 +151,12 @@ const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn,
|
|||||||
return <RedirectToLogin setIsLoggedIn={setIsLoggedIn} setUsername={setUsername} />;
|
return <RedirectToLogin setIsLoggedIn={setIsLoggedIn} setUsername={setUsername} />;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* PartyUser */}
|
||||||
|
<Route path={PATHS.PARTY_USER_SIGN_IN} component={PartyUserLogin} />
|
||||||
|
|
||||||
|
<Route exact={true} path={PATHS.NOT_IMPLEMENTED} component={NotImplemented} />
|
||||||
|
|
||||||
<Route path="/" component={HomeOrTutorial} exact />
|
<Route path="/" component={HomeOrTutorial} exact />
|
||||||
</IonRouterOutlet>
|
</IonRouterOutlet>
|
||||||
</IonSplitPane>
|
</IonSplitPane>
|
||||||
@@ -147,3 +180,7 @@ const IonicAppConnected = connect<{}, StateProps, DispatchProps>({
|
|||||||
},
|
},
|
||||||
component: IonicApp,
|
component: IonicApp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function Helloworld() {
|
||||||
|
return <>helloworld</>;
|
||||||
|
}
|
||||||
|
@@ -1,39 +0,0 @@
|
|||||||
//
|
|
||||||
// pages without bottom tab bar
|
|
||||||
//
|
|
||||||
|
|
||||||
import { Route } from 'react-router';
|
|
||||||
import NotImplemented from './pages/NotImplemented';
|
|
||||||
import EventDetail from './pages/EventDetail';
|
|
||||||
import MemberProfile from './pages/MemberProfile';
|
|
||||||
import PATHS from './PATHS';
|
|
||||||
import Settings from './pages/Settings';
|
|
||||||
import ChangeLanguage from './pages/ChangeLanguage';
|
|
||||||
import ServiceAgreement from './pages/ServiceAgreement';
|
|
||||||
import PrivacyAgreement from './pages/PrivacyAgreement';
|
|
||||||
// import OrderDetails from './pages/OrderDetail';
|
|
||||||
import OrderDetail from './pages/OrderDetail';
|
|
||||||
|
|
||||||
const AppRoute: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Route path="/not_implemented" component={NotImplemented} />
|
|
||||||
|
|
||||||
{/* */}
|
|
||||||
<Route exact={true} path="/event_detail/:id" component={EventDetail} />
|
|
||||||
<Route exact={true} path="/profile/:id" component={MemberProfile} />
|
|
||||||
|
|
||||||
{/* component make the ":id" available in the "OrderDetail" */}
|
|
||||||
<Route exact={true} path="/order_detail/:id" component={OrderDetail} />
|
|
||||||
{/* <Route path="/tabs/speakers/:id" component={SpeakerDetail} exact={true} /> */}
|
|
||||||
|
|
||||||
{/* */}
|
|
||||||
<Route exact={true} path={PATHS.SETTINGS} component={Settings} />
|
|
||||||
<Route exact={true} path={PATHS.CHANGE_LANGUAGE} component={ChangeLanguage} />
|
|
||||||
<Route exact={true} path={PATHS.SERVICE_AGREEMENT} component={ServiceAgreement} />
|
|
||||||
<Route exact={true} path={PATHS.PRIVACY_AGREEMENT} component={PrivacyAgreement} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppRoute;
|
|
@@ -1,3 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Centralized route path constants for the application
|
||||||
|
*
|
||||||
|
* Contains:
|
||||||
|
* - Main app routes
|
||||||
|
* - Tab navigation routes
|
||||||
|
* - Demo/example routes
|
||||||
|
* - Helper functions for dynamic routes
|
||||||
|
*/
|
||||||
const PATHS = {
|
const PATHS = {
|
||||||
NOT_IMPLEMENTED: '/not_implemented',
|
NOT_IMPLEMENTED: '/not_implemented',
|
||||||
SETTINGS: '/settings',
|
SETTINGS: '/settings',
|
||||||
@@ -5,10 +14,11 @@ const PATHS = {
|
|||||||
SERVICE_AGREEMENT: '/service_agreement',
|
SERVICE_AGREEMENT: '/service_agreement',
|
||||||
PRIVACY_AGREEMENT: '/privacy_agreement',
|
PRIVACY_AGREEMENT: '/privacy_agreement',
|
||||||
SIGN_IN: '/mylogin',
|
SIGN_IN: '/mylogin',
|
||||||
//
|
|
||||||
|
// Order-related routes
|
||||||
ORDER_DETAIL: '/order_detail/:id',
|
ORDER_DETAIL: '/order_detail/:id',
|
||||||
getOrderDetail: (id: string) => `/order_detail/${id}`,
|
getOrderDetail: (id: string) => `/order_detail/${id}`,
|
||||||
//
|
// Tab navigation routes
|
||||||
TAB_NOT_IMPLEMENTED: '/tabs/not_implemented',
|
TAB_NOT_IMPLEMENTED: '/tabs/not_implemented',
|
||||||
EVENT_LIST: `/tabs/events`,
|
EVENT_LIST: `/tabs/events`,
|
||||||
MESSAGE_LIST: `/tabs/messages`,
|
MESSAGE_LIST: `/tabs/messages`,
|
||||||
@@ -17,6 +27,15 @@ const PATHS = {
|
|||||||
FAVOURITES_LIST: `/tabs/favourites`,
|
FAVOURITES_LIST: `/tabs/favourites`,
|
||||||
PROFILE: '/tabs/my_profile',
|
PROFILE: '/tabs/my_profile',
|
||||||
|
|
||||||
|
// partyUser
|
||||||
|
PARTY_USER_SIGN_IN: '/partyUserlogin',
|
||||||
|
PARTY_USER_SIGN_UP: '/partyUserSignUp',
|
||||||
|
|
||||||
|
DUMMY_EVENT_PAY_PAGE: '/DummyEventPayPage',
|
||||||
|
|
||||||
|
//
|
||||||
|
TABS_DEBUG: '/tabs/debug',
|
||||||
|
|
||||||
//
|
//
|
||||||
// DEMO_WEATHER_APP: '/demo-weather-app',
|
// DEMO_WEATHER_APP: '/demo-weather-app',
|
||||||
DEMO_WEATHER_APP_UI: '/demo-weather-app-ui',
|
DEMO_WEATHER_APP_UI: '/demo-weather-app-ui',
|
||||||
|
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Route definitions for pages that should show the bottom tab navigation
|
||||||
|
*
|
||||||
|
* These routes are typically main app sections like:
|
||||||
|
* - Nearby members
|
||||||
|
* - Orders
|
||||||
|
* - Messages
|
||||||
|
* - Favorites
|
||||||
|
* - Events
|
||||||
|
* - Profile
|
||||||
|
* - Demo pages
|
||||||
|
*/
|
||||||
import { Route } from 'react-router';
|
import { Route } from 'react-router';
|
||||||
import NotImplemented from './pages/NotImplemented';
|
import NotImplemented from './pages/NotImplemented';
|
||||||
import EventDetail from './pages/EventDetail';
|
import EventDetail from './pages/EventDetail';
|
||||||
@@ -12,6 +24,7 @@ import EventList from './pages/EventList';
|
|||||||
import Helloworld from './pages/Helloworld';
|
import Helloworld from './pages/Helloworld';
|
||||||
// import WeatherDemo from './pages/WeatherDemo/Tab1';
|
// import WeatherDemo from './pages/WeatherDemo/Tab1';
|
||||||
import DemoList from './pages/DemoList';
|
import DemoList from './pages/DemoList';
|
||||||
|
import DebugPage from './pages/DebugPage';
|
||||||
// import DemoReactShop from './pages/DemoReactShop';
|
// import DemoReactShop from './pages/DemoReactShop';
|
||||||
|
|
||||||
const TabAppRoute: React.FC = () => {
|
const TabAppRoute: React.FC = () => {
|
||||||
@@ -19,29 +32,31 @@ const TabAppRoute: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<Route path={PATHS.TAB_NOT_IMPLEMENTED} component={NotImplemented} />
|
<Route path={PATHS.TAB_NOT_IMPLEMENTED} component={NotImplemented} />
|
||||||
|
|
||||||
{/* */}
|
{/* Displays list of members nearby with distance and contact info */}
|
||||||
<Route path={PATHS.NEARBY_LIST} render={() => <MembersNearByList />} exact={true} />
|
<Route path={PATHS.NEARBY_LIST} render={() => <MembersNearByList />} exact={true} />
|
||||||
|
|
||||||
{/* */}
|
{/* Shows user's current and past orders with status */}
|
||||||
<Route path={PATHS.ORDERS_LIST} render={() => <OrderList />} exact={true} />
|
<Route path={PATHS.ORDERS_LIST} render={() => <OrderList />} exact={true} />
|
||||||
|
|
||||||
{/* */}
|
{/* Message inbox showing conversations with other members */}
|
||||||
<Route path={PATHS.MESSAGE_LIST} render={() => <MessageList />} exact={true} />
|
<Route path={PATHS.MESSAGE_LIST} render={() => <MessageList />} exact={true} />
|
||||||
|
|
||||||
{/* */}
|
{/* List of favorited members and events */}
|
||||||
<Route path={PATHS.FAVOURITES_LIST} render={() => <Favourites />} exact={true} />
|
<Route path={PATHS.FAVOURITES_LIST} render={() => <Favourites />} exact={true} />
|
||||||
|
|
||||||
{/* */}
|
{/* Upcoming and past events calendar view */}
|
||||||
<Route path={PATHS.EVENT_LIST} render={() => <EventList />} exact={true} />
|
<Route path={PATHS.EVENT_LIST} render={() => <EventList />} exact={true} />
|
||||||
|
|
||||||
{/* */}
|
{/* User's profile page with personal info and settings */}
|
||||||
<Route path={PATHS.PROFILE} render={() => <MyProfile />} exact={true} />
|
<Route path={PATHS.PROFILE} render={() => <MyProfile />} exact={true} />
|
||||||
|
|
||||||
{/* */}
|
{/* Demo features list for development/testing */}
|
||||||
<Route path="/tabs/demo-list" render={() => <DemoList />} exact={true} />
|
<Route path="/tabs/demo-list" render={() => <DemoList />} exact={true} />
|
||||||
|
|
||||||
{/* */}
|
{/* Simple hello world test page */}
|
||||||
<Route path="/tabs/helloworld" render={() => <Helloworld />} exact={true} />
|
<Route path="/tabs/helloworld" render={() => <Helloworld />} exact={true} />
|
||||||
|
|
||||||
|
<Route path={PATHS.TABS_DEBUG} render={() => <DebugPage />} exact={true} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,17 @@
|
|||||||
const isDev = import.meta.env.DEV;
|
const isDev = import.meta.env.DEV;
|
||||||
|
|
||||||
|
// TODO: Rename API_ENDPOINT to API_HOST in next major version
|
||||||
|
// Current API endpoint configuration - uses different values for dev/prod
|
||||||
|
const API_ENDPOINT = isDev
|
||||||
|
? import.meta.env.VITE_API_ENDPOINT
|
||||||
|
: import.meta.env.VITE_PROD_API_ENDPOINT;
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
API_ENDPOINT: isDev ? import.meta.env.VITE_API_ENDPOINT : import.meta.env.VITE_PROD_API_ENDPOINT,
|
// Base API endpoint URL (e.g. '//localhost:7272' or '//api.example.com')
|
||||||
|
// Used to construct all API request URLs
|
||||||
|
API_ENDPOINT,
|
||||||
|
SIGN_IN: `${API_ENDPOINT}/api/party-user-auth/sign-in`,
|
||||||
|
PARTY_USER_JOIN_EVENT: `${API_ENDPOINT}/api/event/partyUserJoinEvent`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!constants.API_ENDPOINT) {
|
if (!constants.API_ENDPOINT) {
|
||||||
|
@@ -16,6 +16,8 @@ const USERNAME = 'username';
|
|||||||
const ACCESS_TOKEN = 'a_token';
|
const ACCESS_TOKEN = 'a_token';
|
||||||
const ACTIVE_SESSION = 'a_session';
|
const ACTIVE_SESSION = 'a_session';
|
||||||
|
|
||||||
|
const PARTY_USER_META = 'party_user_meta';
|
||||||
|
|
||||||
export const getConfData = async () => {
|
export const getConfData = async () => {
|
||||||
console.log({ t: constants.API_ENDPOINT });
|
console.log({ t: constants.API_ENDPOINT });
|
||||||
|
|
||||||
@@ -89,14 +91,21 @@ export const getUserData = async () => {
|
|||||||
Storage.get({ key: HAS_LOGGED_IN }),
|
Storage.get({ key: HAS_LOGGED_IN }),
|
||||||
Storage.get({ key: HAS_SEEN_TUTORIAL }),
|
Storage.get({ key: HAS_SEEN_TUTORIAL }),
|
||||||
Storage.get({ key: USERNAME }),
|
Storage.get({ key: USERNAME }),
|
||||||
|
Storage.get({ key: PARTY_USER_META }),
|
||||||
]);
|
]);
|
||||||
const isLoggedin = (await response[0].value) === 'true';
|
const isLoggedin = (await response[0].value) === 'true';
|
||||||
const hasSeenTutorial = (await response[1].value) === 'true';
|
const hasSeenTutorial = (await response[1].value) === 'true';
|
||||||
const username = (await response[2].value) || undefined;
|
const username = (await response[2].value) || undefined;
|
||||||
|
|
||||||
|
let result = (await response[3].value) || undefined;
|
||||||
|
const meta = result ? JSON.parse(result) : undefined;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
isLoggedin,
|
isLoggedin,
|
||||||
hasSeenTutorial,
|
hasSeenTutorial,
|
||||||
username,
|
username,
|
||||||
|
|
||||||
|
meta,
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -120,6 +129,14 @@ export const setUsernameData = async (username?: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setPartyUserMetaData = async (party_user?: Record<string, any>) => {
|
||||||
|
if (!party_user) {
|
||||||
|
await Storage.remove({ key: PARTY_USER_META });
|
||||||
|
} else {
|
||||||
|
await Storage.set({ key: PARTY_USER_META, value: JSON.stringify(party_user) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const setAccessTokenData = async (accessToken?: string) => {
|
export const setAccessTokenData = async (accessToken?: string) => {
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
await Storage.remove({ key: ACCESS_TOKEN });
|
await Storage.remove({ key: ACCESS_TOKEN });
|
||||||
|
14
03_source/mobile/src/data/dummy/dummy.actions.ts
Normal file
14
03_source/mobile/src/data/dummy/dummy.actions.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// import { setIsLoggedInData } from '../dataApi';
|
||||||
|
import { ActionType } from '../../util/types';
|
||||||
|
|
||||||
|
export const setEventIdToJoin = (eventId: string) => {
|
||||||
|
// await setIsLoggedInData(loggedIn);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'set-dummy-event-id-to-join',
|
||||||
|
eventId,
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DummyActions = ActionType<typeof setEventIdToJoin>;
|
||||||
|
// | ActionType<typeof checkUserSession>
|
13
03_source/mobile/src/data/dummy/dummy.reducer.ts
Normal file
13
03_source/mobile/src/data/dummy/dummy.reducer.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { DummyActions as DummyActions } from './dummy.actions';
|
||||||
|
import { DummyState as DummyState } from './dummy.state';
|
||||||
|
|
||||||
|
export function dummyReducer(state: DummyState, action: DummyActions): DummyState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'set-dummy-event-id-to-join':
|
||||||
|
console.log('reducer called');
|
||||||
|
|
||||||
|
return { ...state, eventIdToJoin: action.eventId };
|
||||||
|
default:
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
}
|
3
03_source/mobile/src/data/dummy/dummy.state.ts
Normal file
3
03_source/mobile/src/data/dummy/dummy.state.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface DummyState {
|
||||||
|
eventIdToJoin?: string;
|
||||||
|
}
|
@@ -1,3 +1,16 @@
|
|||||||
|
// selectors.ts - Redux selectors for application state
|
||||||
|
//
|
||||||
|
// Contains memoized selector functions that:
|
||||||
|
// - Derive computed data from the Redux store
|
||||||
|
// - Filter and transform state for UI components
|
||||||
|
// - Optimize performance by memoizing results
|
||||||
|
//
|
||||||
|
// Key selectors:
|
||||||
|
// - getFilteredSchedule: Filters sessions by track
|
||||||
|
// - getSearchedSchedule: Filters sessions by search text
|
||||||
|
// - getGroupedFavorites: Gets favorited sessions grouped by time
|
||||||
|
// - Various entity getters (getSession, getSpeaker, etc.)
|
||||||
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { Schedule, Session, ScheduleGroup } from '../models/Schedule';
|
import { Schedule, Session, ScheduleGroup } from '../models/Schedule';
|
||||||
import { Speaker } from '../models/Speaker';
|
import { Speaker } from '../models/Speaker';
|
||||||
@@ -195,3 +208,8 @@ export const mapCenter = (state: AppState) => {
|
|||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPartyUserUsername = (state: AppState) => state.user.username;
|
||||||
|
export const getPartyUserState = (state: AppState) => state.user;
|
||||||
|
|
||||||
|
export const getEventIdToJoin = (state: AppState) => state.dummy.eventIdToJoin;
|
||||||
|
@@ -1,10 +1,21 @@
|
|||||||
import { combineReducers } from './combineReducers';
|
// state.ts - Defines the Redux store state shape and reducers
|
||||||
//
|
//
|
||||||
|
// Initial state structure:
|
||||||
|
// - data: Contains app data like sessions, speakers, events etc.
|
||||||
|
// - user: User preferences and authentication state
|
||||||
|
// - locations: Location data for maps and navigation
|
||||||
|
// - order: Order and transaction related state
|
||||||
|
|
||||||
|
import { combineReducers } from './combineReducers';
|
||||||
|
|
||||||
|
// Main feature reducers
|
||||||
import { sessionsReducer } from './sessions/sessions.reducer';
|
import { sessionsReducer } from './sessions/sessions.reducer';
|
||||||
import { userReducer } from './user/user.reducer';
|
import { userReducer } from './user/user.reducer';
|
||||||
import { locationsReducer } from './locations/locations.reducer';
|
import { locationsReducer } from './locations/locations.reducer';
|
||||||
//
|
|
||||||
|
// Additional feature reducers
|
||||||
import { orderReducer } from './sessions/orders.reducer';
|
import { orderReducer } from './sessions/orders.reducer';
|
||||||
|
import { dummyReducer } from './dummy/dummy.reducer';
|
||||||
|
|
||||||
export const initialState: AppState = {
|
export const initialState: AppState = {
|
||||||
data: {
|
data: {
|
||||||
@@ -30,10 +41,14 @@ export const initialState: AppState = {
|
|||||||
loading: false,
|
loading: false,
|
||||||
//
|
//
|
||||||
isSessionValid: false,
|
isSessionValid: false,
|
||||||
|
//
|
||||||
},
|
},
|
||||||
locations: {
|
locations: {
|
||||||
locations: [],
|
locations: [],
|
||||||
},
|
},
|
||||||
|
dummy: {
|
||||||
|
eventIdToJoin: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducers = combineReducers({
|
export const reducers = combineReducers({
|
||||||
@@ -42,6 +57,7 @@ export const reducers = combineReducers({
|
|||||||
locations: locationsReducer,
|
locations: locationsReducer,
|
||||||
//
|
//
|
||||||
order: orderReducer,
|
order: orderReducer,
|
||||||
|
dummy: dummyReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppState = ReturnType<typeof reducers>;
|
export type AppState = ReturnType<typeof reducers>;
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
setAccessTokenData,
|
setAccessTokenData,
|
||||||
getAccessTokenData,
|
getAccessTokenData,
|
||||||
setActiveSessionData,
|
setActiveSessionData,
|
||||||
|
setPartyUserMetaData,
|
||||||
} from '../dataApi';
|
} from '../dataApi';
|
||||||
import { ActionType } from '../../util/types';
|
import { ActionType } from '../../util/types';
|
||||||
import { UserState } from './user.state';
|
import { UserState } from './user.state';
|
||||||
@@ -34,6 +35,14 @@ export const setData = (data: Partial<UserState>) =>
|
|||||||
data,
|
data,
|
||||||
}) as const;
|
}) as const;
|
||||||
|
|
||||||
|
export const setPartyUserMeta = async (partyUserMeta: Record<string, any>) => {
|
||||||
|
await setPartyUserMetaData(partyUserMeta);
|
||||||
|
return {
|
||||||
|
type: 'set-party-user-meta',
|
||||||
|
partyUserMeta,
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
|
export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
|
||||||
//
|
//
|
||||||
await setIsLoggedInData(false);
|
await setIsLoggedInData(false);
|
||||||
@@ -124,6 +133,7 @@ export const setDarkMode = (darkMode: boolean) =>
|
|||||||
export type UserActions =
|
export type UserActions =
|
||||||
| ActionType<typeof setLoading>
|
| ActionType<typeof setLoading>
|
||||||
| ActionType<typeof setData>
|
| ActionType<typeof setData>
|
||||||
|
| ActionType<typeof setPartyUserMeta>
|
||||||
| ActionType<typeof setIsLoggedIn>
|
| ActionType<typeof setIsLoggedIn>
|
||||||
| ActionType<typeof setUsername>
|
| ActionType<typeof setUsername>
|
||||||
| ActionType<typeof setHasSeenTutorial>
|
| ActionType<typeof setHasSeenTutorial>
|
||||||
|
@@ -19,6 +19,9 @@ export function userReducer(state: UserState, action: UserActions): UserState {
|
|||||||
return { ...state, token: action.token };
|
return { ...state, token: action.token };
|
||||||
case 'check-user-session':
|
case 'check-user-session':
|
||||||
return { ...state, isSessionValid: action.sessionValid };
|
return { ...state, isSessionValid: action.sessionValid };
|
||||||
|
case 'set-party-user-meta':
|
||||||
|
return { ...state, meta: action.partyUserMeta };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { ...state };
|
return { ...state };
|
||||||
}
|
}
|
||||||
|
@@ -7,4 +7,19 @@ export interface UserState {
|
|||||||
isSessionValid: boolean;
|
isSessionValid: boolean;
|
||||||
session?: any;
|
session?: any;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
|
||||||
|
//
|
||||||
|
meta?: {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
company?: string;
|
||||||
|
role?: string;
|
||||||
|
rank?: string;
|
||||||
|
isVerified?: Boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
accessToken?: string;
|
||||||
}
|
}
|
||||||
|
13
03_source/mobile/src/pages/DebugPage/TestContent.tsx
Normal file
13
03_source/mobile/src/pages/DebugPage/TestContent.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
export const TestContent = {
|
||||||
|
eventDate: format(new Date(), 'yyyy-MM-dd'),
|
||||||
|
title: 'helloworld',
|
||||||
|
price: 123,
|
||||||
|
currency: 'HKD',
|
||||||
|
duration_m: 480,
|
||||||
|
ageBottom: 12,
|
||||||
|
ageTop: 48,
|
||||||
|
location: 'Hong Kong Island',
|
||||||
|
avatar: 'https://www.ionics.io/img/ionic-logo.png',
|
||||||
|
};
|
100
03_source/mobile/src/pages/DebugPage/index.tsx
Normal file
100
03_source/mobile/src/pages/DebugPage/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// REQ0054/user-setting
|
||||||
|
//
|
||||||
|
// PURPOSE:
|
||||||
|
// - Provides functionality view user profile
|
||||||
|
//
|
||||||
|
// RULES:
|
||||||
|
// - T.B.A.
|
||||||
|
//
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
IonPage,
|
||||||
|
IonButtons,
|
||||||
|
useIonRouter,
|
||||||
|
IonButton,
|
||||||
|
IonIcon,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import { Speaker } from '../../models/Speaker';
|
||||||
|
import { Session } from '../../models/Schedule';
|
||||||
|
import { connect } from '../../data/connect';
|
||||||
|
import * as selectors from '../../data/selectors';
|
||||||
|
import '../SpeakerList.scss';
|
||||||
|
import { chevronBackOutline, settingsOutline } from 'ionicons/icons';
|
||||||
|
import { logoutUser, setAccessToken, setIsLoggedIn } from '../../data/user/user.actions';
|
||||||
|
import { UserState } from '../../data/user/user.state';
|
||||||
|
|
||||||
|
interface OwnProps {}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
speakers: Speaker[];
|
||||||
|
speakerSessions: { [key: string]: Session[] };
|
||||||
|
partyUserState: UserState;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {
|
||||||
|
logoutUser: typeof logoutUser;
|
||||||
|
setAccessToken: typeof setAccessToken;
|
||||||
|
setIsLoggedIn: typeof setIsLoggedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageProps extends OwnProps, StateProps, DispatchProps {}
|
||||||
|
|
||||||
|
const DemoList: React.FC<PageProps> = ({ partyUserState }) => {
|
||||||
|
const router = useIonRouter();
|
||||||
|
|
||||||
|
function handleBackButtonClick() {
|
||||||
|
router.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonPage id="speaker-list">
|
||||||
|
<IonHeader translucent={true} className="ion-no-border">
|
||||||
|
<IonToolbar>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonButton shape="round" onClick={() => handleBackButtonClick()}>
|
||||||
|
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
|
||||||
|
</IonButton>
|
||||||
|
</IonButtons>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-start' }}>
|
||||||
|
<IonIcon icon={settingsOutline} size="large"></IonIcon>
|
||||||
|
<IonTitle>Debug page</IonTitle>
|
||||||
|
</div>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
|
<IonContent fullscreen={true}>
|
||||||
|
<IonHeader collapse="condense">
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle size="large">Debug page</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
|
<div>helloworld debug page</div>
|
||||||
|
<pre>{JSON.stringify({ partyUserState }, null, 2)}</pre>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect<OwnProps, StateProps, DispatchProps>({
|
||||||
|
mapStateToProps: (state) => {
|
||||||
|
console.log({ state });
|
||||||
|
return {
|
||||||
|
speakers: selectors.getSpeakers(state),
|
||||||
|
speakerSessions: selectors.getSpeakerSessions(state),
|
||||||
|
//
|
||||||
|
partyUserState: selectors.getPartyUserState(state),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mapDispatchToProps: {
|
||||||
|
logoutUser,
|
||||||
|
setAccessToken,
|
||||||
|
setIsLoggedIn,
|
||||||
|
},
|
||||||
|
component: React.memo(DemoList),
|
||||||
|
});
|
103
03_source/mobile/src/pages/DebugPage/style.scss
Normal file
103
03_source/mobile/src/pages/DebugPage/style.scss
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#about-page {
|
||||||
|
ion-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
--background: transparent;
|
||||||
|
--color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-toolbar ion-back-button,
|
||||||
|
ion-toolbar ion-button,
|
||||||
|
ion-toolbar ion-menu-button {
|
||||||
|
--color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .about-image {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
transition: opacity 500ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .madison {
|
||||||
|
background-image: url('/assets/img/about/madison.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .austin {
|
||||||
|
background-image: url('/assets/img/about/austin.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .chicago {
|
||||||
|
background-image: url('/assets/img/about/chicago.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .seattle {
|
||||||
|
background-image: url('/assets/img/about/seattle.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info {
|
||||||
|
position: relative;
|
||||||
|
margin-top: -10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--ion-background-color, #fff);
|
||||||
|
z-index: 2; // display rounded border above header image
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info ion-list {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info p {
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
color: var(--ion-color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info ion-icon {
|
||||||
|
margin-inline-end: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* iOS Only
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ios .about-info {
|
||||||
|
--ion-padding: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios .about-info h3 {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#date-input-popover {
|
||||||
|
--offset-y: -var(--ion-safe-area-bottom);
|
||||||
|
|
||||||
|
--max-width: 90%;
|
||||||
|
--width: 336px;
|
||||||
|
}
|
14
03_source/mobile/src/pages/DebugPage/types.ts
Normal file
14
03_source/mobile/src/pages/DebugPage/types.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export interface Event {
|
||||||
|
eventDate: Date;
|
||||||
|
joinMembers: undefined;
|
||||||
|
title: string;
|
||||||
|
price: number;
|
||||||
|
currency: string;
|
||||||
|
duration_m: number;
|
||||||
|
ageBottom: number;
|
||||||
|
ageTop: number;
|
||||||
|
location: string;
|
||||||
|
avatar: string;
|
||||||
|
//
|
||||||
|
id: string;
|
||||||
|
}
|
131
03_source/mobile/src/pages/DummyEventPayPage/index.tsx
Normal file
131
03_source/mobile/src/pages/DummyEventPayPage/index.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
// REQ0041/home_discover_event_tab
|
||||||
|
|
||||||
|
import {
|
||||||
|
IonPage,
|
||||||
|
IonHeader,
|
||||||
|
IonToolbar,
|
||||||
|
IonButtons,
|
||||||
|
IonButton,
|
||||||
|
IonIcon,
|
||||||
|
IonTitle,
|
||||||
|
IonContent,
|
||||||
|
useIonRouter,
|
||||||
|
IonToast,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import { chevronBackOutline, menuOutline } from 'ionicons/icons';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import './style.scss';
|
||||||
|
import PATHS from '../../PATHS';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { UserState } from '../../data/user/user.state';
|
||||||
|
import { connect } from '../../data/connect';
|
||||||
|
|
||||||
|
import * as selectors from '../../data/selectors';
|
||||||
|
import constants from '../../constants';
|
||||||
|
|
||||||
|
interface OwnProps {}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
isLoggedin: boolean;
|
||||||
|
//
|
||||||
|
partyUserState: UserState;
|
||||||
|
//
|
||||||
|
joinEventId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchProps {}
|
||||||
|
|
||||||
|
interface PageProps extends OwnProps, StateProps, DispatchProps {}
|
||||||
|
|
||||||
|
const DummyPayPage: React.FC<PageProps> = ({
|
||||||
|
isLoggedin,
|
||||||
|
partyUserState,
|
||||||
|
//
|
||||||
|
joinEventId,
|
||||||
|
}) => {
|
||||||
|
const router = useIonRouter();
|
||||||
|
|
||||||
|
// if (!isLoggedin) return <NotLoggedIn />;
|
||||||
|
|
||||||
|
async function handlePayClick() {
|
||||||
|
try {
|
||||||
|
await axios.post(constants.PARTY_USER_JOIN_EVENT, {
|
||||||
|
data: {
|
||||||
|
eventItemId: joinEventId,
|
||||||
|
email: partyUserState.meta?.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
router.goBack();
|
||||||
|
|
||||||
|
setShowJoinOKToast(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancelClick() {
|
||||||
|
router.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [showJoinOKToast, setShowJoinOKToast] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonPage id="speaker-list">
|
||||||
|
<IonHeader translucent={true} className="ion-no-border">
|
||||||
|
<IonToolbar>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
{/* <IonMenuButton /> */}
|
||||||
|
<IonButton shape="round">
|
||||||
|
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
|
||||||
|
</IonButton>
|
||||||
|
</IonButtons>
|
||||||
|
<IonTitle>Dummy pay event page</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
|
<IonContent fullscreen={true}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '1rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
|
||||||
|
padding: '3rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>This is a dummy page to emulate payment gateway work</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>pay for event</div>
|
||||||
|
<pre style={{ backgroundColor: 'RGB(0,0,0, 0.1)' }}>{JSON.stringify(joinEventId)}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IonButton onClick={handlePayClick}>Pay</IonButton>
|
||||||
|
<IonButton onClick={handleCancelClick}>Cancel</IonButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IonToast
|
||||||
|
isOpen={showJoinOKToast}
|
||||||
|
message="ok, event paid, thank you..."
|
||||||
|
duration={2000}
|
||||||
|
// onDidDismiss={() => setShowJoinOKToast(false)}
|
||||||
|
/>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect<OwnProps, StateProps, DispatchProps>({
|
||||||
|
mapStateToProps: (state) => ({
|
||||||
|
isLoggedin: state.user.isLoggedin,
|
||||||
|
//
|
||||||
|
joinEventId: selectors.getEventIdToJoin(state),
|
||||||
|
//
|
||||||
|
partyUserState: selectors.getPartyUserState(state),
|
||||||
|
}),
|
||||||
|
component: DummyPayPage,
|
||||||
|
});
|
103
03_source/mobile/src/pages/DummyEventPayPage/style.scss
Normal file
103
03_source/mobile/src/pages/DummyEventPayPage/style.scss
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#about-page {
|
||||||
|
ion-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
--background: transparent;
|
||||||
|
--color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-toolbar ion-back-button,
|
||||||
|
ion-toolbar ion-button,
|
||||||
|
ion-toolbar ion-menu-button {
|
||||||
|
--color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .about-image {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
transition: opacity 500ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .madison {
|
||||||
|
background-image: url('/assets/img/about/madison.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .austin {
|
||||||
|
background-image: url('/assets/img/about/austin.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .chicago {
|
||||||
|
background-image: url('/assets/img/about/chicago.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header .seattle {
|
||||||
|
background-image: url('/assets/img/about/seattle.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info {
|
||||||
|
position: relative;
|
||||||
|
margin-top: -10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--ion-background-color, #fff);
|
||||||
|
z-index: 2; // display rounded border above header image
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info ion-list {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info p {
|
||||||
|
line-height: 130%;
|
||||||
|
|
||||||
|
color: var(--ion-color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-info ion-icon {
|
||||||
|
margin-inline-end: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* iOS Only
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ios .about-info {
|
||||||
|
--ion-padding: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios .about-info h3 {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#date-input-popover {
|
||||||
|
--offset-y: -var(--ion-safe-area-bottom);
|
||||||
|
|
||||||
|
--max-width: 90%;
|
||||||
|
--width: 336px;
|
||||||
|
}
|
@@ -13,48 +13,27 @@ import {
|
|||||||
IonContent,
|
IonContent,
|
||||||
IonPage,
|
IonPage,
|
||||||
IonButtons,
|
IonButtons,
|
||||||
IonMenuButton,
|
|
||||||
IonButton,
|
IonButton,
|
||||||
IonIcon,
|
IonIcon,
|
||||||
IonDatetime,
|
|
||||||
IonSelectOption,
|
|
||||||
IonList,
|
|
||||||
IonItem,
|
|
||||||
IonLabel,
|
|
||||||
IonSelect,
|
|
||||||
IonPopover,
|
IonPopover,
|
||||||
IonText,
|
|
||||||
IonFooter,
|
IonFooter,
|
||||||
useIonRouter,
|
useIonRouter,
|
||||||
IonAvatar,
|
|
||||||
} from '@ionic/react';
|
} from '@ionic/react';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import {
|
import {
|
||||||
accessibility,
|
accessibility,
|
||||||
accessibilityOutline,
|
|
||||||
chevronBackOutline,
|
chevronBackOutline,
|
||||||
ellipsisHorizontal,
|
ellipsisHorizontal,
|
||||||
ellipsisVertical,
|
ellipsisVertical,
|
||||||
heart,
|
|
||||||
locationOutline,
|
|
||||||
locationSharp,
|
locationSharp,
|
||||||
logoIonic,
|
|
||||||
man,
|
man,
|
||||||
manOutline,
|
|
||||||
people,
|
people,
|
||||||
peopleOutline,
|
|
||||||
timer,
|
|
||||||
timerOutline,
|
|
||||||
timerSharp,
|
timerSharp,
|
||||||
wallet,
|
|
||||||
walletOutline,
|
|
||||||
walletSharp,
|
walletSharp,
|
||||||
woman,
|
woman,
|
||||||
womanOutline,
|
|
||||||
} from 'ionicons/icons';
|
} from 'ionicons/icons';
|
||||||
import AboutPopover from '../../components/AboutPopover';
|
import AboutPopover from '../../components/AboutPopover';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { TestContent } from './TestContent';
|
|
||||||
import { Helloworld } from '../../api/Helloworld';
|
import { Helloworld } from '../../api/Helloworld';
|
||||||
import { getEventById } from '../../api/getEventById';
|
import { getEventById } from '../../api/getEventById';
|
||||||
import { connect } from '../../data/connect';
|
import { connect } from '../../data/connect';
|
||||||
@@ -62,6 +41,9 @@ import * as selectors from '../../data/selectors';
|
|||||||
import { Event } from '../../models/Event';
|
import { Event } from '../../models/Event';
|
||||||
import { RouteComponentProps } from 'react-router';
|
import { RouteComponentProps } from 'react-router';
|
||||||
import AvatarRow from './AvatarRow';
|
import AvatarRow from './AvatarRow';
|
||||||
|
import { setPartyUserMeta } from '../../data/user/user.actions';
|
||||||
|
import { setEventIdToJoin } from '../../data/dummy/dummy.actions';
|
||||||
|
import PATHS from '../../PATHS';
|
||||||
|
|
||||||
const leftShift: number = -25;
|
const leftShift: number = -25;
|
||||||
|
|
||||||
@@ -71,9 +53,12 @@ interface OwnProps extends RouteComponentProps {
|
|||||||
|
|
||||||
interface StateProps {}
|
interface StateProps {}
|
||||||
|
|
||||||
interface DispatchProps {}
|
interface DispatchProps {
|
||||||
|
setPartyUserMeta: typeof setPartyUserMeta;
|
||||||
|
setEventIdToJoin: typeof setEventIdToJoin;
|
||||||
|
}
|
||||||
|
|
||||||
interface EventDetailProps extends OwnProps, StateProps, DispatchProps {}
|
interface PageProps extends OwnProps, StateProps, DispatchProps {}
|
||||||
|
|
||||||
const showJoinedMembers = (joinMembers: Record<string, any>[]) => {
|
const showJoinedMembers = (joinMembers: Record<string, any>[]) => {
|
||||||
const avatars = joinMembers.map((jm) => jm.avatar);
|
const avatars = joinMembers.map((jm) => jm.avatar);
|
||||||
@@ -90,7 +75,7 @@ const showJoinedMembers = (joinMembers: Record<string, any>[]) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EventDetail: React.FC<EventDetailProps> = ({ event_detail }) => {
|
const EventDetail: React.FC<PageProps> = ({ event_detail, setEventIdToJoin }) => {
|
||||||
const router = useIonRouter();
|
const router = useIonRouter();
|
||||||
|
|
||||||
const [showPopover, setShowPopover] = useState(false);
|
const [showPopover, setShowPopover] = useState(false);
|
||||||
@@ -136,6 +121,13 @@ const EventDetail: React.FC<EventDetailProps> = ({ event_detail }) => {
|
|||||||
router.goBack();
|
router.goBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleJoinClick() {
|
||||||
|
if (event_detail && event_detail?.id) {
|
||||||
|
setEventIdToJoin(event_detail.id);
|
||||||
|
router.push(PATHS.DUMMY_EVENT_PAY_PAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!event_detail) return <>loading</>;
|
if (!event_detail) return <>loading</>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -268,7 +260,7 @@ const EventDetail: React.FC<EventDetailProps> = ({ event_detail }) => {
|
|||||||
margin: '1rem',
|
margin: '1rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IonButton expand="full" shape="round">
|
<IonButton expand="full" shape="round" onClick={handleJoinClick}>
|
||||||
Join
|
Join
|
||||||
</IonButton>
|
</IonButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -286,8 +278,10 @@ const EventDetail: React.FC<EventDetailProps> = ({ event_detail }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default connect({
|
export default connect({
|
||||||
|
mapDispatchToProps: {
|
||||||
|
setEventIdToJoin,
|
||||||
|
},
|
||||||
mapStateToProps: (state, ownProps) => {
|
mapStateToProps: (state, ownProps) => {
|
||||||
console.log({ t1: selectors.getEvents(state) });
|
|
||||||
return {
|
return {
|
||||||
event_detail: selectors.getEvent(state, ownProps),
|
event_detail: selectors.getEvent(state, ownProps),
|
||||||
};
|
};
|
||||||
|
@@ -14,7 +14,7 @@ import { menuOutline } from 'ionicons/icons';
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
const Helloworld: React.FC = () => {
|
const Helloworld: React.FC = ({}) => {
|
||||||
return (
|
return (
|
||||||
<IonPage id="speaker-list">
|
<IonPage id="speaker-list">
|
||||||
<IonHeader translucent={true} className="ion-no-border">
|
<IonHeader translucent={true} className="ion-no-border">
|
||||||
|
30
03_source/mobile/src/pages/Login/Login.scss
Normal file
30
03_source/mobile/src/pages/Login/Login.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Shared Authentication Styles
|
||||||
|
*
|
||||||
|
* Contains common styling for:
|
||||||
|
* - Login page
|
||||||
|
* - Signup page
|
||||||
|
* - Support page
|
||||||
|
*
|
||||||
|
* Key features:
|
||||||
|
* - Logo positioning and sizing
|
||||||
|
* - Form list styling
|
||||||
|
* - Responsive design for all screen sizes
|
||||||
|
*/
|
||||||
|
#login-page,
|
||||||
|
#signup-page,
|
||||||
|
#support-page {
|
||||||
|
.login-logo {
|
||||||
|
padding: 20px 0;
|
||||||
|
min-height: 200px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo img {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Login Page Component
|
||||||
|
*
|
||||||
|
* Handles user authentication with:
|
||||||
|
* - Username/password input
|
||||||
|
* - Form validation
|
||||||
|
* - Login state management
|
||||||
|
* - Navigation to signup page
|
||||||
|
*
|
||||||
|
* Connects to Redux store for:
|
||||||
|
* - Setting authentication state
|
||||||
|
* - Storing username
|
||||||
|
*/
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IonHeader,
|
IonHeader,
|
||||||
@@ -16,8 +29,8 @@ import {
|
|||||||
IonText,
|
IonText,
|
||||||
} from '@ionic/react';
|
} from '@ionic/react';
|
||||||
import './Login.scss';
|
import './Login.scss';
|
||||||
import { setIsLoggedIn, setUsername } from '../data/user/user.actions';
|
import { setIsLoggedIn, setUsername } from '../../data/user/user.actions';
|
||||||
import { connect } from '../data/connect';
|
import { connect } from '../../data/connect';
|
||||||
import { RouteComponentProps } from 'react-router';
|
import { RouteComponentProps } from 'react-router';
|
||||||
|
|
||||||
interface OwnProps extends RouteComponentProps {}
|
interface OwnProps extends RouteComponentProps {}
|
||||||
@@ -59,12 +72,12 @@ const Login: React.FC<LoginProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IonPage id="login-page">
|
<IonPage id="login-page">
|
||||||
<IonHeader>
|
<IonHeader className="ion-no-border">
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
<IonButtons slot="start">
|
<IonButtons slot="start">
|
||||||
<IonMenuButton></IonMenuButton>
|
<IonMenuButton></IonMenuButton>
|
||||||
</IonButtons>
|
</IonButtons>
|
||||||
<IonTitle>Login</IonTitle>
|
<IonTitle>Example Login Page</IonTitle>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
<IonContent>
|
<IonContent>
|
@@ -1,11 +1,21 @@
|
|||||||
// REQ0053/profile-page
|
/**
|
||||||
//
|
* Not Logged In Profile View Component
|
||||||
// PURPOSE:
|
*
|
||||||
// - Provides functionality view user profile
|
* Displays when user is not authenticated, with:
|
||||||
//
|
* - Visual placeholder for profile
|
||||||
// RULES:
|
* - Login button (regular user)
|
||||||
// - T.B.A.
|
* - Party member login button
|
||||||
//
|
* - Signup encouragement
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Pull-to-refresh functionality
|
||||||
|
* - Responsive design for all screen sizes
|
||||||
|
* - Clear call-to-action buttons
|
||||||
|
*
|
||||||
|
* Connected to:
|
||||||
|
* - Redux store for speaker data
|
||||||
|
* - Router for navigation
|
||||||
|
*/
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IonHeader,
|
IonHeader,
|
||||||
@@ -104,6 +114,18 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [disableForwardPartyUserLoginButton, setDisableForwardPartyUserLoginButton] =
|
||||||
|
useState(false);
|
||||||
|
function handleForwardPartyUserLoginPage() {
|
||||||
|
try {
|
||||||
|
setDisableForwardPartyUserLoginButton(true);
|
||||||
|
router.push(PATHS.PARTY_USER_SIGN_IN);
|
||||||
|
setDisableForwardPartyUserLoginButton(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getProfileById('2').then(({ data }) => {
|
getProfileById('2').then(({ data }) => {
|
||||||
console.log({ data });
|
console.log({ data });
|
||||||
@@ -180,6 +202,7 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
|
|||||||
not login yet, <br />
|
not login yet, <br />
|
||||||
please login or sign up
|
please login or sign up
|
||||||
</div>
|
</div>
|
||||||
|
{/* */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -192,6 +215,24 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
|
|||||||
Login
|
Login
|
||||||
</IonButton>
|
</IonButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* */}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IonButton
|
||||||
|
disabled={disableForwardPartyUserLoginButton}
|
||||||
|
onClick={handleForwardPartyUserLoginPage}
|
||||||
|
>
|
||||||
|
Party Member Login
|
||||||
|
</IonButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</IonContent>
|
</IonContent>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user