Compare commits

..

7 Commits

97 changed files with 4901 additions and 416 deletions

View File

@@ -28,7 +28,8 @@
"db:generate": "prisma generate",
"db:push": "prisma db push --force-reset",
"db:push:w": "npx nodemon --delay 1 --watch prisma --ext \"ts,tsx,prisma\" --exec \"yarn db:push && yarn seed\"",
"db:studio": "prisma studio"
"db:studio": "prisma studio",
"db:studio:w": "npx nodemon --delay 1 --watch prisma --ext \"prisma\" --exec \"yarn db:studio\""
},
"engines": {
"node": ">=20"

View File

@@ -71,6 +71,15 @@ model VerificationToken {
@@unique([identifier, token])
}
model Student {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
email String @unique
metadata Json @default("{}")
}
// ----------------------------------------------------------------
// frontend/src/_mock/_user.ts
@@ -525,6 +534,7 @@ model UserCard {
totalFollowing Float
}
// `UserItem` obsoleted, use `UserMeta` instead
model UserItem {
id String @id @default(uuid())
createdAt DateTime @default(now())
@@ -550,6 +560,31 @@ model UserItem {
isAdmin Boolean @default(false)
}
model UserMeta {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
name String
city String
role String
email String
state String
status String
address String
country String
zipCode String
company String
avatarUrl String
phoneNumber String
isVerified Boolean
//
username String
password String
//
isAdmin Boolean @default(false)
}
model UserAccountBillingHistory {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())

View File

@@ -30,6 +30,7 @@ import { EventItemSeed } from './seeds/eventItem';
import { EventReviewSeed } from './seeds/eventReview';
import { appLogSeed } from './seeds/AppLog';
import { accessLogSeed } from './seeds/AccessLog';
import { userMetaSeed } from './seeds/userMeta';
//
// import { Blog } from './seeds/blog';
@@ -46,7 +47,10 @@ import { accessLogSeed } from './seeds/AccessLog';
await ProductReview;
await FileStore;
await ProductItem;
await userItemSeed;
await userMetaSeed;
await orderItemSeed;
await invoiceItemSeed;
//

View File

@@ -0,0 +1,130 @@
import { PrismaClient } from '@prisma/client';
import { generateHash } from 'src/utils/hash';
import { Config, names, uniqueNamesGenerator } from 'unique-names-generator';
import { faker } from '@faker-js/faker';
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 jaFaker } from '@faker-js/faker/locale/ja';
import { faker as koFaker } from '@faker-js/faker/locale/ko';
import { faker as twFaker } from '@faker-js/faker/locale/zh_TW';
const SEED_EMAIL_DOMAIN = 'seed.com';
const prisma = new PrismaClient();
async function userMeta() {
const config: Config = { dictionaries: [names] };
const firstName = uniqueNamesGenerator(config);
const lastName = uniqueNamesGenerator(config);
const username = `${firstName.toLowerCase()}-${lastName.toLowerCase()}`;
const alice = await prisma.userMeta.upsert({
where: { id: 'admin_uuid' },
update: {},
create: {
name: `admin test`,
city: '',
role: '',
email: `admin@123.com`,
state: '',
status: '',
address: '',
country: '',
zipCode: '',
company: '',
avatarUrl: '',
phoneNumber: '',
isVerified: true,
//
username: 'admin@123.com',
password: await generateHash('Aa1234567'),
//
isAdmin: true,
},
});
for (let i = 1; i < 3; i++) {
const CJK_LOCALES = {
en: enFaker,
zh: zhFaker,
ja: jaFaker,
ko: koFaker,
tw: twFaker,
};
function getRandomCJKFaker() {
const locales = Object.keys(CJK_LOCALES);
const randomKey = locales[Math.floor(Math.random() * locales.length)] as keyof typeof CJK_LOCALES;
return CJK_LOCALES[randomKey];
}
const randomFaker = getRandomCJKFaker();
await prisma.userMeta.upsert({
where: { id: i.toString() },
update: {},
create: {
name: randomFaker.person.fullName(),
city: randomFaker.location.city(),
role: ROLE[Math.floor(Math.random() * ROLE.length)],
email: randomFaker.internet.email(),
state: randomFaker.location.state(),
status: STATUS[Math.floor(Math.random() * STATUS.length)],
address: randomFaker.location.streetAddress(),
country: randomFaker.location.country(),
zipCode: randomFaker.location.zipCode(),
company: randomFaker.company.name(),
avatarUrl: randomFaker.image.avatar(),
phoneNumber: randomFaker.phone.number(),
isVerified: true,
//
username: randomFaker.internet.username(),
password: await generateHash('Abc1234!'),
//
isAdmin: false,
},
});
}
console.log('seed userMeta done');
}
const userMetaSeed = userMeta()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
export { userMetaSeed };
const ROLE = [
`CEO`,
`CTO`,
`Project Coordinator`,
`Team Leader`,
`Software Developer`,
`Marketing Strategist`,
`Data Analyst`,
`Product Owner`,
`Graphic Designer`,
`Operations Manager`,
`Customer Support Specialist`,
`Sales Manager`,
`HR Recruiter`,
`Business Consultant`,
`Financial Planner`,
`Network Engineer`,
`Content Creator`,
`Quality Assurance Tester`,
`Public Relations Officer`,
`IT Administrator`,
`Compliance Officer`,
`Event Planner`,
`Legal Counsel`,
`Training Coordinator`,
];
const STATUS = ['active', 'pending', 'banned'];

View File

@@ -25,6 +25,7 @@ export async function POST(req: NextRequest) {
const { data } = await req.json();
try {
// TODO: obsolete createNewAppLog
const createResult = await createNewAppLog(data);
return response(createResult, STATUS.OK);

View File

@@ -1,13 +1,20 @@
import type { User } from '@prisma/client';
import type { NextRequest } from 'next/server';
import { headers } from 'next/headers';
import { verify } from 'src/utils/jwt';
import { STATUS, response, handleError } from 'src/utils/response';
import { _users, JWT_SECRET } from 'src/_mock/_auth';
import { JWT_SECRET } from 'src/_mock/_auth';
import { getUserById } from 'src/app/services/user.service';
import { createAccessLog } from 'src/app/services/AccessLog.service';
import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
export const runtime = 'edge';
// export const runtime = 'edge';
/**
* This API is used for demo purpose only
@@ -17,25 +24,44 @@ export const runtime = 'edge';
* You should not expose the JWT_SECRET in the client side
*/
export async function GET() {
const USER_TOKEN_CHECK_FAILED = 'user token check failed';
const INVALID_AUTH_TOKEN = 'Invalid authorization token';
const USER_ID_NOT_FOUND = 'userId not found';
const USER_TOKEN_OK = 'user token check ok';
const AUTHORIZATION_TOKEN_MISSING_OR_INVALID = 'Authorization token missing or invalid';
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: 'Authorization token missing or invalid' }, STATUS.UNAUTHORIZED);
return response({ message: AUTHORIZATION_TOKEN_MISSING_OR_INVALID }, STATUS.UNAUTHORIZED);
}
const accessToken = `${authorization}`.split(' ')[1];
const data = await verify(accessToken, JWT_SECRET);
console.log(data.userId);
const currentUser = _users.find((user) => user.id === data.userId);
if (data.userId) {
// TODO: remove me
// const currentUser = _users.find((user) => user.id === data.userId);
const currentUser: User | null = await getUserById(data.userId);
if (!currentUser) {
return response({ message: 'Invalid authorization token' }, STATUS.UNAUTHORIZED);
if (!currentUser) {
createAccessLog('', USER_TOKEN_CHECK_FAILED, debug);
return response({ message: INVALID_AUTH_TOKEN }, STATUS.UNAUTHORIZED);
}
createAccessLog(currentUser.id, USER_TOKEN_OK, debug);
return response({ user: currentUser }, STATUS.OK);
} else {
return response({ message: USER_ID_NOT_FOUND }, STATUS.ERROR);
}
return response({ user: currentUser }, 200);
} catch (error) {
return handleError('[Auth] - Me', error);
}

View File

@@ -0,0 +1,25 @@
###
# username and password ok
GET http://localhost:7272/api/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWJnbnUyengwMDBjaHEzaGZ3dmtjejlvIiwiaWF0IjoxNzQ4OTY0ODkyLCJleHAiOjE3NTAxNzQ0OTJ9.lo04laCxtm0IVeYaETEV3hXKyDmXPEn7SyWtY2VR4dI
###
# There is no user corresponding to the email address.
POST http://localhost:7272/api/auth/sign-in
content-type: application/json
{
"email": "demo@minimals1.cc",
"password": "@2Minimal"
}
###
# Wrong password
POST http://localhost:7272/api/auth/sign-in
content-type: application/json
{
"email": "demo@minimals.cc",
"password": "@2Min111imal"
}

View File

@@ -0,0 +1,5 @@
import type { NextRequest } from 'next/server';
export function flattenNextjsRequest(req: NextRequest) {
return Object.fromEntries(req.headers.entries());
}

View File

@@ -7,6 +7,7 @@ import { JWT_SECRET, JWT_EXPIRES_IN } from 'src/_mock/_auth';
import { createAccessLog } from 'src/app/services/AccessLog.service';
import prisma from '../../../lib/prisma';
import { flattenNextjsRequest } from './flattenNextjsRequest';
// ----------------------------------------------------------------------
@@ -22,7 +23,7 @@ const ERR_USER_NOT_FOUND = 'There is no user corresponding to the email address.
const ERR_WRONG_PASSWORD = 'Wrong password';
export async function POST(req: NextRequest) {
const debug = { 'req.headers': Object.fromEntries(req.headers.entries()) };
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const { email, password } = await req.json();

View File

@@ -0,0 +1,25 @@
# GUIDELINE
## Event / event
- this is a `event` api endpoint
- this is a demo to handle `event` record
- use single file for single db table/collection only
## `route.ts`
handle `GET`, `POST`, `PUT`, `DELETE`
## `test.http`
store test request
## `../../services/event.service.ts`
event schema CRUD handler
`listEvents` - list `event` record
`getEvent` - get `event` record by id
`createNewEvent` - create `event` record
`updateEvent` - update `event` record by id
`deleteEvent` - delete `event` record by id

View File

@@ -8,10 +8,11 @@
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
import { L_INFO, L_ERROR } from 'src/constants';
import { getEvent } from 'src/app/services/eventItem.service';
import { createAppLog } from 'src/app/services/AppLog.service';
// ----------------------------------------------------------------------
@@ -19,29 +20,32 @@ import prisma from '../../../lib/prisma';
* GET Event detail
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
const debug = { 'req.headers': Object.fromEntries(req.headers.entries()) };
// RULES: eventId must exist
const eventId = searchParams.get('eventId');
const { searchParams } = req.nextUrl;
// RULES: for the incoming request, the `eventId` must exist
const eventId = searchParams.get('eventId');
try {
if (!eventId) {
return response({ message: 'Event ID is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: eventId confirmed exist, run below
const event = await prisma.eventItem.findFirst({
include: { reviews: true },
where: { id: eventId },
});
// NOTE: `eventId` confirmed exist, run below
const event = await getEvent(eventId);
console.log({ event });
// RULES: show error if not found
if (!event) {
return response({ message: 'Event not found!' }, STATUS.NOT_FOUND);
}
logger('[Event] details', event.id);
// logger('[Event] details', event.id);
await createAppLog(L_INFO, 'get event detail ok', { eventId });
return response({ event }, STATUS.OK);
} catch (error) {
await createAppLog(L_ERROR, 'error during getting event detail', { debug, eventId });
return handleError('Event - Get details', error);
}
}

View File

@@ -1,3 +1,5 @@
###
GET http://localhost:7272/api/event/details?eventId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01
###
GET http://localhost:7272/api/event/details

View File

@@ -1,17 +1,17 @@
// src/app/api/event/list/route.ts
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// src/app/api/event/list/route.ts
import { listEvents } from 'src/app/services/eventItem.service';
// ----------------------------------------------------------------------
/** **************************************
* GET - Events
* GET - Events, obsoleted
*************************************** */
export async function GET() {
try {
const events = await prisma.eventItem.findMany();
const events = await listEvents();
logger('[Event] list', events.length);

View File

@@ -10,7 +10,11 @@ import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { L_INFO, L_ERROR } from 'src/constants';
import { createAppLog } from 'src/app/services/AppLog.service';
import prisma from '../../../lib/prisma';
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
@@ -21,6 +25,8 @@ import prisma from '../../../lib/prisma';
*/
export async function GET(req: NextRequest) {
// Original user details functionality
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const { searchParams } = req.nextUrl;
@@ -32,8 +38,12 @@ export async function GET(req: NextRequest) {
if (!helloworld) return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
createAppLog(L_INFO, 'Get OK', debug);
return response({ helloworld }, STATUS.OK);
} catch (error) {
createAppLog(L_ERROR, 'Get error', debug);
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,3 @@
# GUIDELINES
T.B.A.

View File

@@ -1,7 +1,7 @@
// src/app/api/product/details/route.ts
//
// PURPOSE:
// save product to db by id
// get product from db by id
//
// RULES:
// T.B.A.
@@ -11,14 +11,22 @@ import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
import { L_INFO, L_ERROR } from 'src/constants';
import { getProduct } from 'src/app/services/product.service';
import { createAppLog } from 'src/app/services/AppLog.service';
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
/** **************************************
/**
**************************************
* GET Product detail
*************************************** */
***************************************
*/
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const { searchParams } = req.nextUrl;
@@ -29,10 +37,7 @@ export async function GET(req: NextRequest) {
}
// NOTE: productId confirmed exist, run below
const product = await prisma.productItem.findFirst({
include: { reviews: true },
where: { id: productId },
});
const product = await getProduct(productId);
if (!product) {
return response({ message: 'Product not found!' }, STATUS.NOT_FOUND);
@@ -40,8 +45,12 @@ export async function GET(req: NextRequest) {
logger('[Product] details', product.id);
createAppLog(L_INFO, 'Get product detail OK', debug);
return response({ product }, STATUS.OK);
} catch (error) {
createAppLog(L_ERROR, 'product detail error', debug);
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/product/details?productId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01

View File

@@ -1,22 +1,41 @@
// src/app/api/product/list/route.ts
import { logger } from 'src/utils/logger';
//
// PURPOSE:
// save product to db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
import { L_INFO, L_ERROR } from 'src/constants';
import { createAppLog } from 'src/app/services/AppLog.service';
import { listProducts } from 'src/app/services/product.service';
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
/** **************************************
* GET - Products
* GET - Products list
*************************************** */
export async function GET() {
try {
const products = await prisma.productItem.findMany();
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
logger('[Product] list', products.length);
try {
// const products = await prisma.productItem.findMany();
const products = await listProducts();
// logger('[Product] list', products.length);
createAppLog(L_INFO, 'product list ok', {});
return response({ products }, STATUS.OK);
} catch (error) {
createAppLog(L_ERROR, 'product list error', debug);
return handleError('Product - Get list', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/product/list

View File

@@ -0,0 +1,23 @@
# GUIDELINE
- this is a helloworld api endpoint
- this is a demo to handle helloworld record
- use single file for single db table/collection only
## `route.ts`
handle `GET`, `POST`, `PUT`, `DELETE`
## `test.http`
store test request
## `../../services/helloworld.service.ts`
helloworld schema CRUD handler
`listHelloworlds` - list helloworld record
`getHelloworld` - get helloworld record by id
`createNewHelloworld` - create helloworld record
`updateHelloworld` - update helloworld record by id
`deleteHelloworld` - delete helloworld record by id

View File

@@ -0,0 +1,39 @@
// src/app/api/helloworld/detail/route.ts
//
// PURPOSE:
// Get single helloworld record detail
//
// RULES:
// - For helloworld requests, return simple response
//
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/**
**************************************
* GET - Handle both helloworld and user details
**************************************
*/
export async function GET(req: NextRequest) {
// Original user details functionality
try {
const { searchParams } = req.nextUrl;
// RULES: helloworldId must exist
const helloworldId = searchParams.get('helloworldId');
if (!helloworldId) return response({ message: 'helloworldId is required!' }, STATUS.BAD_REQUEST);
const helloworld = await prisma.userItem.findFirst({ where: { id: helloworldId } });
if (!helloworld) return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
return response({ helloworld }, STATUS.OK);
} catch (error) {
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,4 @@
###
GET http://localhost:7272/api/helloworld/details?helloworldId=1165ce3a-29b8-4e1a-9148-1ae08d7e8e01

View File

@@ -0,0 +1,34 @@
import prisma from '@/lib/prisma';
import { NextResponse } from 'next/server';
// GET: 获取所有学生
export async function GET() {
try {
const students = await prisma.student.findMany();
return NextResponse.json(students);
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch students' }, { status: 500 });
}
}
// POST: 创建新学生
export async function POST(request: Request) {
try {
const { email, metadata } = await request.json();
if (!email) {
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
}
const student = await prisma.student.create({
data: {
email,
metadata: metadata || {},
},
});
return NextResponse.json(student, { status: 201 });
} catch (error) {
return NextResponse.json({ error: 'Failed to create student' }, { status: 500 });
}
}

View File

@@ -0,0 +1,25 @@
###
GET http://localhost:7272/api/helloworld
###
GET http://localhost:7272/api/helloworld?helloworldId=1
###
POST http://localhost:7272/api/helloworld?helloworldId=1
content-type: application/json
{
"data":{"hello": "hell"}
}
###
PUT http://localhost:7272/api/helloworld?helloworldId=1
content-type: application/json
{
"data": {"hello": "hell"}
}
###
DELETE http://localhost:7272/api/helloworld?helloworldId=1

View File

@@ -16,6 +16,7 @@ import prisma from '../lib/prisma';
type CreateAppLog = {
level: number;
message: string;
metadata: Record<string, any>;
};
// type UpdateAppLog = {
@@ -31,6 +32,10 @@ async function getAppLog(appLogId: string) {
return prisma.appLog.findFirst({ where: { id: appLogId } });
}
async function createAppLog(level: number, message: string, metadata: Record<string, any>) {
return prisma.appLog.create({ data: { level, message, metadata } });
}
async function createNewAppLog(createForm: CreateAppLog) {
return prisma.appLog.create({ data: createForm });
}
@@ -48,5 +53,6 @@ export {
listAppLogs,
// updateAppLog,
deleteAppLog,
createAppLog,
createNewAppLog,
};

View File

@@ -7,10 +7,6 @@
// - Follows same pattern as helloworld.service.ts
//
import type { Event } from '@prisma/client';
import prisma from '../lib/prisma';
type CreateEvent = {
eventDate: DateTime;
title: string;
@@ -39,27 +35,33 @@ type UpdateEvent = {
memberId?: number;
};
async function listEvents(): Promise<Event[]> {
return prisma.event.findMany();
}
// async function listEvents(): Promise<Event[]> {
// return prisma.event.findMany();
// }
async function getEvent(eventId: number) {
return prisma.event.findFirst({ where: { id: eventId } });
}
// async function getEvent(eventId: number) {
// return prisma.event.findFirst({ where: { id: eventId } });
// }
async function createNewEvent(createForm: CreateEvent) {
return prisma.event.create({ data: createForm });
}
// async function createNewEvent(createForm: CreateEvent) {
// return prisma.event.create({ data: createForm });
// }
async function updateEvent(eventId: number, updateForm: UpdateEvent) {
return prisma.event.update({
where: { id: eventId },
data: updateForm,
});
}
// async function updateEvent(eventId: number, updateForm: UpdateEvent) {
// return prisma.event.update({
// where: { id: eventId },
// data: updateForm,
// });
// }
async function deleteEvent(eventId: number) {
return prisma.event.delete({ where: { id: eventId } });
}
// async function deleteEvent(eventId: number) {
// return prisma.event.delete({ where: { id: eventId } });
// }
export { getEvent, listEvents, updateEvent, deleteEvent, createNewEvent };
export {
listEvents,
// getEvent,
// updateEvent,
// deleteEvent,
// createNewEvent,
};

View File

@@ -0,0 +1,71 @@
// src/app/services/event.service.ts
//
// PURPOSE:
// - Service for handling Event records
//
// RULES:
// - Follows same pattern as helloworld.service.ts
//
import type { EventItem } from '@prisma/client';
import prisma from '../lib/prisma';
type CreateEvent = {
eventDate: DateTime;
title: string;
joinMembers?: Json[];
price: number;
currency: string;
duration_m: number;
ageBottom: number;
ageTop: number;
location: string;
avatar?: string;
memberId?: number;
};
type UpdateEvent = {
eventDate?: DateTime;
title?: string;
joinMembers?: Json[];
price?: number;
currency?: string;
duration_m?: number;
ageBottom?: number;
ageTop?: number;
location?: string;
avatar?: string;
memberId?: number;
};
async function listEvents(): Promise<EventItem[]> {
return prisma.eventItem.findMany();
}
async function getEvent(eventId: string): Promise<EventItem | null> {
return prisma.eventItem.findFirst({ where: { id: eventId } });
}
// async function createNewEvent(createForm: CreateEvent) {
// return prisma.event.create({ data: createForm });
// }
// async function updateEvent(eventId: number, updateForm: UpdateEvent) {
// return prisma.event.update({
// where: { id: eventId },
// data: updateForm,
// });
// }
// async function deleteEvent(eventId: number) {
// return prisma.event.delete({ where: { id: eventId } });
// }
export {
getEvent,
listEvents,
// updateEvent,
// deleteEvent,
// createNewEvent,
};

View File

@@ -68,7 +68,7 @@ async function listProducts(): Promise<ProductItem[]> {
return prisma.productItem.findMany();
}
async function getProduct(productId: string) {
async function getProduct(productId: string): Promise<ProductItem | null> {
return prisma.productItem.findUnique({ where: { id: productId } });
}

View File

@@ -0,0 +1,127 @@
// src/app/services/user.service.ts
//
// PURPOSE:
// - Handle User Record CRUD operations
//
// RULES:
// - Follow Prisma best practices for database operations
// - Validate input data before processing
//
import type { User } from '@prisma/client';
import prisma from '../lib/prisma';
type CreateUser = {
email: string;
// name?: string;
// password: string;
// role?: Role;
// isEmailVerified?: boolean;
// admin?: boolean;
};
type UpdateUser = {
email?: string;
// name?: string;
// password?: string;
// role?: Role;
// isEmailVerified?: boolean;
isAdmin?: boolean;
};
// async function listUsers(): Promise<UserItem[]> {
// return prisma.userItem.findMany();
// }
// async function getUserItem(userId: string): Promise<UserItem | null> {
// return prisma.userItem.findFirst({ where: { id: userId } });
// }
// async function updateUser(userId: string, updateForm: UpdateUser): Promise<User> {
// return prisma.userItem.update({
// where: { id: userId },
// data: updateForm,
// });
// }
// // check if userId is a admin
// // check if userId is a admin
// async function isAdmin(userId: string): Promise<boolean> {
// const user = await getUserItem(userId);
// return user?.isAdmin === true;
// }
// async function changeToAdmin(userIdToPromote: string, userIdOfApplicant: string) {
// // check the applicant is admin or not
// const userApplicant = await getUserItem(userIdOfApplicant);
// let promoteResult = {};
// if (userApplicant && userApplicant.isAdmin) {
// // applicant is an admin
// promoteResult = await updateUser(userIdToPromote, { isAdmin: true });
// } else {
// promoteResult = { status: 'failed', message: 'applicant is not a admin' };
// }
// return promoteResult;
// }
// async function changeToUser(userIdToPromote: string, userIdOfApplicant: string) {
// // check the applicant is admin or not
// const userApplicant = await getUserItem(userIdOfApplicant);
// let promoteResult = {};
// if (userApplicant && userApplicant.isAdmin) {
// // applicant is an admin
// promoteResult = await updateUser(userIdToPromote, { isAdmin: false });
// } else {
// promoteResult = { status: 'failed', message: 'applicant is not a admin' };
// }
// return promoteResult;
// }
async function getUserById(id: string): Promise<User | null> {
return prisma.user.findFirst({ where: { id } });
}
// async function getUserByEmail(email: string): Promise<void> {
// // return prisma.userItem.findUnique({
// // where: { email },
// // include: {
// // Token: true,
// // },
// // });
// }
// async function createNewUser(createForm: CreateUser): Promise<void> {
// // return prisma.userItem.create({
// // data: {
// // email: createForm.email,
// // name: createForm.name,
// // password: createForm.password,
// // role: createForm.role || 'USER',
// // isEmailVerified: createForm.isEmailVerified || false,
// // },
// // });
// }
// async function deleteUser(userId: number): Promise<void> {
// // return prisma.userItem.delete({ where: { id: userId } });
// }
export {
// getUser,
getUserById,
// isAdmin,
// listUsers,
// updateUser,
// deleteUser,
// changeToUser,
// createNewUser,
// changeToAdmin,
// getUserByEmail,
// type CreateUser,
// type UpdateUser,
};

View File

@@ -82,6 +82,10 @@ async function changeToUser(userIdToPromote: string, userIdOfApplicant: string)
return promoteResult;
}
async function getUserById1(id: string): Promise<UserItem | null> {
return prisma.userItem.findFirst({ where: { id } });
}
async function getUserByEmail(email: string): Promise<void> {
// return prisma.userItem.findUnique({
// where: { email },
@@ -113,6 +117,7 @@ export {
listUsers,
updateUser,
deleteUser,
getUserById,
changeToUser,
createNewUser,
changeToAdmin,

View File

@@ -0,0 +1,7 @@
const L_ERROR = 0;
const L_WARN = 1;
const L_INFO = 2;
const L_DEBUG = 3;
const L_TRACE = 4;
export { L_WARN, L_INFO, L_ERROR, L_DEBUG, L_TRACE };

View File

@@ -12,11 +12,7 @@ type Token = {
* SignJWT
* https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md
*/
export async function sign(
payload: Token,
secret: string,
options: { expiresIn: string | number }
): Promise<string> {
export async function sign(payload: Token, secret: string, options: { expiresIn: string | number }): Promise<string> {
const iat = Math.floor(Date.now() / 1000);
return new SignJWT({ ...payload })

View File

@@ -1,7 +1,11 @@
#!/usr/bin/env bash
yarn --dev
while true; do
yarn --dev
# yarn tsc:print
yarn lint:print
yarn dev --force --clearScreen

File diff suppressed because it is too large Load Diff

View File

@@ -105,6 +105,7 @@
"react-organizational-chart": "^2.2.1",
"react-phone-number-input": "^3.4.12",
"react-router": "^7.4.1",
"react-use": "^17.6.0",
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",

View File

@@ -1,7 +1,7 @@
// src/actions/invoice.ts
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import type { IInvoiceItem } from 'src/types/invoice';
import type { IInvoiceItem, SaveInvoiceData } from 'src/types/invoice';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
@@ -97,7 +97,7 @@ export function useSearchInvoices(query: string) {
// ----------------------------------------------------------------------
type SaveInvoiceData = IInvoiceItem;
// type SaveInvoiceData = IInvoiceItem;
export async function saveInvoice(invoiceId: string, saveInvoiceData: SaveInvoiceData) {
const url = endpoints.invoice.saveInvoice(invoiceId);

View File

@@ -99,6 +99,7 @@ export function useSearchProducts(query: string) {
type SaveProductData = {
// id: string;
sku: string;
name: string;
code: string;

View File

@@ -17,6 +17,8 @@ export type SignUpParams = {
lastName: string;
};
const ERR_ACCESS_TOKEN_NOT_FOUND = `Access token not found in response`;
/** **************************************
* Sign in
*************************************** */
@@ -29,7 +31,7 @@ export const signInWithPassword = async ({ email, password }: SignInParams): Pro
const { accessToken } = res.data;
if (!accessToken) {
throw new Error('Access token not found in response');
throw new Error(ERR_ACCESS_TOKEN_NOT_FOUND);
}
setSession(accessToken);

View File

@@ -69,6 +69,8 @@ export function tokenExpired(exp: number) {
// ----------------------------------------------------------------------
const INVALID_ACCESS_TOKEN = 'Invalid access token!';
export async function setSession(accessToken: string | null) {
try {
if (accessToken) {
@@ -81,7 +83,7 @@ export async function setSession(accessToken: string | null) {
if (decodedToken && 'exp' in decodedToken) {
tokenExpired(decodedToken.exp);
} else {
throw new Error('Invalid access token!');
throw new Error(INVALID_ACCESS_TOKEN);
}
} else {
sessionStorage.removeItem(JWT_STORAGE_KEY);

View File

@@ -11,7 +11,7 @@ const metadata = { title: `User edit | Dashboard - ${CONFIG.appName}` };
export default function Page() {
const { id = '' } = useParams();
// TODO: remove me
// TODO: remove unused code
// const currentUser = _userList.find((user) => user.id === id);
const { user } = useGetUser(id);

View File

@@ -106,7 +106,8 @@ export function CalendarToolbar({
<Iconify icon="eva:arrow-ios-back-fill" />
</IconButton>
<Typography variant="h6">{date}</Typography>
{/* FIXME: no raw json output in html */}
<Typography variant="h6">{JSON.stringify({ date })}</Typography>
<IconButton onClick={onNextDate}>
<Iconify icon="eva:arrow-ios-forward-fill" />

View File

@@ -37,19 +37,22 @@ export function InvoiceDetails({ invoice }: Props) {
const { t } = useTranslation();
const [currentStatus, setCurrentStatus] = useState(invoice?.status);
const handleChangeStatus = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
// setCurrentStatus(event.target.value);
const handleChangeStatus = useCallback(
(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
// setCurrentStatus(event.target.value);
try {
changeStatus(invoice.id, event.target.value);
setCurrentStatus(event.target.value);
try {
changeStatus(invoice.id, event.target.value);
setCurrentStatus(event.target.value);
toast.success('status changed!');
} catch (error) {
console.error(error);
toast.warning('error during changing status');
}
}, []);
toast.success('status changed!');
} catch (error) {
console.error(error);
toast.warning('error during changing status');
}
},
[]
);
const renderFooter = () => (
<Box

View File

@@ -1,4 +1,4 @@
import type { IInvoiceItem } from 'src/types/invoice';
import type { IInvoiceItem, IInvoiceItemItem } from 'src/types/invoice';
import { sumBy } from 'es-toolkit';
import { useEffect, useCallback } from 'react';
@@ -22,7 +22,7 @@ import { InvoiceTotalSummary } from './invoice-total-summary';
// ----------------------------------------------------------------------
export const defaultItem: Omit<IInvoiceItem, 'id'> = {
export const defaultItem: Omit<IInvoiceItemItem, 'id'> = {
title: '',
description: '',
service: INVOICE_SERVICE_OPTIONS[0].name,
@@ -50,7 +50,7 @@ export function InvoiceNewEditDetails() {
const discount = getValues('discount');
const shipping = getValues('shipping');
const subtotal = sumBy(items, (item: IInvoiceItem) => item.quantity * item.price);
const subtotal = sumBy(items, (item: IInvoiceItemItem) => item.quantity * item.price);
const subtotalWithTax = subtotal + subtotal * (taxes / 100);
const totalAmount = subtotalWithTax - discount - shipping;

View File

@@ -81,6 +81,8 @@ export function InvoiceNewEditForm({ currentInvoice }: Props) {
const loadingSend = useBoolean();
const defaultValues: NewInvoiceSchemaType = {
id: '',
sent: 0,
invoiceNumber: 'INV-1990',
createDate: today(),
dueDate: null,
@@ -129,6 +131,8 @@ export function InvoiceNewEditForm({ currentInvoice }: Props) {
try {
if (currentInvoice) {
data.dueDate = '2029-01-01';
await saveInvoice(currentInvoice.id, data);
}

View File

@@ -27,7 +27,7 @@ type Props = {
invoice?: IInvoiceItem;
currentStatus: string;
statusOptions: { value: string; label: string }[];
onChangeStatus: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChangeStatus: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
};
export function InvoiceToolbar({ invoice, currentStatus, statusOptions, onChangeStatus }: Props) {

View File

@@ -19,7 +19,7 @@ export type IAddressItem = {
addressType?: string;
};
export type IDateValue = string | number | null;
export type IDateValue = string | number | Date | null;
export type IDatePickerControl = Dayjs | null;

View File

@@ -13,15 +13,16 @@ export type IInvoiceTableFilters = {
export type IInvoiceItemItem = {
id: string;
title: string;
service: string;
price: number;
total: number;
service: string;
quantity: number;
description: string;
};
export type IInvoiceItem = {
id: string;
sent: number;
taxes: number;
status: string;
@@ -36,3 +37,26 @@ export type IInvoiceItem = {
invoiceTo: IAddressItem;
invoiceFrom: IAddressItem;
};
export type SaveInvoiceData = {
sent: number;
taxes: number;
status: string;
subtotal: number;
discount: number;
shipping: number;
totalAmount: number;
dueDate: IDateValue;
invoiceNumber: string;
items: {
title: string;
service: string;
price: number;
total: number;
quantity: number;
description: string;
}[];
createDate: IDateValue;
invoiceTo: IAddressItem | null;
invoiceFrom: IAddressItem | null;
};

View File

@@ -766,6 +766,11 @@
dependencies:
"@babel/types" "^7.27.0"
"@babel/runtime@^7.1.2":
version "7.27.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.4.tgz#a91ec580e6c00c67118127777c316dfd5a5a6abf"
integrity sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==
"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
version "7.27.0"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz"
@@ -1753,7 +1758,7 @@
resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15":
version "1.5.0"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
@@ -3298,6 +3303,11 @@
dependencies:
"@types/unist" "*"
"@types/js-cookie@^2.2.6":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.7.tgz#226a9e31680835a6188e887f3988e60c04d3f6a3"
integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
@@ -3627,6 +3637,11 @@
dependencies:
"@swc/core" "^1.11.11"
"@xobotyi/scrollbar-width@^1.9.5":
version "1.9.5"
resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
"@yr/monotone-cubic-spline@^1.0.3":
version "1.0.3"
resolved "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz"
@@ -4090,6 +4105,13 @@ cookie@^1.0.1:
resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz"
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
copy-to-clipboard@^3.3.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0"
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
dependencies:
toggle-selection "^1.0.6"
cosmiconfig@^7.0.0:
version "7.1.0"
resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz"
@@ -4130,6 +4152,21 @@ crypto-js@^4.2.0:
resolved "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
css-in-js-utils@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb"
integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==
dependencies:
hyphenate-style-name "^1.0.3"
css-tree@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
csscolorparser@~1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz"
@@ -4140,7 +4177,7 @@ cssjanus@^2.0.1:
resolved "https://registry.npmjs.org/cssjanus/-/cssjanus-2.3.0.tgz"
integrity sha512-ZZXXn51SnxRxAZ6fdY7mBDPmA4OZd83q/J9Gdqz3YmE9TUq+9tZl+tdOnCi7PpNygI6PEkehj9rgifv5+W8a5A==
csstype@^3.0.2, csstype@^3.1.3:
csstype@^3.0.2, csstype@^3.1.2, csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
@@ -4332,6 +4369,13 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^2.0.6:
version "2.1.4"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286"
integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==
dependencies:
stackframe "^1.3.4"
es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9:
version "1.23.9"
resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz"
@@ -4749,6 +4793,11 @@ fast-levenshtein@^2.0.6:
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-shallow-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b"
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
fast-xml-parser@4.4.1:
version "4.4.1"
resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz"
@@ -4763,6 +4812,11 @@ fast-xml-parser@^4.4.1:
dependencies:
strnum "^1.1.1"
fastest-stable-stringify@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76"
integrity sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==
fastq@^1.6.0:
version "1.19.1"
resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz"
@@ -5256,6 +5310,11 @@ hyphen@^1.6.4:
resolved "https://registry.npmjs.org/hyphen/-/hyphen-1.10.6.tgz"
integrity sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==
hyphenate-style-name@^1.0.3:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436"
integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==
i18next-browser-languagedetector@^8.0.4:
version "8.0.4"
resolved "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.4.tgz"
@@ -5325,6 +5384,13 @@ inline-style-parser@0.2.4:
resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz"
integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==
inline-style-prefixer@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz#9310f3cfa2c6f3901d1480f373981c02691781e8"
integrity sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==
dependencies:
css-in-js-utils "^3.1.0"
input-format@^0.3.10:
version "0.3.14"
resolved "https://registry.npmjs.org/input-format/-/input-format-0.3.14.tgz"
@@ -5631,6 +5697,11 @@ jay-peg@^1.1.1:
dependencies:
restructure "^3.0.0"
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
js-cookie@^3.0.5:
version "3.0.5"
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz"
@@ -6029,6 +6100,11 @@ mdast-util-to-string@^4.0.0:
dependencies:
"@types/mdast" "^4.0.0"
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
mdurl@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz"
@@ -6397,6 +6473,20 @@ murmurhash-js@^1.0.0:
resolved "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz"
integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==
nano-css@^5.6.2:
version "5.6.2"
resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.6.2.tgz#584884ddd7547278f6d6915b6805069742679a32"
integrity sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
css-tree "^1.1.2"
csstype "^3.1.2"
fastest-stable-stringify "^2.0.2"
inline-style-prefixer "^7.0.1"
rtl-css-js "^1.16.1"
stacktrace-js "^2.0.2"
stylis "^4.3.0"
nanoid@^3.3.8:
version "3.3.11"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
@@ -7014,6 +7104,31 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-universal-interface@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"
integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
react-use@^17.6.0:
version "17.6.0"
resolved "https://registry.yarnpkg.com/react-use/-/react-use-17.6.0.tgz#2101a3a79dc965a25866b21f5d6de4b128488a14"
integrity sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==
dependencies:
"@types/js-cookie" "^2.2.6"
"@xobotyi/scrollbar-width" "^1.9.5"
copy-to-clipboard "^3.3.1"
fast-deep-equal "^3.1.3"
fast-shallow-equal "^1.0.0"
js-cookie "^2.2.1"
nano-css "^5.6.2"
react-universal-interface "^0.6.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.1.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^3.0.1"
ts-easing "^0.2.0"
tslib "^2.1.0"
react@^19.0.0, react@^19.1.0:
version "19.1.0"
resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz"
@@ -7137,6 +7252,11 @@ reselect@^5.1.1:
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
@@ -7216,6 +7336,13 @@ rope-sequence@^1.3.0:
resolved "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz"
integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
rtl-css-js@^1.16.1:
version "1.16.1"
resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.16.1.tgz#4b48b4354b0ff917a30488d95100fbf7219a3e80"
integrity sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==
dependencies:
"@babel/runtime" "^7.1.2"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
@@ -7278,6 +7405,11 @@ scheduler@^0.26.0:
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz"
integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
screenfull@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba"
integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==
semver@^6.3.1:
version "6.3.1"
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
@@ -7325,6 +7457,11 @@ set-function-name@^2.0.2:
functions-have-names "^1.2.3"
has-property-descriptors "^1.0.2"
set-harmonic-interval@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249"
integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==
set-proto@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz"
@@ -7449,11 +7586,21 @@ source-map-js@^1.2.1:
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map@0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==
source-map@^0.5.7:
version "0.5.7"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
space-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
@@ -7471,6 +7618,35 @@ stable-hash@^0.0.5:
resolved "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz"
integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==
stack-generator@^2.0.5:
version "2.0.10"
resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"
integrity sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==
dependencies:
stackframe "^1.3.4"
stackframe@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==
stacktrace-gps@^3.0.4:
version "3.1.2"
resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz#0c40b24a9b119b20da4525c398795338966a2fb0"
integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==
dependencies:
source-map "0.5.6"
stackframe "^1.3.4"
stacktrace-js@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b"
integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==
dependencies:
error-stack-parser "^2.0.6"
stack-generator "^2.0.5"
stacktrace-gps "^3.0.4"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
@@ -7609,7 +7785,7 @@ stylis@4.2.0:
resolved "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz"
integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==
stylis@^4.3.6:
stylis@^4.3.0, stylis@^4.3.6:
version "4.3.6"
resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz"
integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
@@ -7646,6 +7822,11 @@ swr@^2.3.3:
dequal "^2.0.3"
use-sync-external-store "^1.4.0"
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
tiny-inflate@^1.0.0, tiny-inflate@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz"
@@ -7683,6 +7864,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
@@ -7703,6 +7889,11 @@ ts-api-utils@^2.0.1:
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
ts-easing@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
tsconfig-paths@^3.15.0:
version "3.15.0"
resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz"

View File

@@ -2,5 +2,5 @@
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 240
"printWidth": 100
}

View File

@@ -13,6 +13,7 @@
"@capacitor/core": "^7.0.0",
"@capacitor/ios": "7.0.1",
"@capacitor/preferences": "^7.0.0",
"@hookform/resolvers": "^4.1.3",
"@ionic/react": "^8.5.0",
"@ionic/react-router": "^8.5.0",
"@mdx-js/react": "^3.1.0",
@@ -24,15 +25,18 @@
"leaflet": "^1.9.4",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.55.0",
"react-leaflet": "^5.0.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^5.3.4",
"react-router-dom": "^5.3.4",
"react-spinners": "^0.17.0",
"react-use": "^17.6.0",
"reselect": "^4.0.0",
"sass": "^1.85.1",
"swiper": "^9.1.1"
"swiper": "^9.1.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@capacitor/cli": "^7.0.0",
@@ -871,6 +875,18 @@
"node": ">=18"
}
},
"node_modules/@hookform/resolvers": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz",
"integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
},
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
},
"node_modules/@ionic/cli-framework-output": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz",
@@ -1233,7 +1249,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@@ -1865,6 +1880,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@stencil/core": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.20.0.tgz",
@@ -2072,6 +2093,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==",
"license": "MIT"
},
"node_modules/@types/leaflet": {
"version": "1.9.18",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.18.tgz",
@@ -2351,6 +2378,12 @@
"node": ">=10.0.0"
}
},
"node_modules/@xobotyi/scrollbar-width": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==",
"license": "MIT"
},
"node_modules/ansi-escapes": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz",
@@ -2889,6 +2922,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
"license": "MIT",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-js-pure": {
"version": "3.42.0",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz",
@@ -2916,6 +2958,28 @@
"node": ">= 8"
}
},
"node_modules/css-in-js-utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
"license": "MIT",
"dependencies": {
"hyphenate-style-name": "^1.0.3"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -3087,6 +3151,15 @@
"node": ">=6"
}
},
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"license": "MIT",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -3230,6 +3303,23 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-shallow-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
},
"node_modules/fastest-stable-stringify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
"integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==",
"license": "MIT"
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
@@ -3635,6 +3725,12 @@
"node": ">=14.18.0"
}
},
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"license": "BSD-3-Clause"
},
"node_modules/immutable": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
@@ -3664,6 +3760,15 @@
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
"license": "MIT"
},
"node_modules/inline-style-prefixer": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
"integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
"license": "MIT",
"dependencies": {
"css-in-js-utils": "^3.1.0"
}
},
"node_modules/ionicons": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
@@ -3843,6 +3948,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4427,6 +4538,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -5003,6 +5120,26 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nano-css": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz",
"integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==",
"license": "Unlicense",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15",
"css-tree": "^1.1.2",
"csstype": "^3.1.2",
"fastest-stable-stringify": "^2.0.2",
"inline-style-prefixer": "^7.0.1",
"rtl-css-js": "^1.16.1",
"stacktrace-js": "^2.0.2",
"stylis": "^4.3.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -5400,6 +5537,22 @@
"react": "^19.0.0"
}
},
"node_modules/react-hook-form": {
"version": "7.57.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
"integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -5528,6 +5681,41 @@
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
"integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==",
"peerDependencies": {
"react": "*",
"tslib": "*"
}
},
"node_modules/react-use": {
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz",
"integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==",
"license": "Unlicense",
"dependencies": {
"@types/js-cookie": "^2.2.6",
"@xobotyi/scrollbar-width": "^1.9.5",
"copy-to-clipboard": "^3.3.1",
"fast-deep-equal": "^3.1.3",
"fast-shallow-equal": "^1.0.0",
"js-cookie": "^2.2.1",
"nano-css": "^5.6.2",
"react-universal-interface": "^0.6.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.1.0",
"set-harmonic-interval": "^1.0.1",
"throttle-debounce": "^3.0.1",
"ts-easing": "^0.2.0",
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -5603,6 +5791,12 @@
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==",
"license": "MIT"
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
@@ -5719,6 +5913,15 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rtl-css-js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
"integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.1.2"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -5773,6 +5976,18 @@
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT"
},
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -5786,6 +6001,15 @@
"node": ">=10"
}
},
"node_modules/set-harmonic-interval": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz",
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==",
"license": "Unlicense",
"engines": {
"node": ">=6.9"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -5841,6 +6065,15 @@
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5876,6 +6109,51 @@
"integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==",
"license": "MIT"
},
"node_modules/stack-generator": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
"license": "MIT",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
"license": "MIT"
},
"node_modules/stacktrace-gps": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
"license": "MIT",
"dependencies": {
"source-map": "0.5.6",
"stackframe": "^1.3.4"
}
},
"node_modules/stacktrace-gps/node_modules/source-map": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stacktrace-js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
"license": "MIT",
"dependencies": {
"error-stack-parser": "^2.0.6",
"stack-generator": "^2.0.5",
"stacktrace-gps": "^3.0.4"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -5999,6 +6277,12 @@
"inline-style-parser": "0.2.4"
}
},
"node_modules/stylis": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -6069,6 +6353,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/throttle-debounce": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
"integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/through2": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
@@ -6149,6 +6442,12 @@
"node": ">=8.0"
}
},
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
"license": "MIT"
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -6179,6 +6478,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ts-easing": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==",
"license": "Unlicense"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -6637,6 +6942,15 @@
"fd-slicer": "~1.1.0"
}
},
"node_modules/zod": {
"version": "3.25.46",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.46.tgz",
"integrity": "sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",

View File

@@ -10,6 +10,7 @@
"@capacitor/core": "^7.0.0",
"@capacitor/ios": "7.0.1",
"@capacitor/preferences": "^7.0.0",
"@hookform/resolvers": "^4.1.3",
"@ionic/react": "^8.5.0",
"@ionic/react-router": "^8.5.0",
"@mdx-js/react": "^3.1.0",
@@ -21,15 +22,18 @@
"leaflet": "^1.9.4",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.55.0",
"react-leaflet": "^5.0.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^5.3.4",
"react-router-dom": "^5.3.4",
"react-spinners": "^0.17.0",
"react-use": "^17.6.0",
"reselect": "^4.0.0",
"sass": "^1.85.1",
"swiper": "^9.1.1"
"swiper": "^9.1.1",
"zod": "^3.24.2"
},
"scripts": {
"start": "npm run dev",

View File

@@ -47,6 +47,7 @@ import { loadConfData } from './data/sessions/sessions.actions';
import { setIsLoggedIn, setUsername, loadUserData } from './data/user/user.actions';
import Account from './pages/Account';
import Login from './pages/Login';
import MyLogin from './pages/MyLogin';
import Signup from './pages/Signup';
import Support from './pages/Support';
import Tutorial from './pages/Tutorial';
@@ -88,10 +89,18 @@ interface 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,
}) => {
useEffect(() => {
loadUserData();
loadConfData();
// eslint-disable-next-line
}, []);
@@ -112,18 +121,15 @@ const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn,
{/* */}
<Route path="/tabs" render={() => <MainTabs />} />
{/* */}
<Route path="/account" component={Account} />
<Route path="/login" component={Login} />
<Route path="/mylogin" component={MyLogin} />
<Route path="/signup" component={Signup} />
<Route path="/support" component={Support} />
<Route path="/tutorial" component={Tutorial} />
{/* */}
{/* */}
<Route path={paths.SETTINGS} component={Settings} />
<Route path={paths.CHANGE_LANGUAGE} component={ChangeLanguage} />
<Route path={paths.SERVICE_AGREEMENT} component={ServiceAgreement} />
<Route path={paths.PRIVACY_AGREEMENT} component={PrivacyAgreement} />
{/* */}
<Route

View File

@@ -1,7 +1,20 @@
//
// 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 Helloworld from './pages/Helloworld';
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';
import SpeakerDetail from './pages/SpeakerDetail';
const AppRoute: React.FC = () => {
return (
@@ -9,8 +22,18 @@ const AppRoute: React.FC = () => {
<Route path="/not_implemented" component={NotImplemented} />
{/* */}
<Route path="/event_detail/:id" component={EventDetail} />
<Route path="/profile/:id" component={MemberProfile} />
<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} />
</>
);
};

View File

@@ -9,11 +9,12 @@ import MessageList from './pages/MessageList';
import Favourites from './pages/Favourites';
import MyProfile from './pages/MyProfile';
import EventList from './pages/EventList';
import Helloworld from './pages/Helloworld';
const TabAppRoute: React.FC = () => {
return (
<>
<Route path="/tabs/not_implemented" component={NotImplemented} />
<Route path={paths.TAB_NOT_IMPLEMENTED} component={NotImplemented} />
{/* */}
<Route path={paths.NEARBY_LIST} render={() => <MembersNearByList />} exact={true} />
@@ -28,10 +29,10 @@ const TabAppRoute: React.FC = () => {
<Route path={paths.FAVOURITES_LIST} render={() => <Favourites />} exact={true} />
{/* */}
<Route path="/tabs/events" render={() => <EventList />} exact={true} />
<Route path={paths.EVENT_LIST} render={() => <EventList />} exact={true} />
{/* */}
<Route path="/tabs/my_profile" render={() => <MyProfile />} exact={true} />
<Route path={paths.PROFILE} render={() => <MyProfile />} exact={true} />
</>
);
};

View File

@@ -7,11 +7,7 @@ interface StateProps {
}
const HomeOrTutorial: React.FC<StateProps> = ({ hasSeenTutorial }) => {
return hasSeenTutorial ? (
<Redirect to="/tabs/schedule" />
) : (
<Redirect to="/tutorial" />
);
return hasSeenTutorial ? <Redirect to="/tabs/events" /> : <Redirect to="/tutorial" />;
};
export default connect<{}, StateProps, {}>({

View File

@@ -6,15 +6,12 @@ interface RedirectToLoginProps {
setUsername: Function;
}
const RedirectToLogin: React.FC<RedirectToLoginProps> = ({
setIsLoggedIn,
setUsername,
}) => {
const RedirectToLogin: React.FC<RedirectToLoginProps> = ({ setIsLoggedIn, setUsername }) => {
const ionRouterContext = useContext(IonRouterContext);
useEffect(() => {
setIsLoggedIn(false);
setUsername(undefined);
ionRouterContext.push('/tabs/schedule');
ionRouterContext.push('/tabs/events');
}, [setIsLoggedIn, setUsername]);
return null;
};

View File

View File

View File

@@ -0,0 +1,92 @@
import axios, { endpoints } from '../../lib/axios';
import { setSession } from './utils';
import { JWT_STORAGE_KEY } from './constant';
// ----------------------------------------------------------------------
export type SignInParams = {
email: string;
password: string;
};
export type SignUpParams = {
email: string;
password: string;
firstName: string;
lastName: string;
};
const ERR_ACCESS_TOKEN_NOT_FOUND = `Access token not found in response`;
/** **************************************
* Sign in
*************************************** */
export const signInWithPassword = async ({
email,
password,
}: SignInParams): Promise<string | null> => {
try {
const params = { email, password };
const res = await axios.post(endpoints.auth.signIn, params);
const { accessToken } = res.data;
console.log({ t: res.data });
if (!accessToken) {
throw new Error(ERR_ACCESS_TOKEN_NOT_FOUND);
}
// setSession(accessToken);
return 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;
}
};

View File

@@ -0,0 +1 @@
export const JWT_STORAGE_KEY = 'jwt_access_token';

View File

@@ -0,0 +1,97 @@
// import { paths } from 'src/routes/paths';
import axios from '../../lib/axios';
import { JWT_STORAGE_KEY } from './constant.js';
import paths from '../../paths.js';
// ----------------------------------------------------------------------
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.SIGN_IN;
} 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;
}
}

View File

@@ -11,9 +11,7 @@ export const AppContext = createContext<AppContextState>({
dispatch: () => undefined,
});
export const AppContextProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
export const AppContextProvider: React.FC<PropsWithChildren> = ({ children }) => {
const [store, dispatch] = useReducer(reducers, initialState);
return (

View File

@@ -1,9 +1,11 @@
import { Preferences as Storage } from '@capacitor/preferences';
import { GetResult, Preferences as Storage } from '@capacitor/preferences';
import { Schedule, Session } from '../models/Schedule';
import { Speaker } from '../models/Speaker';
import { Location } from '../models/Location';
import axios from 'axios';
import constants from '../constants';
import { IOrderItem } from '../models/Order';
import { Event } from '../models/Event';
const dataUrl = '/assets/data/data.json';
const locationsUrl = '/assets/data/locations.json';
@@ -11,16 +13,20 @@ const locationsUrl = '/assets/data/locations.json';
const HAS_LOGGED_IN = 'hasLoggedIn';
const HAS_SEEN_TUTORIAL = 'hasSeenTutorial';
const USERNAME = 'username';
const ACCESS_TOKEN = 'a_token';
const ACTIVE_SESSION = 'a_session';
export const getConfData = async () => {
const response = await Promise.all([
fetch(dataUrl),
fetch(locationsUrl),
axios.get(`${constants.API_ENDPOINT}/api/order/list`),
fetch(`${constants.API_ENDPOINT}/api/order/list`),
fetch(`${constants.API_ENDPOINT}/api/event/list`),
// axios.get(`${constants.API_ENDPOINT}/v1/events`),
// axios.get(`${constants.API_ENDPOINT}/v1/members`),
//
]);
const responseData = await response[0].json();
const schedule = responseData.schedule[0] as Schedule;
const sessions = parseSessions(schedule);
@@ -33,9 +39,29 @@ export const getConfData = async () => {
// const events = response[2].data;
// const nearByMembers = response[3].data;
const orders = response[2].data.orders;
const events = [];
// TODO: update this due to not use axios anymore
// the data object is not available
// const orders = response[2].data.orders as IOrderItem[];
// const events = response[3].data.events as Event[];
const orderResponse = response[2];
let orders = {
result: { status: orderResponse.status, ok: orderResponse.ok },
data: [],
};
if (orderResponse.status == 200) {
orders = { ...orders, data: await orderResponse.json() };
}
const eventResponse = response[3];
let events = {
result: { status: eventResponse.status, ok: eventResponse.ok },
data: [],
};
if (eventResponse.status == 200) {
events = { ...events, data: await eventResponse.json() };
}
const nearByMembers = [];
const data = {
@@ -47,15 +73,21 @@ export const getConfData = async () => {
filteredTracks: [...allTracks],
//
events,
nearByMembers,
// nearByMembers,
orders,
hello: 'world',
//
};
return data;
};
export const getUserData = async () => {
const response = await Promise.all([Storage.get({ key: HAS_LOGGED_IN }), Storage.get({ key: HAS_SEEN_TUTORIAL }), Storage.get({ key: USERNAME })]);
const response = await Promise.all([
Storage.get({ key: HAS_LOGGED_IN }),
Storage.get({ key: HAS_SEEN_TUTORIAL }),
Storage.get({ key: USERNAME }),
]);
const isLoggedin = (await response[0].value) === 'true';
const hasSeenTutorial = (await response[1].value) === 'true';
const username = (await response[2].value) || undefined;
@@ -86,6 +118,18 @@ export const setUsernameData = async (username?: string) => {
}
};
export const setAccessTokenData = async (accessToken?: string) => {
if (!accessToken) {
await Storage.remove({ key: ACCESS_TOKEN });
} else {
await Storage.set({ key: ACCESS_TOKEN, value: accessToken });
}
};
export const getAccessTokenData = async (): Promise<GetResult> => {
return Storage.get({ key: ACCESS_TOKEN });
};
function parseSessions(schedule: Schedule) {
const sessions: Session[] = [];
schedule.groups.forEach((g) => {
@@ -93,3 +137,15 @@ function parseSessions(schedule: Schedule) {
});
return sessions;
}
export const setActiveSessionData = async (activeSession: any) => {
if (!activeSession) {
await Storage.remove({ key: ACTIVE_SESSION });
} else {
await Storage.set({ key: ACTIVE_SESSION, value: JSON.stringify(activeSession) });
}
};
export const getActiveSessionData = async (): Promise<GetResult> => {
return Storage.get({ key: JSON.parse(ACTIVE_SESSION) });
};

View File

@@ -2,9 +2,10 @@ import { createSelector } from 'reselect';
import { Schedule, Session, ScheduleGroup } from '../models/Schedule';
import { Speaker } from '../models/Speaker';
import { Location } from '../models/Location';
import { Event } from '../models/Event';
import { AppState } from './state';
import { IOrderItem } from '../models/Order';
import { Event } from '../models/Event';
const getSchedule = (state: AppState) => {
return state.data.schedule;
@@ -17,106 +18,126 @@ const getFilteredTracks = (state: AppState) => state.data.filteredTracks;
const getFavoriteIds = (state: AppState) => state.data.favorites;
const getSearchText = (state: AppState) => state.data.searchText;
export const getEvents = (state: AppState) => state.data.events;
export const getEvents = (state: AppState) => {
return state.data.events;
};
export const getNearbyMembers = (state: AppState) => state.data.nearByMembers;
export const getOrders = (state: AppState) => state.data.orders;
export const getFilteredSchedule = createSelector(getSchedule, getFilteredTracks, (schedule, filteredTracks) => {
const groups: ScheduleGroup[] = [];
export const getOrders = (state: AppState) => {
return state.data.orders;
};
// Helper function to convert 12-hour time to 24-hour time for proper sorting
const convertTo24Hour = (timeStr: string) => {
const [time, period] = timeStr.toLowerCase().split(' ');
let [hours, minutes] = time.split(':').map(Number);
export const getFilteredSchedule = createSelector(
getSchedule,
getFilteredTracks,
(schedule, filteredTracks) => {
const groups: ScheduleGroup[] = [];
if (period === 'pm' && hours !== 12) {
hours += 12;
} else if (period === 'am' && hours === 12) {
hours = 0;
}
// Helper function to convert 12-hour time to 24-hour time for proper sorting
const convertTo24Hour = (timeStr: string) => {
const [time, period] = timeStr.toLowerCase().split(' ');
let [hours, minutes] = time.split(':').map(Number);
return `${hours.toString().padStart(2, '0')}:${minutes || '00'}`;
};
if (period === 'pm' && hours !== 12) {
hours += 12;
} else if (period === 'am' && hours === 12) {
hours = 0;
}
// Sort the groups by time
const sortedGroups = [...schedule.groups].sort((a, b) => {
const timeA = convertTo24Hour(a.time);
const timeB = convertTo24Hour(b.time);
return timeA.localeCompare(timeB);
});
return `${hours.toString().padStart(2, '0')}:${minutes || '00'}`;
};
sortedGroups.forEach((group: ScheduleGroup) => {
const sessions: Session[] = [];
group.sessions.forEach((session) => {
session.tracks.forEach((track) => {
if (filteredTracks.indexOf(track) > -1) {
sessions.push(session);
}
});
// Sort the groups by time
const sortedGroups = [...schedule.groups].sort((a, b) => {
const timeA = convertTo24Hour(a.time);
const timeB = convertTo24Hour(b.time);
return timeA.localeCompare(timeB);
});
if (sessions.length) {
// Sort sessions within each group by start time
const sortedSessions = sessions.sort((a, b) => {
const timeA = convertTo24Hour(a.timeStart);
const timeB = convertTo24Hour(b.timeStart);
return timeA.localeCompare(timeB);
sortedGroups.forEach((group: ScheduleGroup) => {
const sessions: Session[] = [];
group.sessions.forEach((session) => {
session.tracks.forEach((track) => {
if (filteredTracks.indexOf(track) > -1) {
sessions.push(session);
}
});
});
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions: sortedSessions,
};
groups.push(groupToAdd);
}
});
if (sessions.length) {
// Sort sessions within each group by start time
const sortedSessions = sessions.sort((a, b) => {
const timeA = convertTo24Hour(a.timeStart);
const timeB = convertTo24Hour(b.timeStart);
return timeA.localeCompare(timeB);
});
return {
date: schedule.date,
groups,
} as Schedule;
});
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions: sortedSessions,
};
groups.push(groupToAdd);
}
});
export const getSearchedSchedule = createSelector(getFilteredSchedule, getSearchText, (schedule, searchText) => {
if (!searchText) {
return schedule;
return {
date: schedule.date,
groups,
} as Schedule;
}
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group) => {
const sessions = group.sessions.filter((s) => s.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
);
export const getSearchedSchedule = createSelector(
getFilteredSchedule,
getSearchText,
(schedule, searchText) => {
if (!searchText) {
return schedule;
}
});
return {
date: schedule.date,
groups,
} as Schedule;
});
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group) => {
const sessions = group.sessions.filter(
(s) => s.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
}
);
export const getScheduleList = createSelector(getSearchedSchedule, (schedule) => schedule);
export const getGroupedFavorites = createSelector(getScheduleList, getFavoriteIds, (schedule, favoriteIds) => {
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group) => {
const sessions = group.sessions.filter((s) => favoriteIds.indexOf(s.id) > -1);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
});
export const getGroupedFavorites = createSelector(
getScheduleList,
getFavoriteIds,
(schedule, favoriteIds) => {
const groups: ScheduleGroup[] = [];
schedule.groups.forEach((group) => {
const sessions = group.sessions.filter((s) => favoriteIds.indexOf(s.id) > -1);
if (sessions.length) {
const groupToAdd: ScheduleGroup = {
time: group.time,
sessions,
};
groups.push(groupToAdd);
}
});
return {
date: schedule.date,
groups,
} as Schedule;
}
);
const getIdParam = (_state: AppState, props: any) => {
return props.match.params['id'];
@@ -126,9 +147,25 @@ export const getSession = createSelector(getSessions, getIdParam, (sessions, id)
return sessions.find((s: Session) => s.id === id);
});
export const getSpeaker = createSelector(getSpeakers, getIdParam, (speakers, id) => speakers.find((x: Speaker) => x.id === id));
export const getSpeaker = createSelector(getSpeakers, getIdParam, (speakers, id) =>
speakers.find((x: Speaker) => x.id === id)
);
export const getEvent = createSelector(getEvents, getIdParam, (events, id) => events.find((x: Event) => x.id === id));
export const getEvent = createSelector(getEvents, getIdParam, (data_events, id) => {
const {
data: { events },
} = data_events;
return events.find((x: Event) => x.id === id);
});
export const getOrder = createSelector(getOrders, getIdParam, (data_orders, id) => {
const {
data: { orders },
} = data_orders;
return orders.find((x: IOrderItem) => x.id === id);
});
export const getSpeakerSessions = createSelector(getSessions, (sessions) => {
const speakerSessions: { [key: string]: Session[] } = {};

View File

@@ -3,6 +3,7 @@ import { Speaker } from '../../models/Speaker';
import { Schedule, Session } from '../../models/Schedule';
//
import { Event } from '../../models/Event';
import { IOrderItem } from '../../models/Order';
export interface ConfState {
schedule: Schedule;
@@ -18,4 +19,5 @@ export interface ConfState {
menuEnabled: boolean;
//
events: Event[];
orders: IOrderItem[];
}

View File

@@ -1,10 +1,7 @@
import { SessionsActions } from './sessions.actions';
import { ConfState } from './conf.state';
export const sessionsReducer = (
state: ConfState,
action: SessionsActions
): ConfState => {
export const sessionsReducer = (state: ConfState, action: SessionsActions): ConfState => {
switch (action.type) {
case 'set-conf-loading': {
return { ...state, loading: action.isLoading };

View File

@@ -28,6 +28,8 @@ export const initialState: AppState = {
darkMode: false,
isLoggedin: false,
loading: false,
//
isSessionValid: false,
},
locations: {
locations: [],

View File

@@ -3,9 +3,15 @@ import {
setIsLoggedInData,
setUsernameData,
setHasSeenTutorialData,
setAccessTokenData,
getAccessTokenData,
setActiveSessionData,
} from '../dataApi';
import { ActionType } from '../../util/types';
import { UserState } from './user.state';
import { isValidToken } from '../../context/jwt/utils';
import axios from 'axios';
import { endpoints } from '../../pages/MyLogin/endpoints';
export const loadUserData = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setLoading(true));
@@ -15,10 +21,7 @@ export const loadUserData = () => async (dispatch: React.Dispatch<any>) => {
};
export const setLoading = (isLoading: boolean) =>
({
type: 'set-user-loading',
isLoading,
} as const);
({ type: 'set-user-loading', isLoading } as const);
export const setData = (data: Partial<UserState>) =>
({
@@ -31,23 +34,68 @@ export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setUsername());
};
export const setIsLoggedIn =
(loggedIn: boolean) => async (dispatch: React.Dispatch<any>) => {
await setIsLoggedInData(loggedIn);
return {
type: 'set-is-loggedin',
loggedIn,
} as const;
};
export const setIsLoggedIn = (loggedIn: boolean) => async (dispatch: React.Dispatch<any>) => {
await setIsLoggedInData(loggedIn);
return {
type: 'set-is-loggedin',
loggedIn,
} as const;
};
export const setUsername =
(username?: string) => async (dispatch: React.Dispatch<any>) => {
await setUsernameData(username);
return {
type: 'set-username',
username,
} as const;
};
export const setUsername = (username?: string) => async (dispatch: React.Dispatch<any>) => {
await setUsernameData(username);
return {
type: 'set-username',
username,
} as const;
};
export const setAccessToken = (token?: string) => async (dispatch: React.Dispatch<any>) => {
await setAccessTokenData(token);
return {
type: 'set-access-token',
token,
} as const;
};
export const setActiveSession = (session: any) => async (dispatch: React.Dispatch<any>) => {
await setActiveSessionData(session);
return {
type: 'set-active-session',
session,
} as const;
};
export const checkUserSession = () => async (dispatch: React.Dispatch<any>) => {
let accessToken = (await getAccessTokenData()).value;
console.log('check user session');
let sessionValid = false;
try {
if (accessToken && isValidToken(accessToken)) {
const res = await axios.get(endpoints.auth.me, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const { user } = res.data;
setActiveSession({ user: { ...user, accessToken }, loading: false });
sessionValid = true;
console.log('session valid');
} else {
setActiveSession({ user: null, loading: false });
console.log('session not valid');
}
} catch (error) {
console.error(error);
setActiveSession({ user: null, loading: false });
}
return {
type: 'check-user-session',
sessionValid,
} as const;
};
export const setHasSeenTutorial =
(hasSeenTutorial: boolean) => async (dispatch: React.Dispatch<any>) => {
@@ -70,4 +118,7 @@ export type UserActions =
| ActionType<typeof setIsLoggedIn>
| ActionType<typeof setUsername>
| ActionType<typeof setHasSeenTutorial>
| ActionType<typeof setDarkMode>;
| ActionType<typeof setDarkMode>
| ActionType<typeof setAccessToken>
// | ActionType<typeof setSession>
| ActionType<typeof checkUserSession>;

View File

@@ -15,5 +15,11 @@ export function userReducer(state: UserState, action: UserActions): UserState {
return { ...state, darkMode: action.darkMode };
case 'set-is-loggedin':
return { ...state, isLoggedin: action.loggedIn };
case 'check-user-session':
return { ...state, isSessionValid: action.sessionValid };
// case 'set-active-session':
// return { ...state, session: action.session };
// case 'set-access-token':
// return { ...state, token: action.token };
}
}

View File

@@ -4,4 +4,7 @@ export interface UserState {
darkMode: boolean;
hasSeenTutorial: boolean;
loading: boolean;
isSessionValid: boolean;
session?: any;
token?: string;
}

View File

@@ -0,0 +1,3 @@
export const CONFIG = {
serverUrl: '',
};

View File

@@ -0,0 +1,32 @@
/**
* Custom hook to manage state with utility functions to set state, set a specific field, and reset state.
*
* @param {T} initialState - The initial state value.
*
* @returns {UseSetStateReturn<T>} - An object containing:
* - `state`: The current state.
* - `resetState`: A function to reset the state to the initial value.
* - `setState`: A function to update the state.
* - `setField`: A function to update a specific field in the state.
*
* @example
* const { state, setState, setField, resetState } = useSetState({ name: '', age: 0 });
*
* return (
* <div>
* <p>Name: {state.name}</p>
* <p>Age: {state.age}</p>
* <button onClick={() => setField('name', 'John')}>Set Name</button>
* <button onClick={resetState}>Reset</button>
* </div>
* );
*/
type UseSetStateReturn<T> = {
state: T;
resetState: (defaultState?: T) => void;
setState: (updateState: T | Partial<T>) => void;
setField: (name: keyof T, updateValue: T[keyof T]) => void;
};
declare function useSetState<T>(initialState?: T): UseSetStateReturn<T>;
export { type UseSetStateReturn, useSetState };

View File

@@ -1,8 +1,19 @@
// 03_source/mobile/src/models/Event.ts
export type IDateValue = string | number | null;
export interface Event {
eventDate: Date;
joinMembers: undefined;
title: string;
id: string;
createdAt: IDateValue;
updatedAt: IDateValue;
//
name: string;
code: string;
price: number;
//
eventDate: Date;
joinMembers: { email: string; avatar: string; sex: string }[];
title: string;
currency: string;
duration_m: number;
ageBottom: number;
@@ -10,5 +21,4 @@ export interface Event {
location: string;
avatar: string;
//
id: string;
}

View File

@@ -1,8 +1,42 @@
export type IDateValue = string | number | null;
export interface Order {
export type IOrderProductItem = {
id: string;
sku: string;
name: string;
price: number;
coverUrl: string;
quantity: number;
};
export type IOrderHistory = {
orderTime: IDateValue;
paymentTime: IDateValue;
deliveryTime: IDateValue;
completionTime: IDateValue;
timeline: { title: string; time: IDateValue }[];
};
export type IOrderDelivery = {
shipBy: string;
speedy: string;
trackingNumber: string;
};
export type IOrderShippingAddress = {
fullAddress: string;
phoneNumber: string;
};
export type IOrderPayment = {
cardType: string;
cardNumber: string;
};
export interface IOrderItem {
id: string;
createdAt: IDateValue;
updatedAt: IDateValue;
//
taxes: number;
status: string;
@@ -12,4 +46,10 @@ export interface Order {
orderNumber: string;
totalAmount: number;
totalQuantity: number;
//
items: IOrderProductItem[];
history: IOrderHistory | undefined;
delivery: IOrderDelivery;
shippingAddress: IOrderShippingAddress;
payment: IOrderPayment;
}

View File

@@ -0,0 +1,134 @@
// REQ0042/event-detail
//
// PURPOSE:
// - show avatar in a row
//
// RULES:
// - T.B.A.
//
import React, { useEffect, useState } from 'react';
import {
IonHeader,
IonToolbar,
IonContent,
IonPage,
IonButtons,
IonMenuButton,
IonButton,
IonIcon,
IonDatetime,
IonSelectOption,
IonList,
IonItem,
IonLabel,
IonSelect,
IonPopover,
IonText,
IonFooter,
useIonRouter,
IonAvatar,
IonThumbnail,
} from '@ionic/react';
import './style.scss';
import {
chevronBackOutline,
ellipsisHorizontal,
ellipsisVertical,
heart,
logoIonic,
} from 'ionicons/icons';
import AboutPopover from '../../components/AboutPopover';
import { format, parseISO } from 'date-fns';
import { TestContent } from './TestContent';
import { Helloworld } from '../../api/Helloworld';
import { getEventById } from '../../api/getEventById';
import { connect } from '../../data/connect';
import * as selectors from '../../data/selectors';
import { Event } from '../../models/Event';
import { RouteComponentProps } from 'react-router';
const leftShift: number = 10;
const thumbnailSize: number = 40;
interface OwnProps extends RouteComponentProps {
event_detail?: Event;
}
interface StateProps {}
interface DispatchProps {}
interface EventDetailProps {
avatars: string[];
}
const AvatarRow: React.FC<{ avatars: string[] }> = ({ avatars }) => {
const router = useIonRouter();
const [showPopover, setShowPopover] = useState(false);
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
const [location, setLocation] = useState<'madison' | 'austin' | 'chicago' | 'seattle'>('madison');
const [conferenceDate, setConferenceDate] = useState('2047-05-17T00:00:00-05:00');
const selectOptions = {
header: 'Select a Location',
};
const presentPopover = (e: React.MouseEvent) => {
setPopoverEvent(e.nativeEvent);
setShowPopover(true);
};
function displayDate(date: string, dateFormat: string) {
return format(parseISO(date), dateFormat);
}
const [eventDetail, setEventDetail] = useState<Event | null>(null);
useEffect(() => {
Helloworld();
getEventById('1').then(({ data }) => {
console.log({ data });
setEventDetail(data);
});
}, []);
function handleBackOnClick() {
router.goBack();
}
return (
<>
<div style={{ display: 'inline-flex', alignItems: 'center' }}>
{avatars.slice(0, 3).map((m_avatar, idx) => (
<div
style={
idx == 0
? {}
: {
position: 'relative',
width: `calc( ${thumbnailSize}px - ${leftShift}px )`,
left: `-${leftShift}px`,
}
}
>
<IonThumbnail
style={{
'--size': `${thumbnailSize}px`,
'--border-radius': `${thumbnailSize / 2}px`,
border: '3px solid white',
}}
>
<img alt="Silhouette of a person's head" src={m_avatar} />
</IonThumbnail>
</div>
))}
<div style={{ marginLeft: '0.1rem', fontWeight: 'bold' }}>
{' '}
+{avatars.length - 3} going{' '}
</div>
</div>
</>
);
};
export default AvatarRow;

View File

@@ -26,14 +26,31 @@ import {
IonText,
IonFooter,
useIonRouter,
IonAvatar,
} from '@ionic/react';
import './style.scss';
import {
accessibility,
accessibilityOutline,
chevronBackOutline,
ellipsisHorizontal,
ellipsisVertical,
heart,
locationOutline,
locationSharp,
logoIonic,
man,
manOutline,
people,
peopleOutline,
timer,
timerOutline,
timerSharp,
wallet,
walletOutline,
walletSharp,
woman,
womanOutline,
} from 'ionicons/icons';
import AboutPopover from '../../components/AboutPopover';
import { format, parseISO } from 'date-fns';
@@ -44,28 +61,46 @@ import { connect } from '../../data/connect';
import * as selectors from '../../data/selectors';
import { Event } from '../../models/Event';
import { RouteComponentProps } from 'react-router';
import AvatarRow from './AvatarRow';
const leftShift: number = -25;
interface OwnProps extends RouteComponentProps {
event?: Event;
event_detail?: Event;
}
interface StateProps {}
interface DispatchProps {}
interface SpeakerDetailProps extends OwnProps, StateProps, DispatchProps {}
interface EventDetailProps extends OwnProps, StateProps, DispatchProps {}
interface AboutProps {}
const showJoinedMembers = (joinMembers: Record<string, any>[]) => {
const avatars = joinMembers.map((jm) => jm.avatar);
console.log({ joinMembers });
return (
<>
<AvatarRow avatars={avatars} />
<IonButton style={{ '--padding-start': '20px', '--padding-end': '20px' }} shape="round">
More
</IonButton>
</>
);
};
const EventDetail: React.FC<EventDetailProps> = ({ event_detail }) => {
const router = useIonRouter();
const EventDetail: React.FC<SpeakerDetailProps> = () => {
const [showPopover, setShowPopover] = useState(false);
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
const [location, setLocation] = useState<
'madison' | 'austin' | 'chicago' | 'seattle'
>('madison');
const [conferenceDate, setConferenceDate] = useState(
'2047-05-17T00:00:00-05:00'
);
const [location, setLocation] = useState<'madison' | 'austin' | 'chicago' | 'seattle'>('madison');
const [conferenceDate, setConferenceDate] = useState('2047-05-17T00:00:00-05:00');
const [totalJoinMembers, setTotalJoinMembers] = useState<number>(0);
const [maleMembers, setMaleMembers] = useState<number>(0);
const [femaleMembers, setFemaleMembers] = useState<number>(0);
const selectOptions = {
header: 'Select a Location',
@@ -80,6 +115,14 @@ const EventDetail: React.FC<SpeakerDetailProps> = () => {
return format(parseISO(date), dateFormat);
}
useEffect(() => {
if (event_detail) {
setTotalJoinMembers(event_detail.joinMembers.length);
setMaleMembers(event_detail.joinMembers.filter((m) => m.sex == 'M').length);
setFemaleMembers(event_detail.joinMembers.filter((m) => m.sex == 'F').length);
}
}, [event_detail]);
const [eventDetail, setEventDetail] = useState<Event | null>(null);
useEffect(() => {
Helloworld();
@@ -89,15 +132,14 @@ const EventDetail: React.FC<SpeakerDetailProps> = () => {
});
}, []);
const router = useIonRouter();
function handleBackOnClick() {
router.goBack();
}
if (!eventDetail) return <>loading</>;
if (!event_detail) return <>loading</>;
return (
<IonPage id="about-page">
<IonPage id="event-detail-page">
<IonContent>
<IonHeader className="ion-no-border">
<IonToolbar>
@@ -109,63 +151,110 @@ const EventDetail: React.FC<SpeakerDetailProps> = () => {
</IonButtons>
<IonButtons slot="end">
<IonButton onClick={presentPopover}>
<IonIcon
slot="icon-only"
ios={ellipsisHorizontal}
md={ellipsisVertical}
></IonIcon>
<IonIcon slot="icon-only" ios={ellipsisHorizontal} md={ellipsisVertical}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<div className="about-header">
<div className="about-image madison" style={{ opacity: 1 }}></div>
</div>
<div>{eventDetail.avatar}</div>
<div>
<div>{format(new Date(eventDetail.eventDate), 'yyyy-MM-dd')}</div>
<h1>{eventDetail.title}</h1>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<div>members place holder</div>
<IonButton shape="round">More</IonButton>
</div>
<div
style={{
marginBottom: '1rem',
paddingTop: '1rem',
borderBottom: '1px solid black',
}}
className="about-image madison"
style={{ opacity: 1, backgroundImage: `url(${event_detail.avatar[0]})` }}
></div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div style={{ display: 'flex', gap: '1rem' }}>
<div>{eventDetail.currency}</div>
<div>{eventDetail.price}</div>
per person
<div style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem' }}>
<div>
<div style={{ paddingTop: '0.25rem', color: '#007AFF' }}>
{format(new Date(event_detail.eventDate), 'EEE, dd MMM yyyy, hh:mm a')}
</div>
<div style={{ paddingTop: '0.25rem', fontSize: '1.8rem', fontWeight: '500' }}>
{event_detail.title}
</div>
<div
style={{
display: 'flex',
gap: '1rem',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{event_detail.joinMembers && event_detail.joinMembers.length > 0 ? (
showJoinedMembers(event_detail.joinMembers)
) : (
<>join fast !</>
)}
</div>
</div>
</div>
<div
style={{
marginBottom: '1rem',
paddingTop: '1rem',
borderBottom: '1px solid gray',
}}
></div>
<div
style={{
paddingLeft: '0.5rem',
paddingRight: '0.5rem',
display: 'flex',
flexDirection: 'column',
gap: '1rem',
}}
>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<IonIcon icon={walletSharp} style={{ fontSize: '1.5rem' }}></IonIcon>
<div style={{ display: 'flex', gap: '0.15rem', alignItems: 'center' }}>
<div style={{ fontWeight: 'bold' }}>{event_detail.currency}</div>
<div style={{ fontWeight: 'bold' }}>{event_detail.price}</div>
per person
</div>
</div>
<div>{eventDetail.duration_m}</div>
<div style={{ display: 'flex', gap: '1rem' }}>
<div>{eventDetail.ageBottom}</div>
<div>{eventDetail.ageTop}</div>
<div>years old</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<IonIcon icon={timerSharp} style={{ fontSize: '1.5rem' }}></IonIcon>
<div style={{ display: 'flex', gap: '0.15rem', alignItems: 'center' }}>
{event_detail.duration_m}
<div>mins</div>
</div>
</div>
<div>{eventDetail.location}</div>
<div style={{ display: 'flex', gap: '1rem' }}>
<IonIcon icon={logoIonic}></IonIcon>
<div>40</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<IonIcon icon={people} style={{ fontSize: '1.5rem' }}></IonIcon>
<div style={{ display: 'flex', gap: '0.15rem', alignItems: 'center' }}>
<div>{event_detail.ageBottom}</div>~<div>{event_detail.ageTop}</div>
<div>years old</div>
</div>
</div>
<IonIcon icon={logoIonic}></IonIcon>
<div>20</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<IonIcon icon={locationSharp} style={{ fontSize: '1.5rem' }}></IonIcon>
{event_detail.location}
</div>
<IonIcon icon={logoIonic}></IonIcon>
<div>20</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<IonIcon
icon={accessibility}
style={{ fontSize: '1.5rem', color: 'rgb(139, 44, 245)' }}
></IonIcon>
<div>{totalJoinMembers}</div>
<IonIcon
icon={man}
style={{ fontSize: '1.5rem', color: 'rgb(67, 110, 205)' }}
></IonIcon>
<div>{maleMembers}</div>
<IonIcon
icon={woman}
style={{ fontSize: '1.5rem', color: 'rgb(235, 50, 35)' }}
></IonIcon>
<div>{femaleMembers}</div>
</div>
</div>
</IonContent>
@@ -197,8 +286,11 @@ const EventDetail: React.FC<SpeakerDetailProps> = () => {
};
export default connect({
mapStateToProps: (state, ownProps) => ({
event: selectors.getEvent(state, ownProps),
}),
mapStateToProps: (state, ownProps) => {
console.log({ t1: selectors.getEvents(state) });
return {
event_detail: selectors.getEvent(state, ownProps),
};
},
component: EventDetail,
});

View File

@@ -1,4 +1,4 @@
#about-page {
#event-detail-page {
ion-toolbar {
position: absolute;

View File

@@ -0,0 +1,12 @@
import { IonIcon } from '@ionic/react';
import { woman } from 'ionicons/icons';
import React from 'react';
export function NumOfFemaleMemberJoin({ joinMembers }) {
return (
<>
<IonIcon icon={woman} style={{ fontSize: '1.1rem', color: 'rgb(235, 50, 35)' }}></IonIcon>
<div>{joinMembers.filter((jm) => jm.sex == 'F').length}</div>
</>
);
}

View File

@@ -0,0 +1,12 @@
import { IonIcon } from '@ionic/react';
import { man } from 'ionicons/icons';
import React from 'react';
export function NumOfMaleMemberJoin({ joinMembers }) {
return (
<>
<IonIcon icon={man} style={{ fontSize: '1.1rem', color: 'rgb(67, 110, 205)' }}></IonIcon>
<div>{joinMembers.filter((jm) => jm.sex == 'M').length}</div>
</>
);
}

View File

@@ -0,0 +1,15 @@
import { IonIcon } from '@ionic/react';
import { accessibility } from 'ionicons/icons';
import React from 'react';
export function NumOfMemberJoin({ joinMembers }) {
return (
<>
<IonIcon
icon={accessibility}
style={{ fontSize: '1.1rem', color: 'rgb(139, 44, 245)' }}
></IonIcon>
<div>{joinMembers.length}</div>
</>
);
}

View File

@@ -1,6 +1,6 @@
// REQ0041/home_discover_event_tab
import React, { useEffect, useRef, useState } from 'react';
import React, { useRef } from 'react';
import {
IonHeader,
IonToolbar,
@@ -8,54 +8,49 @@ import {
IonContent,
IonPage,
IonButtons,
IonMenuButton,
IonGrid,
IonRow,
IonCol,
useIonRouter,
IonButton,
IonIcon,
IonPopover,
IonAvatar,
IonImg,
IonItem,
IonLabel,
IonList,
IonModal,
IonSearchbar,
useIonModal,
IonInput,
IonRefresher,
IonRefresherContent,
RefresherEventDetail,
} from '@ionic/react';
import SpeakerItem from '../../components/SpeakerItem';
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 { getEvents } from '../../api/getEvents';
import { format } from 'date-fns';
import { Event } from './types';
import { chevronDownCircleOutline, heart, menuOutline } from 'ionicons/icons';
import AboutPopover from '../../components/AboutPopover';
// import { Event } from './types';
import { chevronDownCircleOutline, menuOutline } from 'ionicons/icons';
import Loading from '../../components/Loading';
import { Event } from '../../models/Event';
//
import { NumOfMemberJoin } from './NumOfMemberJoin';
import { NumOfMaleMemberJoin } from './NumOfMaleMemberJoin';
import { NumOfFemaleMemberJoin } from './NumOfFemaleMemberJoin';
interface OwnProps {}
interface StateProps {
events: Event[];
fetchEventResult: any;
}
interface DispatchProps {}
interface SpeakerListProps extends OwnProps, StateProps, DispatchProps {}
const EventList: React.FC<SpeakerListProps> = ({ events }) => {
const EventList: React.FC<SpeakerListProps> = ({ fetchEventResult }) => {
const router = useIonRouter();
const modal = useRef<HTMLIonModalElement>(null);
const router = useIonRouter();
const {
result: { status },
data: { events },
} = fetchEventResult;
function handleShowPartyEventDetail(event_id: string) {
router.push(`/event_detail/${event_id}`);
@@ -68,6 +63,9 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
}, 2000);
}
if (status != 200)
return <>Error during fetching event list, check /events endpoint if working</>;
if (!events || events.length == 0) return <Loading />;
return (
@@ -84,9 +82,14 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>
<IonContent className="ion-padding" fullscreen={true}>
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
<IonRefresherContent pullingIcon={chevronDownCircleOutline} pullingText="Pull to refresh" refreshingSpinner="circles" refreshingText="Refreshing..."></IonRefresherContent>
<IonRefresherContent
pullingIcon={chevronDownCircleOutline}
pullingText="Pull to refresh"
refreshingSpinner="circles"
refreshingText="Refreshing..."
></IonRefresherContent>
</IonRefresher>
<IonHeader collapse="condense">
@@ -101,35 +104,43 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
<IonCol size="12" size-md="6" key={idx}>
<div
style={{
padding: '1rem',
border: '1px solid black',
border: '1px solid lightgrey',
borderRadius: '1rem',
}}
onClick={() => handleShowPartyEventDetail(event.id)}
>
<div
style={{
backgroundImage: `url("https://plus.unsplash.com/premium_photo-1683121126477-17ef068309bc")`,
backgroundImage: `url(${event.avatar[0]})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
height: '33vw',
//
borderRadius: '1rem 1rem 0 0',
}}
></div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '1rem',
//
marginTop: '1rem',
}}
>
<div>{format(new Date(event.eventDate), 'yyyy-MM-dd')}</div>
<div>{event.title}</div>
<div>{event.currency}</div>
<div>{event.price}</div>
<div>
{40} {20} {20}
<div style={{ marginTop: '1rem' }}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
padding: '0.5rem',
paddingBottom: '1rem',
}}
>
{/* <div>{format(new Date(event.eve ntDate), 'yyyy-MM-dd')}</div> */}
<div style={{ color: 'rgb(0, 122, 255)' }}>
{format(new Date(event.eventDate), 'EEE, dd MMM yyyy, hh:mm a')}
</div>
<div style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>{event.name}</div>
<div style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>{event.price}</div>
<div style={{ display: 'flex', gap: '0.25rem', alignItems: 'center' }}>
<NumOfMemberJoin joinMembers={event.joinMembers} />
<NumOfMaleMemberJoin joinMembers={event.joinMembers} />
<NumOfFemaleMemberJoin joinMembers={event.joinMembers} />
</div>
</div>
</div>
</div>
@@ -140,7 +151,12 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
</IonContent>
{/* REQ0079/event-filter */}
<IonModal ref={modal} trigger="events-open-modal" initialBreakpoint={0.5} breakpoints={[0, 0.25, 0.5, 0.75]}>
<IonModal
ref={modal}
trigger="events-open-modal"
initialBreakpoint={0.5}
breakpoints={[0, 0.25, 0.5, 0.75]}
>
<IonContent className="ion-padding">
<div
style={{
@@ -203,8 +219,10 @@ const EventList: React.FC<SpeakerListProps> = ({ events }) => {
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
events: selectors.getEvents(state),
}),
mapStateToProps: (state) => {
return {
fetchEventResult: selectors.getEvents(state),
};
},
component: React.memo(EventList),
});

View File

@@ -1,4 +1,5 @@
export interface Event {
// OBSOLETED
export interface EventOBSOLETED {
eventDate: Date;
joinMembers: undefined;
title: string;

View File

@@ -0,0 +1,27 @@
// REQ0041/home_discover_event_tab
import { IonPage, IonHeader, IonToolbar, IonButtons, IonButton, IonIcon, IonTitle, IonContent } from '@ionic/react';
import { menuOutline } from 'ionicons/icons';
import React, { useEffect, useRef, useState } from 'react';
const Helloworld: React.FC = () => {
return (
<IonPage id="speaker-list">
<IonHeader translucent={true} className="ion-no-border">
<IonToolbar>
<IonButtons slot="end">
{/* <IonMenuButton /> */}
<IonButton shape="round" id="events-open-modal" expand="block">
<IonIcon slot="icon-only" icon={menuOutline}></IonIcon>
</IonButton>
</IonButtons>
<IonTitle>Discover Events</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>Helloworld</IonContent>
</IonPage>
);
};
export default Helloworld;

View File

@@ -26,7 +26,7 @@ const MainTabs: React.FC<MainTabsProps> = () => {
<IonTabs>
<IonRouterOutlet>
{/* REQ0117/default-route */}
<Redirect exact path="/tabs" to="/tabs/schedule" />
<Redirect exact path="/tabs" to="/tabs/events" />
{/*
Using the render method prop cuts down the number of renders your components will have due to route changes.
Use the component prop when your component depends on the RouterComponentProps passed in automatically.
@@ -68,7 +68,7 @@ const MainTabs: React.FC<MainTabsProps> = () => {
<IonIcon icon={informationCircle} />
<IonLabel>Message</IonLabel>
</IonTabButton>
<IonTabButton tab="my_profile" href="/tabs/my_profile">
<IonTabButton tab="my_profile" href={paths.PROFILE}>
<IonIcon icon={informationCircle} />
<IonLabel>Profile</IonLabel>
</IonTabButton>

View File

@@ -0,0 +1,12 @@
const CMS_BACKEND_URL = 'http://192.168.10.75:7272';
const endpoints = {
auth: {
me: `http://localhost:7272/api/auth/me`,
signIn: `${CMS_BACKEND_URL}/api/auth/sign-in`,
signUp: `${CMS_BACKEND_URL}/api/auth/sign-up`,
//
},
};
export { endpoints };

View File

@@ -0,0 +1,269 @@
import { z as zod } from 'zod';
import React, { useEffect, useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLocalStorage } from 'react-use';
import {
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonPage,
IonButtons,
IonMenuButton,
IonRow,
IonCol,
IonButton,
IonInput,
IonIcon,
useIonRouter,
} from '@ionic/react';
import { useHistory } from 'react-router';
import './style.scss';
import {
setAccessToken,
setIsLoggedIn,
setUsername,
checkUserSession,
} from '../../data/user/user.actions';
import { connect } from '../../data/connect';
import { chevronBack, chevronBackOutline } from 'ionicons/icons';
import { signInWithPassword } from '../../context/jwt/action';
import axios from 'axios';
import { endpoints } from './endpoints';
export type SignInSchemaType = zod.infer<typeof SignInSchema>;
export 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!' }),
});
interface OwnProps {}
interface DispatchProps {
setIsLoggedIn: typeof setIsLoggedIn;
setUsername: typeof setUsername;
setAccessToken: typeof setAccessToken;
checkUserSession: typeof checkUserSession;
}
interface StateProps {
isSessionValid: boolean;
}
interface LoginProps extends OwnProps, StateProps, DispatchProps {}
type UserType = Record<string, any> | null;
const Login: React.FC<LoginProps> = (props) => {
const {
setAccessToken,
setIsLoggedIn,
setUsername: setUsernameAction,
checkUserSession,
isSessionValid,
} = props;
const history = useHistory();
// TODO: delete unused code
// const [login, setLogin] = useState({ email: '', password: '' });
// const [submitted, setSubmitted] = useState(false);
// const onLogin = async (e: React.FormEvent) => {
// e.preventDefault();
// setSubmitted(true);
// if (login.email && login.password) {
// await setIsLoggedIn(true);
// await login.email;
// history.push('/tabs/events');
// }
// };
const router = useIonRouter();
function handleBackButtonClick() {
router.goBack();
}
const onSignup = () => {
history.push('/signup');
};
// ----------
const defaultValues: SignInSchemaType = {
email: '',
password: '',
};
const methods = useForm<SignInSchemaType>({
resolver: zodResolver(SignInSchema),
defaultValues,
});
const {
reset,
watch,
setValue,
handleSubmit,
register,
formState: { isDirty, dirtyFields, errors, isSubmitting },
} = methods;
const values = watch();
useEffect(() => {
(async () => {
console.log({ isSessionValid });
if (isSessionValid) {
await setIsLoggedIn(true);
router.push('/tabs');
// reset();
reset();
}
})();
}, [isSessionValid]);
const onSubmit = handleSubmit(async (data) => {
console.log({ data });
try {
let token = await signInWithPassword({ email: values.email, password: values.password });
console.log({ token });
if (token) setAccessToken(token);
await checkUserSession();
// NOTE: page forward handled by changing of state `isSessionValid`
} catch (error) {
console.error(error);
// const feedbackMessage = getErrorMessage(error);
// setErrorMessage(feedbackMessage);
}
});
const {
onChange: emailOnChange,
onBlur: emailOnBlur,
name: emailName,
ref: emailRef,
} = register('email');
const {
onChange: passwordOnChange,
onBlur: passwordOnBlur,
name: passwordName,
ref: passwordRef,
} = register('password');
return (
<IonPage id="login-page">
<IonHeader translucent={true} className="ion-no-border">
<IonToolbar>
<IonButton
slot="start"
fill="clear"
shape="round"
onClick={() => handleBackButtonClick()}
>
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
</IonButton>
<IonTitle>Login</IonTitle>
</IonToolbar>
</IonHeader>
{/* */}
<IonContent>
<div className="login-logo">
<img src="/assets/img/appicon.svg" alt="Ionic logo" />
</div>
<div className="login-form">
<form onSubmit={onSubmit} noValidate autoComplete="off">
{/* // ${!errors.email && 'ion-valid'} */}
<IonInput
className={`
${errors.email && 'ion-invalid'}
${dirtyFields.email && 'ion-touched'}
`}
label="Email"
labelPlacement="floating"
placeholder="Email"
//
fill="outline"
// name="email"
type="email"
spellCheck={false}
autocapitalize="off"
//
helperText="Enter a valid email"
errorText={errors.email?.message}
//
required
disabled={isSubmitting}
// value={values.email}
// onIonInput={(e) => setValue('email', e.detail.value!)}
{...register('email')}
/>
{/* // ${!errors.password && 'ion-valid'} */}
<IonInput
className={`
${errors.password && 'ion-invalid'}
${dirtyFields.password && 'ion-touched'}
`}
// label="Password"
// labelPlacement={values.password == '' ? 'start' : 'stacked'}
// name="password"
placeholder="Password"
//
fill="outline"
type="password"
errorText={errors.password?.message}
value={values.password}
onIonInput={(e) => setValue('password', e.detail.value!)}
required
disabled={isSubmitting}
{...register('password')}
/>
<IonRow>
<IonCol>
<IonButton disabled={isSubmitting || !isDirty} type="submit" expand="block">
{isSubmitting ? 'logging in' : 'Login'}
</IonButton>
</IonCol>
<IonCol>
<IonButton onClick={onSignup} color="light" expand="block">
Signup
</IonButton>
</IonCol>
</IonRow>
</form>
</div>
</IonContent>
</IonPage>
);
};
export default connect<{}, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
isSessionValid: state.user.isSessionValid,
}),
mapDispatchToProps: {
setIsLoggedIn,
setUsername,
setAccessToken,
checkUserSession,
},
component: Login,
});

View File

@@ -0,0 +1,22 @@
import { jwtDecode } from './jwtDecode';
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;
}
}

View File

@@ -0,0 +1,19 @@
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;
}
}

View File

@@ -0,0 +1,23 @@
#login-page, #signup-page, #support-page {
.login-logo {
min-height: 200px;
padding: 20px 0;
text-align: center;
}
.login-logo img {
max-width: 150px;
}
.list {
margin-bottom: 0;
}
.login-form {
padding: 16px;
}
ion-input {
margin-bottom: 10px;
}
}

View File

@@ -44,7 +44,14 @@ import '../../SpeakerList.scss';
import { getEvents } from '../../../api/getEvents';
import { format } from 'date-fns';
import { Event } from '../types';
import { alertOutline, chevronDownCircleOutline, createOutline, heart, menuOutline, settingsOutline } from 'ionicons/icons';
import {
alertOutline,
chevronDownCircleOutline,
createOutline,
heart,
menuOutline,
settingsOutline,
} from 'ionicons/icons';
import AboutPopover from '../../../components/AboutPopover';
import paths from '../../../paths';
import { getProfileById } from '../../../api/getProfileById';
@@ -89,7 +96,7 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
function handleForwardLoginPage() {
try {
setDisableForwardLoginButton(true);
router.push(paths.login);
router.push(paths.SIGN_IN);
setDisableForwardLoginButton(false);
} catch (error) {
console.error(error);
@@ -121,7 +128,12 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
<IonContent fullscreen={true}>
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
<IonRefresherContent pullingIcon={chevronDownCircleOutline} pullingText="Pull to refresh" refreshingSpinner="circles" refreshingText="Refreshing..."></IonRefresherContent>
<IonRefresherContent
pullingIcon={chevronDownCircleOutline}
pullingText="Pull to refresh"
refreshingSpinner="circles"
refreshingText="Refreshing..."
></IonRefresherContent>
</IonRefresher>
<IonHeader collapse="condense" className="ion-no-border">
@@ -130,12 +142,28 @@ const MyProfile: React.FC<SpeakerListProps> = ({ speakers, speakerSessions }) =>
</IonToolbar>
</IonHeader>
<div style={{ height: '50vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div
style={{
height: '50vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div>
not login yet, <br />
please login or sign up
</div>
<div style={{ height: '50vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div
style={{
height: '50vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IonButton disabled={disableForwardLoginButton} onClick={handleForwardLoginPage}>
Login
</IonButton>

View File

@@ -0,0 +1,173 @@
// REQ0041/home_discover_event_tab
import {
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonButton,
IonIcon,
IonTitle,
IonContent,
useIonRouter,
} from '@ionic/react';
import { chevronBackOutline, menuOutline } from 'ionicons/icons';
import React, { useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router';
import { IOrderItem } from '../../models/Order';
import { connect } from '../../data/connect';
import * as selectors from '../../data/selectors';
import './style.scss';
import paths from '../../paths';
interface OwnProps {}
interface StateProps {
order: IOrderItem;
}
interface DispatchProps {}
interface OrderDetailProps extends OwnProps, StateProps, DispatchProps {}
const OrderDetail: React.FC<OrderDetailProps> = ({ order }) => {
const { id } = useParams<{ id: string }>();
const router = useIonRouter();
function handleBackClick() {
router.goBack();
}
return (
<IonPage id="speaker-list">
<IonHeader translucent={true} className="ion-no-border">
<IonToolbar>
<IonButtons slot="start">
{/* <IonMenuButton /> */}
<IonButton
shape="round"
id="events-open-modal"
expand="block"
onClick={handleBackClick}
>
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
</IonButton>
</IonButtons>
<IonTitle>Order Details ()</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding" fullscreen={true}>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Total</div>
<div>{order.totalAmount}</div>
</div>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>created at:</div>
<div>{order.createdAt}</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>updated at:</div>
<div>{order.updatedAt}</div>
</div>
</div>
<div>
<h2>History</h2>
<h3>Delivery</h3>
<div style={{ display: 'flex', gap: '1rem' }}>
<div>
{order.history?.timeline.map((t) => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>{t.title}</div>
<div>{t.time}</div>
</div>
))}
</div>
</div>
<div>
<h3></h3>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Order time</div>
<div>29 May 2025 4:01 pm</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Payment time</div>
<div>29 May 2025 4:01 pm</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Delivery time for the carrier</div>
<div>29 May 2025 4:01 pm</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Completion time</div>
<div>29 May 2025 4:01 pm</div>
</div>
</div>
</div>
</div>
<div>
<h2>Delivery</h2>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Ship by</div>
<div>{order.delivery.shipBy}</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Speedy</div>
<div>{order.delivery.speedy}</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Tracking No.</div>
<div>{order.delivery.trackingNumber}</div>
</div>
</div>
<div>
<h2>Shipping</h2>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Address</div>
<div>{order.shippingAddress.fullAddress}</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Phone Number</div>
<div>{order.shippingAddress.phoneNumber}</div>
</div>
</div>
<div>
<h2>Payment</h2>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Card Type</div>
<div>{order.payment.cardType}</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>Card Number</div>
<div>{order.payment.cardNumber}</div>
</div>
</div>
</div>
</IonContent>
</IonPage>
);
};
export default connect({
mapStateToProps: (state, ownProps) => ({
order: selectors.getOrder(state, ownProps),
}),
component: React.memo(OrderDetail),
});

View File

@@ -0,0 +1,2 @@
#order-detail-page {
}

View File

@@ -31,7 +31,7 @@ import {
} from '@ionic/react';
import SpeakerItem from '../../components/SpeakerItem';
import { Speaker } from '../../models/Speaker';
import { Order } from '../../models/Order';
import { IOrderItem } from '../../models/Order';
import { Session } from '../../models/Schedule';
import { connect } from '../../data/connect';
import * as selectors from '../../data/selectors';
@@ -39,7 +39,15 @@ import '../SpeakerList.scss';
import { getEvents } from '../../api/getEvents';
import { format } from 'date-fns';
// import { Order } from './types';
import { bookmarksOutline, chevronBackOutline, chevronDownCircleOutline, chevronForwardOutline, heart, logoIonic, menuOutline } from 'ionicons/icons';
import {
bookmarksOutline,
chevronBackOutline,
chevronDownCircleOutline,
chevronForwardOutline,
heart,
logoIonic,
menuOutline,
} from 'ionicons/icons';
import AboutPopover from '../../components/AboutPopover';
import { getOrders } from '../../api/getOrders';
import Loading from '../../components/Loading';
@@ -48,7 +56,7 @@ import paths from '../../paths';
interface OwnProps {}
interface StateProps {
orders: Order[];
fetchOrderResult: { result: { status: number; ok: boolean }; data: IOrderItem[] };
//
speakerSessions: { [key: string]: Session[] };
}
@@ -78,9 +86,19 @@ const NumApplicants: React.FC<{ amount: number }> = ({ amount }) => {
const TotalAmount: React.FC<{ amount: number }> = ({ amount }) => {
return (
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '1.1rem' }}>
<div style={{ display: 'flex', gap: '1rem', fontWeight: 'bold', fontSize: '1.2rem', opacity: 0.8 }}>
<div
style={{
display: 'flex',
gap: '1rem',
fontWeight: 'bold',
fontSize: '1.2rem',
opacity: 0.8,
}}
>
<div>Total:</div>
<div style={{ minWidth: '75px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
<div style={{ minWidth: '75px', display: 'inline-flex', justifyContent: 'flex-end' }}>
{amount}{' '}
</div>
</div>
</div>
);
@@ -91,7 +109,9 @@ const Subtotal: React.FC<{ amount: number }> = ({ amount }) => {
return (
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>
<div>Subtotal:</div>
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>
{amount}{' '}
</div>
</div>
);
};
@@ -102,7 +122,9 @@ const Shipping: React.FC<{ amount: number }> = ({ amount }) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={{ display: 'flex', gap: '1rem', fontWeight: 'bold' }}>
<div>Shipping:</div>
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>
{amount}{' '}
</div>
</div>
</div>
);
@@ -114,7 +136,9 @@ const Discount: React.FC<{ amount: number }> = ({ amount }) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={{ display: 'flex', gap: '1rem', fontWeight: 'bold' }}>
<div>Discount:</div>
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>
{amount}{' '}
</div>
</div>
</div>
);
@@ -126,25 +150,25 @@ const Tax: React.FC<{ amount: number }> = ({ amount }) => {
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div style={{ display: 'flex', gap: '1rem', fontWeight: 'bold' }}>
<div>Tax:</div>
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>{amount} </div>
<div style={{ minWidth: '50px', display: 'inline-flex', justifyContent: 'flex-end' }}>
{amount}{' '}
</div>
</div>
</div>
);
};
const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
const OrderList: React.FC<SpeakerListProps> = ({ fetchOrderResult, speakerSessions }) => {
const router = useIonRouter();
const [showPopover, setShowPopover] = useState(false);
const [popoverEvent, setPopoverEvent] = useState<MouseEvent>();
const modal = useRef<HTMLIonModalElement>(null);
useEffect(() => {
// getOrders().then(({ data }) => {
// console.log({ data });
// setEvents(data);
// });
}, []);
const {
result: { status },
data: { orders },
} = fetchOrderResult;
function handleRefresh(event: CustomEvent<RefresherEventDetail>) {
setTimeout(() => {
@@ -165,8 +189,13 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
router.push(paths.FAVOURITES_LIST);
}
if (status != 200)
return <>Error during fetching order list, check /orders endpoint if working</>;
if (!orders) return <Loading />;
if (orders.length == 0) return <>order list is empty</>;
return (
<IonPage id="speaker-list">
<IonHeader translucent={true} className="ion-no-border">
@@ -183,7 +212,12 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
<IonContent fullscreen={true}>
<IonRefresher slot="fixed" onIonRefresh={handleRefresh}>
<IonRefresherContent pullingIcon={chevronDownCircleOutline} pullingText="Pull to refresh" refreshingSpinner="circles" refreshingText="Refreshing..."></IonRefresherContent>
<IonRefresherContent
pullingIcon={chevronDownCircleOutline}
pullingText="Pull to refresh"
refreshingSpinner="circles"
refreshingText="Refreshing..."
></IonRefresherContent>
</IonRefresher>
<IonHeader collapse="condense">
@@ -193,17 +227,27 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
</IonHeader>
<IonList>
{orders.map((order, idx) => (
<IonItem button onClick={handleNotImplemented}>
{orders.map((order: IOrderItem, idx: number) => (
<IonItem button onClick={() => handleShowOrderDetail(order.id)} key={idx}>
<div style={{ paddingBottom: '1rem', paddingTop: '1rem' }}>
<div style={{ display: 'flex', gap: '0.5rem', width: 'calc( 100vw - 35px )' }}>
<div style={{}}>
<div>
<div style={{ width: '70px' }}>
<IonAvatar>
<img alt="Silhouette of a person's head" src="https://plus.unsplash.com/premium_photo-1683121126477-17ef068309bc" />
<img
alt="Silhouette of a person's head"
src="https://plus.unsplash.com/premium_photo-1683121126477-17ef068309bc"
/>
</IonAvatar>
<div style={{ marginTop: '1rem', display: 'inline-flex', flexDirection: 'column', gap: '0.5rem' }}>
<div
style={{
marginTop: '1rem',
display: 'inline-flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
<NumApplicants amount={38} />
<RemainingDays amount={50} />
</div>
@@ -228,8 +272,12 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
}}
>
<div style={{ fontSize: '1.2rem' }}>{order.orderNumber}</div>
<IonButton shape="round" onClick={() => handleShowOrderDetail('1')} size="small" fill="clear">
<IonIcon slot="icon-only" icon={chevronForwardOutline} size="small"></IonIcon>
<IonButton shape="round" size="small" fill="clear">
<IonIcon
slot="icon-only"
icon={chevronForwardOutline}
size="small"
></IonIcon>
</IonButton>
</div>
@@ -272,9 +320,9 @@ const EventList: React.FC<SpeakerListProps> = ({ orders, speakerSessions }) => {
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
orders: selectors.getOrders(state),
// TODO: review below
fetchOrderResult: selectors.getOrders(state),
// TODO: review unused code
speakerSessions: selectors.getSpeakerSessions(state),
}),
component: React.memo(EventList),
component: React.memo(OrderList),
});

View File

@@ -44,9 +44,7 @@ interface SpeakerDetailProps extends OwnProps, StateProps, DispatchProps {}
const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
const [showActionSheet, setShowActionSheet] = useState(false);
const [actionSheetButtons, setActionSheetButtons] = useState<
ActionSheetButton[]
>([]);
const [actionSheetButtons, setActionSheetButtons] = useState<ActionSheetButton[]>([]);
const [actionSheetHeader, setActionSheetHeader] = useState('');
function openSpeakerShare(speaker: Speaker) {
@@ -112,18 +110,10 @@ const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
</IonButtons>
<IonButtons slot="end">
<IonButton onClick={() => openContact(speaker)}>
<IonIcon
slot="icon-only"
ios={callOutline}
md={callSharp}
></IonIcon>
<IonIcon slot="icon-only" ios={callOutline} md={callSharp}></IonIcon>
</IonButton>
<IonButton onClick={() => openSpeakerShare(speaker)}>
<IonIcon
slot="icon-only"
ios={shareOutline}
md={shareSharp}
></IonIcon>
<IonIcon slot="icon-only" ios={shareOutline} md={shareSharp}></IonIcon>
</IonButton>
</IonButtons>
</IonToolbar>
@@ -141,9 +131,7 @@ const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
<IonChip
color="twitter"
onClick={() =>
openExternalUrl(`https://twitter.com/${speaker.twitter}`)
}
onClick={() => openExternalUrl(`https://twitter.com/${speaker.twitter}`)}
>
<IonIcon icon={logoTwitter}></IonIcon>
<IonLabel>Twitter</IonLabel>
@@ -151,9 +139,7 @@ const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
<IonChip
color="dark"
onClick={() =>
openExternalUrl('https://github.com/ionic-team/ionic-framework')
}
onClick={() => openExternalUrl('https://github.com/ionic-team/ionic-framework')}
>
<IonIcon icon={logoGithub}></IonIcon>
<IonLabel>GitHub</IonLabel>
@@ -161,9 +147,7 @@ const SpeakerDetail: React.FC<SpeakerDetailProps> = ({ speaker }) => {
<IonChip
color="instagram"
onClick={() =>
openExternalUrl('https://instagram.com/ionicframework')
}
onClick={() => openExternalUrl('https://instagram.com/ionicframework')}
>
<IonIcon icon={logoInstagram}></IonIcon>
<IonLabel>Instagram</IonLabel>

View File

@@ -1,17 +1,25 @@
const paths = {
NOT_IMPLEMENTED: '/not_implemented',
TAB_NOT_IMPLEMENTED: '/tabs/not_implemented',
//
SETTINGS: '/settings',
//
EVENT_LIST: `/tabs/events`,
MESSAGE_LIST: `/tabs/messages`,
NEARBY_LIST: '/tabs/nearby',
//
ORDERS_LIST: '/tabs/orders',
FAVOURITES_LIST: `/tabs/favourites`,
//
ORDER_DETAIL: '/order_detail/:id',
getOrderDetail: (id: string) => `/order_detail/${id}`,
//
FAVOURITES_LIST: `/tabs/favourites`,
CHANGE_LANGUAGE: '/change_language',
SERVICE_AGREEMENT: '/service_agreement',
PRIVACY_AGREEMENT: '/privacy_agreement',
//
login: '/login',
PROFILE: '/tabs/my_profile',
//
SIGN_IN: '/mylogin',
};
export default paths;

View File

@@ -225,6 +225,13 @@
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz"
integrity sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==
"@hookform/resolvers@^4.1.3":
version "4.1.3"
resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz"
integrity sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==
dependencies:
"@standard-schema/utils" "^0.3.0"
"@ionic/cli-framework-output@^2.2.8":
version "2.2.8"
resolved "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz"
@@ -385,7 +392,7 @@
resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15":
version "1.5.0"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
@@ -464,6 +471,11 @@
resolved "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz"
integrity sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw==
"@standard-schema/utils@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@stencil/core@^4.0.3", "@stencil/core@4.20.0":
version "4.20.0"
resolved "https://registry.npmjs.org/@stencil/core/-/core-4.20.0.tgz"
@@ -607,6 +619,11 @@
dependencies:
"@types/jest-diff" "*"
"@types/js-cookie@^2.2.6":
version "2.2.7"
resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz"
integrity sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==
"@types/leaflet@^1.9.17":
version "1.9.18"
resolved "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.18.tgz"
@@ -754,6 +771,11 @@
resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz"
integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==
"@xobotyi/scrollbar-width@^1.9.5":
version "1.9.5"
resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
ansi-escapes@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz"
@@ -1017,6 +1039,13 @@ convert-source-map@^2.0.0:
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
copy-to-clipboard@^3.3.1:
version "3.3.3"
resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz"
integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
dependencies:
toggle-selection "^1.0.6"
core-js-pure@^3.30.2:
version "3.42.0"
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz"
@@ -1031,7 +1060,22 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6:
shebang-command "^2.0.0"
which "^2.0.1"
csstype@^3.0.2:
css-in-js-utils@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz"
integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==
dependencies:
hyphenate-style-name "^1.0.3"
css-tree@^1.1.2:
version "1.1.3"
resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz"
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
csstype@^3.0.2, csstype@^3.1.2:
version "3.1.3"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
@@ -1137,6 +1181,13 @@ env-paths@^2.2.0:
resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
error-stack-parser@^2.0.6:
version "2.1.4"
resolved "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz"
integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==
dependencies:
stackframe "^1.3.4"
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz"
@@ -1230,6 +1281,21 @@ extend@^3.0.0:
resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-shallow-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz"
integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==
fastest-stable-stringify@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz"
integrity sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
@@ -1445,6 +1511,11 @@ human-signals@^4.3.0:
resolved "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz"
integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==
hyphenate-style-name@^1.0.3:
version "1.1.0"
resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz"
integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==
immutable@^5.0.2:
version "5.1.2"
resolved "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz"
@@ -1465,6 +1536,13 @@ inline-style-parser@0.2.4:
resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz"
integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==
inline-style-prefixer@^7.0.1:
version "7.0.1"
resolved "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz"
integrity sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==
dependencies:
css-in-js-utils "^3.1.0"
ionicons@^7.0.0, ionicons@^7.1.2, ionicons@^7.2.2:
version "7.4.0"
resolved "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz"
@@ -1561,6 +1639,11 @@ jackspeak@^4.0.1:
dependencies:
"@isaacs/cliui" "^8.0.2"
js-cookie@^2.2.1:
version "2.2.1"
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
@@ -1778,6 +1861,11 @@ mdast-util-to-string@^4.0.0:
dependencies:
"@types/mdast" "^4.0.0"
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
@@ -2054,6 +2142,20 @@ ms@2.1.2:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nano-css@^5.6.2:
version "5.6.2"
resolved "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz"
integrity sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
css-tree "^1.1.2"
csstype "^3.1.2"
fastest-stable-stringify "^2.0.2"
inline-style-prefixer "^7.0.1"
rtl-css-js "^1.16.1"
stacktrace-js "^2.0.2"
stylis "^4.3.0"
nanoid@^3.3.8:
version "3.3.11"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
@@ -2266,6 +2368,11 @@ react-dom@*, "react-dom@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", react-dom@^19
dependencies:
scheduler "^0.25.0"
react-hook-form@^7.0.0, react-hook-form@^7.55.0:
version "7.57.0"
resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz"
integrity sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
@@ -2341,7 +2448,32 @@ react-spinners@^0.17.0:
resolved "https://registry.npmjs.org/react-spinners/-/react-spinners-0.17.0.tgz"
integrity sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==
react@*, "react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18.0 || ^19", react@^19.0.0, react@>=15, react@>=16, react@>=16.8.6, react@>=18, react@19.0.0:
react-universal-interface@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz"
integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==
react-use@^17.6.0:
version "17.6.0"
resolved "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz"
integrity sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==
dependencies:
"@types/js-cookie" "^2.2.6"
"@xobotyi/scrollbar-width" "^1.9.5"
copy-to-clipboard "^3.3.1"
fast-deep-equal "^3.1.3"
fast-shallow-equal "^1.0.0"
js-cookie "^2.2.1"
nano-css "^5.6.2"
react-universal-interface "^0.6.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.1.0"
set-harmonic-interval "^1.0.1"
throttle-debounce "^3.0.1"
ts-easing "^0.2.0"
tslib "^2.1.0"
react@*, "react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18.0 || ^19", react@^19.0.0, react@>=15, react@>=16, react@>=16.8.6, react@>=18, react@19.0.0:
version "19.0.0"
resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
@@ -2398,6 +2530,11 @@ reselect@^4.0.0:
resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz"
integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz"
@@ -2453,6 +2590,13 @@ rollup@^4.34.9:
"@rollup/rollup-win32-x64-msvc" "4.41.1"
fsevents "~2.3.2"
rtl-css-js@^1.16.1:
version "1.16.1"
resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz"
integrity sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==
dependencies:
"@babel/runtime" "^7.1.2"
safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
@@ -2479,6 +2623,11 @@ scheduler@^0.25.0:
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz"
integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==
screenfull@^5.1.0:
version "5.2.0"
resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz"
integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==
semver@^6.3.1:
version "6.3.1"
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
@@ -2489,6 +2638,11 @@ semver@^7.6.3:
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
set-harmonic-interval@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz"
integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
@@ -2538,6 +2692,16 @@ source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0":
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@0.5.6:
version "0.5.6"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"
integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==
space-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
@@ -2553,6 +2717,35 @@ ssr-window@^4.0.2:
resolved "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz"
integrity sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==
stack-generator@^2.0.5:
version "2.0.10"
resolved "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz"
integrity sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==
dependencies:
stackframe "^1.3.4"
stackframe@^1.3.4:
version "1.3.4"
resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz"
integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==
stacktrace-gps@^3.0.4:
version "3.1.2"
resolved "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz"
integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==
dependencies:
source-map "0.5.6"
stackframe "^1.3.4"
stacktrace-js@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz"
integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==
dependencies:
error-stack-parser "^2.0.6"
stack-generator "^2.0.5"
stacktrace-gps "^3.0.4"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
@@ -2649,6 +2842,11 @@ style-to-object@1.0.8:
dependencies:
inline-style-parser "0.2.4"
stylis@^4.3.0:
version "4.3.6"
resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz"
integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
@@ -2675,6 +2873,11 @@ tar@^6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz"
integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
through2@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz"
@@ -2707,6 +2910,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz"
@@ -2722,6 +2930,11 @@ trough@^2.0.0:
resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz"
integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==
ts-easing@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
tslib@*, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.6.2, tslib@^2.8.1:
version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
@@ -2950,6 +3163,11 @@ yauzl@^2.10.0:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
zod@^3.24.2:
version "3.25.46"
resolved "https://registry.npmjs.org/zod/-/zod-3.25.46.tgz"
integrity sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ==
zwitch@^2.0.0:
version "2.0.4"
resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"

View File

@@ -21,5 +21,6 @@
{
"path": "98_AI_workspace"
}
]
],
"settings": {}
}