Compare commits

...

18 Commits

Author SHA1 Message Date
louiscklaw
a4d0d8b746 feat: clarify FAQ guidance on updating git staged files, adding reference to sibling files in same directory 2025-06-17 20:15:57 +08:00
louiscklaw
f8919b8c84 feat: update FAQ with guidance on updating git staged files, specifying to preserve format and detail level while avoiding unintended code changes 2025-06-17 20:12:39 +08:00
louiscklaw
583e31fd4d feat: refactor party event action types and functions, update imports and naming conventions to use PartyEvent naming scheme, add createPartyUser function to party-user actions 2025-06-17 20:06:47 +08:00
louiscklaw
ae7f005236 feat: enhance party user schema with address fields, update frontend form and API calls to use PartyUser naming convention 2025-06-17 19:59:09 +08:00
louiscklaw
834f9360ba feat: update import path for UserCreateView to use party-user view module 2025-06-17 19:58:56 +08:00
louiscklaw
1cb018d4d5 "feat: simplify db push script by removing restart loop and directly executing db:push and seed" 2025-06-17 19:58:45 +08:00
louiscklaw
448825545e "feat: refactor party user data fetching to use new endpoint URL builder and implement 2025-06-17 19:18:21 +08:00
louiscklaw
7c7a532381 fix: disable i18n debug mode in production 2025-06-17 19:18:13 +08:00
louiscklaw
eb515dbe68 feat: enhance party user schema with company, status, role and verification fields, update seeding and frontend form 2025-06-17 18:31:20 +08:00
louiscklaw
7a793be610 feat: update imports and component structure in party-user list view 2025-06-17 18:31:07 +08:00
louiscklaw
1d89134ea2 fix: remove unused tax calculation code in product form 2025-06-17 18:30:30 +08:00
louiscklaw
44d324b40e feat: update frontend references from User to PartyUser for party user management module 2025-06-16 10:20:09 +08:00
louiscklaw
ee2a377bf6 feat: add frontend environment variables template with server URL, API keys and service configurations 2025-06-16 10:20:01 +08:00
louiscklaw
0941ab6dd1 feat: update frontend component names from User to PartyUser for consistency with party-user module 2025-06-16 10:08:43 +08:00
louiscklaw
e93c5dcf62 in the middle of party-user frontend, 2025-06-16 03:03:54 +08:00
louiscklaw
17aaf97722 feat: add REQ0188 frontend party-user CRUD functionality with backend API endpoints and database schema 2025-06-16 02:26:41 +08:00
louiscklaw
47660be0cd docs: update FAQ with question about introducing new format or ideas 2025-06-16 02:24:47 +08:00
louiscklaw
cf5cfb8d63 "feat: add mobile app support with Dockerfile and dev/build scripts, update all projects to use psmisc for process management" 2025-06-16 01:46:15 +08:00
74 changed files with 3952 additions and 29 deletions

View File

@@ -0,0 +1,20 @@
---
tags: frontend, party-user
---
# REQ0188 frontend party-user
frontend page to handle party-user (CRUD)
edit page T.B.A.
## TODO
## sources
T.B.A.
## branch
develop/requirements/REQ0188
develop/frontend/party-user/trunk

View File

@@ -6,6 +6,7 @@ RUN npm install -g pnpm
RUN apt-get update -y
RUN apt-get install -y openssl
RUN apt-get install -qqy psmisc
# Set working directory
WORKDIR /app

View File

@@ -31,17 +31,21 @@ model Account {
oauth_token_secret String?
oauth_token String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
PartyUser PartyUser? @relation(fields: [partyUserId], references: [id])
partyUserId String?
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
PartyUser PartyUser? @relation(fields: [partyUserId], references: [id])
partyUserId String?
}
model User {
@@ -1257,3 +1261,33 @@ model PartyOrderItem {
// OrderPayment OrderPayment[]
// OrderShippingAddress OrderShippingAddress[]
}
model PartyUser {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
username String? @unique
password String?
//
name String?
email String @unique
emailVerified DateTime?
avatarUrl String?
bucketImage String?
admin Boolean @default(false)
accounts Account[]
sessions Session[]
info Json?
phoneNumber String @default("")
company String @default("")
status String @default("pending")
role String @default("")
isVerified Boolean @default(false)
//
country String @default("")
state String @default("")
city String @default("")
address String @default("")
zipCode String @default("")
}

View File

@@ -31,7 +31,9 @@ import { EventReviewSeed } from './seeds/eventReview';
import { appLogSeed } from './seeds/AppLog';
import { accessLogSeed } from './seeds/AccessLog';
import { userMetaSeed } from './seeds/userMeta';
//
import { partyOrderItemSeed } from './seeds/partyOrderItem';
import { partyUserSeed } from './seeds/partyUser';
//
// import { Blog } from './seeds/blog';
@@ -60,8 +62,9 @@ import { partyOrderItemSeed } from './seeds/partyOrderItem';
//
await appLogSeed;
await accessLogSeed;
//
await partyOrderItemSeed;
await partyUserSeed;
// await Blog;
// await Mail;

View File

@@ -0,0 +1,122 @@
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';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
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'];
async function partyUser() {
const alice = await prisma.partyUser.upsert({
where: { email: 'alice@prisma.io' },
update: {},
create: {
email: 'alice@prisma.io',
name: 'Alice',
username: 'pualice',
password: 'Aa12345678',
emailVerified: new Date(),
phoneNumber: '+85291234567',
company: 'helloworld company',
status: STATUS[0],
role: ROLE[0],
isVerified: true,
},
});
await prisma.partyUser.upsert({
where: { email: 'demo@minimals.cc' },
update: {},
create: {
email: 'demo@minimals.cc',
name: 'Demo',
username: 'pudemo',
password: '@2Minimal',
emailVerified: new Date(),
phoneNumber: '+85291234568',
company: 'helloworld company',
status: STATUS[1],
role: ROLE[1],
isVerified: true,
},
});
for (let i = 0; i < 5; 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.partyUser.upsert({
where: { email: `party_user${i}@prisma.io` },
update: {},
create: {
email: `party_user${i}@prisma.io`,
name: `Party Dummy ${i}`,
username: `pu${i.toString()}`,
password: 'Aa12345678',
emailVerified: new Date(),
phoneNumber: `+8529123456${i.toString()}`,
company: randomFaker.company.name(),
role: ROLE[Math.floor(Math.random() * ROLE.length)],
status: STATUS[Math.floor(Math.random() * STATUS.length)],
isVerified: true,
},
});
}
console.log('seed partyUser done');
}
const partyUserSeed = partyUser()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
export { partyUserSeed };

View File

@@ -7,9 +7,13 @@ clear
while true; do
yarn db:studio &
npx nodemon --ext ts,tsx,prisma --exec "yarn db:push && yarn seed && yarn dev"
npx nodemon --ext ts,tsx,prisma --exec "yarn dev"
# npx nodemon --ext ts,tsx,prisma --exec "yarn db:push && yarn seed && yarn dev"
# yarn dev
killall node
killall yarn
echo "restarting..."
sleep 1
done

View File

@@ -4,8 +4,6 @@ yarn --dev
clear
while true; do
npx nodemon --ext prisma --exec "yarn db:push && yarn seed"
echo "restarting..."
sleep 1
done
yarn db:push && yarn seed
echo "done"

View File

@@ -2,6 +2,8 @@
set -x
killall node
killall yarn
rm -rf ./**/*Zone.Identifier
# yarn db:push

View File

@@ -0,0 +1,60 @@
<!-- NOTES to AI: please maintain same format and level of detail when edit this file -->
# GUIDELINE
- Party-user Management API endpoint for managing party-user accounts
- Handles party-user CRUD operations and profile updates
- Follows Next.js API route conventions
## Endpoints
### `create/route.ts`
Creates new party-user account with required details
### `delete/route.ts`
Deletes party-user account by ID
### `details/route.ts`
Gets detailed party-user information
### `helloworld/route.ts`
Simple test endpoint returning "Hello World"
### `list/route.ts`
Lists all party-users with pagination support
### `search/route.ts`
Searches party-users by name or email
### `update/route.ts`
Updates party-user profile information
## Testing
Test files are available per endpoint in their respective directories:
- `create/test.http`
- `delete/test.http`
- `details/test.http`
- `helloworld/test.http`
- `list/test.http`
- `update/test.http`
## Related Services
`../../services/party-user.service.ts` (assumed) would handle:
`createUser` - Register new party-user account
`updateUser` - Update party-user profile
`deleteUser` - Remove party-user account
`listUsers` - Get paginated party-user list
`searchUsers` - Search party-users by criteria
`changeUserRole` - Modify party-user permissions
`uploadUserImage` - Handle profile picture uploads

View File

@@ -0,0 +1,34 @@
// src/app/api/party-user/create/route.ts
//
// PURPOSE:
// create new party user in db
//
// RULES:
// 1. Validates input data shape
// 2. Returns created party user data
//
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { createPartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/**
***************************************
* POST - create PartyUser
***************************************
*/
export async function POST(req: NextRequest) {
const { partyUserData } = await req.json();
try {
const partyUser = await createPartyUser(partyUserData);
return response({ partyUser }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Create', error);
}
}

View File

@@ -0,0 +1,19 @@
###
POST http://localhost:7272/api/party-user/create
Content-Type: application/json
{
"partyUserData": {
"name": "Alice 123321",
"username": null,
"email": "alice@123111321.io",
"emailVerified": "2025-06-15T17:47:23.919Z",
"password": "Aa12345678",
"bucketImage": null,
"admin": false,
"info": null,
"phoneNumber": "+85291234567",
"avatarUrl": ""
}
}

View File

@@ -0,0 +1,35 @@
// src/app/api/party-user/delete/route.ts
//
// PURPOSE:
// delete party user from db by id
//
// RULES:
// 1. Requires valid party user ID
// 2. Returns deleted party user ID
// 3. Handles errors appropriately
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { deletePartyUser } from 'src/app/services/party-user.service';
/**
**************************************
* PATCH - Delete party user
***************************************
*/
export async function PATCH(req: NextRequest) {
try {
const { partyUserId } = await req.json();
if (!partyUserId) throw new Error('partyUserId cannot be null');
await deletePartyUser(partyUserId);
return response({ partyUserId }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Delete', error);
}
}

View File

@@ -0,0 +1,8 @@
###
PATCH http://localhost:7272/api/party-user/delete
Content-Type: application/json
{
"partyUserId": "cmbxz8t2b000oiigxib3o4jla"
}

View File

@@ -0,0 +1,43 @@
// src/app/api/party-user/details/route.ts
//
// PURPOSE:
// get party user details from db by id
//
// RULES:
// 1. Requires valid partyUserId parameter
// 2. Returns party user details if found
// 3. Handles not found case appropriately
// 4. Logs the operation
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { getPartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/** **************************************
* GET PartyUser detail
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: userId must exist
const partyUserId = searchParams.get('partyUserId');
if (!partyUserId) return response({ message: 'partyUserId is required!' }, STATUS.BAD_REQUEST);
// NOTE: userId confirmed exist, run below
const partyUser = await getPartyUser(partyUserId);
if (!partyUser) return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
logger('[User] details', partyUser.id);
return response({ partyUser }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Get details', error);
}
}

View File

@@ -0,0 +1,4 @@
###
GET http://localhost:7272/api/party-user/details?partyUserId=cmbxziat6000b715hmhrnwlx2

View File

@@ -0,0 +1,11 @@
import type { NextRequest, NextResponse } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
export async function GET(req: NextRequest, res: NextResponse) {
try {
return response({ helloworld: 'party-user' }, STATUS.OK);
} catch (error) {
return handleError('GET - helloworld', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/party-user/helloworld

View File

@@ -0,0 +1,33 @@
// src/app/api/party-user/update/route.ts
//
// PURPOSE:
// update existing party user in db
//
// RULES:
// 1. Requires valid party user ID
// 2. Validates input data shape
//
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { listPartyUsers } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/**
***************************************
* GET - Products
***************************************
*/
export async function GET() {
try {
const partyUsers = await listPartyUsers();
logger('[User] list', partyUsers.length);
return response({ partyUsers }, STATUS.OK);
} catch (error) {
return handleError('Product - Get list', error);
}
}

View File

@@ -0,0 +1,2 @@
###
GET http://localhost:7272/api/party-user/list

View File

@@ -0,0 +1,35 @@
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { _products } from 'src/_mock/_product';
// ----------------------------------------------------------------------
export const runtime = 'edge';
/** **************************************
* GET - Search products
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
const query = searchParams.get('query')?.trim().toLowerCase();
if (!query) {
return response({ results: [] }, STATUS.OK);
}
const products = _products();
// Accept search by name or sku
const results = products.filter(({ name, sku }) => name.toLowerCase().includes(query) || sku?.toLowerCase().includes(query));
logger('[Product] search-results', results.length);
return response({ results }, STATUS.OK);
} catch (error) {
return handleError('Product - Get search', error);
}
}

View File

@@ -0,0 +1,88 @@
// src/app/api/party-user/update/route.ts
//
// PURPOSE:
// update existing party user in db
//
// RULES:
// 1. Requires valid party user ID
// 2. Validates input data shape
// 3. Returns updated party user data
// 4. Handles errors appropriately
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { updatePartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/** **************************************
* PUT - Update PartyUser
*************************************** */
export async function PUT(req: NextRequest) {
// logger('[Product] list', products.length);
const { partyUserData } = await req.json();
try {
await updatePartyUser(partyUserData.id, partyUserData);
return response({ partyUserData }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Update', error);
}
}
export type IProductItem = {
id: string;
sku: string;
name: string;
code: string;
price: number;
taxes: number;
tags: string[];
sizes: string[];
publish: string;
gender: string[];
coverUrl: string;
images: string[];
colors: string[];
quantity: number;
category: string;
available: number;
totalSold: number;
description: string;
totalRatings: number;
totalReviews: number;
// createdAt: IDateValue;
inventoryType: string;
subDescription: string;
priceSale: number | null;
// reviews: IProductReview[];
newLabel: {
content: string;
enabled: boolean;
};
saleLabel: {
content: string;
enabled: boolean;
};
ratings: {
name: string;
starCount: number;
reviewCount: number;
}[];
};
export type IDateValue = string | number | null;
export type IProductReview = {
id: string;
name: string;
rating: number;
comment: string;
helpful: number;
avatarUrl: string;
postedAt: IDateValue;
isPurchased: boolean;
attachments?: string[];
};

View File

@@ -0,0 +1,22 @@
###
PUT http://localhost:7272/api/party-user/update
Content-Type: application/json
{
"partyUserData": {
"id": "cmc0cedkx000boln3viy77598",
"createdAt": "2025-06-15T17:47:24.547Z",
"updatedAt": "2025-06-15T17:47:24.547Z",
"name": "Alice 123321",
"username": null,
"email": "alice@prisma.io",
"emailVerified": "2025-06-15T17:47:23.919Z",
"password": "Aa12345678",
"image": null,
"bucketImage": null,
"admin": false,
"info": null,
"phoneNumber": "+85291234567"
}
}

View File

@@ -13,10 +13,7 @@ import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { isDev } from 'src/constants';
import prisma from '../../../lib/prisma';
import { createProduct } from 'src/app/services/product.service';
// import { createProduct } from 'src/app/services/product.service';
// ----------------------------------------------------------------------

View File

@@ -0,0 +1,81 @@
<!-- AI: please maintain same format and level of detail when edit this file -->
# GUIDELINE
- User Management API endpoint for managing user accounts
- Handles user CRUD operations, role management and profile updates
- Follows Next.js API route conventions
## `route.ts`
Main user route handler with HTTP methods:
- `GET` - Get current user details
- `POST` - Create new user account
- `PUT` - Update user profile
- `DELETE` - Delete user account
## Sub-routes
### `changeToAdmin/route.ts`
Changes user role to admin
### `changeToUser/route.ts`
Changes user role to regular user
### `checkAdmin/route.ts`
Verifies if user has admin privileges
### `createUser/route.ts`
Creates new user account with required details
### `deleteUser/route.ts`
Deletes user account by ID
### `details/route.ts`
Gets detailed user information
### `list/route.ts`
Lists all users with pagination support
### `saveUser/route.ts`
Saves user profile changes
### `search/route.ts`
Searches users by name or email
### `image/upload/route.ts`
Handles user profile picture uploads
## `test.http`
Contains test requests for:
- User authentication
- Role changes (admin/user)
- Profile updates
- Account creation/deletion
- User listing/searching
- Image uploads
## Related Services
`../../services/user.service.ts` (assumed) would handle:
`createUser` - Register new user account
`updateUser` - Update user profile
`deleteUser` - Remove user account
`listUsers` - Get paginated user list
`searchUsers` - Search users by criteria
`changeUserRole` - Modify user permissions
`uploadUserImage` - Handle profile picture uploads

View File

@@ -1,13 +1,13 @@
Hi,
i copied from
`03_source/cms_backend/src/app/services/party-event.service.ts`
`03_source/cms_backend/src/app/services/user.service.ts`
to
`03_source/cms_backend/src/app/services/party-order.service.ts`
`03_source/cms_backend/src/app/services/party-user.service.ts`
with knowledge in `schema.prisma` file, and reference to the sibling files in same folder
i want you to update `party-order.service.ts` content to handle party order (the purchase order of the party)
please use the model `PartyOrderItem` to handle it.
i want you to update `party-user.service.ts` content to handle `party user` (the user joining party)
please use the model `PartyUser` to handle it.
thanks.

View File

@@ -0,0 +1,76 @@
// 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, PartyUser } 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 listPartyUsers(): Promise<PartyUser[]> {
return prisma.partyUser.findMany({});
}
async function getPartyUser(partyUserId: string): Promise<PartyUser | null> {
return prisma.partyUser.findUnique({
where: { id: partyUserId },
// include: { reviews: true },
});
}
async function getUserById(id: string): Promise<User | null> {
return prisma.user.findFirst({ where: { id } });
}
async function createPartyUser(partyUserData: any): Promise<PartyUser> {
return prisma.partyUser.create({ data: partyUserData });
}
async function updatePartyUser(partyUserId: string, partyUserData: any): Promise<PartyUser | null> {
return prisma.partyUser.update({
where: { id: partyUserId },
data: partyUserData,
});
}
async function deletePartyUser(partyUserId: string): Promise<PartyUser | null> {
return prisma.partyUser.delete({
where: { id: partyUserId },
});
}
export {
getUserById,
getPartyUser,
listPartyUsers,
createPartyUser,
updatePartyUser,
deletePartyUser,
//
type CreateUser,
type UpdateUser,
//
};

View File

@@ -6,6 +6,7 @@ set -ex
DOCKER_COMPOSE_FILES=" -f docker-compose.yml -f docker-compose.dev.yml"
# docker compose $DOCKER_COMPOSE_FILES build
# docker compose $DOCKER_COMPOSE_FILES push
docker compose $DOCKER_COMPOSE_FILES up -d
# cd ../api_server

View File

@@ -0,0 +1,40 @@
# Server url
# Local server configuration at: https://docs.minimals.cc/mock-server/
VITE_SERVER_URL=http://localhost:7272
# VITE_SERVER_URL=https://api-dev-minimal-v630.pages.dev
# Public resource directory
VITE_ASSETS_DIR=
# Mapbox
VITE_MAPBOX_API_KEY=
# Firebase
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_APPID=
# Aws
VITE_AWS_AMPLIFY_USER_POOL_ID=
VITE_AWS_AMPLIFY_USER_POOL_WEB_CLIENT_ID=
VITE_AWS_AMPLIFY_REGION=
# Auth0
VITE_AUTH0_DOMAIN=
VITE_AUTH0_CLIENT_ID=
VITE_AUTH0_CALLBACK_URL=
# Supabase
VITE_SUPABASE_URL=https://prhsilyjzxbkufchywxt.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InByaHNpbHlqenhia3VmY2h5d3h0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzY2NjM5MTEsImV4cCI6MjA1MjIzOTkxMX0.UBvxEhKEMtsRuDWBSDTglfnupKf9fyPI9IvQBxS5F6U
# Google Calendar
GOOGLE_CLIENT_ID=463956183839-i7j5nt5rkpbm4npukg21vfnhav5vvgeh.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_CALENDAR_API_KEY=AIzaSyDh4-SOuKTopXe45oDM9nQA7R4cNJ1So0c
VITE_GOOGLE_CLIENT_ID=463956183839-i7j5nt5rkpbm4npukg21vfnhav5vvgeh.apps.googleusercontent.com
VITE_GOOGLE_CALENDAR_API_KEY=AIzaSyDh4-SOuKTopXe45oDM9nQA7R4cNJ1So0c

View File

@@ -3,6 +3,8 @@ FROM node:20-slim
# Install pnpm globally
RUN npm install -g pnpm
RUN apt-get update
RUN apt-get install -qqy psmisc
# Set working directory
WORKDIR /app

View File

@@ -10,6 +10,9 @@ while true; do
yarn dev --force --clearScreen
killall node
killall yarn
echo "restarting..."
sleep 1
done

View File

@@ -2,6 +2,8 @@
set -x
killall node
killall yarn
rm -rf ./**/*Zone.Identifier
set -ex

View File

@@ -1,4 +1,4 @@
// src/actions/product.ts
// src/actions/party-event.ts
//
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
@@ -74,6 +74,7 @@ type SearchResultsData = {
results: IPartyEventItem[];
};
// TODO: update useSearchPartyEvents
export function useSearchProducts(query: string) {
const url = query ? [endpoints.product.search, { params: { query } }] : '';
@@ -135,7 +136,6 @@ export async function updatePartyEvent(partyEventData: Partial<IPartyEventItem>)
/**
* Work in local
*/
mutate(
endpoints.partyEvent.list,
(currentData: any) => {
@@ -163,7 +163,6 @@ export async function deletePartyEvent(partyEventId: string) {
/**
* Work in local
*/
mutate(
endpoints.partyEvent.list,
(currentData: any) => {

View File

@@ -0,0 +1,237 @@
// src/actions/party-user.ts
//
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import type { IPartyUserItem } from 'src/types/party-user';
import type { IProductItem } from 'src/types/product';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';
// ----------------------------------------------------------------------
const swrOptions: SWRConfiguration = {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
};
// ----------------------------------------------------------------------
type PartyUsersData = {
partyUsers: IPartyUserItem[];
};
// TODO: i want to refactor / tidy here
export function useGetPartyUsers() {
const url = endpoints.partyUser.list;
const { data, isLoading, error, isValidating } = useSWR<PartyUsersData>(url, fetcher, swrOptions);
const memoizedValue = useMemo(
() => ({
partyUsers: data?.partyUsers || [],
partyUsersLoading: isLoading,
partyUsersError: error,
partyUsersValidating: isValidating,
partyUsersEmpty: !isLoading && !isValidating && !data?.partyUsers.length,
}),
[data?.partyUsers, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type PartyUserData = {
partyUser: IPartyUserItem;
};
export function useGetPartyUser(partyUserId: string) {
const { data, isLoading, error, isValidating } = useSWR<PartyUserData>(
endpoints.partyUser.detailsByPartyUserId(partyUserId),
fetcher,
swrOptions
);
const memoizedValue = useMemo(
() => ({
partyUser: data?.partyUser,
partyUserLoading: isLoading,
partyUserError: error,
partyUserValidating: isValidating,
}),
[data?.partyUser, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SearchResultsData = {
results: IProductItem[];
};
// TODO: update useSearchProducts
export function useSearchProducts(query: string) {
const url = query ? [endpoints.product.search, { params: { query } }] : '';
const { data, isLoading, error, isValidating } = useSWR<SearchResultsData>(url, fetcher, {
...swrOptions,
keepPreviousData: true,
});
const memoizedValue = useMemo(
() => ({
searchResults: data?.results || [],
searchLoading: isLoading,
searchError: error,
searchValidating: isValidating,
searchEmpty: !isLoading && !isValidating && !data?.results.length,
}),
[data?.results, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SaveUserData = {
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;
};
export async function createPartyUser(partyUserData: CreateUserData) {
/**
* Work on server
*/
const data = { partyUserData };
const {
data: { id },
} = await axiosInstance.post(endpoints.partyUser.create, data);
/**
* Work in local
*/
mutate(
endpoints.partyUser.list,
(currentData: any) => {
const currentPartyUsers: IPartyUserItem[] = currentData?.partyUsers;
const partyUsers = [...currentPartyUsers, { ...partyUserData, id }];
return { ...currentData, partyUsers };
},
false
);
}
// ----------------------------------------------------------------------
export async function updatePartyUser(partyUserData: Partial<IPartyUserItem>) {
/**
* Work on server
*/
const data = { partyUserData };
await axiosInstance.put(endpoints.partyUser.update, data);
/**
* Work in local
*/
mutate(
endpoints.partyUser.list,
(currentData: any) => {
const currentPartyUsers: IPartyUserItem[] = currentData?.partyUsers;
const partyUsers = currentPartyUsers.map((partyUser) =>
partyUser.id === partyUserData.id ? { ...partyUser, ...partyUserData } : partyUser
);
return { ...currentData, partyUsers };
},
false
);
const partyUserId: string = partyUserData.id || '';
mutate(
endpoints.partyUser.detailsByPartyUserId(partyUserId),
(currentData: any) => {
const currentPartyUser: IPartyUserItem = currentData?.partyUser;
console.log({ currentPartyUser });
const partyUser = partyUserData;
return { ...currentData, partyUser };
},
false
);
}
export async function uploadUserImage(saveUserData: SaveUserData) {
console.log('uploadUserImage ?');
// const url = userId ? [endpoints.user.details, { params: { userId } }] : '';
const res = await axiosInstance.get('http://localhost:7272/api/product/helloworld');
return res;
}
// ----------------------------------------------------------------------
type CreateUserData = {
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;
};
export async function deletePartyUser(partyUserId: string) {
/**
* Work on server
*/
const data = { partyUserId };
await axiosInstance.patch(endpoints.partyUser.delete, data);
/**
* Work in local
*/
mutate(
endpoints.partyUser.list,
(currentData: any) => {
const currentPartyUsers: IPartyUserItem[] = currentData?.partyUsers;
const partyUsers = currentPartyUsers.filter((partyUser) => partyUser.id !== partyUserId);
return { ...currentData, partyUsers };
},
false
);
}

View File

@@ -111,6 +111,19 @@ export const navData: NavSectionProps['data'] = [
{ title: 'Details', path: paths.dashboard.partyOrder.demo.details },
],
},
{
title: 'party-user',
path: paths.dashboard.partyUser.root,
icon: ICONS.user,
children: [
{ title: 'Profile', path: paths.dashboard.partyUser.root },
{ title: 'Cards', path: paths.dashboard.partyUser.cards },
{ title: 'List', path: paths.dashboard.partyUser.list },
{ title: 'Create', path: paths.dashboard.partyUser.new },
{ title: 'Edit', path: paths.dashboard.partyUser.demo.edit },
{ title: 'Account', path: paths.dashboard.partyUser.account },
],
},
{
title: 'Product',
path: paths.dashboard.product.root,

View File

@@ -106,4 +106,15 @@ export const endpoints = {
changeStatus: (partyOrderId: string) =>
`/api/party-order/changeStatus?partyOrderId=${partyOrderId}`,
},
partyUser: {
list: '/api/party-user/list',
details: '/api/party-user/details',
search: '/api/party-user/search',
create: '/api/party-user/create',
update: '/api/party-user/update',
delete: '/api/party-user/delete',
//
detailsByPartyUserId: (partyUserId: string) =>
`/api/party-user/details?partyUserId=${partyUserId}`,
},
};

View File

@@ -22,7 +22,7 @@ i18next
.init({
...i18nOptions(lng),
detection: { caches: ['localStorage'] },
debug: isDev,
debug: false,
});
// ----------------------------------------------------------------------

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { AccountBillingView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = { title: `Account billing settings | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountBillingView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { AccountChangePasswordView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = { title: `Account change password settings | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountChangePasswordView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { AccountGeneralView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = { title: `Account general settings | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountGeneralView />
</>
);
}

View File

@@ -0,0 +1,18 @@
import { CONFIG } from 'src/global-config';
import { AccountNotificationsView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = {
title: `Account notifications settings | Dashboard - ${CONFIG.appName}`,
};
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountNotificationsView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { AccountSocialsView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = { title: `Account socials settings | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountSocialsView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { UserCardsView } from 'src/sections/user/view';
// ----------------------------------------------------------------------
const metadata = { title: `User cards | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<UserCardsView />
</>
);
}

View File

@@ -0,0 +1,25 @@
// import { _userList } from 'src/_mock/_user';
import { useGetPartyUser } from 'src/actions/party-user';
import { CONFIG } from 'src/global-config';
import { useParams } from 'src/routes/hooks';
import { PartyUserEditView } from 'src/sections/party-user/view';
// ----------------------------------------------------------------------
const metadata = { title: `User edit | Dashboard - ${CONFIG.appName}` };
export default function Page() {
const { id = '' } = useParams();
// TODO: remove unused code
// const currentUser = _userList.find((user) => user.id === id);
const { partyUser: user } = useGetPartyUser(id);
return (
<>
<title>{metadata.title}</title>
<PartyUserEditView user={user} />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { PartyUserListView } from 'src/sections/party-user/view';
// ----------------------------------------------------------------------
const metadata = { title: `User list | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<PartyUserListView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { UserCreateView } from 'src/sections/party-user/view';
// ----------------------------------------------------------------------
const metadata = { title: `Create a new user | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<UserCreateView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { UserProfileView } from 'src/sections/user/view';
// ----------------------------------------------------------------------
const metadata = { title: `User profile | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<UserProfileView />
</>
);
}

View File

@@ -196,5 +196,15 @@ export const paths = {
details: (id: string) => `${ROOTS.DASHBOARD}/party-order/${id}`,
demo: { details: `${ROOTS.DASHBOARD}/party-order/${MOCK_ID}` },
},
partyUser: {
root: `${ROOTS.DASHBOARD}/party-user`,
new: `${ROOTS.DASHBOARD}/party-user/new`,
list: `${ROOTS.DASHBOARD}/party-user/list`,
cards: `${ROOTS.DASHBOARD}/party-user/cards`,
profile: `${ROOTS.DASHBOARD}/party-user/profile`,
account: `${ROOTS.DASHBOARD}/party-user/account`,
edit: (id: string) => `${ROOTS.DASHBOARD}/party-user/${id}/edit`,
demo: { edit: `${ROOTS.DASHBOARD}/party-user/${MOCK_ID}/edit` },
},
},
};

View File

@@ -87,6 +87,13 @@ const PartyEventEditPage = lazy(() => import('src/pages/dashboard/party-event/ed
const PartyOrderListPage = lazy(() => import('src/pages/dashboard/party-order/list'));
const PartyOrderDetailsPage = lazy(() => import('src/pages/dashboard/party-order/details'));
// PartyUser
const PartyUserProfilePage = lazy(() => import('src/pages/dashboard/party-user/profile'));
const PartyUserCardsPage = lazy(() => import('src/pages/dashboard/party-user/cards'));
const PartyUserListPage = lazy(() => import('src/pages/dashboard/party-user/list'));
const PartyUserCreatePage = lazy(() => import('src/pages/dashboard/party-user/new'));
const PartyUserEditPage = lazy(() => import('src/pages/dashboard/party-user/edit'));
// ----------------------------------------------------------------------
function SuspenseOutlet() {
@@ -229,6 +236,28 @@ export const dashboardRoutes: RouteObject[] = [
{ path: ':id', element: <PartyOrderDetailsPage /> },
],
},
{
path: 'party-user',
children: [
{ index: true, element: <PartyUserProfilePage /> },
{ path: 'profile', element: <PartyUserProfilePage /> },
{ path: 'cards', element: <PartyUserCardsPage /> },
{ path: 'list', element: <PartyUserListPage /> },
{ path: 'new', element: <PartyUserCreatePage /> },
{ path: ':id/edit', element: <PartyUserEditPage /> },
{
path: 'account',
element: accountLayout(),
children: [
{ index: true, element: <AccountGeneralPage /> },
{ path: 'billing', element: <AccountBillingPage /> },
{ path: 'notifications', element: <AccountNotificationsPage /> },
{ path: 'socials', element: <AccountSocialsPage /> },
{ path: 'change-password', element: <AccountChangePasswordPage /> },
],
},
],
},
],
},
];

View File

@@ -1,3 +1,5 @@
// AI: this file store page routeing of app
//
import { lazy, Suspense } from 'react';
import type { RouteObject } from 'react-router';
import { SplashScreen } from 'src/components/loading-screen';

View File

@@ -0,0 +1,47 @@
import Box from '@mui/material/Box';
import Pagination from '@mui/material/Pagination';
import { useCallback, useState } from 'react';
import type { IPartyUserCard } from 'src/types/party-user';
import { UserCard } from './party-user-card';
// ----------------------------------------------------------------------
type Props = {
users: IPartyUserCard[];
};
export function UserCardList({ users }: Props) {
const [page, setPage] = useState(1);
const rowsPerPage = 12;
const handleChangePage = useCallback((event: React.ChangeEvent<unknown>, newPage: number) => {
setPage(newPage);
}, []);
return (
<>
<Box
sx={{
gap: 3,
display: 'grid',
gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
}}
>
{users
.slice((page - 1) * rowsPerPage, (page - 1) * rowsPerPage + rowsPerPage)
.map((user) => (
<UserCard key={user.id} user={user} />
))}
</Box>
<Pagination
page={page}
shape="circular"
count={Math.ceil(users.length / rowsPerPage)}
onChange={handleChangePage}
sx={{ mt: { xs: 5, md: 8 }, mx: 'auto' }}
/>
</>
);
}

View File

@@ -0,0 +1,119 @@
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import type { CardProps } from '@mui/material/Card';
import Card from '@mui/material/Card';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import ListItemText from '@mui/material/ListItemText';
import { varAlpha } from 'minimal-shared/utils';
import { _socials } from 'src/_mock';
import { AvatarShape } from 'src/assets/illustrations';
import { Iconify } from 'src/components/iconify';
import { Image } from 'src/components/image';
import type { IPartyUserCard } from 'src/types/party-user';
import { fShortenNumber } from 'src/utils/format-number';
// ----------------------------------------------------------------------
type Props = CardProps & {
user: IPartyUserCard;
};
export function UserCard({ user, sx, ...other }: Props) {
return (
<Card sx={[{ textAlign: 'center' }, ...(Array.isArray(sx) ? sx : [sx])]} {...other}>
<Box sx={{ position: 'relative' }}>
<AvatarShape
sx={{
left: 0,
right: 0,
zIndex: 10,
mx: 'auto',
bottom: -26,
position: 'absolute',
}}
/>
<Avatar
alt={user.name}
src={user.avatarUrl}
sx={{
left: 0,
right: 0,
width: 64,
height: 64,
zIndex: 11,
mx: 'auto',
bottom: -32,
position: 'absolute',
}}
/>
<Image
src={user.coverUrl}
alt={user.coverUrl}
ratio="16/9"
slotProps={{
overlay: {
sx: (theme) => ({
bgcolor: varAlpha(theme.vars.palette.common.blackChannel, 0.48),
}),
},
}}
/>
</Box>
<ListItemText
sx={{ mt: 7, mb: 1 }}
primary={user.name}
secondary={user.role}
slotProps={{
primary: { sx: { typography: 'subtitle1' } },
secondary: { sx: { mt: 0.5 } },
}}
/>
<Box
sx={{
mb: 2.5,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{_socials.map((social) => (
<IconButton key={social.label}>
{social.value === 'twitter' && <Iconify icon="socials:twitter" />}
{social.value === 'facebook' && <Iconify icon="socials:facebook" />}
{social.value === 'instagram' && <Iconify icon="socials:instagram" />}
{social.value === 'linkedin' && <Iconify icon="socials:linkedin" />}
</IconButton>
))}
</Box>
<Divider sx={{ borderStyle: 'dashed' }} />
<Box
sx={{
py: 3,
display: 'grid',
typography: 'subtitle1',
gridTemplateColumns: 'repeat(3, 1fr)',
}}
>
{[
{ label: 'Follower', value: user.totalFollowers },
{ label: 'Following', value: user.totalFollowing },
{ label: 'Total post', value: user.totalPosts },
].map((stat) => (
<Box key={stat.label} sx={{ gap: 0.5, display: 'flex', flexDirection: 'column' }}>
<Box component="span" sx={{ typography: 'caption', color: 'text.secondary' }}>
{stat.label}
</Box>
{fShortenNumber(stat.value)}
</Box>
))}
</Box>
</Card>
);
}

View File

@@ -0,0 +1,304 @@
import { zodResolver } from '@hookform/resolvers/zod';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import FormControlLabel from '@mui/material/FormControlLabel';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { isValidPhoneNumber } from 'react-phone-number-input/input';
import { createPartyUser, deletePartyUser, updatePartyUser } from 'src/actions/party-user';
import { Field, Form, schemaHelper } from 'src/components/hook-form';
import { Label } from 'src/components/label';
import { toast } from 'src/components/snackbar';
import { useRouter } from 'src/routes/hooks';
import { paths } from 'src/routes/paths';
import type { IPartyUserItem } from 'src/types/party-user';
import { fileToBase64 } from 'src/utils/file-to-base64';
import { fData } from 'src/utils/format-number';
import { z as zod } from 'zod';
// ----------------------------------------------------------------------
export type NewUserSchemaType = zod.infer<typeof NewUserSchema>;
export const NewUserSchema = zod.object({
name: zod.string().min(1, { message: 'Name is required!' }).optional().or(zod.literal('')),
city: zod.string().min(1, { message: 'City is required!' }).optional().or(zod.literal('')),
role: zod.string().min(1, { message: 'Role is required!' }),
email: zod
.string()
.min(1, { message: 'Email is required!' })
.email({ message: 'Email must be a valid email address!' }),
state: zod.string().min(1, { message: 'State is required!' }).optional().or(zod.literal('')),
status: zod.string(),
address: zod.string().min(1, { message: 'Address is required!' }).optional().or(zod.literal('')),
country: schemaHelper
.nullableInput(zod.string().min(1, { message: 'Country is required!' }), {
// message for null value
message: 'Country is required!',
})
.optional()
.or(zod.literal('')),
zipCode: zod.string().min(1, { message: 'Zip code is required!' }).optional().or(zod.literal('')),
company: zod.string().min(1, { message: 'Company is required!' }).optional().or(zod.literal('')),
avatarUrl: zod.string().optional().or(zod.literal('')),
phoneNumber: zod.string().optional().or(zod.literal('')),
isVerified: zod.boolean().default(true),
//
username: zod.string().optional().or(zod.literal('')),
password: zod.string().optional().or(zod.literal('')),
});
// ----------------------------------------------------------------------
type Props = {
currentUser?: IPartyUserItem;
};
export function PartyUserNewEditForm({ currentUser }: Props) {
const { t } = useTranslation();
const router = useRouter();
const defaultValues: NewUserSchemaType = {
status: '',
avatarUrl: '',
name: '新用戶名字',
email: 'user@123.com',
phoneNumber: '',
country: '',
state: '',
city: '',
address: '',
zipCode: '',
company: '',
role: 'user',
// Email is verified
isVerified: true,
//
username: '',
password: '',
};
const methods = useForm<NewUserSchemaType>({
mode: 'onSubmit',
resolver: zodResolver(NewUserSchema),
defaultValues,
values: currentUser,
});
const {
reset,
watch,
control,
handleSubmit,
formState: { errors, isSubmitting },
} = methods;
const values = watch();
const [disableDeleteUserButton, setDisableDeleteUserButton] = useState<boolean>(false);
const handleDeleteUserClick = async () => {
setDisableDeleteUserButton(true);
try {
if (currentUser) {
await deletePartyUser(currentUser.id);
toast.success(t('party user deleted'));
router.push(paths.dashboard.partyUser.list);
}
} catch (error) {
console.error(error);
}
setDisableDeleteUserButton(false);
};
const onSubmit = handleSubmit(async (data: any) => {
try {
const temp: any = data.avatarUrl;
if (temp instanceof File) {
data.avatarUrl = await fileToBase64(temp);
}
const sanitizedValues: IPartyUserItem = values as unknown as IPartyUserItem;
if (currentUser) {
// perform
await updatePartyUser(sanitizedValues);
} else {
// perform create
await createPartyUser(sanitizedValues);
}
toast.success(currentUser ? t('Update success!') : t('Create success!'));
router.push(paths.dashboard.partyUser.list);
// console.info('DATA', data);
} catch (error) {
console.error(error);
}
});
return (
<Form methods={methods} onSubmit={onSubmit}>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 4 }}>
<Card sx={{ pt: 10, pb: 5, px: 3 }}>
{currentUser && (
<Label
color={
(values.status === 'active' && 'success') ||
(values.status === 'banned' && 'error') ||
'warning'
}
sx={{ position: 'absolute', top: 24, right: 24 }}
>
{values.status}
</Label>
)}
<Box sx={{ mb: 5 }}>
<Field.UploadAvatar
name="avatarUrl"
maxSize={3145728}
helperText={
<Typography
variant="caption"
sx={{
mt: 3,
mx: 'auto',
display: 'block',
textAlign: 'center',
color: 'text.disabled',
}}
>
{t('Allowed')} *.jpeg, *.jpg, *.png, *.gif
<br /> {t('max size of')} {fData(3145728)}
</Typography>
}
/>
</Box>
{currentUser && (
<FormControlLabel
labelPlacement="start"
control={
<Controller
name="status"
control={control}
render={({ field }) => (
<Switch
{...field}
checked={field.value !== 'active'}
onChange={(event) =>
field.onChange(event.target.checked ? 'banned' : 'active')
}
/>
)}
/>
}
label={
<>
<Typography variant="subtitle2" sx={{ mb: 0.5 }}>
{t('Banned')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('Apply disable account')}
</Typography>
</>
}
sx={{
mx: 0,
mb: 3,
width: 1,
justifyContent: 'space-between',
}}
/>
)}
<Field.Switch
name="isVerified"
labelPlacement="start"
label={
<>
<Typography variant="subtitle2" sx={{ mb: 0.5 }}>
{t('Email verified')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('Disabling this will automatically send the user a verification email')}
</Typography>
</>
}
sx={{ mx: 0, width: 1, justifyContent: 'space-between' }}
/>
{currentUser && (
<Stack sx={{ mt: 3, alignItems: 'center', justifyContent: 'center' }}>
<Button
disabled={disableDeleteUserButton}
loading={disableDeleteUserButton}
variant="soft"
color="error"
onClick={handleDeleteUserClick}
>
{t('Delete user')}
</Button>
</Stack>
)}
</Card>
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<Card sx={{ p: 3 }}>
<Box
sx={{
rowGap: 3,
columnGap: 2,
display: 'grid',
gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)' },
}}
>
<Field.Text name="username" label={t('username')} />
<Field.Text name="password" label={t('password')} />
<Field.Text name="name" label={t('Full name')} />
<Field.Text name="email" label={t('Email address')} />
<Field.Phone name="phoneNumber" label={t('Phone number')} country="HK" />
<Field.CountrySelect
fullWidth
name="country"
label={t('Country')}
placeholder={t('Choose a country')}
/>
<Field.Text name="state" label={t('State/region')} />
<Field.Text name="city" label={t('City')} />
<Field.Text name="address" label={t('Address')} />
<Field.Text name="zipCode" label={t('Zip/code')} required={false} />
<Field.Text name="company" label={t('Company')} />
<Field.Text name="role" label={t('Role')} />
</Box>
<Stack sx={{ mt: 3, alignItems: 'flex-end' }}>
<Button
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
variant="contained"
>
{!currentUser ? t('create user') : t('save changes')}
</Button>
</Stack>
</Card>
</Grid>
</Grid>
</Form>
);
}

View File

@@ -0,0 +1,173 @@
import { zodResolver } from '@hookform/resolvers/zod';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MenuItem from '@mui/material/MenuItem';
import { useForm } from 'react-hook-form';
import { isValidPhoneNumber } from 'react-phone-number-input/input';
import { USER_STATUS_OPTIONS } from 'src/_mock';
import { Field, Form, schemaHelper } from 'src/components/hook-form';
import { toast } from 'src/components/snackbar';
import { useTranslate } from 'src/locales';
import type { IUserItem } from 'src/types/user';
import { z as zod } from 'zod';
// ----------------------------------------------------------------------
export type UserQuickEditSchemaType = zod.infer<typeof UserQuickEditSchema>;
export const UserQuickEditSchema = zod.object({
name: zod.string().min(1, { message: 'Name is required!' }),
email: zod
.string()
.min(1, { message: 'Email is required!' })
.email({ message: 'Email must be a valid email address!' }),
phoneNumber: schemaHelper.phoneNumber({ isValid: isValidPhoneNumber }),
country: schemaHelper.nullableInput(zod.string().min(1, { message: 'Country is required!' }), {
// message for null value
message: 'Country is required!',
}),
state: zod.string().min(1, { message: 'State is required!' }),
city: zod.string().min(1, { message: 'City is required!' }),
address: zod.string().min(1, { message: 'Address is required!' }),
zipCode: zod.string().min(1, { message: 'Zip code is required!' }),
company: zod.string().min(1, { message: 'Company is required!' }),
role: zod.string().min(1, { message: 'Role is required!' }),
// Not required
status: zod.string(),
});
// ----------------------------------------------------------------------
type Props = {
open: boolean;
onClose: () => void;
currentUser?: IUserItem;
};
export function PartyUserQuickEditForm({ currentUser, open, onClose }: Props) {
const { t } = useTranslate();
const defaultValues: UserQuickEditSchemaType = {
name: '',
email: '',
phoneNumber: '',
address: '',
country: '',
state: '',
city: '',
zipCode: '',
status: '',
company: '',
role: '',
};
const methods = useForm<UserQuickEditSchemaType>({
mode: 'all',
resolver: zodResolver(UserQuickEditSchema),
defaultValues,
values: currentUser,
});
const {
reset,
handleSubmit,
formState: { isSubmitting },
} = methods;
const onSubmit = handleSubmit(async (data) => {
const promise = new Promise((resolve) => setTimeout(resolve, 1000));
try {
reset();
onClose();
toast.promise(promise, {
loading: 'Loading...',
success: 'Update success!',
error: 'Update error!',
});
await promise;
console.info('DATA', data);
} catch (error) {
console.error(error);
}
});
return (
<Dialog
fullWidth
maxWidth={false}
open={open}
onClose={onClose}
slotProps={{
paper: {
sx: { maxWidth: 720 },
},
}}
>
<DialogTitle>{t('Quick update')}</DialogTitle>
<Form methods={methods} onSubmit={onSubmit}>
<DialogContent>
<Alert variant="outlined" severity="info" sx={{ mb: 3 }}>
Account is waiting for confirmation
</Alert>
<Box
sx={{
rowGap: 3,
columnGap: 2,
display: 'grid',
gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)' },
}}
>
<Field.Select name="status" label="Status">
{USER_STATUS_OPTIONS.map((status) => (
<MenuItem key={status.value} value={status.value}>
{status.label}
</MenuItem>
))}
</Field.Select>
<Box sx={{ display: { xs: 'none', sm: 'block' } }} />
<Field.Text name="name" label="Full name" />
<Field.Text name="email" label="Email address" />
<Field.Phone name="phoneNumber" label="Phone number" />
<Field.CountrySelect
fullWidth
name="country"
label="Country"
placeholder="Choose a country"
/>
<Field.Text name="state" label="State/region" />
<Field.Text name="city" label="City" />
<Field.Text name="address" label="Address" />
<Field.Text name="zipCode" label="Zip/code" />
<Field.Text name="company" label="Company" />
<Field.Text name="role" label="Role" />
</Box>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={onClose}>
Cancel
</Button>
<Button type="submit" variant="contained" loading={isSubmitting}>
Update
</Button>
</DialogActions>
</Form>
</Dialog>
);
}

View File

@@ -0,0 +1,65 @@
import Chip from '@mui/material/Chip';
import type { UseSetStateReturn } from 'minimal-shared/hooks';
import { useCallback } from 'react';
import type { FiltersResultProps } from 'src/components/filters-result';
import { chipProps, FiltersBlock, FiltersResult } from 'src/components/filters-result';
import type { IPartyUserTableFilters } from 'src/types/party-user';
// ----------------------------------------------------------------------
type Props = FiltersResultProps & {
onResetPage: () => void;
filters: UseSetStateReturn<IPartyUserTableFilters>;
};
export function PartyUserTableFiltersResult({ filters, onResetPage, totalResults, sx }: Props) {
const { state: currentFilters, setState: updateFilters, resetState: resetFilters } = filters;
const handleRemoveKeyword = useCallback(() => {
onResetPage();
updateFilters({ name: '' });
}, [onResetPage, updateFilters]);
const handleRemoveStatus = useCallback(() => {
onResetPage();
updateFilters({ status: 'all' });
}, [onResetPage, updateFilters]);
const handleRemoveRole = useCallback(
(inputValue: string) => {
const newValue = currentFilters.role.filter((item) => item !== inputValue);
onResetPage();
updateFilters({ role: newValue });
},
[onResetPage, updateFilters, currentFilters.role]
);
const handleReset = useCallback(() => {
onResetPage();
resetFilters();
}, [onResetPage, resetFilters]);
return (
<FiltersResult totalResults={totalResults} onReset={handleReset} sx={sx}>
<FiltersBlock label="Status:" isShow={currentFilters.status !== 'all'}>
<Chip
{...chipProps}
label={currentFilters.status}
onDelete={handleRemoveStatus}
sx={{ textTransform: 'capitalize' }}
/>
</FiltersBlock>
<FiltersBlock label="Role:" isShow={!!currentFilters.role.length}>
{currentFilters.role.map((item) => (
<Chip {...chipProps} key={item} label={item} onDelete={() => handleRemoveRole(item)} />
))}
</FiltersBlock>
<FiltersBlock label="Keyword:" isShow={!!currentFilters.name}>
<Chip {...chipProps} label={currentFilters.name} onDelete={handleRemoveKeyword} />
</FiltersBlock>
</FiltersResult>
);
}

View File

@@ -0,0 +1,183 @@
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import IconButton from '@mui/material/IconButton';
import Link from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import Stack from '@mui/material/Stack';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Tooltip from '@mui/material/Tooltip';
import { useBoolean, usePopover } from 'minimal-shared/hooks';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ConfirmDialog } from 'src/components/custom-dialog';
import { CustomPopover } from 'src/components/custom-popover';
import { Iconify } from 'src/components/iconify';
import { Label } from 'src/components/label';
import { RouterLink } from 'src/routes/components';
import type { IPartyUserItem } from 'src/types/party-user';
import { PartyUserQuickEditForm } from './party-user-quick-edit-form';
// ----------------------------------------------------------------------
type Props = {
row: IPartyUserItem;
selected: boolean;
editHref: string;
onSelectRow: () => void;
onDeleteRow: () => void;
};
export function PartyUserTableRow({ row, selected, editHref, onSelectRow, onDeleteRow }: Props) {
const menuActions = usePopover();
const confirmDialog = useBoolean();
const quickEditForm = useBoolean();
const { t } = useTranslation();
const renderQuickEditForm = () => (
<PartyUserQuickEditForm
currentUser={row}
open={quickEditForm.value}
onClose={quickEditForm.onFalse}
/>
);
const renderMenuActions = () => (
<CustomPopover
open={menuActions.open}
anchorEl={menuActions.anchorEl}
onClose={menuActions.onClose}
slotProps={{ arrow: { placement: 'right-top' } }}
>
<MenuList>
<li>
<MenuItem component={RouterLink} href={editHref} onClick={() => menuActions.onClose()}>
<Iconify icon="solar:pen-bold" />
{t('Edit')}
</MenuItem>
</li>
<MenuItem
onClick={() => {
confirmDialog.onTrue();
menuActions.onClose();
}}
sx={{ color: 'error.main' }}
>
<Iconify icon="solar:trash-bin-trash-bold" />
{t('Delete')}
</MenuItem>
</MenuList>
</CustomPopover>
);
const [disableDeleteButton, setDisableDeleteButton] = useState<boolean>(false);
const renderConfirmDialog = () => (
<ConfirmDialog
open={confirmDialog.value}
onClose={confirmDialog.onFalse}
title={t('Delete')}
content={t('Are you sure want to delete user?')}
action={
<Button
disabled={disableDeleteButton}
loading={disableDeleteButton}
variant="contained"
color="error"
onClick={() => {
setDisableDeleteButton(true);
onDeleteRow();
}}
>
{t('Delete')}
</Button>
}
/>
);
return (
<>
<TableRow hover selected={selected} aria-checked={selected} tabIndex={-1}>
<TableCell padding="checkbox">
<Checkbox
checked={selected}
onClick={onSelectRow}
slotProps={{
input: {
id: `${row.id}-checkbox`,
'aria-label': `${row.id} checkbox`,
},
}}
/>
</TableCell>
<TableCell>
<Box sx={{ gap: 2, display: 'flex', alignItems: 'center' }}>
<Avatar alt={row.name} src={row.avatarUrl} />
<Stack sx={{ typography: 'body2', flex: '1 1 auto', alignItems: 'flex-start' }}>
<Link
component={RouterLink}
href={editHref}
color="inherit"
sx={{ cursor: 'pointer' }}
>
{row.name}
</Link>
<Box component="span" sx={{ color: 'text.disabled' }}>
{row.email}
</Box>
</Stack>
</Box>
</TableCell>
<TableCell sx={{ whiteSpace: 'nowrap' }}>{row.phoneNumber}</TableCell>
<TableCell sx={{ whiteSpace: 'nowrap' }}>{row.company}</TableCell>
<TableCell sx={{ whiteSpace: 'nowrap' }}>{row.role}</TableCell>
<TableCell>
<Label
variant="soft"
color={
(row.status === 'active' && 'success') ||
(row.status === 'pending' && 'warning') ||
(row.status === 'banned' && 'error') ||
'default'
}
>
{row.status}
</Label>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title={t('Quick Edit')} placement="top" arrow>
<IconButton
color={quickEditForm.value ? 'inherit' : 'default'}
onClick={quickEditForm.onTrue}
>
<Iconify icon="solar:pen-bold" />
</IconButton>
</Tooltip>
<IconButton
color={menuActions.open ? 'inherit' : 'default'}
onClick={menuActions.onOpen}
>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
</Box>
</TableCell>
</TableRow>
{renderQuickEditForm()}
{renderMenuActions()}
{renderConfirmDialog()}
</>
);
}

View File

@@ -0,0 +1,150 @@
import Box from '@mui/material/Box';
import Checkbox from '@mui/material/Checkbox';
import FormControl from '@mui/material/FormControl';
import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import OutlinedInput from '@mui/material/OutlinedInput';
import type { SelectChangeEvent } from '@mui/material/Select';
import Select from '@mui/material/Select';
import TextField from '@mui/material/TextField';
import type { UseSetStateReturn } from 'minimal-shared/hooks';
import { usePopover } from 'minimal-shared/hooks';
import { useCallback } from 'react';
import { CustomPopover } from 'src/components/custom-popover';
import { Iconify } from 'src/components/iconify';
import type { IPartyUserTableFilters } from 'src/types/party-user';
// ----------------------------------------------------------------------
type Props = {
onResetPage: () => void;
filters: UseSetStateReturn<IPartyUserTableFilters>;
options: {
roles: string[];
};
};
export function PartyUserTableToolbar({ filters, options, onResetPage }: Props) {
const menuActions = usePopover();
const { state: currentFilters, setState: updateFilters } = filters;
const handleFilterName = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
onResetPage();
updateFilters({ name: event.target.value });
},
[onResetPage, updateFilters]
);
const handleFilterRole = useCallback(
(event: SelectChangeEvent<string[]>) => {
const newValue =
typeof event.target.value === 'string' ? event.target.value.split(',') : event.target.value;
onResetPage();
updateFilters({ role: newValue });
},
[onResetPage, updateFilters]
);
const renderMenuActions = () => (
<CustomPopover
open={menuActions.open}
anchorEl={menuActions.anchorEl}
onClose={menuActions.onClose}
slotProps={{ arrow: { placement: 'right-top' } }}
>
<MenuList>
<MenuItem onClick={() => menuActions.onClose()}>
<Iconify icon="solar:printer-minimalistic-bold" />
Print
</MenuItem>
<MenuItem onClick={() => menuActions.onClose()}>
<Iconify icon="solar:import-bold" />
Import
</MenuItem>
<MenuItem onClick={() => menuActions.onClose()}>
<Iconify icon="solar:export-bold" />
Export
</MenuItem>
</MenuList>
</CustomPopover>
);
return (
<>
<Box
sx={{
p: 2.5,
gap: 2,
display: 'flex',
pr: { xs: 2.5, md: 1 },
flexDirection: { xs: 'column', md: 'row' },
alignItems: { xs: 'flex-end', md: 'center' },
}}
>
<FormControl sx={{ flexShrink: 0, width: { xs: 1, md: 200 } }}>
<InputLabel htmlFor="filter-role-select">Role</InputLabel>
<Select
multiple
value={currentFilters.role}
onChange={handleFilterRole}
input={<OutlinedInput label="Role" />}
renderValue={(selected) => selected.map((value) => value).join(', ')}
inputProps={{ id: 'filter-role-select' }}
MenuProps={{ PaperProps: { sx: { maxHeight: 240 } } }}
>
{options.roles.map((option) => (
<MenuItem key={option} value={option}>
<Checkbox
disableRipple
size="small"
checked={currentFilters.role.includes(option)}
/>
{option}
</MenuItem>
))}
</Select>
</FormControl>
<Box
sx={{
gap: 2,
width: 1,
flexGrow: 1,
display: 'flex',
alignItems: 'center',
}}
>
<TextField
fullWidth
value={currentFilters.name}
onChange={handleFilterName}
placeholder="Search..."
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<Iconify icon="eva:search-fill" sx={{ color: 'text.disabled' }} />
</InputAdornment>
),
},
}}
/>
<IconButton onClick={menuActions.onOpen}>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
</Box>
</Box>
{renderMenuActions()}
</>
);
}

View File

@@ -0,0 +1,75 @@
import Avatar from '@mui/material/Avatar';
import type { BoxProps } from '@mui/material/Box';
import Box from '@mui/material/Box';
import ListItemText from '@mui/material/ListItemText';
import { varAlpha } from 'minimal-shared/utils';
import type { IPartyUserProfileCover } from 'src/types/party-user';
// ----------------------------------------------------------------------
export function ProfileCover({
sx,
name,
role,
coverUrl,
avatarUrl,
...other
}: BoxProps & IPartyUserProfileCover) {
return (
<Box
sx={[
(theme) => ({
...theme.mixins.bgGradient({
images: [
`linear-gradient(0deg, ${varAlpha(theme.vars.palette.primary.darkerChannel, 0.8)}, ${varAlpha(theme.vars.palette.primary.darkerChannel, 0.8)})`,
`url(${coverUrl})`,
],
}),
height: 1,
color: 'common.white',
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<Box
sx={{
display: 'flex',
left: { md: 24 },
bottom: { md: 24 },
zIndex: { md: 10 },
pt: { xs: 6, md: 0 },
position: { md: 'absolute' },
flexDirection: { xs: 'column', md: 'row' },
}}
>
<Avatar
alt={name}
src={avatarUrl}
sx={[
(theme) => ({
mx: 'auto',
width: { xs: 64, md: 128 },
height: { xs: 64, md: 128 },
border: `solid 2px ${theme.vars.palette.common.white}`,
}),
]}
>
{name?.charAt(0).toUpperCase()}
</Avatar>
<ListItemText
primary={name}
secondary={role}
slotProps={{
primary: { sx: { typography: 'h4' } },
secondary: {
sx: { mt: 0.5, opacity: 0.48, color: 'inherit' },
},
}}
sx={{ mt: 3, ml: { md: 3 }, textAlign: { xs: 'center', md: 'unset' } }}
/>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,123 @@
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import type { CardProps } from '@mui/material/Card';
import Card from '@mui/material/Card';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import { useCallback, useState } from 'react';
import { Iconify } from 'src/components/iconify';
import type { IPartyUserProfileFollower } from 'src/types/party-user';
// ----------------------------------------------------------------------
type Props = {
followers: IPartyUserProfileFollower[];
};
export function ProfileFollowers({ followers }: Props) {
const _mockFollowed = followers.slice(4, 8).map((i) => i.id);
const [followed, setFollowed] = useState<string[]>(_mockFollowed);
const handleClick = useCallback(
(item: string) => {
const selected = followed.includes(item)
? followed.filter((value) => value !== item)
: [...followed, item];
setFollowed(selected);
},
[followed]
);
return (
<>
<Typography variant="h4" sx={{ my: 5 }}>
Followers
</Typography>
<Box
sx={{
gap: 3,
display: 'grid',
gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
}}
>
{followers.map((follower) => (
<CardItem
key={follower.id}
follower={follower}
selected={followed.includes(follower.id)}
onSelected={() => handleClick(follower.id)}
/>
))}
</Box>
</>
);
}
// ----------------------------------------------------------------------
type CardItemProps = CardProps & {
selected: boolean;
onSelected: () => void;
follower: IPartyUserProfileFollower;
};
function CardItem({ follower, selected, onSelected, sx, ...other }: CardItemProps) {
return (
<Card
sx={[
(theme) => ({
display: 'flex',
alignItems: 'center',
p: theme.spacing(3, 2, 3, 3),
}),
...(Array.isArray(sx) ? sx : [sx]),
]}
{...other}
>
<Avatar
alt={follower?.name}
src={follower?.avatarUrl}
sx={{ width: 48, height: 48, mr: 2 }}
/>
<ListItemText
primary={follower?.name}
secondary={
<>
<Iconify icon="mingcute:location-fill" width={16} sx={{ flexShrink: 0, mr: 0.5 }} />
{follower?.country}
</>
}
slotProps={{
primary: { noWrap: true },
secondary: {
noWrap: true,
sx: {
mt: 0.5,
display: 'flex',
alignItems: 'center',
typography: 'caption',
color: 'text.disabled',
},
},
}}
/>
<Button
size="small"
variant={selected ? 'text' : 'outlined'}
color={selected ? 'success' : 'inherit'}
startIcon={
selected ? <Iconify width={18} icon="eva:checkmark-fill" sx={{ mr: -0.75 }} /> : null
}
onClick={onSelected}
sx={{ flexShrink: 0, ml: 1.5 }}
>
{selected ? 'Followed' : 'Follow'}
</Button>
</Card>
);
}

View File

@@ -0,0 +1,183 @@
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
import Link from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { usePopover } from 'minimal-shared/hooks';
import { _socials } from 'src/_mock';
import { CustomPopover } from 'src/components/custom-popover';
import { Iconify } from 'src/components/iconify';
import { SearchNotFound } from 'src/components/search-not-found';
import type { IPartyUserProfileFriend } from 'src/types/party-user';
// ----------------------------------------------------------------------
type Props = {
searchFriends: string;
friends: IPartyUserProfileFriend[];
onSearchFriends: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
export function ProfileFriends({ friends, searchFriends, onSearchFriends }: Props) {
const dataFiltered = applyFilter({ inputData: friends, query: searchFriends });
const notFound = !dataFiltered.length && !!searchFriends;
return (
<>
<Box
sx={{
my: 5,
gap: 2,
display: 'flex',
justifyContent: 'space-between',
flexDirection: { xs: 'column', sm: 'row' },
}}
>
<Typography variant="h4">Friends</Typography>
<TextField
value={searchFriends}
onChange={onSearchFriends}
placeholder="Search friends..."
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<Iconify icon="eva:search-fill" sx={{ color: 'text.disabled' }} />
</InputAdornment>
),
},
}}
sx={{ width: { xs: 1, sm: 260 } }}
/>
</Box>
{notFound ? (
<SearchNotFound query={searchFriends} sx={{ py: 10 }} />
) : (
<Box
sx={{
gap: 3,
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(1, 1fr)',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
},
}}
>
{dataFiltered.map((item) => (
<FriendCard key={item.id} item={item} />
))}
</Box>
)}
</>
);
}
// ----------------------------------------------------------------------
type FriendCardProps = {
item: IPartyUserProfileFriend;
};
function FriendCard({ item }: FriendCardProps) {
const menuActions = usePopover();
const handleDelete = () => {
menuActions.onClose();
console.info('DELETE', item.name);
};
const handleEdit = () => {
menuActions.onClose();
console.info('EDIT', item.name);
};
const renderMenuActions = () => (
<CustomPopover
open={menuActions.open}
anchorEl={menuActions.anchorEl}
onClose={menuActions.onClose}
slotProps={{ arrow: { placement: 'right-top' } }}
>
<MenuList>
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
<Iconify icon="solar:trash-bin-trash-bold" />
Delete
</MenuItem>
<MenuItem onClick={handleEdit}>
<Iconify icon="solar:pen-bold" />
Edit
</MenuItem>
</MenuList>
</CustomPopover>
);
return (
<>
<Card
sx={{
py: 5,
display: 'flex',
position: 'relative',
alignItems: 'center',
flexDirection: 'column',
}}
>
<Avatar alt={item.name} src={item.avatarUrl} sx={{ width: 64, height: 64, mb: 3 }} />
<Link variant="subtitle1" color="text.primary">
{item.name}
</Link>
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 1, mt: 0.5 }}>
{item.role}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{_socials.map((social) => (
<IconButton key={social.label}>
{social.value === 'twitter' && <Iconify icon="socials:twitter" />}
{social.value === 'facebook' && <Iconify icon="socials:facebook" />}
{social.value === 'instagram' && <Iconify icon="socials:instagram" />}
{social.value === 'linkedin' && <Iconify icon="socials:linkedin" />}
</IconButton>
))}
</Box>
<IconButton
color={menuActions.open ? 'inherit' : 'default'}
onClick={menuActions.onOpen}
sx={{ top: 8, right: 8, position: 'absolute' }}
>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
</Card>
{renderMenuActions()}
</>
);
}
// ----------------------------------------------------------------------
type ApplyFilterProps = {
query: string;
inputData: IPartyUserProfileFriend[];
};
function applyFilter({ inputData, query }: ApplyFilterProps) {
if (!query) return inputData;
return inputData.filter(({ name, role }) =>
[name, role].some((field) => field?.toLowerCase().includes(query.toLowerCase()))
);
}

View File

@@ -0,0 +1,89 @@
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import IconButton from '@mui/material/IconButton';
import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import { Iconify } from 'src/components/iconify';
import { Image } from 'src/components/image';
import { Lightbox, useLightBox } from 'src/components/lightbox';
import type { IPartyUserProfileGallery } from 'src/types/party-user';
import { fDate } from 'src/utils/format-time';
// ----------------------------------------------------------------------
type Props = {
gallery: IPartyUserProfileGallery[];
};
export function ProfileGallery({ gallery }: Props) {
const slides = gallery.map((slide) => ({ src: slide.imageUrl }));
const lightbox = useLightBox(slides);
return (
<>
<Typography variant="h4" sx={{ my: 5 }}>
Gallery
</Typography>
<Box
sx={{
gap: 3,
display: 'grid',
gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
}}
>
{gallery.map((image) => (
<Card key={image.id} sx={{ cursor: 'pointer', color: 'common.white' }}>
<IconButton
color="inherit"
sx={{
top: 8,
right: 8,
zIndex: 9,
position: 'absolute',
}}
>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
<ListItemText
sx={{ p: 3, left: 0, width: 1, bottom: 0, zIndex: 9, position: 'absolute' }}
primary={image.title}
secondary={fDate(image.postedAt)}
slotProps={{
primary: {
noWrap: true,
sx: { typography: 'subtitle1' },
},
secondary: {
sx: { mt: 0.5, opacity: 0.48, color: 'inherit' },
},
}}
/>
<Image
alt="Gallery"
ratio="1/1"
src={image.imageUrl}
onClick={() => lightbox.onOpen(image.imageUrl)}
slotProps={{
overlay: {
sx: (theme) => ({
backgroundImage: `linear-gradient(to bottom, transparent 0%, ${theme.vars.palette.common.black} 75%)`,
}),
},
}}
/>
</Card>
))}
</Box>
<Lightbox
index={lightbox.selected}
slides={slides}
open={lightbox.open}
close={lightbox.onClose}
/>
</>
);
}

View File

@@ -0,0 +1,208 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import Divider from '@mui/material/Divider';
import Fab from '@mui/material/Fab';
import Grid from '@mui/material/Grid';
import InputBase from '@mui/material/InputBase';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import { varAlpha } from 'minimal-shared/utils';
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { _socials } from 'src/_mock';
import { Iconify } from 'src/components/iconify';
import type { IPartyUserProfile, IPartyUserProfilePost } from 'src/types/party-user';
import { fNumber } from 'src/utils/format-number';
import { ProfilePostItem } from './profile-post-item';
// ----------------------------------------------------------------------
type Props = {
info: IPartyUserProfile;
posts: IPartyUserProfilePost[];
};
export function ProfileHome({ info, posts }: Props) {
const fileRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const handleAttach = () => {
if (fileRef.current) {
fileRef.current.click();
}
};
const renderFollows = () => (
<Card sx={{ py: 3, textAlign: 'center', typography: 'h4' }}>
<Stack
divider={<Divider orientation="vertical" flexItem sx={{ borderStyle: 'dashed' }} />}
sx={{ flexDirection: 'row' }}
>
<Stack sx={{ width: 1 }}>
{fNumber(info.totalFollowers)}
<Box component="span" sx={{ color: 'text.secondary', typography: 'body2' }}>
{t('Follower')}
</Box>
</Stack>
<Stack sx={{ width: 1 }}>
{fNumber(info.totalFollowing)}
<Box component="span" sx={{ color: 'text.secondary', typography: 'body2' }}>
{t('Following')}
</Box>
</Stack>
</Stack>
</Card>
);
const renderAbout = () => (
<Card>
<CardHeader title={t('About')} />
<Box
sx={{
p: 3,
gap: 2,
display: 'flex',
typography: 'body2',
flexDirection: 'column',
}}
>
<div>{info.quote}</div>
<Box sx={{ gap: 2, display: 'flex', lineHeight: '24px' }}>
<Iconify width={24} icon="mingcute:location-fill" />
<span>
Live at
<Link variant="subtitle2" color="inherit">
&nbsp;{info.country}
</Link>
</span>
</Box>
<Box sx={{ gap: 2, display: 'flex', lineHeight: '24px' }}>
<Iconify width={24} icon="solar:letter-bold" />
{info.email}
</Box>
<Box sx={{ gap: 2, display: 'flex', lineHeight: '24px' }}>
<Iconify width={24} icon="solar:case-minimalistic-bold" />
<span>
{info.role} at
<Link variant="subtitle2" color="inherit">
&nbsp;{info.company}
</Link>
</span>
</Box>
<Box sx={{ gap: 2, display: 'flex', lineHeight: '24px' }}>
<Iconify width={24} icon="solar:case-minimalistic-bold" />
<span>
Studied at
<Link variant="subtitle2" color="inherit">
&nbsp;{info.school}
</Link>
</span>
</Box>
</Box>
</Card>
);
const renderPostInput = () => (
<Card sx={{ p: 3 }}>
<InputBase
multiline
fullWidth
rows={4}
placeholder="Share what you are thinking here..."
inputProps={{ id: 'post-input' }}
sx={[
(theme) => ({
p: 2,
mb: 3,
borderRadius: 1,
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.2)}`,
}),
]}
/>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box
sx={{
gap: 1,
display: 'flex',
alignItems: 'center',
color: 'text.secondary',
}}
>
<Fab size="small" color="inherit" variant="softExtended" onClick={handleAttach}>
<Iconify icon="solar:gallery-wide-bold" width={24} sx={{ color: 'success.main' }} />
{t('Image/Video')}
</Fab>
<Fab size="small" color="inherit" variant="softExtended">
<Iconify icon="solar:videocamera-record-bold" width={24} sx={{ color: 'error.main' }} />
{t('Streaming')}
</Fab>
</Box>
<Button variant="contained">{t('Post')}</Button>
</Box>
<input ref={fileRef} type="file" style={{ display: 'none' }} />
</Card>
);
const renderSocials = () => (
<Card>
<CardHeader title={t('Social')} />
<Box sx={{ p: 3, gap: 2, display: 'flex', flexDirection: 'column', typography: 'body2' }}>
{_socials.map((social) => (
<Box
key={social.label}
sx={{
gap: 2,
display: 'flex',
lineHeight: '20px',
wordBreak: 'break-all',
alignItems: 'flex-start',
}}
>
{social.value === 'twitter' && <Iconify icon="socials:twitter" />}
{social.value === 'facebook' && <Iconify icon="socials:facebook" />}
{social.value === 'instagram' && <Iconify icon="socials:instagram" />}
{social.value === 'linkedin' && <Iconify icon="socials:linkedin" />}
<Link color="inherit">
{social.value === 'facebook' && info.socialLinks.facebook}
{social.value === 'instagram' && info.socialLinks.instagram}
{social.value === 'linkedin' && info.socialLinks.linkedin}
{social.value === 'twitter' && info.socialLinks.twitter}
</Link>
</Box>
))}
</Box>
</Card>
);
return (
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 4 }} sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
{renderFollows()}
{renderAbout()}
{renderSocials()}
</Grid>
<Grid size={{ xs: 12, md: 8 }} sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
{renderPostInput()}
{posts.map((post) => (
<ProfilePostItem key={post.id} post={post} />
))}
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,221 @@
import Avatar from '@mui/material/Avatar';
import AvatarGroup, { avatarGroupClasses } from '@mui/material/AvatarGroup';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
import InputBase from '@mui/material/InputBase';
import Link from '@mui/material/Link';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { varAlpha } from 'minimal-shared/utils';
import { useCallback, useRef, useState } from 'react';
import { useMockedUser } from 'src/auth/hooks';
import { Iconify } from 'src/components/iconify';
import { Image } from 'src/components/image';
import type { IPartyUserProfilePost } from 'src/types/party-user';
import { fShortenNumber } from 'src/utils/format-number';
import { fDate } from 'src/utils/format-time';
// ----------------------------------------------------------------------
type Props = {
post: IPartyUserProfilePost;
};
export function ProfilePostItem({ post }: Props) {
const { user } = useMockedUser();
const commentRef = useRef<HTMLInputElement>(null);
const fileRef = useRef<HTMLInputElement>(null);
const [message, setMessage] = useState('');
const handleChangeMessage = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setMessage(event.target.value);
}, []);
const handleAttach = useCallback(() => {
if (fileRef.current) {
fileRef.current.click();
}
}, []);
const handleClickComment = useCallback(() => {
if (commentRef.current) {
commentRef.current.focus();
}
}, []);
const renderHead = () => (
<CardHeader
disableTypography
avatar={
<Avatar src={user?.photoURL} alt={user?.displayName}>
{user?.displayName?.charAt(0).toUpperCase()}
</Avatar>
}
title={
<Link color="inherit" variant="subtitle1">
{user?.displayName}
</Link>
}
subheader={
<Box sx={{ color: 'text.disabled', typography: 'caption', mt: 0.5 }}>
{fDate(post.createdAt)}
</Box>
}
action={
<IconButton>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
}
/>
);
const renderCommentList = () => (
<Stack spacing={1.5} sx={{ px: 3, pb: 2 }}>
{post.comments.map((comment) => (
<Box key={comment.id} sx={{ gap: 2, display: 'flex' }}>
<Avatar alt={comment.author.name} src={comment.author.avatarUrl} />
<Paper sx={{ p: 1.5, flexGrow: 1, bgcolor: 'background.neutral' }}>
<Box
sx={{
mb: 0.5,
display: 'flex',
alignItems: { sm: 'center' },
justifyContent: 'space-between',
flexDirection: { xs: 'column', sm: 'row' },
}}
>
<Box sx={{ typography: 'subtitle2' }}>{comment.author.name}</Box>
<Box sx={{ typography: 'caption', color: 'text.disabled' }}>
{fDate(comment.createdAt)}
</Box>
</Box>
<Box sx={{ typography: 'body2', color: 'text.secondary' }}>{comment.message}</Box>
</Paper>
</Box>
))}
</Stack>
);
const renderInput = () => (
<Box
sx={[
(theme) => ({
gap: 2,
display: 'flex',
alignItems: 'center',
p: theme.spacing(0, 3, 3, 3),
}),
]}
>
<Avatar src={user?.photoURL} alt={user?.displayName}>
{user?.displayName?.charAt(0).toUpperCase()}
</Avatar>
<InputBase
fullWidth
value={message}
inputRef={commentRef}
placeholder="Write a comment…"
onChange={handleChangeMessage}
endAdornment={
<InputAdornment position="end" sx={{ mr: 1 }}>
<IconButton size="small" onClick={handleAttach}>
<Iconify icon="solar:gallery-add-bold" />
</IconButton>
<IconButton size="small">
<Iconify icon="eva:smiling-face-fill" />
</IconButton>
</InputAdornment>
}
inputProps={{
id: `comment-${post.id}-input`,
'aria-label': `Comment ${post.id} input`,
}}
sx={[
(theme) => ({
pl: 1.5,
height: 40,
borderRadius: 1,
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.32)}`,
}),
]}
/>
<input type="file" ref={fileRef} style={{ display: 'none' }} />
</Box>
);
const renderActions = () => (
<Box
sx={[(theme) => ({ display: 'flex', alignItems: 'center', p: theme.spacing(2, 3, 3, 3) })]}
>
<FormControlLabel
control={
<Checkbox
defaultChecked
color="error"
icon={<Iconify icon="solar:heart-bold" />}
checkedIcon={<Iconify icon="solar:heart-bold" />}
slotProps={{
input: {
id: `favorite-${post.id}-checkbox`,
'aria-label': `Favorite ${post.id} checkbox`,
},
}}
/>
}
label={fShortenNumber(post.personLikes.length)}
sx={{ mr: 1 }}
/>
{!!post.personLikes.length && (
<AvatarGroup sx={{ [`& .${avatarGroupClasses.avatar}`]: { width: 32, height: 32 } }}>
{post.personLikes.map((person) => (
<Avatar key={person.name} alt={person.name} src={person.avatarUrl} />
))}
</AvatarGroup>
)}
<Box sx={{ flexGrow: 1 }} />
<IconButton onClick={handleClickComment}>
<Iconify icon="solar:chat-round-dots-bold" />
</IconButton>
<IconButton>
<Iconify icon="solar:share-bold" />
</IconButton>
</Box>
);
return (
<Card>
{renderHead()}
<Typography variant="body2" sx={[(theme) => ({ p: theme.spacing(3, 3, 2, 3) })]}>
{post.message}
</Typography>
<Box sx={{ p: 1 }}>
<Image alt={post.media} src={post.media} ratio="16/9" sx={{ borderRadius: 1.5 }} />
</Box>
{renderActions()}
{!!post.comments.length && renderCommentList()}
{renderInput()}
</Card>
);
}

View File

@@ -0,0 +1,9 @@
export * from './party-user-edit-view';
export * from './party-user-list-view';
export * from './party-user-cards-view';
export * from './party-user-create-view';
export * from './party-user-profile-view';

View File

@@ -0,0 +1,41 @@
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import { _userCards } from 'src/_mock';
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { Iconify } from 'src/components/iconify';
import { DashboardContent } from 'src/layouts/dashboard';
import { RouterLink } from 'src/routes/components';
import { paths } from 'src/routes/paths';
import { UserCardList } from '../party-user-card-list';
// ----------------------------------------------------------------------
export function UserCardsView() {
const { t } = useTranslation();
return (
<DashboardContent>
<CustomBreadcrumbs
heading="User cards"
links={[
//
{ name: t('Dashboard'), href: paths.dashboard.root },
{ name: t('User'), href: paths.dashboard.user.root },
{ name: t('Cards') },
]}
action={
<Button
component={RouterLink}
href={paths.dashboard.user.new}
variant="contained"
startIcon={<Iconify icon="mingcute:add-line" />}
>
{t('New user')}
</Button>
}
sx={{ mb: { xs: 3, md: 5 } }}
/>
<UserCardList users={_userCards} />
</DashboardContent>
);
}

View File

@@ -0,0 +1,28 @@
import { useTranslation } from 'react-i18next';
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { DashboardContent } from 'src/layouts/dashboard';
import { paths } from 'src/routes/paths';
import { PartyUserNewEditForm } from '../party-user-new-edit-form';
// ----------------------------------------------------------------------
export function UserCreateView() {
const { t } = useTranslation();
return (
<DashboardContent>
<CustomBreadcrumbs
heading={t('Create a new party user')}
links={[
//
{ name: t('Dashboard'), href: paths.dashboard.root },
{ name: t('Party User'), href: paths.dashboard.user.root },
{ name: t('New') },
]}
sx={{ mb: { xs: 3, md: 5 } }}
/>
<PartyUserNewEditForm />
</DashboardContent>
);
}

View File

@@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next';
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { DashboardContent } from 'src/layouts/dashboard';
import { paths } from 'src/routes/paths';
import type { IPartyUserItem } from 'src/types/party-user';
import { PartyUserNewEditForm } from '../party-user-new-edit-form';
// ----------------------------------------------------------------------
type Props = {
user?: IPartyUserItem;
};
export function PartyUserEditView({ user: currentUser }: Props) {
const { t } = useTranslation();
return (
<DashboardContent>
<CustomBreadcrumbs
heading="Edit"
backHref={paths.dashboard.partyUser.list}
links={[
{ name: t('Dashboard'), href: paths.dashboard.root },
{ name: t('Party User'), href: paths.dashboard.partyUser.root },
{ name: currentUser?.name },
]}
sx={{ mb: { xs: 3, md: 5 } }}
/>
<PartyUserNewEditForm currentUser={currentUser} />
</DashboardContent>
);
}

View File

@@ -0,0 +1,354 @@
// src/sections/user/view/user-list-view.tsx
//
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card';
import IconButton from '@mui/material/IconButton';
import Tab from '@mui/material/Tab';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import Tabs from '@mui/material/Tabs';
import Tooltip from '@mui/material/Tooltip';
import { useBoolean, useSetState } from 'minimal-shared/hooks';
import { varAlpha } from 'minimal-shared/utils';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { _roles, USER_STATUS_OPTIONS } from 'src/_mock';
import { deletePartyUser, useGetPartyUsers } from 'src/actions/party-user';
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { ConfirmDialog } from 'src/components/custom-dialog';
import { Iconify } from 'src/components/iconify';
import { Label } from 'src/components/label';
import { Scrollbar } from 'src/components/scrollbar';
import { toast } from 'src/components/snackbar';
import type { TableHeadCellProps } from 'src/components/table';
import {
emptyRows,
getComparator,
rowInPage,
TableEmptyRows,
TableHeadCustom,
TableNoData,
TablePaginationCustom,
TableSelectedAction,
useTable,
} from 'src/components/table';
import { DashboardContent } from 'src/layouts/dashboard';
import { useRouter } from 'src/routes/hooks';
import { paths } from 'src/routes/paths';
import type { IPartyUserItem, IPartyUserTableFilters } from 'src/types/party-user';
import { PartyUserTableFiltersResult } from '../party-user-table-filters-result';
import { PartyUserTableRow } from '../party-user-table-row';
import { PartyUserTableToolbar } from '../party-user-table-toolbar';
// ----------------------------------------------------------------------
const STATUS_OPTIONS = [{ value: 'all', label: 'All' }, ...USER_STATUS_OPTIONS];
// ----------------------------------------------------------------------
export function PartyUserListView() {
const { t } = useTranslation();
const router = useRouter();
const TABLE_HEAD: TableHeadCellProps[] = [
{ id: 'name', label: t('Name') },
{ id: 'phoneNumber', label: t('Phone number'), width: 180 },
{ id: 'company', label: t('Company'), width: 220 },
{ id: 'role', label: t('Role'), width: 180 },
{ id: 'status', label: t('Status'), width: 100 },
{ id: '', width: 88 },
];
const { partyUsers } = useGetPartyUsers();
const [processNewUser, setProcessNewUser] = useState<boolean>(false);
const table = useTable();
const confirmDialog = useBoolean();
const [tableData, setTableData] = useState<IPartyUserItem[]>([]);
useEffect(() => {
setTableData(partyUsers);
}, [partyUsers]);
const filters = useSetState<IPartyUserTableFilters>({ name: '', role: [], status: 'all' });
const { state: currentFilters, setState: updateFilters } = filters;
const dataFiltered = applyFilter({
inputData: tableData,
comparator: getComparator(table.order, table.orderBy),
filters: currentFilters,
});
const dataInPage = rowInPage(dataFiltered, table.page, table.rowsPerPage);
const canReset =
!!currentFilters.name || currentFilters.role.length > 0 || currentFilters.status !== 'all';
const notFound = (!dataFiltered.length && canReset) || !dataFiltered.length;
const handleDeleteRow = useCallback(
async (id: string) => {
// const deleteRow = tableData.filter((row) => row.id !== id);
// toast.success('Delete success!');
// setTableData(deleteRow);
try {
await deletePartyUser(id);
toast.success('Delete success!');
table.onUpdatePageDeleteRow(dataInPage.length);
} catch (error) {
console.error(error);
toast.error('Delete failed!');
}
},
[table, tableData]
);
const handleDeleteRows = useCallback(() => {
const deleteRows = tableData.filter((row) => !table.selected.includes(row.id));
toast.success('Delete success!');
setTableData(deleteRows);
table.onUpdatePageDeleteRows(dataInPage.length, dataFiltered.length);
}, [dataFiltered.length, dataInPage.length, table, tableData]);
const handleFilterStatus = useCallback(
(event: React.SyntheticEvent, newValue: string) => {
table.onResetPage();
updateFilters({ status: newValue });
},
[updateFilters, table]
);
const renderConfirmDialog = () => (
<ConfirmDialog
open={confirmDialog.value}
onClose={confirmDialog.onFalse}
title={t('Delete')}
content={
<>
Are you sure want to delete <strong> {table.selected.length} </strong> items?
</>
}
action={
<Button
variant="contained"
color="error"
onClick={() => {
handleDeleteRows();
// confirmDialog.onFalse();
}}
>
{t('Delete')}
</Button>
}
/>
);
return (
<>
<DashboardContent>
<CustomBreadcrumbs
heading="List"
links={[
//
{ name: t('Dashboard'), href: paths.dashboard.root },
{ name: t('PartyUser'), href: paths.dashboard.partyUser.root },
{ name: t('List') },
]}
action={
<Button
disabled={processNewUser}
loading={processNewUser}
// component={RouterLink}
// href={paths.dashboard.partyUser.new}
variant="contained"
startIcon={<Iconify icon="mingcute:add-line" />}
onClick={() => {
setProcessNewUser(true);
router.push(paths.dashboard.partyUser.new);
}}
>
{t('New user')}
</Button>
}
sx={{ mb: { xs: 3, md: 5 } }}
/>
<Card>
<Tabs
value={currentFilters.status}
onChange={handleFilterStatus}
sx={[
(theme) => ({
px: 2.5,
boxShadow: `inset 0 -2px 0 0 ${varAlpha(theme.vars.palette.grey['500Channel'], 0.08)}`,
}),
]}
>
{STATUS_OPTIONS.map((tab) => (
<Tab
key={tab.value}
iconPosition="end"
value={tab.value}
label={t(tab.label)}
icon={
<Label
variant={
((tab.value === 'all' || tab.value === currentFilters.status) && 'filled') ||
'soft'
}
color={
(tab.value === 'active' && 'success') ||
(tab.value === 'pending' && 'warning') ||
(tab.value === 'banned' && 'error') ||
'default'
}
>
{['active', 'pending', 'banned', 'rejected'].includes(tab.value)
? tableData.filter((partyUser) => partyUser.status === tab.value).length
: tableData.length}
</Label>
}
/>
))}
</Tabs>
<PartyUserTableToolbar
filters={filters}
onResetPage={table.onResetPage}
options={{ roles: _roles }}
/>
{canReset && (
<PartyUserTableFiltersResult
filters={filters}
totalResults={dataFiltered.length}
onResetPage={table.onResetPage}
sx={{ p: 2.5, pt: 0 }}
/>
)}
<Box sx={{ position: 'relative' }}>
<TableSelectedAction
dense={table.dense}
numSelected={table.selected.length}
rowCount={dataFiltered.length}
onSelectAllRows={(checked) =>
table.onSelectAllRows(
checked,
dataFiltered.map((row) => row.id)
)
}
action={
<Tooltip title={t('Delete')}>
<IconButton color="primary" onClick={confirmDialog.onTrue}>
<Iconify icon="solar:trash-bin-trash-bold" />
</IconButton>
</Tooltip>
}
/>
<Scrollbar>
<Table size={table.dense ? 'small' : 'medium'} sx={{ minWidth: 960 }}>
<TableHeadCustom
order={table.order}
orderBy={table.orderBy}
headCells={TABLE_HEAD}
rowCount={dataFiltered.length}
numSelected={table.selected.length}
onSort={table.onSort}
onSelectAllRows={(checked) =>
table.onSelectAllRows(
checked,
dataFiltered.map((row) => row.id)
)
}
/>
<TableBody>
{dataFiltered
.slice(
table.page * table.rowsPerPage,
table.page * table.rowsPerPage + table.rowsPerPage
)
.map((row) => (
<PartyUserTableRow
key={row.id}
row={row}
selected={table.selected.includes(row.id)}
onSelectRow={() => table.onSelectRow(row.id)}
onDeleteRow={() => handleDeleteRow(row.id)}
editHref={paths.dashboard.partyUser.edit(row.id)}
/>
))}
<TableEmptyRows
height={table.dense ? 56 : 56 + 20}
emptyRows={emptyRows(table.page, table.rowsPerPage, dataFiltered.length)}
/>
<TableNoData notFound={notFound} />
</TableBody>
</Table>
</Scrollbar>
</Box>
<TablePaginationCustom
page={table.page}
dense={table.dense}
count={dataFiltered.length}
rowsPerPage={table.rowsPerPage}
onPageChange={table.onChangePage}
onChangeDense={table.onChangeDense}
onRowsPerPageChange={table.onChangeRowsPerPage}
/>
</Card>
</DashboardContent>
{renderConfirmDialog()}
</>
);
}
// ----------------------------------------------------------------------
type ApplyFilterProps = {
inputData: IPartyUserItem[];
filters: IPartyUserTableFilters;
comparator: (a: any, b: any) => number;
};
function applyFilter({ inputData, comparator, filters }: ApplyFilterProps) {
const { name, status, role } = filters;
const stabilizedThis = inputData.map((el, index) => [el, index] as const);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) return order;
return a[1] - b[1];
});
inputData = stabilizedThis.map((el) => el[0]);
if (name) {
inputData = inputData.filter((user) => user.name.toLowerCase().includes(name.toLowerCase()));
}
if (status !== 'all') {
inputData = inputData.filter((user) => user.status === status);
}
if (role.length) {
inputData = inputData.filter((user) => role.includes(user.role));
}
return inputData;
}

View File

@@ -0,0 +1,132 @@
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { _userAbout, _userFeeds, _userFollowers, _userFriends, _userGallery } from 'src/_mock';
import { useMockedUser } from 'src/auth/hooks';
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { Iconify } from 'src/components/iconify';
import { DashboardContent } from 'src/layouts/dashboard';
import { RouterLink } from 'src/routes/components';
import { usePathname, useSearchParams } from 'src/routes/hooks';
import { paths } from 'src/routes/paths';
import { ProfileCover } from '../profile-cover';
import { ProfileFollowers } from '../profile-followers';
import { ProfileFriends } from '../profile-friends';
import { ProfileGallery } from '../profile-gallery';
import { ProfileHome } from '../profile-home';
// ----------------------------------------------------------------------
const NAV_ITEMS = [
{
value: '',
label: 'Profile',
icon: <Iconify width={24} icon="solar:user-id-bold" />,
},
{
value: 'followers',
label: 'Followers',
icon: <Iconify width={24} icon="solar:heart-bold" />,
},
{
value: 'friends',
label: 'Friends',
icon: <Iconify width={24} icon="solar:users-group-rounded-bold" />,
},
{
value: 'gallery',
label: 'Gallery',
icon: <Iconify width={24} icon="solar:gallery-wide-bold" />,
},
];
// ----------------------------------------------------------------------
const TAB_PARAM = 'tab';
export function UserProfileView() {
const { t } = useTranslation();
const pathname = usePathname();
const searchParams = useSearchParams();
const selectedTab = searchParams.get(TAB_PARAM) ?? '';
const { user } = useMockedUser();
const [searchFriends, setSearchFriends] = useState('');
const handleSearchFriends = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setSearchFriends(event.target.value);
}, []);
const createRedirectPath = (currentPath: string, query: string) => {
const queryString = new URLSearchParams({ [TAB_PARAM]: query }).toString();
return query ? `${currentPath}?${queryString}` : currentPath;
};
return (
<DashboardContent>
<CustomBreadcrumbs
heading="Profile"
links={[
{ name: t('Dashboard'), href: paths.dashboard.root },
{ name: t('User'), href: paths.dashboard.user.root },
{ name: user?.displayName },
]}
sx={{ mb: { xs: 3, md: 5 } }}
/>
<Card sx={{ mb: 3, height: 290 }}>
<ProfileCover
role={_userAbout.role}
name={user?.displayName}
avatarUrl={user?.photoURL}
coverUrl={_userAbout.coverUrl}
/>
<Box
sx={{
width: 1,
bottom: 0,
zIndex: 9,
px: { md: 3 },
display: 'flex',
position: 'absolute',
bgcolor: 'background.paper',
justifyContent: { xs: 'center', md: 'flex-end' },
}}
>
<Tabs value={selectedTab}>
{NAV_ITEMS.map((tab) => (
<Tab
component={RouterLink}
key={tab.value}
value={tab.value}
icon={tab.icon}
label={t(tab.label)}
href={createRedirectPath(pathname, tab.value)}
/>
))}
</Tabs>
</Box>
</Card>
{selectedTab === '' && <ProfileHome info={_userAbout} posts={_userFeeds} />}
{selectedTab === 'followers' && <ProfileFollowers followers={_userFollowers} />}
{selectedTab === 'friends' && (
<ProfileFriends
friends={_userFriends}
searchFriends={searchFriends}
onSearchFriends={handleSearchFriends}
/>
)}
{selectedTab === 'gallery' && <ProfileGallery gallery={_userGallery} />}
</DashboardContent>
);
}

View File

@@ -188,10 +188,11 @@ export function ProductNewEditForm({ currentProduct }: Props) {
const values = watch();
const onSubmit = handleSubmit(async (data) => {
const updatedData = {
...data,
taxes: includeTaxes ? defaultValues.taxes : data.taxes,
};
// TODO: remove unused code
// const updatedData = {
// ...data,
// taxes: includeTaxes ? defaultValues.taxes : data.taxes,
// };
try {
// sanitize file field

View File

@@ -0,0 +1,102 @@
import type { IDateValue, ISocialLink } from './common';
// ----------------------------------------------------------------------
export type IPartyUserTableFilters = {
name: string;
role: string[];
status: string;
};
export type IPartyUserProfileCover = {
name: string;
role: string;
coverUrl: string;
avatarUrl: string;
};
export type IPartyUserProfile = {
id: string;
role: string;
quote: string;
email: string;
school: string;
country: string;
company: string;
totalFollowers: number;
totalFollowing: number;
socialLinks: ISocialLink;
};
export type IPartyUserProfileFollower = {
id: string;
name: string;
country: string;
avatarUrl: string;
};
export type IPartyUserProfileGallery = {
id: string;
title: string;
imageUrl: string;
postedAt: IDateValue;
};
export type IPartyUserProfileFriend = {
id: string;
name: string;
role: string;
avatarUrl: string;
};
export type IPartyUserProfilePost = {
id: string;
media: string;
message: string;
createdAt: IDateValue;
personLikes: { name: string; avatarUrl: string }[];
comments: {
id: string;
message: string;
createdAt: IDateValue;
author: { id: string; name: string; avatarUrl: string };
}[];
};
export type IPartyUserCard = {
id: string;
name: string;
role: string;
coverUrl: string;
avatarUrl: string;
totalPosts: number;
totalFollowers: number;
totalFollowing: number;
};
export type IPartyUserItem = {
id: string;
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;
};
export type IPartyUserAccountBillingHistory = {
id: string;
price: number;
invoiceNumber: string;
createdAt: IDateValue;
};

View File

@@ -0,0 +1,17 @@
# Use official Node 18 base image
FROM node:20-slim
# Install pnpm globally
# RUN npm install -g pnpm
RUN apt update
RUN apt-get install -qqy psmisc
# Set working directory
WORKDIR /app
# Copy your application code (optional, comment out if not needed)
# COPY . /app
# RUN yarn
# Default command (optional)
CMD ["npm run start"]

View File

@@ -7,6 +7,9 @@ clear
while true; do
npm run dev
killall node
killall yarn
echo "restarting..."
sleep 1
done

View File

@@ -2,10 +2,12 @@
set -x
npm i -D
killall node
killall yarn
rm -rf ./**/*Zone.Identifier
npm i -D
set -ex
npm run format

View File

@@ -23,3 +23,9 @@ A: No, no don't need to, user will handle the remaining modifications. please re
Q: when user want you to replace something, where should you start ?
A: you should look for a `helloworld` example and start with it when available. by doing this, you can get familiar to the user coding style and convention.
Q: shall AI introduce new format or ideas?
A: No, AI should not. Most of the time the user passing the update job. Unless user mentioned explicitly. AI only need to do the replacement following the format and convention is good enough.
Q: What should I do when user ask to update git staged files ?
A: You only need to update the comment with same format and detail levels of the staged files. Do not change any other code. The sibling files in the same directory is a good reference.