Compare commits
23 Commits
develop/fr
...
develop/re
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a686dd55dd | ||
![]() |
8d52be9b96 | ||
![]() |
db16b2d5dd | ||
![]() |
7370316ea0 | ||
![]() |
77f7211317 | ||
![]() |
dfc9873815 | ||
![]() |
53b112e488 | ||
![]() |
a9dd265658 | ||
![]() |
ae39b7ca67 | ||
![]() |
4c2a06585d | ||
![]() |
816d88c2c1 | ||
![]() |
843e459527 | ||
![]() |
cd0ae5ba62 | ||
![]() |
ecdbc45c4a | ||
![]() |
4b64778b59 | ||
![]() |
a88de2f17f | ||
![]() |
d987b0fe36 | ||
![]() |
0142c9ba24 | ||
![]() |
48e1f821ad | ||
![]() |
9943283eff | ||
![]() |
9ac13787aa | ||
![]() |
c1b71fca64 | ||
![]() |
08642a2bf6 |
@@ -1,10 +1,10 @@
|
||||
---
|
||||
tags: frontend, product, update
|
||||
tags: frontend, party-event
|
||||
---
|
||||
|
||||
# REQ0185 frontend product update
|
||||
# REQ0185 frontend party-event
|
||||
|
||||
frontend page to update product
|
||||
frontend page to handle party-event (CRUD)
|
||||
|
||||
edit page T.B.A.
|
||||
|
||||
@@ -14,4 +14,5 @@ T.B.A.
|
||||
|
||||
## branch
|
||||
|
||||
develop/frontend/party-event/trunk
|
||||
develop/requirements/REQ0185
|
||||
|
17
01_Requirements/REQ0186/index.md
Normal file
17
01_Requirements/REQ0186/index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
tags: frontend, product, update
|
||||
---
|
||||
|
||||
# REQ0185 frontend product update
|
||||
|
||||
frontend page to update product
|
||||
|
||||
edit page T.B.A.
|
||||
|
||||
## sources
|
||||
|
||||
T.B.A.
|
||||
|
||||
## branch
|
||||
|
||||
develop/requirements/REQ0185
|
23
01_Requirements/REQ0187/index.md
Normal file
23
01_Requirements/REQ0187/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
tags: frontend, party-order
|
||||
---
|
||||
|
||||
# REQ0185 frontend party-order
|
||||
|
||||
frontend page to handle party-order (CRUD)
|
||||
|
||||
edit page T.B.A.
|
||||
|
||||
## TODO
|
||||
|
||||
- remove detail in left nav bar
|
||||
- implement `changeStatus`
|
||||
|
||||
## sources
|
||||
|
||||
T.B.A.
|
||||
|
||||
## branch
|
||||
|
||||
develop/frontend/party-order/trunk
|
||||
develop/requirements/REQ0187
|
@@ -5,6 +5,7 @@
|
||||
"description": "Mock server & assets",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev:check": "yarn tsc:w",
|
||||
"dev": "next dev -p 7272 -H 0.0.0.0",
|
||||
"start": "next start -p 7272 -H 0.0.0.0",
|
||||
"build": "next build",
|
||||
@@ -20,7 +21,7 @@
|
||||
"re:build-npm": "npm run clean && npm install && npm run build",
|
||||
"tsc:dev": "yarn dev & yarn tsc:watch",
|
||||
"tsc:print": "npx tsc --showConfig",
|
||||
"tsc:w": "npx nodemon --delay 3 --ext ts,tsx --exec \"yarn tsc\"",
|
||||
"tsc:w": "npx nodemon --delay 1 --ext ts,tsx --exec \"yarn tsc\"",
|
||||
"tsc:watch": "tsc --noEmit --watch",
|
||||
"tsc": "tsc --noEmit",
|
||||
"migrate": "npx prisma migrate dev --skip-seed",
|
||||
@@ -31,7 +32,9 @@
|
||||
"db:push": "prisma db push --force-reset",
|
||||
"db:push:w": "npx nodemon --delay 1 --watch prisma --ext \"ts,tsx,prisma\" --exec \"yarn db:push && yarn seed\"",
|
||||
"db:studio": "prisma studio",
|
||||
"db:studio:w": "npx nodemon --delay 1 --watch prisma --ext \"prisma\" --exec \"yarn db:studio\""
|
||||
"db:studio:w": "npx nodemon --delay 1 --watch prisma --ext \"prisma\" --exec \"yarn db:studio\"",
|
||||
"db:dev": "yarn db:push && yarn seed && yarn dev",
|
||||
"db:dev:w": "npx nodemon --delay 3 --ext \"ts,tsx,prisma\" --exec \"yarn db:dev\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -68,6 +71,7 @@
|
||||
"@types/react": "^18.3.20",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@typescript-eslint/parser": "^8.28.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-import-resolver-typescript": "^4.2.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
|
@@ -1146,48 +1146,60 @@ model EventReview {
|
||||
|
||||
// NOTE: need to consider with Event
|
||||
// mapped to IEventItem
|
||||
// a.k.a. PartyEvent party-event
|
||||
model EventItem {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
//
|
||||
sku String
|
||||
name String
|
||||
code String
|
||||
price Float
|
||||
taxes Float
|
||||
tags String[]
|
||||
sizes String[]
|
||||
publish String
|
||||
gender String[]
|
||||
coverUrl String
|
||||
images String[]
|
||||
colors String[]
|
||||
quantity Int
|
||||
category String
|
||||
available Int
|
||||
totalSold Int
|
||||
description String
|
||||
totalRatings Float
|
||||
totalReviews Int
|
||||
inventoryType String
|
||||
subDescription String
|
||||
priceSale Float?
|
||||
newLabel Json
|
||||
saleLabel Json
|
||||
ratings Json[]
|
||||
available Int @default(99)
|
||||
category String
|
||||
code String @default("")
|
||||
colors String[]
|
||||
coverUrl String
|
||||
description String
|
||||
gender String[]
|
||||
images String[]
|
||||
inventoryType String @default("")
|
||||
name String @default("")
|
||||
newLabel Json @default("{}")
|
||||
price Float @default(999.9)
|
||||
priceSale Float? @default(111.1)
|
||||
publish String @default("")
|
||||
quantity Int @default(99)
|
||||
ratings Json[]
|
||||
saleLabel Json @default("{}")
|
||||
sizes String[] @default([""])
|
||||
sku String @default("")
|
||||
subDescription String @default("")
|
||||
tags String[] @default([""])
|
||||
taxes Float @default(5.0)
|
||||
totalRatings Float @default(5.0)
|
||||
totalReviews Int @default(10)
|
||||
totalSold Int @default(10)
|
||||
//
|
||||
eventDate DateTime @default(now())
|
||||
joinMembers Json[]
|
||||
title String
|
||||
currency String
|
||||
duration_m Float
|
||||
ageBottom Float
|
||||
ageTop Float
|
||||
location String
|
||||
avatar String[]
|
||||
ageBottom Float @default(-1)
|
||||
ageTop Float @default(-1)
|
||||
avatar String[] @default([""])
|
||||
currency String @default("HKD")
|
||||
capacity Int @default(10)
|
||||
duration_m Float @default(180)
|
||||
endDate String? @default("")
|
||||
eventDate DateTime @default(now())
|
||||
isFeatured Boolean @default(false)
|
||||
joinMembers Json[] @default([])
|
||||
location String @default("HK")
|
||||
organizer String @default("")
|
||||
registrationDeadline String @default("")
|
||||
requirements String @default("")
|
||||
schedule String @default("")
|
||||
speakers String[] @default([])
|
||||
sponsors String[] @default([])
|
||||
startDate String? @default("")
|
||||
status String? @default("")
|
||||
title String @default("")
|
||||
//
|
||||
reviews EventReview[]
|
||||
reviews EventReview[]
|
||||
}
|
||||
|
||||
model AppLog {
|
||||
@@ -1218,3 +1230,30 @@ model AccessLog {
|
||||
@@index([timestamp])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model PartyOrderItem {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
//
|
||||
taxes Float
|
||||
status String
|
||||
shipping Float
|
||||
discount Float
|
||||
subtotal Float
|
||||
orderNumber String
|
||||
totalAmount Float
|
||||
totalQuantity Float
|
||||
history Json
|
||||
payment Json
|
||||
customer Json
|
||||
delivery Json
|
||||
items Json[]
|
||||
shippingAddress Json
|
||||
// OrderProductItem OrderProductItem[]
|
||||
// OrderHistory OrderHistory[]
|
||||
// OrderDelivery OrderDelivery[]
|
||||
// OrderCustomer OrderCustomer[]
|
||||
// OrderPayment OrderPayment[]
|
||||
// OrderShippingAddress OrderShippingAddress[]
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@ 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 { Blog } from './seeds/blog';
|
||||
@@ -60,6 +61,8 @@ import { userMetaSeed } from './seeds/userMeta';
|
||||
await appLogSeed;
|
||||
await accessLogSeed;
|
||||
|
||||
await partyOrderItemSeed;
|
||||
|
||||
// await Blog;
|
||||
// await Mail;
|
||||
// await File;
|
||||
|
@@ -125,7 +125,7 @@ const generateRatings = () =>
|
||||
const generateImages = () => Array.from({ length: 8 }, (_, index) => _mock.image.event(index));
|
||||
|
||||
const _events = () =>
|
||||
Array.from({ length: 2 }, (_, index) => {
|
||||
Array.from({ length: 5 }, (_, index) => {
|
||||
const reviews = generateReviews();
|
||||
const images = generateImages();
|
||||
const ratings = generateRatings();
|
||||
|
94
03_source/cms_backend/prisma/seeds/partyOrderItem.ts
Normal file
94
03_source/cms_backend/prisma/seeds/partyOrderItem.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { _mock } from './_mock';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const ITEMS = Array.from({ length: 3 }, (_, index) => ({
|
||||
id: _mock.id(index),
|
||||
sku: `16H9UR${index}`,
|
||||
quantity: index + 1,
|
||||
name: _mock.productName(index),
|
||||
coverUrl: _mock.image.product(index),
|
||||
price: _mock.number.price(index),
|
||||
}));
|
||||
|
||||
async function partyOrderItem() {
|
||||
await prisma.partyOrderItem.deleteMany({});
|
||||
|
||||
for (let index = 1; index < 20 + 1; index++) {
|
||||
const shipping = 10;
|
||||
const discount = 10;
|
||||
const taxes = 10;
|
||||
const items = (index % 2 && ITEMS.slice(0, 1)) || (index % 3 && ITEMS.slice(1, 3)) || ITEMS;
|
||||
const totalQuantity = items.reduce((accumulator, item) => accumulator + item.quantity, 0);
|
||||
const subtotal = items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0);
|
||||
const totalAmount = subtotal - shipping - discount + taxes;
|
||||
|
||||
const customer = {
|
||||
id: _mock.id(index),
|
||||
name: _mock.fullName(index),
|
||||
email: _mock.email(index),
|
||||
avatarUrl: _mock.image.avatar(index),
|
||||
ipAddress: '192.158.1.38',
|
||||
};
|
||||
|
||||
const delivery = { shipBy: 'DHL', speedy: 'Standard', trackingNumber: 'SPX037739199373' };
|
||||
|
||||
const history = {
|
||||
orderTime: _mock.time(1),
|
||||
paymentTime: _mock.time(2),
|
||||
deliveryTime: _mock.time(3),
|
||||
completionTime: _mock.time(4),
|
||||
timeline: [
|
||||
{ title: 'Delivery successful', time: _mock.time(1) },
|
||||
{ title: 'Transporting to [2]', time: _mock.time(2) },
|
||||
{ title: 'Transporting to [1]', time: _mock.time(3) },
|
||||
{ title: 'The shipping unit has picked up the goods', time: _mock.time(4) },
|
||||
{ title: 'Order has been created', time: _mock.time(5) },
|
||||
],
|
||||
};
|
||||
|
||||
const temp = await prisma.partyOrderItem.upsert({
|
||||
where: { id: index.toString() },
|
||||
update: {},
|
||||
create: {
|
||||
id: _mock.id(index),
|
||||
orderNumber: `#601${index}`,
|
||||
taxes,
|
||||
items,
|
||||
history,
|
||||
subtotal: items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0),
|
||||
shipping,
|
||||
discount,
|
||||
customer,
|
||||
delivery,
|
||||
totalAmount,
|
||||
totalQuantity,
|
||||
shippingAddress: {
|
||||
fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535',
|
||||
phoneNumber: '365-374-4961',
|
||||
},
|
||||
payment: {
|
||||
//
|
||||
cardType: 'mastercard',
|
||||
cardNumber: '4111 1111 1111 1111',
|
||||
},
|
||||
status: (index % 2 && 'completed') || (index % 3 && 'pending') || (index % 4 && 'cancelled') || 'refunded',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('seed partyOrderItemSeed done');
|
||||
}
|
||||
|
||||
const partyOrderItemSeed = partyOrderItem()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
export { partyOrderItemSeed };
|
@@ -5,13 +5,10 @@ yarn --dev
|
||||
clear
|
||||
|
||||
while true; do
|
||||
yarn db:push
|
||||
|
||||
yarn seed
|
||||
|
||||
yarn db:studio &
|
||||
|
||||
yarn dev
|
||||
npx nodemon --ext ts,tsx,prisma --exec "yarn db:push && yarn seed && yarn dev"
|
||||
# yarn dev
|
||||
|
||||
echo "restarting..."
|
||||
sleep 1
|
||||
|
11
03_source/cms_backend/scripts/01_db_push.sh
Executable file
11
03_source/cms_backend/scripts/01_db_push.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
yarn --dev
|
||||
|
||||
clear
|
||||
|
||||
while true; do
|
||||
npx nodemon --ext prisma --exec "yarn db:push && yarn seed"
|
||||
echo "restarting..."
|
||||
sleep 1
|
||||
done
|
14
03_source/cms_backend/src/app/api/_PROMPTS/clone_srevice.md
Normal file
14
03_source/cms_backend/src/app/api/_PROMPTS/clone_srevice.md
Normal file
@@ -0,0 +1,14 @@
|
||||
Hi,
|
||||
|
||||
i copied from
|
||||
`03_source/cms_backend/src/app/api/party-event`
|
||||
to
|
||||
`03_source/cms_backend/src/app/api/party-order`
|
||||
|
||||
with knowledge in `schema.prisma` file, and take a look into the sibling files in the same directory.
|
||||
|
||||
i want you to update `03_source/cms_backend/src/app/api/party-order` content to handle `party-order` (the purchase order of the party)
|
||||
`party-order.service.ts` is already prepared and you can use it
|
||||
please maintain same format and level of detail when you edit.
|
||||
|
||||
thanks.
|
35
03_source/cms_backend/src/app/api/party-event/_GUIDELINES.md
Normal file
35
03_source/cms_backend/src/app/api/party-event/_GUIDELINES.md
Normal file
@@ -0,0 +1,35 @@
|
||||
<!-- AI: please maintain same format and level of detail when edit this file -->
|
||||
|
||||
# GUIDELINE
|
||||
|
||||
- Party Event API endpoint for managing party events
|
||||
- Handles CRUD operations for party events
|
||||
- Follows single file for single db table/collection pattern
|
||||
|
||||
## `route.ts`
|
||||
|
||||
Handles HTTP methods:
|
||||
|
||||
- `GET` - Retrieve party events
|
||||
- `POST` - Create new party event
|
||||
- `PUT` - Update existing party event
|
||||
- `DELETE` - Remove party event
|
||||
|
||||
## `test.http`
|
||||
|
||||
Contains test requests for:
|
||||
|
||||
- Listing all party events
|
||||
- Creating new party event
|
||||
- Updating existing party event
|
||||
- Deleting party event
|
||||
|
||||
## `../../services/party-event.service.ts`
|
||||
|
||||
Party Event CRUD operations:
|
||||
|
||||
`listPartyEvents` - List all party events with optional filters
|
||||
`getPartyEvent` - Get single party event by ID
|
||||
`createNewPartyEvent` - Create new party event record
|
||||
`updatePartyEvent` - Update existing party event by ID
|
||||
`deletePartyEvent` - Delete party event by ID
|
@@ -0,0 +1,40 @@
|
||||
// src/app/api/party-event/create/route.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// Create new party event in db
|
||||
//
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { isDev } from 'src/constants';
|
||||
import { createEvent } from 'src/app/services/party-event.service';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** **************************************
|
||||
* POST - Create PartyEvent
|
||||
*************************************** */
|
||||
export async function POST(req: NextRequest) {
|
||||
const { partyEventData } = await req.json();
|
||||
|
||||
try {
|
||||
if (isDev) {
|
||||
console.log({ partyEventData });
|
||||
}
|
||||
|
||||
const created = await createEvent(partyEventData);
|
||||
|
||||
if (isDev) {
|
||||
console.log('Event created successfully');
|
||||
}
|
||||
|
||||
return response(created, STATUS.OK);
|
||||
} catch (error) {
|
||||
console.error('Error creating event:', { partyEventData });
|
||||
return handleError('PartyEvent - Create', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
###
|
||||
|
||||
POST http://localhost:7272/api/party-event/create
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"partyEventData": {
|
||||
"title": "Summer Music Festival",
|
||||
"description": "Annual summer music festival featuring local bands and artists",
|
||||
"startDate": "2024-07-15T18:00:00Z",
|
||||
"endDate": "2024-07-15T23:00:00Z",
|
||||
"location": "Central Park, Hong Kong",
|
||||
"coverUrl": "",
|
||||
"images": [
|
||||
"data:image/png;base64,C",
|
||||
"data:image/png;base64,C"
|
||||
],
|
||||
"tags": [
|
||||
"Music",
|
||||
"Festival"
|
||||
],
|
||||
"status": "upcoming",
|
||||
"capacity": 500,
|
||||
"price": 150.00,
|
||||
"organizer": "HK Music Society",
|
||||
"category": "Music",
|
||||
"isFeatured": true,
|
||||
"registrationDeadline": "2024-07-10T00:00:00Z",
|
||||
"requirements": "Age 18+",
|
||||
"schedule": "18:00 Doors open\n19:00 First performance\n21:00 Main act",
|
||||
"speakers": ["DJ Lee", "Band XYZ"],
|
||||
"sponsors": ["HK Radio", "Music Magazine"]
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { deleteEvent } from 'src/app/services/party-event.service';
|
||||
|
||||
/** **************************************
|
||||
* PATCH - Delete PartyEvent
|
||||
*************************************** */
|
||||
export async function PATCH(req: NextRequest) {
|
||||
try {
|
||||
const { partyEventId } = await req.json();
|
||||
|
||||
if (!partyEventId) throw new Error('partyEventId cannot be null');
|
||||
|
||||
await deleteEvent(partyEventId);
|
||||
|
||||
return response({ partyEventId }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyEvent - Delete', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
###
|
||||
|
||||
PATCH http://localhost:7272/api/party-event/delete
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"partyEventId": "e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01"
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
// src/app/api/party-event/details/route.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// Get party event from db by id
|
||||
//
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { logger } from 'src/utils/logger';
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { L_INFO, L_ERROR } from 'src/constants';
|
||||
import { getEvent } from 'src/app/services/party-event.service';
|
||||
import { createAppLog } from 'src/app/services/app-log.service';
|
||||
|
||||
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
**************************************
|
||||
* GET PartyEvent detail
|
||||
***************************************
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||
|
||||
try {
|
||||
const { searchParams } = req.nextUrl;
|
||||
|
||||
// RULES: eventId must exist
|
||||
const partyEventId = searchParams.get('partyEventId');
|
||||
if (!partyEventId) {
|
||||
return response({ message: 'PartyEvent ID is required!' }, STATUS.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// NOTE: eventId confirmed exist, run below
|
||||
const partyEvent = await getEvent(partyEventId);
|
||||
|
||||
if (!partyEvent) {
|
||||
return response({ message: 'PartyEvent not found!' }, STATUS.NOT_FOUND);
|
||||
}
|
||||
|
||||
logger('[PartyEvent] details', partyEvent.id);
|
||||
|
||||
createAppLog(L_INFO, 'Get event detail OK', debug);
|
||||
|
||||
return response({ partyEvent }, STATUS.OK);
|
||||
} catch (error) {
|
||||
createAppLog(L_ERROR, 'event detail error', debug);
|
||||
|
||||
return handleError('PartyEvent - Get details', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
###
|
||||
|
||||
# Get details for a specific party event
|
||||
GET http://localhost:7272/api/party-event/details?partyEventId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01
|
||||
|
||||
###
|
||||
|
||||
# Alternative format with different ID
|
||||
GET http://localhost:7272/api/party-event/details?eventId=evt_987654321
|
@@ -0,0 +1,16 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* GET - helloworld
|
||||
***************************************
|
||||
*/
|
||||
export async function GET(req: NextRequest, res: NextResponse) {
|
||||
try {
|
||||
return response({ helloworld: 'party-event' }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('Helloworld - Get all', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
###
|
||||
GET /api/party-event/helloworld HTTP/1.1
|
||||
Host: localhost:7272
|
||||
|
38
03_source/cms_backend/src/app/api/party-event/list/route.ts
Normal file
38
03_source/cms_backend/src/app/api/party-event/list/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// src/app/api/party-event/list/route.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// List all party events from db
|
||||
//
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { L_INFO, L_ERROR } from 'src/constants';
|
||||
import { createAppLog } from 'src/app/services/app-log.service';
|
||||
import { listEvents } from 'src/app/services/party-event.service';
|
||||
|
||||
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** **************************************
|
||||
* GET - PartyEvents list
|
||||
*************************************** */
|
||||
export async function GET(req: NextRequest) {
|
||||
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||
|
||||
try {
|
||||
const partyEvents = await listEvents();
|
||||
|
||||
createAppLog(L_INFO, 'party-event list ok', {});
|
||||
|
||||
return response({ partyEvents }, STATUS.OK);
|
||||
} catch (error) {
|
||||
createAppLog(L_ERROR, 'party-event list error', debug);
|
||||
|
||||
return handleError('PartyEvent - Get list', error);
|
||||
}
|
||||
}
|
14
03_source/cms_backend/src/app/api/party-event/list/test.http
Normal file
14
03_source/cms_backend/src/app/api/party-event/list/test.http
Normal file
@@ -0,0 +1,14 @@
|
||||
###
|
||||
|
||||
# Basic list all party events
|
||||
GET http://localhost:7272/api/party-event/list
|
||||
|
||||
###
|
||||
|
||||
# List upcoming party events
|
||||
GET http://localhost:7272/api/party-event/list?status=upcoming
|
||||
|
||||
###
|
||||
|
||||
# List featured party events
|
||||
GET http://localhost:7272/api/party-event/list?isFeatured=true
|
75
03_source/cms_backend/src/app/api/party-event/route.ts
Normal file
75
03_source/cms_backend/src/app/api/party-event/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { listEvents, deleteEvent, updateEvent, createEvent } from 'src/app/services/party-event.service';
|
||||
|
||||
/**
|
||||
**************************************
|
||||
* GET - PartyEvent
|
||||
***************************************
|
||||
*/
|
||||
export async function GET(req: NextRequest, res: NextResponse) {
|
||||
try {
|
||||
const events = await listEvents();
|
||||
return response(events, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyEvent - Get list', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* POST - Create PartyEvent
|
||||
***************************************
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const OPERATION = 'PartyEvent - Create';
|
||||
const { data } = await req.json();
|
||||
|
||||
try {
|
||||
const event = await createEvent(data);
|
||||
return response(OPERATION, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError(OPERATION, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* PUT - Update PartyEvent
|
||||
***************************************
|
||||
*/
|
||||
export async function PUT(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const eventId = searchParams.get('eventId');
|
||||
const { data } = await req.json();
|
||||
|
||||
try {
|
||||
if (!eventId) throw new Error('eventId cannot be null');
|
||||
|
||||
const result = await updateEvent(eventId, data);
|
||||
return response(result, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyEvent - Update', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* DELETE - Delete PartyEvent
|
||||
***************************************
|
||||
*/
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const eventId = searchParams.get('eventId');
|
||||
|
||||
try {
|
||||
if (!eventId) throw new Error('eventId cannot be null');
|
||||
|
||||
await deleteEvent(eventId);
|
||||
return response({ success: true }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyEvent - Delete', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { logger } from 'src/utils/logger';
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
import { IEventItem } from '../update/route';
|
||||
|
||||
// import { searchEvents } from 'src/app/services/party-event.service';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** **************************************
|
||||
* GET - Search PartyEvents
|
||||
*************************************** */
|
||||
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 results: IEventItem[] = [];
|
||||
// TODO: search party event not implemented
|
||||
console.log('search party event not implemented');
|
||||
// const results = await searchEvents(query);
|
||||
|
||||
logger('[PartyEvent] search-results', results?.length);
|
||||
|
||||
return response({ results }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyEvent - Get search', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
###
|
||||
|
||||
# Search party events by title
|
||||
GET http://localhost:7272/api/party-event/search?query=Music
|
||||
|
||||
###
|
||||
|
||||
# Search party events by location
|
||||
GET http://localhost:7272/api/party-event/search?query=Central+Park
|
||||
|
||||
###
|
||||
|
||||
# Search party events by tag
|
||||
GET http://localhost:7272/api/party-event/search?query=Festival
|
||||
|
||||
###
|
||||
|
||||
# Combined search with multiple parameters
|
||||
GET http://localhost:7272/api/party-event/search?query=Summer&location=Hong+Kong&category=Music
|
||||
|
||||
###
|
||||
|
||||
# No results expected
|
||||
GET http://localhost:7272/api/party-event/search?query=zzzzzz
|
@@ -0,0 +1,57 @@
|
||||
// src/app/api/party-event/update/route.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// Update party event in db by id
|
||||
//
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { updateEvent } from 'src/app/services/party-event.service';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** **************************************
|
||||
* PUT - Update PartyEvent
|
||||
*************************************** */
|
||||
export async function PUT(req: NextRequest) {
|
||||
const { partyEventData } = await req.json();
|
||||
const { id } = partyEventData;
|
||||
|
||||
if (!id) return response({ message: 'id not found' }, STATUS.ERROR);
|
||||
|
||||
try {
|
||||
const result = await updateEvent(id, partyEventData);
|
||||
|
||||
return response({ result }, STATUS.OK);
|
||||
} catch (error) {
|
||||
console.error('Error updating event:', { partyEventData });
|
||||
return handleError('PartyEvent - Update', error);
|
||||
}
|
||||
}
|
||||
|
||||
export type IEventItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
location: string;
|
||||
coverUrl: string;
|
||||
images: string[];
|
||||
tags: string[];
|
||||
status: string;
|
||||
capacity: number;
|
||||
price: number;
|
||||
organizer: string;
|
||||
category: string;
|
||||
isFeatured: boolean;
|
||||
registrationDeadline: Date;
|
||||
requirements: string[];
|
||||
schedule: string;
|
||||
speakers: string[];
|
||||
sponsors: string[];
|
||||
};
|
@@ -0,0 +1,31 @@
|
||||
###
|
||||
|
||||
PUT http://localhost:7272/api/party-event/update
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"partyEventData": {
|
||||
"id":"e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01",
|
||||
"title": "Summer Music Festival 111",
|
||||
"name": "Summer Music Festival 111",
|
||||
"description": "Annual summer music festival featuring local bands and artists",
|
||||
"startDate": "2024-07-15T18:00:00Z",
|
||||
"endDate": "2024-07-15T23:00:00Z",
|
||||
"location": "Central Park, Hong Kong",
|
||||
"coverUrl": "",
|
||||
"images": [ "data:image/png;base64,C", "data:image/png;base64,C" ],
|
||||
"tags": [ "Music", "Festival" ],
|
||||
"status": "upcoming",
|
||||
"capacity": 500,
|
||||
"price": 150.00,
|
||||
"organizer": "HK Music Society",
|
||||
"category": "Music",
|
||||
"isFeatured": true,
|
||||
"registrationDeadline": "2024-07-10T00:00:00Z",
|
||||
"requirements": "Age 18+",
|
||||
"schedule": "18:00 Doors open\n19:00 First performance\n21:00 Main act",
|
||||
"speakers": ["DJ Lee", "Band XYZ"],
|
||||
"sponsors": ["HK Radio", "Music Magazine"],
|
||||
"reviews":[]
|
||||
}
|
||||
}
|
35
03_source/cms_backend/src/app/api/party-order/_GUIDELINES.md
Normal file
35
03_source/cms_backend/src/app/api/party-order/_GUIDELINES.md
Normal file
@@ -0,0 +1,35 @@
|
||||
<!-- AI: please maintain same format and level of detail when edit this file -->
|
||||
|
||||
# GUIDELINE
|
||||
|
||||
- Party Order API endpoint for managing party orders
|
||||
- Handles CRUD operations for party orders
|
||||
- Follows single file for single db table/collection pattern
|
||||
|
||||
## `route.ts`
|
||||
|
||||
Handles HTTP methods:
|
||||
|
||||
- `GET` - Retrieve party orders
|
||||
- `POST` - Create new party order
|
||||
- `PUT` - Update existing party order
|
||||
- `DELETE` - Remove party order
|
||||
|
||||
## `test.http`
|
||||
|
||||
Contains test requests for:
|
||||
|
||||
- Listing all party orders
|
||||
- Creating new party order
|
||||
- Updating existing party order
|
||||
- Deleting party order
|
||||
|
||||
## `../../services/party-order.service.ts`
|
||||
|
||||
Party Order CRUD operations:
|
||||
|
||||
`listPartyOrders` - List all party orders with optional filters
|
||||
`getPartyOrder` - Get single party order by ID
|
||||
`createNewPartyOrder` - Create new party order record
|
||||
`updatePartyOrder` - Update existing party order by ID
|
||||
`deletePartyOrder` - Delete party order by ID
|
@@ -0,0 +1,40 @@
|
||||
// src/app/api/party-order/create/route.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// Create new party order in db
|
||||
//
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { isDev } from 'src/constants';
|
||||
import { createOrder } from 'src/app/services/party-order.service';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** **************************************
|
||||
* POST - Create PartyOrder
|
||||
*************************************** */
|
||||
export async function POST(req: NextRequest) {
|
||||
const { partyOrderData } = await req.json();
|
||||
|
||||
try {
|
||||
if (isDev) {
|
||||
console.log({ partyOrderData });
|
||||
}
|
||||
|
||||
const created = await createOrder(partyOrderData);
|
||||
|
||||
if (isDev) {
|
||||
console.log('Order created successfully');
|
||||
}
|
||||
|
||||
return response(created, STATUS.OK);
|
||||
} catch (error) {
|
||||
console.error('Error creating order:', { partyOrderData });
|
||||
return handleError('PartyOrder - Create', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
###
|
||||
|
||||
POST http://localhost:7272/api/party-order/create
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"partyOrderData": {
|
||||
"taxes": 15.00,
|
||||
"status": "pending",
|
||||
"shipping": 10.00,
|
||||
"discount": 20.00,
|
||||
"subtotal": 290.00,
|
||||
"orderNumber": "ORD_20231115001",
|
||||
"totalAmount": 300.00,
|
||||
"totalQuantity": 2.0,
|
||||
"history": "{\"actions\":[{\"timestamp\":\"2023-11-15T10:00:00Z\",\"action\":\"order_created\"}]}",
|
||||
"payment": "{\"method\":\"credit_card\",\"status\":\"unpaid\",\"transaction_id\":\"txn_123456\"}",
|
||||
"customer": "{\"name\":\"John Doe\",\"email\":\"john.doe@example.com\",\"phone\":\"+1234567890\"}",
|
||||
"delivery": "{\"method\":\"courier\",\"estimated_delivery\":\"2023-11-18\"}",
|
||||
"items": [
|
||||
{
|
||||
"id": "ticket_001",
|
||||
"name": "General Admission",
|
||||
"quantity": 2,
|
||||
"price": 150.00
|
||||
}
|
||||
],
|
||||
"shippingAddress": "{\"street\":\"123 Main St\",\"city\":\"New York\",\"state\":\"NY\",\"zip\":\"10001\",\"country\":\"USA\"}"
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { deleteOrder } from 'src/app/services/party-order.service';
|
||||
|
||||
/** **************************************
|
||||
* PATCH - Delete PartyOrder
|
||||
*************************************** */
|
||||
export async function PATCH(req: NextRequest) {
|
||||
try {
|
||||
const { partyOrderId } = await req.json();
|
||||
|
||||
if (!partyOrderId) throw new Error('orderId cannot be null');
|
||||
|
||||
await deleteOrder(partyOrderId);
|
||||
|
||||
return response({ partyOrderId }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyOrder - Delete', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
###
|
||||
|
||||
PATCH http://localhost:7272/api/party-order/delete
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"partyOrderId": "e99f09a7-dd88-49d5-b1c8-1daf80c2d7b02"
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
// src/app/api/party-order/details/route.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// Get party order from db by id
|
||||
//
|
||||
// RULES:
|
||||
// Do not modify the format and level of details thanks.
|
||||
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { logger } from 'src/utils/logger';
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { L_INFO, L_ERROR } from 'src/constants';
|
||||
import { getOrder } from 'src/app/services/party-order.service';
|
||||
import { createAppLog } from 'src/app/services/app-log.service';
|
||||
|
||||
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
**************************************
|
||||
* GET PartyOrder detail
|
||||
***************************************
|
||||
*/
|
||||
export async function GET(req: NextRequest) {
|
||||
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||
|
||||
try {
|
||||
const { searchParams } = req.nextUrl;
|
||||
|
||||
// RULES: partyOrderId must exist
|
||||
const partyOrderId = searchParams.get('partyOrderId');
|
||||
if (!partyOrderId) {
|
||||
return response({ message: 'Order ID is required!' }, STATUS.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// NOTE: partyOrderId confirmed exist, run below
|
||||
const partyOrder = await getOrder(partyOrderId);
|
||||
|
||||
if (!partyOrder) {
|
||||
return response({ message: 'Order not found!' }, STATUS.NOT_FOUND);
|
||||
}
|
||||
|
||||
logger('[PartyOrder] details', partyOrder.id);
|
||||
|
||||
createAppLog(L_INFO, 'Get order detail OK', debug);
|
||||
|
||||
return response({ partyOrder }, STATUS.OK);
|
||||
} catch (error) {
|
||||
createAppLog(L_ERROR, 'order detail error', debug);
|
||||
|
||||
return handleError('PartyOrder - Get details', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
###
|
||||
|
||||
# Get details for a specific party order
|
||||
GET http://localhost:7272/api/party-order/details?partyOrderId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b13
|
||||
|
||||
###
|
||||
|
||||
# Alternative format with different ID
|
||||
GET http://localhost:7272/api/party-order/details?id=ord_987654321
|
@@ -0,0 +1,16 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* GET - helloworld
|
||||
***************************************
|
||||
*/
|
||||
export async function GET(req: NextRequest, res: NextResponse) {
|
||||
try {
|
||||
return response({ helloworld: 'party-order' }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('Helloworld - Get all', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,2 @@
|
||||
###
|
||||
GET http://localhost:7272/api/party-order/helloworld
|
38
03_source/cms_backend/src/app/api/party-order/list/route.ts
Normal file
38
03_source/cms_backend/src/app/api/party-order/list/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// src/app/api/party-order/list/route.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// List all party orders from db
|
||||
//
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { L_INFO, L_ERROR } from 'src/constants';
|
||||
import { createAppLog } from 'src/app/services/app-log.service';
|
||||
import { listPartyOrders } from 'src/app/services/party-order.service';
|
||||
|
||||
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** **************************************
|
||||
* GET - PartyOrders list
|
||||
*************************************** */
|
||||
export async function GET(req: NextRequest) {
|
||||
const debug = { 'req.headers': flattenNextjsRequest(req) };
|
||||
|
||||
try {
|
||||
const partyOrders = await listPartyOrders();
|
||||
|
||||
createAppLog(L_INFO, 'party-order list ok', {});
|
||||
|
||||
return response({ partyOrders }, STATUS.OK);
|
||||
} catch (error) {
|
||||
createAppLog(L_ERROR, 'party-order list error', debug);
|
||||
|
||||
return handleError('PartyOrder - Get list', error);
|
||||
}
|
||||
}
|
19
03_source/cms_backend/src/app/api/party-order/list/test.http
Normal file
19
03_source/cms_backend/src/app/api/party-order/list/test.http
Normal file
@@ -0,0 +1,19 @@
|
||||
###
|
||||
|
||||
# Basic list all orders
|
||||
GET http://localhost:7272/api/party-order/list
|
||||
|
||||
###
|
||||
|
||||
# List orders by status
|
||||
GET http://localhost:7272/api/party-order/list?status=completed
|
||||
|
||||
###
|
||||
|
||||
# List orders by user
|
||||
GET http://localhost:7272/api/party-order/list?userId=usr_987654321
|
||||
|
||||
###
|
||||
|
||||
# List orders by payment status
|
||||
GET http://localhost:7272/api/party-order/list?paymentStatus=paid
|
75
03_source/cms_backend/src/app/api/party-order/route.ts
Normal file
75
03_source/cms_backend/src/app/api/party-order/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { listPartyOrders, deleteOrder, updateOrder, createOrder } from 'src/app/services/party-order.service';
|
||||
|
||||
/**
|
||||
**************************************
|
||||
* GET - PartyOrder
|
||||
***************************************
|
||||
*/
|
||||
export async function GET(req: NextRequest, res: NextResponse) {
|
||||
try {
|
||||
const orders = await listPartyOrders();
|
||||
return response(orders, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyOrder - Get list', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* POST - Create PartyOrder
|
||||
***************************************
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const OPERATION = 'PartyOrder - Create';
|
||||
const { data } = await req.json();
|
||||
|
||||
try {
|
||||
const order = await createOrder(data);
|
||||
return response(OPERATION, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError(OPERATION, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* PUT - Update PartyOrder
|
||||
***************************************
|
||||
*/
|
||||
export async function PUT(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const orderId = searchParams.get('orderId');
|
||||
const { data } = await req.json();
|
||||
|
||||
try {
|
||||
if (!orderId) throw new Error('orderId cannot be null');
|
||||
|
||||
const result = await updateOrder(orderId, data);
|
||||
return response(result, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyOrder - Update', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* DELETE - Delete PartyOrder
|
||||
***************************************
|
||||
*/
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const orderId = searchParams.get('orderId');
|
||||
|
||||
try {
|
||||
if (!orderId) throw new Error('orderId cannot be null');
|
||||
|
||||
await deleteOrder(orderId);
|
||||
return response({ success: true }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyOrder - Delete', error);
|
||||
}
|
||||
}
|
@@ -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 type { IOrderItem } from '../update/route';
|
||||
|
||||
// import { searchEvents } from 'src/app/services/party-event.service';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** **************************************
|
||||
* GET - Search PartyEvents
|
||||
*************************************** */
|
||||
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 results: IOrderItem[] = [];
|
||||
// TODO: search party event not implemented
|
||||
console.log('search party event not implemented');
|
||||
// const results = await searchEvents(query);
|
||||
|
||||
logger('[PartyEvent] search-results', results?.length);
|
||||
|
||||
return response({ results }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('PartyEvent - Get search', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
###
|
||||
|
||||
# Search party events by title
|
||||
GET http://localhost:7272/api/party-event/search?query=Music
|
||||
|
||||
###
|
||||
|
||||
# Search party events by location
|
||||
GET http://localhost:7272/api/party-event/search?query=Central+Park
|
||||
|
||||
###
|
||||
|
||||
# Search party events by tag
|
||||
GET http://localhost:7272/api/party-event/search?query=Festival
|
||||
|
||||
###
|
||||
|
||||
# Combined search with multiple parameters
|
||||
GET http://localhost:7272/api/party-event/search?query=Summer&location=Hong+Kong&category=Music
|
||||
|
||||
###
|
||||
|
||||
# No results expected
|
||||
GET http://localhost:7272/api/party-event/search?query=zzzzzz
|
@@ -0,0 +1,51 @@
|
||||
// src/app/api/party-order/update/route.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// Update party order in db by id
|
||||
//
|
||||
// RULES:
|
||||
// T.B.A.
|
||||
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
import { updateOrder } from 'src/app/services/party-order.service';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/** **************************************
|
||||
* PUT - Update PartyOrder
|
||||
*************************************** */
|
||||
export async function PUT(req: NextRequest) {
|
||||
const { orderData } = await req.json();
|
||||
const { id } = orderData;
|
||||
|
||||
if (!id) return response({ message: 'id not found' }, STATUS.ERROR);
|
||||
|
||||
try {
|
||||
const result = await updateOrder(id, orderData);
|
||||
|
||||
return response({ result }, STATUS.OK);
|
||||
} catch (error) {
|
||||
console.error('Error updating order:', { orderData });
|
||||
return handleError('PartyOrder - Update', error);
|
||||
}
|
||||
}
|
||||
|
||||
export type IOrderItem = {
|
||||
id: string;
|
||||
eventId: string;
|
||||
userId: string;
|
||||
status: string;
|
||||
paymentStatus: string;
|
||||
totalAmount: number;
|
||||
items: {
|
||||
id: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
@@ -0,0 +1,51 @@
|
||||
###
|
||||
|
||||
PUT http://localhost:7272/api/party-order/update
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"orderData": {
|
||||
"id":"e99f09a7-dd88-49d5-b1c8-1daf80c2d7b02",
|
||||
"taxes": 10.50,
|
||||
"status": "completed",
|
||||
"shipping": 5.00,
|
||||
"discount": 20.00,
|
||||
"subtotal": 290.00,
|
||||
"orderNumber": "ORD-2023-001",
|
||||
"totalAmount": 300.00,
|
||||
"totalQuantity": 2,
|
||||
"history": {
|
||||
"payment": "2023-01-01",
|
||||
"status_changes": [
|
||||
"pending",
|
||||
"paid"
|
||||
]
|
||||
},
|
||||
"payment": {
|
||||
"method": "credit_card",
|
||||
"card_last4": "4321"
|
||||
},
|
||||
"customer": {
|
||||
"name": "John Doe",
|
||||
"email": "john@example.com"
|
||||
},
|
||||
"delivery": {
|
||||
"method": "express",
|
||||
"tracking_number": "TRK123456"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"id": "ticket_001",
|
||||
"name": "General Admission",
|
||||
"quantity": 2,
|
||||
"price": 150.00,
|
||||
"status": "used"
|
||||
}
|
||||
],
|
||||
"shippingAddress": {
|
||||
"street": "123 Main St",
|
||||
"city": "New York",
|
||||
"zip": "10001"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { STATUS, response, handleError } from 'src/utils/response';
|
||||
|
||||
/**
|
||||
***************************************
|
||||
* GET - helloworld
|
||||
***************************************
|
||||
*/
|
||||
export async function GET(req: NextRequest, res: NextResponse) {
|
||||
try {
|
||||
return response({ helloworld: 'product' }, STATUS.OK);
|
||||
} catch (error) {
|
||||
return handleError('Helloworld - Get all', error);
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
###
|
||||
GET /api/product/helloworld HTTP/1.1
|
||||
Host: localhost:7272
|
||||
|
@@ -1,7 +1,13 @@
|
||||
with knowledge in schema.prisma file,
|
||||
please refer the below helloworld example `helloworld.service.ts`
|
||||
and create `user.service.ts` to cover user record
|
||||
Hi,
|
||||
|
||||
thanks
|
||||
i copied from
|
||||
`03_source/cms_backend/src/app/services/party-event.service.ts`
|
||||
to
|
||||
`03_source/cms_backend/src/app/services/party-order.service.ts`
|
||||
|
||||
`/home/logic/_wsl_workspace/001_github_ws/HKSingleParty-ws/HKSingleParty/03_source/cms_backend/src/app/services/helloworld.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.
|
||||
|
||||
thanks.
|
||||
|
139
03_source/cms_backend/src/app/services/party-event.service.ts
Normal file
139
03_source/cms_backend/src/app/services/party-event.service.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
// src/app/services/party-event.service.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// - Service for handling EventItem (PartyEvent) Record
|
||||
//
|
||||
|
||||
import type { EventItem } from '@prisma/client';
|
||||
|
||||
import prisma from '../lib/prisma';
|
||||
|
||||
type CreateEvent = {
|
||||
name: string;
|
||||
title: string;
|
||||
eventDate: Date;
|
||||
location: string;
|
||||
duration_m: number;
|
||||
ageBottom?: number;
|
||||
ageTop?: number;
|
||||
currency?: string;
|
||||
price?: number;
|
||||
priceSale?: number;
|
||||
coverUrl?: string;
|
||||
images?: string[];
|
||||
description?: string;
|
||||
subDescription?: string;
|
||||
publish?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
joinMembers?: any[];
|
||||
};
|
||||
|
||||
type UpdateEvent = {
|
||||
name?: string;
|
||||
title?: string;
|
||||
eventDate?: Date;
|
||||
location?: string;
|
||||
duration_m?: number;
|
||||
ageBottom?: number;
|
||||
ageTop?: number;
|
||||
currency?: string;
|
||||
price?: number;
|
||||
priceSale?: number;
|
||||
coverUrl?: string;
|
||||
images?: string[];
|
||||
description?: string;
|
||||
subDescription?: string;
|
||||
publish?: string;
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
joinMembers?: any[];
|
||||
};
|
||||
|
||||
async function listEvents(): Promise<EventItem[]> {
|
||||
return prisma.eventItem.findMany({
|
||||
include: { reviews: true },
|
||||
});
|
||||
}
|
||||
|
||||
async function getEvent(eventId: string): Promise<EventItem | null> {
|
||||
return prisma.eventItem.findUnique({
|
||||
where: { id: eventId },
|
||||
include: { reviews: true },
|
||||
});
|
||||
}
|
||||
|
||||
async function getEventByNameOrTitle(searchText: string): Promise<EventItem[] | null> {
|
||||
return prisma.eventItem.findMany({
|
||||
where: {
|
||||
OR: [{ name: { contains: searchText, mode: 'insensitive' } }, { title: { contains: searchText, mode: 'insensitive' } }],
|
||||
},
|
||||
include: { reviews: true },
|
||||
});
|
||||
}
|
||||
|
||||
async function createEvent(eventData: any) {
|
||||
return await prisma.eventItem.create({ data: eventData });
|
||||
}
|
||||
|
||||
async function updateEvent(eventId: string, updateForm: any) {
|
||||
return prisma.eventItem.update({
|
||||
where: { id: eventId },
|
||||
data: {
|
||||
available: updateForm.available,
|
||||
category: updateForm.category,
|
||||
code: updateForm.code,
|
||||
colors: updateForm.colors,
|
||||
coverUrl: updateForm.coverUrl,
|
||||
description: updateForm.description,
|
||||
gender: updateForm.gender,
|
||||
images: updateForm.images,
|
||||
inventoryType: updateForm.inventoryType,
|
||||
name: updateForm.name,
|
||||
newLabel: updateForm.newLabel,
|
||||
price: updateForm.price,
|
||||
priceSale: updateForm.priceSale,
|
||||
publish: updateForm.publish,
|
||||
quantity: updateForm.quantity,
|
||||
ratings: updateForm.ratings,
|
||||
saleLabel: updateForm.saleLabel,
|
||||
sizes: updateForm.sizes,
|
||||
sku: updateForm.sku,
|
||||
subDescription: updateForm.subDescription,
|
||||
tags: updateForm.tags,
|
||||
taxes: updateForm.taxes,
|
||||
totalRatings: updateForm.totalRatings,
|
||||
totalReviews: updateForm.totalReviews,
|
||||
totalSold: updateForm.totalSold,
|
||||
//
|
||||
ageBottom: updateForm.ageBottom,
|
||||
ageTop: updateForm.ageTop,
|
||||
avatar: updateForm.avatar,
|
||||
currency: updateForm.currency,
|
||||
capacity: updateForm.capacity,
|
||||
duration_m: updateForm.duration_m,
|
||||
endDate: updateForm.endDate,
|
||||
eventDate: updateForm.eventDate,
|
||||
isFeatured: updateForm.isFeatured,
|
||||
joinMembers: updateForm.joinMembers,
|
||||
location: updateForm.location,
|
||||
organizer: updateForm.organizer,
|
||||
registrationDeadline: updateForm.registrationDeadline,
|
||||
requirements: updateForm.requirements,
|
||||
schedule: updateForm.schedule,
|
||||
speakers: updateForm.speakers,
|
||||
sponsors: updateForm.sponsors,
|
||||
startDate: updateForm.startDate,
|
||||
status: updateForm.status,
|
||||
title: updateForm.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteEvent(eventId: string) {
|
||||
return prisma.eventItem.delete({
|
||||
where: { id: eventId },
|
||||
});
|
||||
}
|
||||
|
||||
export { getEvent, listEvents, createEvent, updateEvent, deleteEvent, getEventByNameOrTitle, type CreateEvent, type UpdateEvent };
|
@@ -0,0 +1,66 @@
|
||||
// src/app/services/party-order.service.ts
|
||||
//
|
||||
// PURPOSE:
|
||||
// - Service for handling PartyOrderItem Record
|
||||
//
|
||||
|
||||
import type { PartyOrderItem } from '@prisma/client';
|
||||
|
||||
import prisma from '../lib/prisma';
|
||||
|
||||
type CreateOrder = {
|
||||
status: string;
|
||||
taxes: number;
|
||||
shipping: number;
|
||||
discount: number;
|
||||
items: any[];
|
||||
customer: any;
|
||||
payment: any;
|
||||
delivery: any;
|
||||
shippingAddress: any;
|
||||
billingAddress: any;
|
||||
};
|
||||
|
||||
type UpdateOrder = {
|
||||
status?: string;
|
||||
taxes?: number;
|
||||
shipping?: number;
|
||||
discount?: number;
|
||||
items?: any[];
|
||||
customer?: any;
|
||||
payment?: any;
|
||||
delivery?: any;
|
||||
shippingAddress?: any;
|
||||
billingAddress?: any;
|
||||
};
|
||||
|
||||
async function listPartyOrders(): Promise<PartyOrderItem[]> {
|
||||
return prisma.partyOrderItem.findMany();
|
||||
}
|
||||
|
||||
async function getPartyOrder(partyOrderId: string): Promise<PartyOrderItem | null> {
|
||||
return prisma.partyOrderItem.findUnique({
|
||||
where: { id: partyOrderId },
|
||||
});
|
||||
}
|
||||
|
||||
async function createOrder(orderData: any) {
|
||||
return prisma.partyOrderItem.create({
|
||||
data: orderData,
|
||||
});
|
||||
}
|
||||
|
||||
async function updateOrder(orderId: string, updateData: UpdateOrder) {
|
||||
return prisma.partyOrderItem.update({
|
||||
where: { id: orderId },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteOrder(orderId: string) {
|
||||
return prisma.partyOrderItem.delete({
|
||||
where: { id: orderId },
|
||||
});
|
||||
}
|
||||
|
||||
export { getPartyOrder as getOrder, createOrder, updateOrder, deleteOrder, listPartyOrders, type CreateOrder, type UpdateOrder };
|
@@ -1021,7 +1021,7 @@ ansi-regex@^5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||
|
||||
ansi-styles@^4.1.0:
|
||||
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
|
||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||
@@ -1239,7 +1239,7 @@ caniuse-lite@^1.0.30001579:
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz"
|
||||
integrity sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==
|
||||
|
||||
chalk@^4.0.0:
|
||||
chalk@^4.0.0, chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
@@ -1257,6 +1257,15 @@ client-only@0.0.1:
|
||||
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
|
||||
@@ -1284,6 +1293,19 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
concurrently@^9.1.2:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c"
|
||||
integrity sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
lodash "^4.17.21"
|
||||
rxjs "^7.8.1"
|
||||
shell-quote "^1.8.1"
|
||||
supports-color "^8.1.1"
|
||||
tree-kill "^1.2.2"
|
||||
yargs "^17.7.2"
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
@@ -1614,6 +1636,11 @@ esbuild@~0.25.0:
|
||||
"@esbuild/win32-ia32" "0.25.5"
|
||||
"@esbuild/win32-x64" "0.25.5"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
|
||||
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
|
||||
|
||||
escape-string-regexp@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
|
||||
@@ -1945,6 +1972,11 @@ gauge@^3.0.0:
|
||||
strip-ansi "^6.0.1"
|
||||
wide-align "^1.1.2"
|
||||
|
||||
get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||
@@ -2985,6 +3017,11 @@ regexp.prototype.flags@^1.5.3:
|
||||
gopd "^1.2.0"
|
||||
set-function-name "^2.0.2"
|
||||
|
||||
require-directory@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
|
||||
@@ -3049,6 +3086,13 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rxjs@^7.8.1:
|
||||
version "7.8.2"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b"
|
||||
integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
safe-array-concat@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz"
|
||||
@@ -3152,6 +3196,11 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-quote@^1.8.1:
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b"
|
||||
integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
|
||||
|
||||
side-channel-list@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz"
|
||||
@@ -3222,7 +3271,7 @@ streamsearch@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz"
|
||||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
|
||||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -3297,7 +3346,7 @@ string_decoder@^1.1.1:
|
||||
dependencies:
|
||||
safe-buffer "~5.2.0"
|
||||
|
||||
strip-ansi@^6.0.1:
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -3333,6 +3382,13 @@ supports-color@^7.1.0:
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-color@^8.1.1:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
|
||||
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-preserve-symlinks-flag@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
|
||||
@@ -3370,6 +3426,11 @@ tr46@~0.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
tree-kill@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
ts-api-utils@^2.0.1:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
|
||||
@@ -3404,7 +3465,7 @@ tsconfig-paths@^3.15.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^2.4.0:
|
||||
tslib@^2.1.0, tslib@^2.4.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
@@ -3612,6 +3673,15 @@ word-wrap@^1.2.5:
|
||||
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
@@ -3622,6 +3692,11 @@ xtend@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
@@ -3632,6 +3707,24 @@ yaml@^1.10.0:
|
||||
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
||||
yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
|
||||
|
||||
yargs@^17.7.2:
|
||||
version "17.7.2"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
|
||||
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
|
||||
dependencies:
|
||||
cliui "^8.0.1"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz"
|
||||
|
@@ -6,6 +6,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:check": "yarn tsc:w",
|
||||
"dev": "vite",
|
||||
"start": "vite preview",
|
||||
"build": "tsc && vite build",
|
||||
@@ -22,7 +23,7 @@
|
||||
"re:build-npm": "npm run clean && npm install && npm run build",
|
||||
"tsc:dev": "yarn dev & yarn tsc:watch",
|
||||
"tsc:print": "npx tsc --showConfig",
|
||||
"tsc:w": "npx nodemon --delay 3 --ext ts,tsx --exec \"yarn tsc\"",
|
||||
"tsc:w": "npx nodemon --delay 1 --ext ts,tsx --exec \"yarn tsc\"",
|
||||
"tsc:watch": "tsc --noEmit --watch",
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
|
70
03_source/frontend/src/_mock/_party-event.ts
Normal file
70
03_source/frontend/src/_mock/_party-event.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export const PARTY_EVENT_GENDER_OPTIONS = [
|
||||
{ label: 'Men', value: 'Men' },
|
||||
{ label: 'Women', value: 'Women' },
|
||||
{ label: 'Kids', value: 'Kids' },
|
||||
];
|
||||
|
||||
export const PARTY_EVENT_CATEGORY_OPTIONS = ['Shose', 'Apparel', 'Accessories'];
|
||||
|
||||
export const PARTY_EVENT_RATING_OPTIONS = ['up4Star', 'up3Star', 'up2Star', 'up1Star'];
|
||||
|
||||
export const PARTY_EVENT_COLOR_OPTIONS = [
|
||||
'#FF4842',
|
||||
'#1890FF',
|
||||
'#FFC0CB',
|
||||
'#00AB55',
|
||||
'#FFC107',
|
||||
'#7F00FF',
|
||||
'#000000',
|
||||
'#FFFFFF',
|
||||
];
|
||||
|
||||
export const PARTY_EVENT_COLOR_NAME_OPTIONS = [
|
||||
{ value: '#FF4842', label: 'Red' },
|
||||
{ value: '#1890FF', label: 'Blue' },
|
||||
{ value: '#FFC0CB', label: 'Pink' },
|
||||
{ value: '#00AB55', label: 'Green' },
|
||||
{ value: '#FFC107', label: 'Yellow' },
|
||||
{ value: '#7F00FF', label: 'Violet' },
|
||||
{ value: '#000000', label: 'Black' },
|
||||
{ value: '#FFFFFF', label: 'White' },
|
||||
];
|
||||
|
||||
export const PARTY_EVENT_SIZE_OPTIONS = [
|
||||
{ value: '7', label: '7' },
|
||||
{ value: '8', label: '8' },
|
||||
{ value: '8.5', label: '8.5' },
|
||||
{ value: '9', label: '9' },
|
||||
{ value: '9.5', label: '9.5' },
|
||||
{ value: '10', label: '10' },
|
||||
{ value: '10.5', label: '10.5' },
|
||||
{ value: '11', label: '11' },
|
||||
{ value: '11.5', label: '11.5' },
|
||||
{ value: '12', label: '12' },
|
||||
{ value: '13', label: '13' },
|
||||
];
|
||||
|
||||
export const PARTY_EVENT_STOCK_OPTIONS = [
|
||||
{ value: 'in stock', label: 'In stock' },
|
||||
{ value: 'low stock', label: 'Low stock' },
|
||||
{ value: 'out of stock', label: 'Out of stock' },
|
||||
];
|
||||
|
||||
// not used due to i18n
|
||||
export const PARTY_EVENT_PUBLISH_OPTIONS = [
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
];
|
||||
|
||||
export const PARTY_EVENT_SORT_OPTIONS = [
|
||||
{ value: 'featured', label: 'Featured' },
|
||||
{ value: 'newest', label: 'Newest' },
|
||||
{ value: 'priceDesc', label: 'Price: High - Low' },
|
||||
{ value: 'priceAsc', label: 'Price: Low - High' },
|
||||
];
|
||||
|
||||
export const PARTY_EVENT_CATEGORY_GROUP_OPTIONS = [
|
||||
{ group: 'Clothing', classify: ['Shirts', 'T-shirts', 'Jeans', 'Leather', 'Accessories'] },
|
||||
{ group: 'Tailored', classify: ['Suits', 'Blazers', 'Trousers', 'Waistcoats', 'Apparel'] },
|
||||
{ group: 'Accessories', classify: ['Shoes', 'Backpacks and bags', 'Bracelets', 'Face masks'] },
|
||||
];
|
89
03_source/frontend/src/_mock/_party-order.ts
Normal file
89
03_source/frontend/src/_mock/_party-order.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { _mock } from './_mock';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export const PARTY_ORDER_STATUS_OPTIONS = [
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'completed', label: 'Completed' },
|
||||
{ value: 'cancelled', label: 'Cancelled' },
|
||||
{ value: 'refunded', label: 'Refunded' },
|
||||
];
|
||||
|
||||
const ITEMS = Array.from({ length: 3 }, (_, index) => ({
|
||||
id: _mock.id(index),
|
||||
sku: `16H9UR${index}`,
|
||||
quantity: index + 1,
|
||||
name: _mock.productName(index),
|
||||
coverUrl: _mock.image.product(index),
|
||||
price: _mock.number.price(index),
|
||||
}));
|
||||
|
||||
export const _party_orders = Array.from({ length: 20 }, (_, index) => {
|
||||
const shipping = 10;
|
||||
|
||||
const discount = 10;
|
||||
|
||||
const taxes = 10;
|
||||
|
||||
const items = (index % 2 && ITEMS.slice(0, 1)) || (index % 3 && ITEMS.slice(1, 3)) || ITEMS;
|
||||
|
||||
const totalQuantity = items.reduce((accumulator, item) => accumulator + item.quantity, 0);
|
||||
|
||||
const subtotal = items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0);
|
||||
|
||||
const totalAmount = subtotal - shipping - discount + taxes;
|
||||
|
||||
const customer = {
|
||||
id: _mock.id(index),
|
||||
name: _mock.fullName(index),
|
||||
email: _mock.email(index),
|
||||
avatarUrl: _mock.image.avatar(index),
|
||||
ipAddress: '192.158.1.38',
|
||||
};
|
||||
|
||||
const delivery = { shipBy: 'DHL', speedy: 'Standard', trackingNumber: 'SPX037739199373' };
|
||||
|
||||
const history = {
|
||||
orderTime: _mock.time(1),
|
||||
paymentTime: _mock.time(2),
|
||||
deliveryTime: _mock.time(3),
|
||||
completionTime: _mock.time(4),
|
||||
timeline: [
|
||||
{ title: 'Delivery successful', time: _mock.time(1) },
|
||||
{ title: 'Transporting to [2]', time: _mock.time(2) },
|
||||
{ title: 'Transporting to [1]', time: _mock.time(3) },
|
||||
{ title: 'The shipping unit has picked up the goods', time: _mock.time(4) },
|
||||
{ title: 'Order has been created', time: _mock.time(5) },
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
id: _mock.id(index),
|
||||
orderNumber: `#601${index}`,
|
||||
createdAt: _mock.time(index),
|
||||
taxes,
|
||||
items,
|
||||
history,
|
||||
subtotal,
|
||||
shipping,
|
||||
discount,
|
||||
customer,
|
||||
delivery,
|
||||
totalAmount,
|
||||
totalQuantity,
|
||||
shippingAddress: {
|
||||
fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535',
|
||||
phoneNumber: '365-374-4961',
|
||||
},
|
||||
payment: {
|
||||
//
|
||||
cardType: 'mastercard',
|
||||
cardNumber: '**** **** **** 5678',
|
||||
},
|
||||
status:
|
||||
(index % 2 && 'completed') ||
|
||||
(index % 3 && 'pending') ||
|
||||
(index % 4 && 'cancelled') ||
|
||||
'refunded',
|
||||
};
|
||||
});
|
@@ -23,3 +23,7 @@ export * from './_product';
|
||||
export * from './_overview';
|
||||
|
||||
export * from './_calendar';
|
||||
|
||||
export * from './_party-event';
|
||||
|
||||
export * from './_party-order';
|
||||
|
178
03_source/frontend/src/actions/party-event.ts
Normal file
178
03_source/frontend/src/actions/party-event.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
// src/actions/product.ts
|
||||
//
|
||||
import { useMemo } from 'react';
|
||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const swrOptions: SWRConfiguration = {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type PartyEventsData = {
|
||||
partyEvents: IPartyEventItem[];
|
||||
};
|
||||
|
||||
export function useGetPartyEvents() {
|
||||
const url = endpoints.partyEvent.list;
|
||||
|
||||
const { data, isLoading, error, isValidating } = useSWR<PartyEventsData>(
|
||||
url,
|
||||
fetcher,
|
||||
swrOptions
|
||||
);
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
partyEvents: data?.partyEvents || [],
|
||||
partyEventsLoading: isLoading,
|
||||
partyEventsError: error,
|
||||
partyEventsValidating: isValidating,
|
||||
partyEventsEmpty: !isLoading && !isValidating && !data?.partyEvents.length,
|
||||
}),
|
||||
[data?.partyEvents, error, isLoading, isValidating]
|
||||
);
|
||||
|
||||
return memoizedValue;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type PartyEventData = {
|
||||
partyEvent: IPartyEventItem;
|
||||
};
|
||||
|
||||
export function useGetPartyEvent(partyEventId: string) {
|
||||
const url = partyEventId ? [endpoints.partyEvent.details, { params: { partyEventId } }] : '';
|
||||
|
||||
const { data, isLoading, error, isValidating } = useSWR<PartyEventData>(url, fetcher, swrOptions);
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
partyEvent: data?.partyEvent,
|
||||
partyEventLoading: isLoading,
|
||||
partyEventError: error,
|
||||
partyEventValidating: isValidating,
|
||||
mutate,
|
||||
}),
|
||||
[data?.partyEvent, error, isLoading, isValidating]
|
||||
);
|
||||
|
||||
return memoizedValue;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type SearchResultsData = {
|
||||
results: IPartyEventItem[];
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export async function createPartyEvent(partyEventData: IPartyEventItem) {
|
||||
/**
|
||||
* Work on server
|
||||
*/
|
||||
const data = { partyEventData };
|
||||
const {
|
||||
data: { id },
|
||||
} = await axiosInstance.post(endpoints.partyEvent.create, data);
|
||||
|
||||
/**
|
||||
* Work in local
|
||||
*/
|
||||
mutate(
|
||||
endpoints.partyEvent.list,
|
||||
(currentData: any) => {
|
||||
const currentPartyEvents: IPartyEventItem[] = currentData?.partyEvents;
|
||||
|
||||
const partyEvents = [...currentPartyEvents, { ...partyEventData, id }];
|
||||
|
||||
return { ...currentData, partyEvents };
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export async function updatePartyEvent(partyEventData: Partial<IPartyEventItem>) {
|
||||
/**
|
||||
* Work on server
|
||||
*/
|
||||
const data = { partyEventData };
|
||||
await axiosInstance.put(endpoints.partyEvent.update, data);
|
||||
|
||||
/**
|
||||
* Work in local
|
||||
*/
|
||||
|
||||
mutate(
|
||||
endpoints.partyEvent.list,
|
||||
(currentData: any) => {
|
||||
const currentPartyEvents: IPartyEventItem[] = currentData?.partyEvents;
|
||||
|
||||
const partyEvents = currentPartyEvents.map((partyEvent) =>
|
||||
partyEvent.id === partyEventData.id ? { ...partyEvent, ...partyEventData } : partyEvent
|
||||
);
|
||||
|
||||
return { ...currentData, partyEvents };
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export async function deletePartyEvent(partyEventId: string) {
|
||||
/**
|
||||
* Work on server
|
||||
*/
|
||||
const data = { partyEventId };
|
||||
await axiosInstance.patch(endpoints.partyEvent.delete, data);
|
||||
|
||||
/**
|
||||
* Work in local
|
||||
*/
|
||||
|
||||
mutate(
|
||||
endpoints.partyEvent.list,
|
||||
(currentData: any) => {
|
||||
const currentProducts: IPartyEventItem[] = currentData?.partyEvents;
|
||||
|
||||
const partyEvents = currentProducts.filter((partyEvent) => partyEvent.id !== partyEventId);
|
||||
|
||||
return { ...currentData, partyEvents };
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
208
03_source/frontend/src/actions/party-order.ts
Normal file
208
03_source/frontend/src/actions/party-order.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
// src/actions/party-order.ts
|
||||
//
|
||||
import { useMemo } from 'react';
|
||||
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
|
||||
import type { IPartyOrderItem } from 'src/types/party-order';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const swrOptions: SWRConfiguration = {
|
||||
revalidateIfStale: false,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type PartyOrdersData = {
|
||||
partyOrders: IPartyOrderItem[];
|
||||
};
|
||||
|
||||
export function useGetPartyOrders() {
|
||||
const url = endpoints.partyOrder.list;
|
||||
|
||||
const { data, isLoading, error, isValidating } = useSWR<PartyOrdersData>(
|
||||
url,
|
||||
fetcher,
|
||||
swrOptions
|
||||
);
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
partyOrders: data?.partyOrders || [],
|
||||
partyOrdersLoading: isLoading,
|
||||
partyOrdersError: error,
|
||||
partyOrdersValidating: isValidating,
|
||||
partyOrdersEmpty: !isLoading && !isValidating && !data?.partyOrders.length,
|
||||
}),
|
||||
[data?.partyOrders, error, isLoading, isValidating]
|
||||
);
|
||||
|
||||
return memoizedValue;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type PartyOrderData = {
|
||||
partyOrder: IPartyOrderItem;
|
||||
};
|
||||
|
||||
export function useGetPartyOrder(partyOrderId: string) {
|
||||
const url = partyOrderId ? [endpoints.partyOrder.details, { params: { partyOrderId } }] : '';
|
||||
|
||||
const { data, isLoading, error, isValidating } = useSWR<PartyOrderData>(url, fetcher, swrOptions);
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
partyOrder: data?.partyOrder,
|
||||
partyOrderLoading: isLoading,
|
||||
partyOrderError: error,
|
||||
partyOrderValidating: isValidating,
|
||||
}),
|
||||
[data?.partyOrder, error, isLoading, isValidating]
|
||||
);
|
||||
|
||||
return memoizedValue;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type SearchResultsData = {
|
||||
results: IPartyOrderItem[];
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export async function createPartyOrder(partyOrderData: IPartyOrderItem) {
|
||||
/**
|
||||
* Work on server
|
||||
*/
|
||||
const data = { partyOrderData };
|
||||
const {
|
||||
data: { id },
|
||||
} = await axiosInstance.post(endpoints.partyOrder.create, data);
|
||||
|
||||
/**
|
||||
* Work in local
|
||||
*/
|
||||
mutate(
|
||||
endpoints.partyOrder.list,
|
||||
(currentData: any) => {
|
||||
const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
|
||||
|
||||
const partyOrders = [...currentPartyOrders, { ...partyOrderData, id }];
|
||||
|
||||
return { ...currentData, partyOrders };
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export async function updatePartyOrder(partyOrderData: Partial<IPartyOrderItem>) {
|
||||
/**
|
||||
* Work on server
|
||||
*/
|
||||
const data = { partyOrderData };
|
||||
await axiosInstance.put(endpoints.partyOrder.update, data);
|
||||
|
||||
/**
|
||||
* Work in local
|
||||
*/
|
||||
|
||||
mutate(
|
||||
endpoints.partyOrder.list,
|
||||
(currentData: any) => {
|
||||
const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
|
||||
|
||||
const partyOrders = currentPartyOrders.map((partyOrder) =>
|
||||
partyOrder.id === partyOrderData.id ? { ...partyOrder, ...partyOrderData } : partyOrder
|
||||
);
|
||||
|
||||
return { ...currentData, partyOrders };
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export async function deletePartyOrder(partyOrderId: string) {
|
||||
/**
|
||||
* Work on server
|
||||
*/
|
||||
const data = { partyOrderId };
|
||||
await axiosInstance.patch(endpoints.partyOrder.delete, data);
|
||||
|
||||
/**
|
||||
* Work in local
|
||||
*/
|
||||
|
||||
mutate(
|
||||
endpoints.partyOrder.list,
|
||||
(currentData: any) => {
|
||||
const currentProducts: IPartyOrderItem[] = currentData?.partyOrders;
|
||||
|
||||
const partyOrders = currentProducts.filter((partyOrder) => partyOrder.id !== partyOrderId);
|
||||
|
||||
return { ...currentData, partyOrders };
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// TODO: implement partyOrder changeStatus with url below
|
||||
// const url = endpoints.order.changeStatus(orderId);
|
||||
export async function changeStatus(partyOrderData: any, dummy: any) {
|
||||
return true;
|
||||
// /**
|
||||
// * Work on server
|
||||
// */
|
||||
// const data = { partyOrderData };
|
||||
// await axiosInstance.put(endpoints.partyOrder.update, data);
|
||||
|
||||
// /**
|
||||
// * Work in local
|
||||
// */
|
||||
|
||||
// mutate(
|
||||
// endpoints.partyOrder.list,
|
||||
// (currentData: any) => {
|
||||
// const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
|
||||
|
||||
// const partyOrders = currentPartyOrders.map((partyOrder) =>
|
||||
// partyOrder.id === partyOrderData.id ? { ...partyOrder, ...partyOrderData } : partyOrder
|
||||
// );
|
||||
|
||||
// return { ...currentData, partyOrders };
|
||||
// },
|
||||
// false
|
||||
// );
|
||||
}
|
@@ -91,6 +91,26 @@ export const navData: NavSectionProps['data'] = [
|
||||
{ title: 'Account', path: paths.dashboard.user.account },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'party-event',
|
||||
path: paths.dashboard.partyEvent.root,
|
||||
icon: ICONS.product,
|
||||
children: [
|
||||
{ title: 'List', path: paths.dashboard.partyEvent.root },
|
||||
{ title: 'Details', path: paths.dashboard.partyEvent.demo.details },
|
||||
{ title: 'Create', path: paths.dashboard.partyEvent.new },
|
||||
{ title: 'Edit', path: paths.dashboard.partyEvent.demo.edit },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'party-order',
|
||||
path: paths.dashboard.partyOrder.root,
|
||||
icon: ICONS.order,
|
||||
children: [
|
||||
{ title: 'List', path: paths.dashboard.partyOrder.root },
|
||||
{ title: 'Details', path: paths.dashboard.partyOrder.demo.details },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Product',
|
||||
path: paths.dashboard.product.root,
|
||||
|
@@ -84,4 +84,26 @@ export const endpoints = {
|
||||
changeStatus: (invoiceId: string) => `/api/invoice/changeStatus?invoiceId=${invoiceId}`,
|
||||
search: '/api/invoice/search',
|
||||
},
|
||||
//
|
||||
//
|
||||
//
|
||||
partyEvent: {
|
||||
list: '/api/party-event/list',
|
||||
details: '/api/party-event/details',
|
||||
search: '/api/party-event/search',
|
||||
create: '/api/party-event/create',
|
||||
update: '/api/party-event/update',
|
||||
delete: '/api/party-event/delete',
|
||||
},
|
||||
partyOrder: {
|
||||
create: '/api/party-order/create',
|
||||
delete: '/api/party-order/delete',
|
||||
list: '/api/party-order/list',
|
||||
profile: '/api/party-order/profile',
|
||||
update: '/api/party-order/update',
|
||||
settings: '/api/party-order/settings',
|
||||
details: '/api/party-order/details',
|
||||
changeStatus: (partyOrderId: string) =>
|
||||
`/api/party-order/changeStatus?partyOrderId=${partyOrderId}`,
|
||||
},
|
||||
};
|
||||
|
@@ -0,0 +1,28 @@
|
||||
// src/pages/dashboard/party-event/details.tsx
|
||||
|
||||
import { useGetPartyEvent } from 'src/actions/party-event';
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { useParams } from 'src/routes/hooks';
|
||||
import { PartyEventDetailsView } from 'src/sections/party-event/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `PartyEvent details | Dashboard - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
const { id = '' } = useParams();
|
||||
|
||||
const { partyEvent, partyEventLoading, partyEventError } = useGetPartyEvent(id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<PartyEventDetailsView
|
||||
partyEvent={partyEvent}
|
||||
loading={partyEventLoading}
|
||||
error={partyEventError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
22
03_source/frontend/src/pages/dashboard/party-event/edit.tsx
Normal file
22
03_source/frontend/src/pages/dashboard/party-event/edit.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useGetPartyEvent } from 'src/actions/party-event';
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { useParams } from 'src/routes/hooks';
|
||||
import { PartyEventEditView } from 'src/sections/party-event/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `PartyEvent edit | Dashboard - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
const { id = '' } = useParams();
|
||||
|
||||
const { partyEvent } = useGetPartyEvent(id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<PartyEventEditView partyEvent={partyEvent} />
|
||||
</>
|
||||
);
|
||||
}
|
18
03_source/frontend/src/pages/dashboard/party-event/list.tsx
Normal file
18
03_source/frontend/src/pages/dashboard/party-event/list.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// src/pages/dashboard/party-event/list.tsx
|
||||
//
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { PartyEventListView } from 'src/sections/party-event/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `PartyEvent list | Dashboard - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<PartyEventListView />
|
||||
</>
|
||||
);
|
||||
}
|
16
03_source/frontend/src/pages/dashboard/party-event/new.tsx
Normal file
16
03_source/frontend/src/pages/dashboard/party-event/new.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { PartyEventCreateView } from 'src/sections/party-event/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `Create a new party-event | Dashboard - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<PartyEventCreateView />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
// src/pages/dashboard/order/details.tsx
|
||||
//
|
||||
import { useGetPartyOrder } from 'src/actions/party-order';
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { useParams } from 'src/routes/hooks';
|
||||
import { PartyOrderDetailsView } from 'src/sections/party-order/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `Order details | Dashboard - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
const { id = '' } = useParams();
|
||||
|
||||
// const currentOrder = _orders.find((order) => order.id === id);
|
||||
// TODO: error handling
|
||||
const { partyOrder, partyOrderLoading, partyOrderError } = useGetPartyOrder(id);
|
||||
|
||||
if (!partyOrder) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<PartyOrderDetailsView partyOrder={partyOrder} />
|
||||
</>
|
||||
);
|
||||
}
|
18
03_source/frontend/src/pages/dashboard/party-order/list.tsx
Normal file
18
03_source/frontend/src/pages/dashboard/party-order/list.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// src/pages/dashboard/party-order/list.tsx
|
||||
//
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { PartyOrderListView } from 'src/sections/party-order/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `Order list | Dashboard - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<PartyOrderListView />
|
||||
</>
|
||||
);
|
||||
}
|
16
03_source/frontend/src/pages/party-event/checkout.tsx
Normal file
16
03_source/frontend/src/pages/party-event/checkout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { CheckoutView } from 'src/sections/checkout/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `Checkout - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<CheckoutView />
|
||||
</>
|
||||
);
|
||||
}
|
22
03_source/frontend/src/pages/party-event/details.tsx
Normal file
22
03_source/frontend/src/pages/party-event/details.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useGetProduct } from 'src/actions/product';
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { useParams } from 'src/routes/hooks';
|
||||
import { ProductShopDetailsView } from 'src/sections/product/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `Product details - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
const { id = '' } = useParams();
|
||||
|
||||
const { product, productLoading, productError } = useGetProduct(id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<ProductShopDetailsView product={product} loading={productLoading} error={productError} />
|
||||
</>
|
||||
);
|
||||
}
|
5
03_source/frontend/src/pages/party-event/helloworld.tsx
Normal file
5
03_source/frontend/src/pages/party-event/helloworld.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
function Helloworld() {
|
||||
return <>helloworld</>;
|
||||
}
|
||||
|
||||
export default Helloworld;
|
19
03_source/frontend/src/pages/party-event/list.tsx
Normal file
19
03_source/frontend/src/pages/party-event/list.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useGetPartyEvents } from 'src/actions/party-event';
|
||||
import { CONFIG } from 'src/global-config';
|
||||
import { PartyEventShopView } from 'src/sections/party-event/view';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const metadata = { title: `Product shop - ${CONFIG.appName}` };
|
||||
|
||||
export default function Page() {
|
||||
const { partyEvents, partyEventsLoading } = useGetPartyEvents();
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{metadata.title}</title>
|
||||
|
||||
<PartyEventShopView products={partyEvents} loading={partyEventsLoading} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -87,6 +87,13 @@ export const paths = {
|
||||
verify: `${ROOTS.AUTH_DEMO}/centered/verify`,
|
||||
},
|
||||
},
|
||||
//
|
||||
partyEvent: {
|
||||
root: `/party-event`,
|
||||
checkout: `/party-event/checkout`,
|
||||
details: (id: string) => `/party-event/${id}`,
|
||||
demo: { details: `/party-event/${MOCK_ID}` },
|
||||
},
|
||||
// DASHBOARD
|
||||
dashboard: {
|
||||
root: ROOTS.DASHBOARD,
|
||||
@@ -171,5 +178,23 @@ export const paths = {
|
||||
edit: `${ROOTS.DASHBOARD}/tour/${MOCK_ID}/edit`,
|
||||
},
|
||||
},
|
||||
//
|
||||
//
|
||||
//
|
||||
partyEvent: {
|
||||
root: `${ROOTS.DASHBOARD}/party-event`,
|
||||
new: `${ROOTS.DASHBOARD}/party-event/new`,
|
||||
details: (id: string) => `${ROOTS.DASHBOARD}/party-event/${id}`,
|
||||
edit: (id: string) => `${ROOTS.DASHBOARD}/party-event/${id}/edit`,
|
||||
demo: {
|
||||
details: `${ROOTS.DASHBOARD}/party-event/${MOCK_ID}`,
|
||||
edit: `${ROOTS.DASHBOARD}/party-event/${MOCK_ID}/edit`,
|
||||
},
|
||||
},
|
||||
partyOrder: {
|
||||
root: `${ROOTS.DASHBOARD}/party-order`,
|
||||
details: (id: string) => `${ROOTS.DASHBOARD}/party-order/${id}`,
|
||||
demo: { details: `${ROOTS.DASHBOARD}/party-order/${MOCK_ID}` },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@@ -1,3 +1,5 @@
|
||||
// src/routes/sections/dashboard.tsx
|
||||
//
|
||||
import { lazy, Suspense } from 'react';
|
||||
import type { RouteObject } from 'react-router';
|
||||
import { Outlet } from 'react-router';
|
||||
@@ -75,6 +77,16 @@ const PermissionDeniedPage = lazy(() => import('src/pages/dashboard/permission')
|
||||
const ParamsPage = lazy(() => import('src/pages/dashboard/params'));
|
||||
const BlankPage = lazy(() => import('src/pages/dashboard/blank'));
|
||||
|
||||
// PartyEvent
|
||||
const PartyEventDetailsPage = lazy(() => import('src/pages/dashboard/party-event/details'));
|
||||
const PartyEventListPage = lazy(() => import('src/pages/dashboard/party-event/list'));
|
||||
const PartyEventCreatePage = lazy(() => import('src/pages/dashboard/party-event/new'));
|
||||
const PartyEventEditPage = lazy(() => import('src/pages/dashboard/party-event/edit'));
|
||||
|
||||
// PartyOrder
|
||||
const PartyOrderListPage = lazy(() => import('src/pages/dashboard/party-order/list'));
|
||||
const PartyOrderDetailsPage = lazy(() => import('src/pages/dashboard/party-order/details'));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function SuspenseOutlet() {
|
||||
@@ -198,6 +210,25 @@ export const dashboardRoutes: RouteObject[] = [
|
||||
{ path: 'permission', element: <PermissionDeniedPage /> },
|
||||
{ path: 'params', element: <ParamsPage /> },
|
||||
{ path: 'blank', element: <BlankPage /> },
|
||||
//
|
||||
{
|
||||
path: 'party-event',
|
||||
children: [
|
||||
{ index: true, element: <PartyEventListPage /> },
|
||||
{ path: 'list', element: <PartyEventListPage /> },
|
||||
{ path: ':id', element: <PartyEventDetailsPage /> },
|
||||
{ path: 'new', element: <PartyEventCreatePage /> },
|
||||
{ path: ':id/edit', element: <PartyEventEditPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'party-order',
|
||||
children: [
|
||||
{ index: true, element: <PartyOrderListPage /> },
|
||||
{ path: 'list', element: <PartyOrderListPage /> },
|
||||
{ path: ':id', element: <PartyOrderDetailsPage /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -27,6 +27,10 @@ const Page403 = lazy(() => import('src/pages/error/403'));
|
||||
const Page404 = lazy(() => import('src/pages/error/404'));
|
||||
// Blank
|
||||
const BlankPage = lazy(() => import('src/pages/blank'));
|
||||
// PartyEvent
|
||||
const PartyEventDetailsPage = lazy(() => import('src/pages/dashboard/party-event/details'));
|
||||
const PartyEventListPage = lazy(() => import('src/pages/dashboard/party-event/list'));
|
||||
const PartyEventCheckoutPage = lazy(() => import('src/pages/party-event/checkout'));
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
@@ -58,6 +62,15 @@ export const mainRoutes: RouteObject[] = [
|
||||
{ path: 'checkout', element: <ProductCheckoutPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'party-event',
|
||||
children: [
|
||||
{ index: true, element: <PartyEventListPage /> },
|
||||
{ path: 'list', element: <PartyEventListPage /> },
|
||||
{ path: ':id', element: <PartyEventDetailsPage /> },
|
||||
{ path: 'checkout', element: <PartyEventCheckoutPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'post',
|
||||
children: [
|
||||
|
@@ -13,7 +13,7 @@ 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 { _orders, ORDER_STATUS_OPTIONS } from 'src/_mock';
|
||||
import { _party_orders, ORDER_STATUS_OPTIONS } from 'src/_mock';
|
||||
import { deleteOrder, useGetOrders } from 'src/actions/order';
|
||||
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||
import { ConfirmDialog } from 'src/components/custom-dialog';
|
||||
|
45
03_source/frontend/src/sections/party-event/cart-icon.tsx
Normal file
45
03_source/frontend/src/sections/party-event/cart-icon.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import Badge from '@mui/material/Badge';
|
||||
import type { BoxProps } from '@mui/material/Box';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = BoxProps<'a'> & {
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
export function CartIcon({ totalItems, sx, ...other }: Props) {
|
||||
return (
|
||||
<Box
|
||||
component={RouterLink}
|
||||
href={paths.product.checkout}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
right: 0,
|
||||
top: 112,
|
||||
zIndex: 999,
|
||||
display: 'flex',
|
||||
cursor: 'pointer',
|
||||
position: 'fixed',
|
||||
color: 'text.primary',
|
||||
borderTopLeftRadius: 16,
|
||||
borderBottomLeftRadius: 16,
|
||||
bgcolor: 'background.paper',
|
||||
padding: theme.spacing(1, 3, 1, 2),
|
||||
boxShadow: theme.vars.customShadows.dropdown,
|
||||
transition: theme.transitions.create(['opacity']),
|
||||
'&:hover': { opacity: 0.72 },
|
||||
}),
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...other}
|
||||
>
|
||||
<Badge showZero badgeContent={totalItems} color="error" max={99}>
|
||||
<Iconify icon="solar:cart-3-bold" width={24} />
|
||||
</Badge>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
Carousel,
|
||||
CarouselArrowNumberButtons,
|
||||
CarouselThumb,
|
||||
CarouselThumbs,
|
||||
useCarousel,
|
||||
} from 'src/components/carousel';
|
||||
import { Image } from 'src/components/image';
|
||||
import { Lightbox, useLightBox } from 'src/components/lightbox';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
images?: IPartyEventItem['images'];
|
||||
};
|
||||
|
||||
export function ProductDetailsCarousel({ images }: Props) {
|
||||
const carousel = useCarousel({ thumbs: { slidesToShow: 'auto' } });
|
||||
|
||||
const slides = images?.map((img) => ({ src: img })) || [];
|
||||
|
||||
const lightbox = useLightBox(slides);
|
||||
|
||||
useEffect(() => {
|
||||
if (lightbox.open) {
|
||||
carousel.mainApi?.scrollTo(lightbox.selected, true);
|
||||
}
|
||||
}, [carousel.mainApi, lightbox.open, lightbox.selected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Box sx={{ mb: 2.5, position: 'relative' }}>
|
||||
<CarouselArrowNumberButtons
|
||||
{...carousel.arrows}
|
||||
options={carousel.options}
|
||||
totalSlides={carousel.dots.dotCount}
|
||||
selectedIndex={carousel.dots.selectedIndex + 1}
|
||||
sx={{ right: 16, bottom: 16, position: 'absolute' }}
|
||||
/>
|
||||
|
||||
<Carousel carousel={carousel} sx={{ borderRadius: 2 }}>
|
||||
{slides.map((slide) => (
|
||||
<Image
|
||||
key={slide.src}
|
||||
alt={slide.src}
|
||||
src={slide.src}
|
||||
ratio="1/1"
|
||||
onClick={() => lightbox.onOpen(slide.src)}
|
||||
sx={{ cursor: 'zoom-in', minWidth: 320 }}
|
||||
/>
|
||||
))}
|
||||
</Carousel>
|
||||
</Box>
|
||||
|
||||
<CarouselThumbs
|
||||
ref={carousel.thumbs.thumbsRef}
|
||||
options={carousel.options?.thumbs}
|
||||
slotProps={{ disableMask: true }}
|
||||
sx={{ width: 360 }}
|
||||
>
|
||||
{slides.map((item, index) => (
|
||||
<CarouselThumb
|
||||
key={item.src}
|
||||
index={index}
|
||||
src={item.src}
|
||||
selected={index === carousel.thumbs.selectedIndex}
|
||||
onClick={() => carousel.thumbs.onClickThumb(index)}
|
||||
/>
|
||||
))}
|
||||
</CarouselThumbs>
|
||||
</div>
|
||||
|
||||
<Lightbox
|
||||
index={lightbox.selected}
|
||||
slides={slides}
|
||||
open={lightbox.open}
|
||||
close={lightbox.onClose}
|
||||
onGetCurrentIndex={(index) => lightbox.setSelected(index)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Markdown } from 'src/components/markdown';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
description?: string;
|
||||
sx?: SxProps<Theme>;
|
||||
};
|
||||
|
||||
export function ProductDetailsDescription({ description, sx }: Props) {
|
||||
return (
|
||||
<Markdown
|
||||
children={description}
|
||||
sx={[
|
||||
() => ({
|
||||
p: 3,
|
||||
'& p, li, ol, table': { typography: 'body2' },
|
||||
'& table': {
|
||||
mt: 2,
|
||||
maxWidth: 640,
|
||||
'& td': { px: 2 },
|
||||
'& td:first-of-type': { color: 'text.secondary' },
|
||||
'tbody tr:nth-of-type(odd)': { bgcolor: 'transparent' },
|
||||
},
|
||||
}),
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -0,0 +1,125 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Rating from '@mui/material/Rating';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { sumBy } from 'es-toolkit';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IProductReview } from 'src/types/party-event';
|
||||
import { fShortenNumber } from 'src/utils/format-number';
|
||||
import { ProductReviewList } from './party-event-review-list';
|
||||
import { ProductReviewNewForm } from './party-event-review-new-form';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
totalRatings?: number;
|
||||
totalReviews?: number;
|
||||
reviews?: IProductReview[];
|
||||
ratings?: { name: string; starCount: number; reviewCount: number }[];
|
||||
};
|
||||
|
||||
export function ProductDetailsReview({
|
||||
totalRatings,
|
||||
totalReviews,
|
||||
ratings = [],
|
||||
reviews = [],
|
||||
}: Props) {
|
||||
const review = useBoolean();
|
||||
|
||||
const total = sumBy(ratings, (star) => star.starCount);
|
||||
|
||||
const renderSummary = () => (
|
||||
<Stack spacing={1} sx={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Typography variant="subtitle2">Average rating</Typography>
|
||||
|
||||
<Typography variant="h2">
|
||||
{totalRatings}
|
||||
/5
|
||||
</Typography>
|
||||
|
||||
<Rating readOnly value={totalRatings} precision={0.1} />
|
||||
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
({fShortenNumber(totalReviews)} reviews)
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const renderProgress = () => (
|
||||
<Stack
|
||||
spacing={1.5}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
py: 5,
|
||||
px: { xs: 3, md: 5 },
|
||||
borderLeft: { md: `dashed 1px ${theme.vars.palette.divider}` },
|
||||
borderRight: { md: `dashed 1px ${theme.vars.palette.divider}` },
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{ratings
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map((rating) => (
|
||||
<Box key={rating.name} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Typography variant="subtitle2" component="span" sx={{ width: 42 }}>
|
||||
{rating.name}
|
||||
</Typography>
|
||||
|
||||
<LinearProgress
|
||||
color="inherit"
|
||||
variant="determinate"
|
||||
value={(rating.starCount / total) * 100}
|
||||
sx={{ mx: 2, flexGrow: 1 }}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
sx={{ minWidth: 48, color: 'text.secondary' }}
|
||||
>
|
||||
{fShortenNumber(rating.reviewCount)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const renderReviewButton = () => (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Button
|
||||
size="large"
|
||||
variant="soft"
|
||||
color="inherit"
|
||||
onClick={review.onTrue}
|
||||
startIcon={<Iconify icon="solar:pen-bold" />}
|
||||
>
|
||||
Write your review
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
py: { xs: 5, md: 0 },
|
||||
gridTemplateColumns: { xs: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' },
|
||||
}}
|
||||
>
|
||||
{renderSummary()}
|
||||
{renderProgress()}
|
||||
{renderReviewButton()}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
<ProductReviewList reviews={reviews} />
|
||||
<ProductReviewNewForm open={review.value} onClose={review.onFalse} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,321 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import { formHelperTextClasses } from '@mui/material/FormHelperText';
|
||||
import Link, { linkClasses } from '@mui/material/Link';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Rating from '@mui/material/Rating';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useCallback } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { ColorPicker } from 'src/components/color-utils';
|
||||
import { Field, Form } from 'src/components/hook-form';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Label } from 'src/components/label';
|
||||
import { NumberInput } from 'src/components/number-input';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { CheckoutContextValue } from 'src/types/checkout';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
import { fCurrency, fShortenNumber } from 'src/utils/format-number';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
partyEvent: IPartyEventItem;
|
||||
disableActions?: boolean;
|
||||
items?: CheckoutContextValue['state']['items'];
|
||||
onAddToCart?: CheckoutContextValue['onAddToCart'];
|
||||
};
|
||||
|
||||
export function PartyEventDetailsSummary({
|
||||
items,
|
||||
partyEvent,
|
||||
onAddToCart,
|
||||
disableActions,
|
||||
...other
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
sizes,
|
||||
price,
|
||||
colors,
|
||||
coverUrl,
|
||||
newLabel,
|
||||
available,
|
||||
priceSale,
|
||||
saleLabel,
|
||||
totalRatings,
|
||||
totalReviews,
|
||||
inventoryType,
|
||||
subDescription,
|
||||
} = partyEvent;
|
||||
|
||||
const existProduct = !!items?.length && items.map((item) => item.id).includes(id);
|
||||
|
||||
const isMaxQuantity =
|
||||
!!items?.length &&
|
||||
items.filter((item) => item.id === id).map((item) => item.quantity)[0] >= available;
|
||||
|
||||
const defaultValues = {
|
||||
id,
|
||||
name,
|
||||
coverUrl,
|
||||
available,
|
||||
price,
|
||||
colors: colors[0],
|
||||
size: sizes[4],
|
||||
quantity: available < 1 ? 0 : 1,
|
||||
};
|
||||
|
||||
const methods = useForm<typeof defaultValues>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const { watch, control, setValue, handleSubmit } = methods;
|
||||
|
||||
const values = watch();
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
console.info('DATA', JSON.stringify(data, null, 2));
|
||||
|
||||
try {
|
||||
if (!existProduct) {
|
||||
onAddToCart?.({ ...data, colors: [values.colors] });
|
||||
}
|
||||
router.push(paths.product.checkout);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const handleAddCart = useCallback(() => {
|
||||
try {
|
||||
onAddToCart?.({
|
||||
...values,
|
||||
colors: [values.colors],
|
||||
subtotal: values.price * values.quantity,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [onAddToCart, values]);
|
||||
|
||||
const renderPrice = () => (
|
||||
<Box sx={{ typography: 'h5' }}>
|
||||
{priceSale && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ color: 'text.disabled', textDecoration: 'line-through', mr: 0.5 }}
|
||||
>
|
||||
{fCurrency(priceSale)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{fCurrency(price)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderShare = () => (
|
||||
<Box
|
||||
sx={{
|
||||
gap: 3,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
[`& .${linkClasses.root}`]: {
|
||||
gap: 1,
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
color: 'text.secondary',
|
||||
typography: 'subtitle2',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Link>
|
||||
<Iconify icon="mingcute:add-line" width={16} />
|
||||
Compare
|
||||
</Link>
|
||||
|
||||
<Link>
|
||||
<Iconify icon="solar:heart-bold" width={16} />
|
||||
Favorite
|
||||
</Link>
|
||||
|
||||
<Link>
|
||||
<Iconify icon="solar:share-bold" width={16} />
|
||||
Share
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderColorOptions = () => (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Typography variant="subtitle2" sx={{ flexGrow: 1 }}>
|
||||
Color
|
||||
</Typography>
|
||||
|
||||
<Controller
|
||||
name="colors"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<ColorPicker
|
||||
options={colors}
|
||||
value={field.value}
|
||||
onChange={(color) => field.onChange(color as string)}
|
||||
limit={4}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderSizeOptions = () => (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Typography variant="subtitle2" sx={{ flexGrow: 1 }}>
|
||||
Size
|
||||
</Typography>
|
||||
|
||||
<Field.Select
|
||||
name="size"
|
||||
size="small"
|
||||
helperText={
|
||||
<Link underline="always" color="text.primary">
|
||||
Size chart
|
||||
</Link>
|
||||
}
|
||||
sx={{
|
||||
maxWidth: 88,
|
||||
[`& .${formHelperTextClasses.root}`]: { mx: 0, mt: 1, textAlign: 'right' },
|
||||
}}
|
||||
>
|
||||
{sizes.map((size) => (
|
||||
<MenuItem key={size} value={size}>
|
||||
{size}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Field.Select>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderQuantity = () => (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Typography variant="subtitle2" sx={{ flexGrow: 1 }}>
|
||||
Quantity
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={1}>
|
||||
<NumberInput
|
||||
hideDivider
|
||||
value={values.quantity}
|
||||
onChange={(event, quantity: number) => setValue('quantity', quantity)}
|
||||
max={available}
|
||||
sx={{ maxWidth: 112 }}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{ textAlign: 'right', color: 'text.secondary' }}
|
||||
>
|
||||
Available: {available}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderActions = () => (
|
||||
<Box sx={{ gap: 2, display: 'flex' }}>
|
||||
<Button
|
||||
fullWidth
|
||||
disabled={isMaxQuantity || disableActions}
|
||||
size="large"
|
||||
color="warning"
|
||||
variant="contained"
|
||||
startIcon={<Iconify icon="solar:cart-plus-bold" width={24} />}
|
||||
onClick={handleAddCart}
|
||||
sx={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Add to cart
|
||||
</Button>
|
||||
|
||||
<Button fullWidth size="large" type="submit" variant="contained" disabled={disableActions}>
|
||||
Buy now
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderSubDescription = () => (
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{subDescription}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const renderRating = () => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
typography: 'body2',
|
||||
alignItems: 'center',
|
||||
color: 'text.disabled',
|
||||
}}
|
||||
>
|
||||
<Rating size="small" value={totalRatings} precision={0.1} readOnly sx={{ mr: 1 }} />
|
||||
{`(${fShortenNumber(totalReviews)} reviews)`}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderLabels = () =>
|
||||
(newLabel.enabled || saleLabel.enabled) && (
|
||||
<Box sx={{ gap: 1, display: 'flex', alignItems: 'center' }}>
|
||||
{newLabel.enabled && <Label color="info">{newLabel.content}</Label>}
|
||||
{saleLabel.enabled && <Label color="error">{saleLabel.content}</Label>}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderInventoryType = () => (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
typography: 'overline',
|
||||
color:
|
||||
(inventoryType === 'out of stock' && 'error.main') ||
|
||||
(inventoryType === 'low stock' && 'warning.main') ||
|
||||
'success.main',
|
||||
}}
|
||||
>
|
||||
{inventoryType}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
<Stack spacing={3} sx={{ pt: 3 }} {...other}>
|
||||
<Stack spacing={2} alignItems="flex-start">
|
||||
{renderLabels()}
|
||||
{renderInventoryType()}
|
||||
|
||||
<Typography variant="h5">{name}</Typography>
|
||||
|
||||
{renderRating()}
|
||||
{renderPrice()}
|
||||
{renderSubDescription()}
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
|
||||
{renderColorOptions()}
|
||||
{renderSizeOptions()}
|
||||
{renderQuantity()}
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
|
||||
{renderActions()}
|
||||
{renderShare()}
|
||||
</Stack>
|
||||
</Form>
|
||||
);
|
||||
}
|
@@ -0,0 +1,113 @@
|
||||
import type { BoxProps } from '@mui/material/Box';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { usePopover } from 'minimal-shared/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomPopover } from 'src/components/custom-popover';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = BoxProps & {
|
||||
backHref: string;
|
||||
editHref: string;
|
||||
liveHref: string;
|
||||
publish: string;
|
||||
onChangePublish: (newValue: string) => void;
|
||||
publishOptions: { value: string; label: string }[];
|
||||
};
|
||||
|
||||
export function ProductDetailsToolbar({
|
||||
sx,
|
||||
publish,
|
||||
backHref,
|
||||
editHref,
|
||||
liveHref,
|
||||
publishOptions,
|
||||
onChangePublish,
|
||||
...other
|
||||
}: Props) {
|
||||
const menuActions = usePopover();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const renderMenuActions = () => (
|
||||
<CustomPopover
|
||||
open={menuActions.open}
|
||||
anchorEl={menuActions.anchorEl}
|
||||
onClose={menuActions.onClose}
|
||||
slotProps={{ arrow: { placement: 'top-right' } }}
|
||||
>
|
||||
<MenuList>
|
||||
{publishOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
selected={option.value === publish}
|
||||
onClick={() => {
|
||||
menuActions.onClose();
|
||||
onChangePublish(option.value);
|
||||
}}
|
||||
>
|
||||
{option.value === 'published' && <Iconify icon="eva:cloud-upload-fill" />}
|
||||
{option.value === 'draft' && <Iconify icon="solar:file-text-bold" />}
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</CustomPopover>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={[
|
||||
{ gap: 1.5, display: 'flex', mb: { xs: 3, md: 5 } },
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...other}
|
||||
>
|
||||
<Button
|
||||
component={RouterLink}
|
||||
href={backHref}
|
||||
startIcon={<Iconify icon="eva:arrow-ios-back-fill" width={16} />}
|
||||
>
|
||||
{t('back')}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
{publish === 'published' && (
|
||||
<Tooltip title="Go Live">
|
||||
<IconButton component={RouterLink} href={liveHref}>
|
||||
<Iconify icon="eva:external-link-fill" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip title="Edit">
|
||||
<IconButton component={RouterLink} href={editHref}>
|
||||
<Iconify icon="solar:pen-bold" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
color="inherit"
|
||||
variant="contained"
|
||||
loading={!publish}
|
||||
loadingIndicator="Loading…"
|
||||
endIcon={<Iconify icon="eva:arrow-ios-downward-fill" />}
|
||||
onClick={menuActions.onOpen}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{publish}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{renderMenuActions()}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,334 @@
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { inputBaseClasses } from '@mui/material/InputBase';
|
||||
import Radio from '@mui/material/Radio';
|
||||
import Rating from '@mui/material/Rating';
|
||||
import Slider from '@mui/material/Slider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||
import { useCallback } from 'react';
|
||||
import { ColorPicker } from 'src/components/color-utils';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { NumberInput } from 'src/components/number-input';
|
||||
import { Scrollbar } from 'src/components/scrollbar';
|
||||
import type { IProductFilters } from 'src/types/party-event';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
canReset: boolean;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
filters: UseSetStateReturn<IProductFilters>;
|
||||
options: {
|
||||
colors: string[];
|
||||
ratings: string[];
|
||||
categories: string[];
|
||||
genders: { value: string; label: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
const MAX_AMOUNT = 200;
|
||||
|
||||
const marksLabel = Array.from({ length: 21 }, (_, index) => {
|
||||
const value = index * 10;
|
||||
const firstValue = index === 0 ? `$${value}` : `${value}`;
|
||||
|
||||
return {
|
||||
value,
|
||||
label: index % 4 ? '' : firstValue,
|
||||
};
|
||||
});
|
||||
|
||||
export function ProductFiltersDrawer({ open, onOpen, onClose, canReset, filters, options }: Props) {
|
||||
const { state: currentFilters, setState: updateFilters, resetState: resetFilters } = filters;
|
||||
|
||||
const handleFilterGender = useCallback(
|
||||
(newValue: string) => {
|
||||
const checked = currentFilters.gender.includes(newValue)
|
||||
? currentFilters.gender.filter((value) => value !== newValue)
|
||||
: [...currentFilters.gender, newValue];
|
||||
|
||||
updateFilters({ gender: checked });
|
||||
},
|
||||
[updateFilters, currentFilters.gender]
|
||||
);
|
||||
|
||||
const handleFilterCategory = useCallback(
|
||||
(newValue: string) => {
|
||||
updateFilters({ category: newValue });
|
||||
},
|
||||
[updateFilters]
|
||||
);
|
||||
|
||||
const handleFilterColors = useCallback(
|
||||
(newValue: string[]) => {
|
||||
updateFilters({ colors: newValue });
|
||||
},
|
||||
[updateFilters]
|
||||
);
|
||||
|
||||
const handleFilterPriceRange = useCallback(
|
||||
(event: Event, newValue: number | number[]) => {
|
||||
updateFilters({ priceRange: newValue as number[] });
|
||||
},
|
||||
[updateFilters]
|
||||
);
|
||||
|
||||
const handleFilterRating = useCallback(
|
||||
(newValue: string) => {
|
||||
updateFilters({ rating: newValue });
|
||||
},
|
||||
[updateFilters]
|
||||
);
|
||||
|
||||
const renderHead = () => (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
py: 2,
|
||||
pr: 1,
|
||||
pl: 2.5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
||||
Filters
|
||||
</Typography>
|
||||
|
||||
<Tooltip title="Reset">
|
||||
<IconButton onClick={() => resetFilters()}>
|
||||
<Badge color="error" variant="dot" invisible={!canReset}>
|
||||
<Iconify icon="solar:restart-bold" />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<IconButton onClick={onClose}>
|
||||
<Iconify icon="mingcute:close-line" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
</>
|
||||
);
|
||||
|
||||
const renderGender = () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||
Gender
|
||||
</Typography>
|
||||
{options.genders.map((option) => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={currentFilters.gender.includes(option.label)}
|
||||
onClick={() => handleFilterGender(option.label)}
|
||||
slotProps={{ input: { id: `${option.value}-checkbox` } }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderCategory = () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||
Category
|
||||
</Typography>
|
||||
{options.categories.map((option) => (
|
||||
<FormControlLabel
|
||||
key={option}
|
||||
label={option}
|
||||
control={
|
||||
<Radio
|
||||
checked={option === currentFilters.category}
|
||||
onClick={() => handleFilterCategory(option)}
|
||||
slotProps={{ input: { id: `${option}-radio` } }}
|
||||
/>
|
||||
}
|
||||
sx={{ ...(option === 'all' && { textTransform: 'capitalize' }) }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderColor = () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
||||
Color
|
||||
</Typography>
|
||||
|
||||
<ColorPicker
|
||||
options={options.colors}
|
||||
value={currentFilters.colors}
|
||||
onChange={(colors) => handleFilterColors(colors as string[])}
|
||||
limit={6}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderPrice = () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle2">Price</Typography>
|
||||
|
||||
<Box sx={{ my: 2, gap: 5, display: 'flex' }}>
|
||||
<InputRange type="min" value={currentFilters.priceRange} onChange={updateFilters} />
|
||||
<InputRange type="max" value={currentFilters.priceRange} onChange={updateFilters} />
|
||||
</Box>
|
||||
|
||||
<Slider
|
||||
value={currentFilters.priceRange}
|
||||
onChange={handleFilterPriceRange}
|
||||
step={10}
|
||||
min={0}
|
||||
max={MAX_AMOUNT}
|
||||
marks={marksLabel}
|
||||
getAriaValueText={(value) => `$${value}`}
|
||||
valueLabelFormat={(value) => `$${value}`}
|
||||
sx={{ alignSelf: 'center', width: `calc(100% - 24px)` }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderRating = () => (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 2 }}>
|
||||
Rating
|
||||
</Typography>
|
||||
|
||||
{options.ratings.map((item, index) => (
|
||||
<Box
|
||||
key={item}
|
||||
onClick={() => handleFilterRating(item)}
|
||||
sx={{
|
||||
mb: 1,
|
||||
gap: 1,
|
||||
ml: -1,
|
||||
p: 0.5,
|
||||
display: 'flex',
|
||||
borderRadius: 1,
|
||||
cursor: 'pointer',
|
||||
typography: 'body2',
|
||||
alignItems: 'center',
|
||||
'&:hover': { opacity: 0.48 },
|
||||
...(currentFilters.rating === item && { bgcolor: 'action.selected' }),
|
||||
}}
|
||||
>
|
||||
<Rating readOnly value={4 - index} /> & Up
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
disableRipple
|
||||
color="inherit"
|
||||
endIcon={
|
||||
<Badge color="error" variant="dot" invisible={!canReset}>
|
||||
<Iconify icon="ic:round-filter-list" />
|
||||
</Badge>
|
||||
}
|
||||
onClick={onOpen}
|
||||
>
|
||||
Filters
|
||||
</Button>
|
||||
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
slotProps={{
|
||||
backdrop: { invisible: true },
|
||||
paper: { sx: { width: 320 } },
|
||||
}}
|
||||
>
|
||||
{renderHead()}
|
||||
|
||||
<Scrollbar sx={{ px: 2.5, py: 3 }}>
|
||||
<Stack spacing={3}>
|
||||
{renderGender()}
|
||||
{renderCategory()}
|
||||
{renderColor()}
|
||||
{renderPrice()}
|
||||
{renderRating()}
|
||||
</Stack>
|
||||
</Scrollbar>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type InputRangeProps = {
|
||||
value: number[];
|
||||
type: 'min' | 'max';
|
||||
onChange: UseSetStateReturn<IProductFilters>['setState'];
|
||||
};
|
||||
|
||||
function InputRange({ type, value, onChange: onFilters }: InputRangeProps) {
|
||||
const minValue = value[0];
|
||||
const maxValue = value[1];
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
const newMin = Math.max(0, Math.min(minValue, MAX_AMOUNT));
|
||||
const newMax = Math.max(0, Math.min(maxValue, MAX_AMOUNT));
|
||||
|
||||
if (newMin !== minValue || newMax !== maxValue) {
|
||||
onFilters({ priceRange: [newMin, newMax] });
|
||||
}
|
||||
}, [minValue, maxValue, onFilters]);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: 1, display: 'flex', alignItems: 'center' }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
color: 'text.disabled',
|
||||
textTransform: 'capitalize',
|
||||
fontWeight: 'fontWeightSemiBold',
|
||||
}}
|
||||
>
|
||||
{`${type} ($)`}
|
||||
</Typography>
|
||||
|
||||
<NumberInput
|
||||
hideButtons
|
||||
max={MAX_AMOUNT}
|
||||
value={type === 'min' ? minValue : maxValue}
|
||||
onChange={(event, newValue) =>
|
||||
onFilters({ priceRange: type === 'min' ? [newValue, maxValue] : [minValue, newValue] })
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
sx={{ maxWidth: 64 }}
|
||||
slotProps={{
|
||||
input: {
|
||||
sx: {
|
||||
[`& .${inputBaseClasses.input}`]: {
|
||||
pr: 1,
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||
import { varAlpha } from 'minimal-shared/utils';
|
||||
import { useCallback } from 'react';
|
||||
import type { FiltersResultProps } from 'src/components/filters-result';
|
||||
import { chipProps, FiltersBlock, FiltersResult } from 'src/components/filters-result';
|
||||
import type { IProductFilters } from 'src/types/party-event';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = FiltersResultProps & {
|
||||
filters: UseSetStateReturn<IProductFilters>;
|
||||
};
|
||||
|
||||
export function ProductFiltersResult({ filters, totalResults, sx }: Props) {
|
||||
const { state: currentFilters, setState: updateFilters, resetState: resetFilters } = filters;
|
||||
|
||||
const handleRemoveGender = useCallback(
|
||||
(inputValue: string) => {
|
||||
const newValue = currentFilters.gender.filter((item) => item !== inputValue);
|
||||
|
||||
updateFilters({ gender: newValue });
|
||||
},
|
||||
[updateFilters, currentFilters.gender]
|
||||
);
|
||||
|
||||
const handleRemoveCategory = useCallback(() => {
|
||||
updateFilters({ category: 'all' });
|
||||
}, [updateFilters]);
|
||||
|
||||
const handleRemoveColor = useCallback(
|
||||
(inputValue: string | string[]) => {
|
||||
const newValue = currentFilters.colors.filter((item: string) => item !== inputValue);
|
||||
|
||||
updateFilters({ colors: newValue });
|
||||
},
|
||||
[updateFilters, currentFilters.colors]
|
||||
);
|
||||
|
||||
const handleRemovePrice = useCallback(() => {
|
||||
updateFilters({ priceRange: [0, 200] });
|
||||
}, [updateFilters]);
|
||||
|
||||
const handleRemoveRating = useCallback(() => {
|
||||
updateFilters({ rating: '' });
|
||||
}, [updateFilters]);
|
||||
|
||||
return (
|
||||
<FiltersResult totalResults={totalResults} onReset={() => resetFilters()} sx={sx}>
|
||||
<FiltersBlock label="Gender:" isShow={!!currentFilters.gender.length}>
|
||||
{currentFilters.gender.map((item) => (
|
||||
<Chip {...chipProps} key={item} label={item} onDelete={() => handleRemoveGender(item)} />
|
||||
))}
|
||||
</FiltersBlock>
|
||||
|
||||
<FiltersBlock label="Category:" isShow={currentFilters.category !== 'all'}>
|
||||
<Chip {...chipProps} label={currentFilters.category} onDelete={handleRemoveCategory} />
|
||||
</FiltersBlock>
|
||||
|
||||
<FiltersBlock label="Colors:" isShow={!!currentFilters.colors.length}>
|
||||
{currentFilters.colors.map((item) => (
|
||||
<Chip
|
||||
{...chipProps}
|
||||
key={item}
|
||||
label={
|
||||
<Box
|
||||
sx={[
|
||||
(theme) => ({
|
||||
ml: -0.5,
|
||||
width: 18,
|
||||
height: 18,
|
||||
bgcolor: item,
|
||||
borderRadius: '50%',
|
||||
border: `solid 1px ${varAlpha(theme.vars.palette.common.whiteChannel, 0.24)}`,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
}
|
||||
onDelete={() => handleRemoveColor(item)}
|
||||
/>
|
||||
))}
|
||||
</FiltersBlock>
|
||||
|
||||
<FiltersBlock
|
||||
label="Price:"
|
||||
isShow={currentFilters.priceRange[0] !== 0 || currentFilters.priceRange[1] !== 200}
|
||||
>
|
||||
<Chip
|
||||
{...chipProps}
|
||||
label={`$${currentFilters.priceRange[0]} - ${currentFilters.priceRange[1]}`}
|
||||
onDelete={handleRemovePrice}
|
||||
/>
|
||||
</FiltersBlock>
|
||||
|
||||
<FiltersBlock label="Rating:" isShow={!!currentFilters.rating}>
|
||||
<Chip {...chipProps} label={currentFilters.rating} onDelete={handleRemoveRating} />
|
||||
</FiltersBlock>
|
||||
</FiltersResult>
|
||||
);
|
||||
}
|
147
03_source/frontend/src/sections/party-event/party-event-item.tsx
Normal file
147
03_source/frontend/src/sections/party-event/party-event-item.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import Fab, { fabClasses } from '@mui/material/Fab';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { ColorPreview } from 'src/components/color-utils';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Image } from 'src/components/image';
|
||||
import { Label } from 'src/components/label';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
import { fCurrency } from 'src/utils/format-number';
|
||||
import { useCheckoutContext } from '../checkout/context';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
partyEvent: IPartyEventItem;
|
||||
detailsHref: string;
|
||||
};
|
||||
|
||||
export function ProductItem({ partyEvent, detailsHref }: Props) {
|
||||
const { onAddToCart } = useCheckoutContext();
|
||||
|
||||
const { id, name, coverUrl, price, colors, available, sizes, priceSale, newLabel, saleLabel } =
|
||||
partyEvent;
|
||||
|
||||
const handleAddCart = async () => {
|
||||
const newProduct = {
|
||||
id,
|
||||
name,
|
||||
coverUrl,
|
||||
available,
|
||||
price,
|
||||
colors: [colors[0]],
|
||||
size: sizes[0],
|
||||
quantity: 1,
|
||||
};
|
||||
try {
|
||||
onAddToCart(newProduct);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderLabels = () =>
|
||||
(newLabel.enabled || saleLabel.enabled) && (
|
||||
<Box
|
||||
sx={{
|
||||
gap: 1,
|
||||
top: 16,
|
||||
zIndex: 9,
|
||||
right: 16,
|
||||
display: 'flex',
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{newLabel.enabled && (
|
||||
<Label variant="filled" color="info">
|
||||
{newLabel.content}
|
||||
</Label>
|
||||
)}
|
||||
{saleLabel.enabled && (
|
||||
<Label variant="filled" color="error">
|
||||
{saleLabel.content}
|
||||
</Label>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderImage = () => (
|
||||
<Box sx={{ position: 'relative', p: 1 }}>
|
||||
{!!available && (
|
||||
<Fab
|
||||
size="medium"
|
||||
color="warning"
|
||||
onClick={handleAddCart}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
right: 16,
|
||||
zIndex: 9,
|
||||
bottom: 16,
|
||||
opacity: 0,
|
||||
position: 'absolute',
|
||||
transform: 'scale(0)',
|
||||
transition: theme.transitions.create(['opacity', 'transform'], {
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.shorter,
|
||||
}),
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Iconify icon="solar:cart-plus-bold" width={24} />
|
||||
</Fab>
|
||||
)}
|
||||
|
||||
<Tooltip title={!available && 'Out of stock'} placement="bottom-end">
|
||||
<Image
|
||||
alt={name}
|
||||
src={coverUrl}
|
||||
ratio="1/1"
|
||||
sx={{ borderRadius: 1.5, ...(!available && { opacity: 0.48, filter: 'grayscale(1)' }) }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderContent = () => (
|
||||
<Stack spacing={2.5} sx={{ p: 3, pt: 2 }}>
|
||||
<Link component={RouterLink} href={detailsHref} color="inherit" variant="subtitle2" noWrap>
|
||||
{name}
|
||||
</Link>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Tooltip title="Color">
|
||||
<ColorPreview colors={colors} />
|
||||
</Tooltip>
|
||||
|
||||
<Box sx={{ gap: 0.5, display: 'flex', typography: 'subtitle1' }}>
|
||||
{priceSale && (
|
||||
<Box component="span" sx={{ color: 'text.disabled', textDecoration: 'line-through' }}>
|
||||
{fCurrency(priceSale)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box component="span">{fCurrency(price)}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
'&:hover': {
|
||||
[`& .${fabClasses.root}`]: { opacity: 1, transform: 'scale(1)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{renderLabels()}
|
||||
{renderImage()}
|
||||
{renderContent()}
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import type { BoxProps } from '@mui/material/Box';
|
||||
import Box from '@mui/material/Box';
|
||||
import Pagination, { paginationClasses } from '@mui/material/Pagination';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
import { ProductItem } from './party-event-item';
|
||||
import { ProductItemSkeleton } from './party-event-skeleton';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = BoxProps & {
|
||||
loading?: boolean;
|
||||
partyEvents: IPartyEventItem[];
|
||||
};
|
||||
|
||||
export function PartyEventList({ partyEvents, loading, sx, ...other }: Props) {
|
||||
const renderLoading = () => <ProductItemSkeleton />;
|
||||
|
||||
const renderList = () =>
|
||||
partyEvents.map((partyEvent) => (
|
||||
<ProductItem
|
||||
key={partyEvent.id}
|
||||
partyEvent={partyEvent}
|
||||
detailsHref={paths.partyEvent.details(partyEvent.id)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={[
|
||||
() => ({
|
||||
gap: 3,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: {
|
||||
xs: 'repeat(1, 1fr)',
|
||||
sm: 'repeat(2, 1fr)',
|
||||
md: 'repeat(3, 1fr)',
|
||||
lg: 'repeat(4, 1fr)',
|
||||
},
|
||||
}),
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...other}
|
||||
>
|
||||
{loading ? renderLoading() : renderList()}
|
||||
</Box>
|
||||
|
||||
{partyEvents.length > 8 && (
|
||||
<Pagination
|
||||
count={8}
|
||||
sx={{
|
||||
mt: { xs: 5, md: 8 },
|
||||
[`& .${paginationClasses.ul}`]: { justifyContent: 'center' },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,560 @@
|
||||
// src/sections/product/product-new-edit-form.tsx
|
||||
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 CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
PRODUCT_CATEGORY_GROUP_OPTIONS,
|
||||
PRODUCT_COLOR_NAME_OPTIONS,
|
||||
PRODUCT_SIZE_OPTIONS,
|
||||
} from 'src/_mock';
|
||||
import { createPartyEvent, updatePartyEvent } from 'src/actions/party-event';
|
||||
import { Field, Form, schemaHelper } from 'src/components/hook-form';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { toast } from 'src/components/snackbar';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
import { fileToBase64 } from 'src/utils/file-to-base64';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
const PRODUCT_PUBLISH_OPTIONS = [
|
||||
{ value: 'published', label: 'Published' },
|
||||
{ value: 'draft', label: 'Draft' },
|
||||
];
|
||||
|
||||
const PRODUCT_COLOR_OPTIONS = [
|
||||
'#FF4842',
|
||||
'#1890FF',
|
||||
'#FFC0CB',
|
||||
'#00AB55',
|
||||
'#FFC107',
|
||||
'#7F00FF',
|
||||
'#000000',
|
||||
'#FFFFFF',
|
||||
];
|
||||
|
||||
const _tags = [
|
||||
`Technology`,
|
||||
`Health and Wellness`,
|
||||
`Travel`,
|
||||
`Finance`,
|
||||
`Education`,
|
||||
`Food and Beverage`,
|
||||
`Fashion`,
|
||||
`Home and Garden`,
|
||||
`Sports`,
|
||||
`Entertainment`,
|
||||
`Business`,
|
||||
`Science`,
|
||||
`Automotive`,
|
||||
`Beauty`,
|
||||
`Fitness`,
|
||||
`Lifestyle`,
|
||||
`Real Estate`,
|
||||
`Parenting`,
|
||||
`Pet Care`,
|
||||
`Environmental`,
|
||||
`DIY and Crafts`,
|
||||
`Gaming`,
|
||||
`Photography`,
|
||||
`Music`,
|
||||
];
|
||||
|
||||
const PRODUCT_GENDER_OPTIONS = [
|
||||
{ label: 'Men', value: 'Men' },
|
||||
{ label: 'Women', value: 'Women' },
|
||||
{ label: 'Kids', value: 'Kids' },
|
||||
];
|
||||
|
||||
export type NewPartyEventSchemaType = zod.infer<typeof NewProductSchema>;
|
||||
|
||||
export const NewProductSchema = zod.object({
|
||||
sku: zod.string().min(1, { message: 'Product sku is required!' }),
|
||||
name: zod.string().min(1, { message: 'Name is required!' }),
|
||||
code: zod.string().min(1, { message: 'Product code is required!' }),
|
||||
price: schemaHelper.nullableInput(
|
||||
zod.number({ coerce: true }).min(1, { message: 'Price is required!' }),
|
||||
{
|
||||
// message for null value
|
||||
message: 'Price is required!',
|
||||
}
|
||||
),
|
||||
taxes: zod.number({ coerce: true }).nullable(),
|
||||
tags: zod.string().array().min(2, { message: 'Must have at least 2 items!' }),
|
||||
sizes: zod.string().array().min(1, { message: 'Choose at least one option!' }),
|
||||
publish: zod.string(),
|
||||
gender: zod.array(zod.string()).min(1, { message: 'Choose at least one option!' }),
|
||||
coverUrl: zod.string(),
|
||||
images: schemaHelper.files({ message: 'Images is required!' }),
|
||||
colors: zod.string().array().min(1, { message: 'Choose at least one option!' }),
|
||||
quantity: schemaHelper.nullableInput(
|
||||
zod.number({ coerce: true }).min(1, { message: 'Quantity is required!' }),
|
||||
{
|
||||
// message for null value
|
||||
message: 'Quantity is required!',
|
||||
}
|
||||
),
|
||||
category: zod.string(),
|
||||
available: zod.number(),
|
||||
totalSold: zod.number(),
|
||||
description: schemaHelper
|
||||
.editor({ message: 'Description is required!' })
|
||||
.min(10, { message: 'Description must be at least 10 characters' })
|
||||
.max(50000, { message: 'Description must be less than 50000 characters' }),
|
||||
totalRatings: zod.number(),
|
||||
totalReviews: zod.number(),
|
||||
inventoryType: zod.string(),
|
||||
subDescription: zod.string(),
|
||||
priceSale: zod.number({ coerce: true }).nullable(),
|
||||
newLabel: zod.object({ enabled: zod.boolean(), content: zod.string() }),
|
||||
saleLabel: zod.object({ enabled: zod.boolean(), content: zod.string() }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
currentPartyEvent?: IPartyEventItem;
|
||||
};
|
||||
|
||||
export function PartyEventNewEditForm({ currentPartyEvent }: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const openDetails = useBoolean(true);
|
||||
const openProperties = useBoolean(true);
|
||||
const openPricing = useBoolean(true);
|
||||
|
||||
const [includeTaxes, setIncludeTaxes] = useState(false);
|
||||
|
||||
const defaultValues: NewPartyEventSchemaType = {
|
||||
sku: '321',
|
||||
name: 'hello party event',
|
||||
code: '123',
|
||||
price: 1.1,
|
||||
taxes: 1.1,
|
||||
tags: [_tags[0], _tags[1]],
|
||||
sizes: ['9'],
|
||||
publish: PRODUCT_PUBLISH_OPTIONS[0].value,
|
||||
gender: [
|
||||
PRODUCT_GENDER_OPTIONS[0].value,
|
||||
PRODUCT_GENDER_OPTIONS[1].value,
|
||||
PRODUCT_GENDER_OPTIONS[2].value,
|
||||
],
|
||||
coverUrl: '',
|
||||
images: [],
|
||||
colors: [PRODUCT_COLOR_OPTIONS[0], PRODUCT_COLOR_OPTIONS[1]],
|
||||
quantity: 3,
|
||||
category: PRODUCT_CATEGORY_GROUP_OPTIONS[0].classify[1],
|
||||
available: 0,
|
||||
totalSold: 0,
|
||||
description: 'hello description',
|
||||
totalRatings: 0,
|
||||
totalReviews: 0,
|
||||
inventoryType: '',
|
||||
subDescription: '',
|
||||
priceSale: 0.9,
|
||||
newLabel: { enabled: false, content: '' },
|
||||
saleLabel: { enabled: false, content: '' },
|
||||
};
|
||||
|
||||
const methods = useForm<NewPartyEventSchemaType>({
|
||||
resolver: zodResolver(NewProductSchema),
|
||||
defaultValues,
|
||||
values: currentPartyEvent,
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
watch,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const values = watch();
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
const updatedData = {
|
||||
...data,
|
||||
taxes: includeTaxes ? defaultValues.taxes : data.taxes,
|
||||
};
|
||||
|
||||
try {
|
||||
// sanitize file field
|
||||
for (let i = 0; i < values.images.length; i++) {
|
||||
const temp: any = values.images[i];
|
||||
if (temp instanceof File) {
|
||||
values.images[i] = await fileToBase64(temp);
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedValues: IPartyEventItem = values as unknown as IPartyEventItem;
|
||||
|
||||
if (currentPartyEvent) {
|
||||
// perform save
|
||||
updatePartyEvent(sanitizedValues);
|
||||
} else {
|
||||
// perform create
|
||||
createPartyEvent(sanitizedValues);
|
||||
}
|
||||
|
||||
toast.success(currentPartyEvent ? 'update-success!' : 'create-success!');
|
||||
|
||||
router.push(paths.dashboard.partyEvent.root);
|
||||
|
||||
// console.info('DATA', updatedData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const handleRemoveFile = useCallback(
|
||||
(inputFile: File | string) => {
|
||||
const filtered = values.images && values.images?.filter((file) => file !== inputFile);
|
||||
setValue('images', filtered);
|
||||
},
|
||||
[setValue, values.images]
|
||||
);
|
||||
|
||||
const handleRemoveAllFiles = useCallback(() => {
|
||||
setValue('images', [], { shouldValidate: true });
|
||||
}, [setValue]);
|
||||
|
||||
const handleChangeIncludeTaxes = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIncludeTaxes(event.target.checked);
|
||||
}, []);
|
||||
|
||||
const renderCollapseButton = (value: boolean, onToggle: () => void) => (
|
||||
<IconButton onClick={onToggle}>
|
||||
<Iconify icon={value ? 'eva:arrow-ios-downward-fill' : 'eva:arrow-ios-forward-fill'} />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
function handleProductImageUpload() {
|
||||
console.log(values);
|
||||
}
|
||||
|
||||
const [disableUserInput, setDisableUserInput] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDisableUserInput(isSubmitting);
|
||||
}, [isSubmitting]);
|
||||
|
||||
const renderDetails = () => (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Details"
|
||||
subheader="Title, short description, image..."
|
||||
action={renderCollapseButton(openDetails.value, openDetails.onToggle)}
|
||||
sx={{ mb: 3 }}
|
||||
/>
|
||||
|
||||
<Collapse in={openDetails.value}>
|
||||
<Divider />
|
||||
|
||||
<Stack spacing={3} sx={{ p: 3 }}>
|
||||
<Field.Text disabled={disableUserInput} name="name" label="產品名稱 / Product name" />
|
||||
|
||||
<Field.Text
|
||||
disabled={disableUserInput}
|
||||
name="subDescription"
|
||||
label="Sub description"
|
||||
multiline
|
||||
rows={4}
|
||||
/>
|
||||
|
||||
<Stack spacing={1.5}>
|
||||
<Typography variant="subtitle2">Content</Typography>
|
||||
<Field.Editor name="description" sx={{ maxHeight: 480 }} />
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={1.5}>
|
||||
<Typography variant="subtitle2">Images</Typography>
|
||||
<Field.Upload
|
||||
multiple
|
||||
thumbnail
|
||||
name="images"
|
||||
maxSize={3145728}
|
||||
onRemove={handleRemoveFile}
|
||||
onRemoveAll={handleRemoveAllFiles}
|
||||
onUpload={handleProductImageUpload}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderProperties = () => (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Properties"
|
||||
subheader="Additional functions and attributes..."
|
||||
action={renderCollapseButton(openProperties.value, openProperties.onToggle)}
|
||||
sx={{ mb: 3 }}
|
||||
/>
|
||||
|
||||
<Collapse in={openProperties.value}>
|
||||
<Divider />
|
||||
|
||||
<Stack spacing={3} sx={{ p: 3 }}>
|
||||
<Box
|
||||
sx={{
|
||||
rowGap: 3,
|
||||
columnGap: 2,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: 'repeat(1, 1fr)', md: 'repeat(2, 1fr)' },
|
||||
}}
|
||||
>
|
||||
<Field.Text disabled={disableUserInput} name="code" label="Product code" />
|
||||
|
||||
<Field.Text disabled={disableUserInput} name="sku" label="Product SKU" />
|
||||
|
||||
<Field.Text
|
||||
disabled={disableUserInput}
|
||||
name="quantity"
|
||||
label="Quantity"
|
||||
placeholder="0"
|
||||
type="number"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Field.Select
|
||||
disabled={disableUserInput}
|
||||
name="category"
|
||||
label="Category"
|
||||
slotProps={{
|
||||
select: { native: true },
|
||||
inputLabel: { shrink: true },
|
||||
}}
|
||||
>
|
||||
{PRODUCT_CATEGORY_GROUP_OPTIONS.map((category) => (
|
||||
<optgroup key={category.group} label={category.group}>
|
||||
{category.classify.map((classify) => (
|
||||
<option key={classify} value={classify}>
|
||||
{classify}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</Field.Select>
|
||||
|
||||
<Field.MultiSelect
|
||||
checkbox
|
||||
name="colors"
|
||||
label="Colors"
|
||||
options={PRODUCT_COLOR_NAME_OPTIONS}
|
||||
/>
|
||||
|
||||
<Field.MultiSelect checkbox name="sizes" label="Sizes" options={PRODUCT_SIZE_OPTIONS} />
|
||||
</Box>
|
||||
|
||||
<Field.Autocomplete
|
||||
disabled={disableUserInput}
|
||||
name="tags"
|
||||
label="Tags"
|
||||
placeholder="+ Tags"
|
||||
multiple
|
||||
freeSolo
|
||||
disableCloseOnSelect
|
||||
options={_tags.map((option) => option)}
|
||||
getOptionLabel={(option) => option}
|
||||
renderOption={(props, option) => (
|
||||
<li {...props} key={option}>
|
||||
{option}
|
||||
</li>
|
||||
)}
|
||||
renderTags={(selected, getTagProps) =>
|
||||
selected.map((option, index) => (
|
||||
<Chip
|
||||
{...getTagProps({ index })}
|
||||
key={option}
|
||||
label={option}
|
||||
size="small"
|
||||
color="info"
|
||||
variant="soft"
|
||||
/>
|
||||
))
|
||||
}
|
||||
/>
|
||||
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="subtitle2">Gender</Typography>
|
||||
<Field.MultiCheckbox
|
||||
row
|
||||
name="gender"
|
||||
options={PRODUCT_GENDER_OPTIONS}
|
||||
sx={{ gap: 2 }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
|
||||
<Box sx={{ gap: 3, display: 'flex', alignItems: 'center' }}>
|
||||
<Field.Switch name="saleLabel.enabled" label={null} sx={{ m: 0 }} />
|
||||
<Field.Text
|
||||
name="saleLabel.content"
|
||||
label="Sale label"
|
||||
fullWidth
|
||||
disabled={!values.saleLabel.enabled}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ gap: 3, display: 'flex', alignItems: 'center' }}>
|
||||
<Field.Switch name="newLabel.enabled" label={null} sx={{ m: 0 }} />
|
||||
<Field.Text
|
||||
name="newLabel.content"
|
||||
label="New label"
|
||||
fullWidth
|
||||
disabled={!values.newLabel.enabled}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderPricing = () => (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="Pricing"
|
||||
subheader="Price related inputs"
|
||||
action={renderCollapseButton(openPricing.value, openPricing.onToggle)}
|
||||
sx={{ mb: 3 }}
|
||||
/>
|
||||
|
||||
<Collapse in={openPricing.value}>
|
||||
<Divider />
|
||||
|
||||
<Stack spacing={3} sx={{ p: 3 }}>
|
||||
<Field.Text
|
||||
disabled={disableUserInput}
|
||||
name="price"
|
||||
label="Regular price"
|
||||
placeholder="0.00"
|
||||
type="number"
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start" sx={{ mr: 0.75 }}>
|
||||
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||
$
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Field.Text
|
||||
disabled={disableUserInput}
|
||||
name="priceSale"
|
||||
label="Sale price"
|
||||
placeholder="0.00"
|
||||
type="number"
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start" sx={{ mr: 0.75 }}>
|
||||
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||
$
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={disableUserInput}
|
||||
id="toggle-taxes"
|
||||
checked={includeTaxes}
|
||||
onChange={handleChangeIncludeTaxes}
|
||||
/>
|
||||
}
|
||||
label="Price includes taxes"
|
||||
/>
|
||||
|
||||
{!includeTaxes && (
|
||||
<Field.Text
|
||||
disabled={disableUserInput}
|
||||
name="taxes"
|
||||
label="Tax (%)"
|
||||
placeholder="0.00"
|
||||
type="number"
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start" sx={{ mr: 0.75 }}>
|
||||
<Box component="span" sx={{ color: 'text.disabled' }}>
|
||||
%
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderActions = () => (
|
||||
<Box
|
||||
sx={{
|
||||
gap: 3,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div>{JSON.stringify({ errors })}</div>
|
||||
<FormControlLabel
|
||||
label="Publish"
|
||||
control={
|
||||
<Switch
|
||||
disabled={disableUserInput}
|
||||
defaultChecked
|
||||
slotProps={{ input: { id: 'publish-switch' } }}
|
||||
/>
|
||||
}
|
||||
sx={{ pl: 3, flexGrow: 1 }}
|
||||
/>
|
||||
|
||||
<Button type="submit" variant="contained" size="large" loading={isSubmitting}>
|
||||
{!currentPartyEvent ? 'create-party' : 'save-edit'}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
<Stack spacing={{ xs: 3, md: 5 }} sx={{ mx: 'auto', maxWidth: { xs: 720, xl: 880 } }}>
|
||||
{renderDetails()}
|
||||
{renderProperties()}
|
||||
{renderPricing()}
|
||||
{renderActions()}
|
||||
</Stack>
|
||||
</Form>
|
||||
);
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import ButtonBase from '@mui/material/ButtonBase';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Rating from '@mui/material/Rating';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IProductReview } from 'src/types/party-event';
|
||||
import { fDate } from 'src/utils/format-time';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
review: IProductReview;
|
||||
};
|
||||
|
||||
export function ProductReviewItem({ review }: Props) {
|
||||
const renderInfo = () => (
|
||||
<Box
|
||||
sx={{
|
||||
gap: 2,
|
||||
display: 'flex',
|
||||
width: { md: 240 },
|
||||
alignItems: 'center',
|
||||
textAlign: { md: 'center' },
|
||||
flexDirection: { xs: 'row', md: 'column' },
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
src={review.avatarUrl}
|
||||
sx={{ width: { xs: 48, md: 64 }, height: { xs: 48, md: 64 } }}
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primary={review.name}
|
||||
secondary={fDate(review.postedAt)}
|
||||
slotProps={{
|
||||
primary: { noWrap: true },
|
||||
secondary: {
|
||||
noWrap: true,
|
||||
sx: { mt: 0.5, typography: 'caption' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderContent = () => (
|
||||
<Box
|
||||
sx={{
|
||||
gap: 1,
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Rating size="small" value={review.rating} precision={0.1} readOnly />
|
||||
|
||||
{review.isPurchased && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'success.main',
|
||||
typography: 'caption',
|
||||
}}
|
||||
>
|
||||
<Iconify icon="solar:verified-check-bold" width={16} sx={{ mr: 0.5 }} />
|
||||
Verified purchase
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Typography variant="body2">{review.comment}</Typography>
|
||||
|
||||
{!!review.attachments?.length && (
|
||||
<Box
|
||||
sx={{
|
||||
pt: 1,
|
||||
gap: 1,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{review.attachments.map((attachment) => (
|
||||
<Box
|
||||
key={attachment}
|
||||
component="img"
|
||||
alt={attachment}
|
||||
src={attachment}
|
||||
sx={{ width: 64, height: 64, borderRadius: 1.5 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ gap: 2, pt: 1.5, display: 'flex' }}>
|
||||
<ButtonBase disableRipple sx={{ gap: 0.5, typography: 'caption' }}>
|
||||
<Iconify icon="solar:like-outline" width={16} />
|
||||
123
|
||||
</ButtonBase>
|
||||
|
||||
<ButtonBase disableRipple sx={{ gap: 0.5, typography: 'caption' }}>
|
||||
<Iconify icon="solar:dislike-outline" width={16} />
|
||||
34
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 5,
|
||||
gap: 2,
|
||||
display: 'flex',
|
||||
px: { xs: 2.5, md: 0 },
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
}}
|
||||
>
|
||||
{renderInfo()}
|
||||
{renderContent()}
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import Pagination, { paginationClasses } from '@mui/material/Pagination';
|
||||
import type { IProductReview } from 'src/types/party-event';
|
||||
import { ProductReviewItem } from './party-event-review-item';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
reviews: IProductReview[];
|
||||
};
|
||||
|
||||
export function ProductReviewList({ reviews }: Props) {
|
||||
return (
|
||||
<>
|
||||
{reviews.map((review) => (
|
||||
<ProductReviewItem key={review.id} review={review} />
|
||||
))}
|
||||
|
||||
<Pagination
|
||||
count={10}
|
||||
sx={{
|
||||
mx: 'auto',
|
||||
[`& .${paginationClasses.ul}`]: { my: 5, mx: 'auto', justifyContent: 'center' },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import Button from '@mui/material/Button';
|
||||
import type { DialogProps } from '@mui/material/Dialog';
|
||||
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 Typography from '@mui/material/Typography';
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Field, Form } from 'src/components/hook-form';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type ReviewSchemaType = zod.infer<typeof ReviewSchema>;
|
||||
|
||||
export const ReviewSchema = zod.object({
|
||||
rating: zod.number().min(1, 'Rating must be greater than or equal to 1!'),
|
||||
name: zod.string().min(1, { message: 'Name is required!' }),
|
||||
review: zod.string().min(1, { message: 'Review is required!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = DialogProps & {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function ProductReviewNewForm({ onClose, ...other }: Props) {
|
||||
const defaultValues: ReviewSchemaType = {
|
||||
rating: 0,
|
||||
review: '',
|
||||
name: '',
|
||||
email: '',
|
||||
};
|
||||
|
||||
const methods = useForm<ReviewSchemaType>({
|
||||
mode: 'all',
|
||||
resolver: zodResolver(ReviewSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
reset();
|
||||
onClose();
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
onClose();
|
||||
reset();
|
||||
}, [onClose, reset]);
|
||||
|
||||
return (
|
||||
<Dialog onClose={onClose} {...other}>
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
<DialogTitle> Add Review </DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<div>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
Your review about this product:
|
||||
</Typography>
|
||||
<Field.Rating name="rating" />
|
||||
</div>
|
||||
|
||||
<Field.Text name="review" label="Review *" multiline rows={3} sx={{ mt: 3 }} />
|
||||
|
||||
<Field.Text name="name" label="Name *" sx={{ mt: 3 }} />
|
||||
|
||||
<Field.Text name="email" label="Email *" sx={{ mt: 3 }} />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button color="inherit" variant="outlined" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button type="submit" variant="contained" loading={isSubmitting}>
|
||||
Post
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -0,0 +1,150 @@
|
||||
import Autocomplete, { autocompleteClasses, createFilterOptions } from '@mui/material/Autocomplete';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import Link, { linkClasses } from '@mui/material/Link';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import match from 'autosuggest-highlight/match';
|
||||
import parse from 'autosuggest-highlight/parse';
|
||||
import { useDebounce } from 'minimal-shared/hooks';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useSearchProducts } from 'src/actions/party-event';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { SearchNotFound } from 'src/components/search-not-found';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
sx?: SxProps<Theme>;
|
||||
redirectPath: (id: string) => string;
|
||||
};
|
||||
|
||||
export function ProductSearch({ redirectPath, sx }: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<IPartyEventItem | null>(null);
|
||||
|
||||
const debouncedQuery = useDebounce(searchQuery);
|
||||
const { searchResults: options, searchLoading: loading } = useSearchProducts(debouncedQuery);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(item: IPartyEventItem | null) => {
|
||||
setSelectedItem(item);
|
||||
if (item) {
|
||||
router.push(redirectPath(item.id));
|
||||
}
|
||||
},
|
||||
[redirectPath, router]
|
||||
);
|
||||
|
||||
const filterOptions = createFilterOptions({
|
||||
matchFrom: 'any',
|
||||
stringify: (option: IPartyEventItem) => `${option.name} ${option.sku}`,
|
||||
});
|
||||
|
||||
const paperStyles: SxProps<Theme> = {
|
||||
width: 320,
|
||||
[`& .${autocompleteClasses.listbox}`]: {
|
||||
[`& .${autocompleteClasses.option}`]: {
|
||||
p: 0,
|
||||
[`& .${linkClasses.root}`]: {
|
||||
p: 0.75,
|
||||
gap: 1.5,
|
||||
width: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
autoHighlight
|
||||
popupIcon={null}
|
||||
loading={loading}
|
||||
options={options}
|
||||
value={selectedItem}
|
||||
filterOptions={filterOptions}
|
||||
onChange={(event, newValue) => handleChange(newValue)}
|
||||
onInputChange={(event, newValue) => setSearchQuery(newValue)}
|
||||
getOptionLabel={(option) => option.name}
|
||||
noOptionsText={<SearchNotFound query={debouncedQuery} />}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
slotProps={{ paper: { sx: paperStyles } }}
|
||||
sx={[{ width: { xs: 1, sm: 260 } }, ...(Array.isArray(sx) ? sx : [sx])]}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder="Search..."
|
||||
slotProps={{
|
||||
input: {
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Iconify icon="eva:search-fill" sx={{ ml: 1, color: 'text.disabled' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<>
|
||||
{loading ? <CircularProgress size={18} color="inherit" sx={{ mr: -3 }} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
renderOption={(props, product, { inputValue }) => {
|
||||
const matches = match(product.name, inputValue);
|
||||
const parts = parse(product.name, matches);
|
||||
|
||||
return (
|
||||
<li {...props} key={product.id}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href={redirectPath(product.id)}
|
||||
color="inherit"
|
||||
underline="none"
|
||||
>
|
||||
<Avatar
|
||||
key={product.id}
|
||||
alt={product.name}
|
||||
src={product.coverUrl}
|
||||
variant="rounded"
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
flexShrink: 0,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div key={inputValue}>
|
||||
{parts.map((part, index) => (
|
||||
<Typography
|
||||
key={index}
|
||||
component="span"
|
||||
color={part.highlight ? 'primary' : 'textPrimary'}
|
||||
sx={{
|
||||
typography: 'body2',
|
||||
fontWeight: part.highlight ? 'fontWeightSemiBold' : 'fontWeightMedium',
|
||||
}}
|
||||
>
|
||||
{part.text}
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import type { GridProps } from '@mui/material/Grid';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import type { PaperProps } from '@mui/material/Paper';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type ProductItemSkeletonProps = PaperProps & {
|
||||
itemCount?: number;
|
||||
};
|
||||
|
||||
export function ProductItemSkeleton({ sx, itemCount = 16, ...other }: ProductItemSkeletonProps) {
|
||||
return Array.from({ length: itemCount }, (_, index) => (
|
||||
<Paper
|
||||
key={index}
|
||||
variant="outlined"
|
||||
sx={[{ borderRadius: 2 }, ...(Array.isArray(sx) ? sx : [sx])]}
|
||||
{...other}
|
||||
>
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Skeleton sx={{ pt: '100%' }} />
|
||||
</Box>
|
||||
|
||||
<Stack spacing={2} sx={{ p: 3, pt: 2 }}>
|
||||
<Skeleton sx={{ width: 0.5, height: 16 }} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Skeleton variant="circular" sx={{ width: 16, height: 16 }} />
|
||||
<Skeleton variant="circular" sx={{ width: 16, height: 16 }} />
|
||||
<Skeleton variant="circular" sx={{ width: 16, height: 16 }} />
|
||||
</Box>
|
||||
|
||||
<Skeleton sx={{ width: 40, height: 16 }} />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function ProductDetailsSkeleton({ ...other }: GridProps) {
|
||||
return (
|
||||
<Grid container spacing={8} {...other}>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 7 }}>
|
||||
<Skeleton sx={{ pt: '100%' }} />
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6, lg: 5 }}>
|
||||
<Stack spacing={3}>
|
||||
<Skeleton sx={{ height: 16, width: 48 }} />
|
||||
<Skeleton sx={{ height: 16, width: 80 }} />
|
||||
<Skeleton sx={{ height: 16, width: 0.5 }} />
|
||||
<Skeleton sx={{ height: 16, width: 0.75 }} />
|
||||
<Skeleton sx={{ height: 120 }} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{Array.from({ length: 3 }, (_, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
gap: 2,
|
||||
width: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="circular" sx={{ width: 80, height: 80 }} />
|
||||
<Skeleton sx={{ height: 16, width: 160 }} />
|
||||
<Skeleton sx={{ height: 16, width: 80 }} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import { usePopover } from 'minimal-shared/hooks';
|
||||
import { CustomPopover } from 'src/components/custom-popover';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
sort: string;
|
||||
onSort: (newValue: string) => void;
|
||||
sortOptions: {
|
||||
value: string;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export function ProductSort({ sort, onSort, sortOptions }: Props) {
|
||||
const menuActions = usePopover();
|
||||
|
||||
const sortLabel = sortOptions.find((option) => option.value === sort)?.label;
|
||||
|
||||
const renderMenuActions = () => (
|
||||
<CustomPopover
|
||||
open={menuActions.open}
|
||||
anchorEl={menuActions.anchorEl}
|
||||
onClose={menuActions.onClose}
|
||||
>
|
||||
<MenuList>
|
||||
{sortOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
selected={option.value === sort}
|
||||
onClick={() => {
|
||||
menuActions.onClose();
|
||||
onSort(option.value);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</CustomPopover>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
disableRipple
|
||||
color="inherit"
|
||||
onClick={menuActions.onOpen}
|
||||
endIcon={
|
||||
<Iconify
|
||||
icon={menuActions.open ? 'eva:arrow-ios-upward-fill' : 'eva:arrow-ios-downward-fill'}
|
||||
/>
|
||||
}
|
||||
sx={{ fontWeight: 'fontWeightSemiBold' }}
|
||||
>
|
||||
Sort by:
|
||||
<Box component="span" sx={{ ml: 0.5, fontWeight: 'fontWeightBold' }}>
|
||||
{sortLabel}
|
||||
</Box>
|
||||
</Button>
|
||||
|
||||
{renderMenuActions()}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
import Chip from '@mui/material/Chip';
|
||||
import { upperFirst } from 'es-toolkit';
|
||||
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 { IProductTableFilters } from 'src/types/party-event';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = FiltersResultProps & {
|
||||
filters: UseSetStateReturn<IProductTableFilters>;
|
||||
};
|
||||
|
||||
export function ProductTableFiltersResult({ filters, totalResults, sx }: Props) {
|
||||
const { state: currentFilters, setState: updateFilters, resetState: resetFilters } = filters;
|
||||
|
||||
const handleRemoveStock = useCallback(
|
||||
(inputValue: string) => {
|
||||
const newValue = currentFilters.stock.filter((item) => item !== inputValue);
|
||||
|
||||
updateFilters({ stock: newValue });
|
||||
},
|
||||
[updateFilters, currentFilters.stock]
|
||||
);
|
||||
|
||||
const handleRemovePublish = useCallback(
|
||||
(inputValue: string) => {
|
||||
const newValue = currentFilters.publish.filter((item) => item !== inputValue);
|
||||
|
||||
updateFilters({ publish: newValue });
|
||||
},
|
||||
[updateFilters, currentFilters.publish]
|
||||
);
|
||||
|
||||
return (
|
||||
<FiltersResult totalResults={totalResults} onReset={() => resetFilters()} sx={sx}>
|
||||
<FiltersBlock label="Stock:" isShow={!!currentFilters.stock.length}>
|
||||
{currentFilters.stock.map((item) => (
|
||||
<Chip
|
||||
{...chipProps}
|
||||
key={item}
|
||||
label={upperFirst(item)}
|
||||
onDelete={() => handleRemoveStock(item)}
|
||||
/>
|
||||
))}
|
||||
</FiltersBlock>
|
||||
|
||||
<FiltersBlock label="Publish:" isShow={!!currentFilters.publish.length}>
|
||||
{currentFilters.publish.map((item) => (
|
||||
<Chip
|
||||
{...chipProps}
|
||||
key={item}
|
||||
label={upperFirst(item)}
|
||||
onDelete={() => handleRemovePublish(item)}
|
||||
/>
|
||||
))}
|
||||
</FiltersBlock>
|
||||
</FiltersResult>
|
||||
);
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
// src/sections/product/product-table-row.tsx
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import type { GridCellParams } from '@mui/x-data-grid';
|
||||
import { Label } from 'src/components/label';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
import { fCurrency } from 'src/utils/format-number';
|
||||
import { fDate, fTime } from 'src/utils/format-time';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type ParamsProps = {
|
||||
params: GridCellParams;
|
||||
};
|
||||
|
||||
export function RenderCellPrice({ params }: ParamsProps) {
|
||||
return fCurrency(params.row.price);
|
||||
}
|
||||
|
||||
export function RenderCellPublish({ params }: ParamsProps) {
|
||||
return (
|
||||
<Label variant="soft" color={params.row.publish === 'published' ? 'info' : 'default'}>
|
||||
{params.row.publish}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderCellCreatedAt({ params }: ParamsProps) {
|
||||
return (
|
||||
<Box sx={{ gap: 0.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<span>{fDate(params.row.createdAt)}</span>
|
||||
<Box component="span" sx={{ typography: 'caption', color: 'text.secondary' }}>
|
||||
{fTime(params.row.createdAt)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderCellStock({ params }: ParamsProps) {
|
||||
return (
|
||||
<Box sx={{ width: 1, typography: 'caption', color: 'text.secondary' }}>
|
||||
<LinearProgress
|
||||
value={(params.row.available * 100) / params.row.quantity}
|
||||
variant="determinate"
|
||||
color={
|
||||
(params.row.inventoryType === 'out of stock' && 'error') ||
|
||||
(params.row.inventoryType === 'low stock' && 'warning') ||
|
||||
'success'
|
||||
}
|
||||
sx={{ mb: 1, height: 6, width: 80 }}
|
||||
/>
|
||||
{!!params.row.available && params.row.available} {params.row.inventoryType}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderCellProduct({ params, href }: ParamsProps & { href: string }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
py: 2,
|
||||
gap: 2,
|
||||
width: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
alt={params.row.name}
|
||||
src={params.row.coverUrl}
|
||||
variant="rounded"
|
||||
sx={{ width: 64, height: 64 }}
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primary={
|
||||
<Link component={RouterLink} href={href} color="inherit">
|
||||
{params.row.name}
|
||||
</Link>
|
||||
}
|
||||
secondary={params.row.category}
|
||||
slotProps={{
|
||||
primary: { noWrap: true },
|
||||
secondary: { sx: { color: 'text.disabled' } },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,184 @@
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
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 type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||
import { usePopover } from 'minimal-shared/hooks';
|
||||
import { varAlpha } from 'minimal-shared/utils';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomPopover } from 'src/components/custom-popover';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IProductTableFilters } from 'src/types/party-event';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
filters: UseSetStateReturn<IProductTableFilters>;
|
||||
options: {
|
||||
stocks: { value: string; label: string }[];
|
||||
publishs: { value: string; label: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
export function PartyEventTableToolbar({ filters, options }: Props) {
|
||||
const menuActions = usePopover();
|
||||
|
||||
const { state: currentFilters, setState: updateFilters } = filters;
|
||||
|
||||
const [stock, setStock] = useState(currentFilters.stock);
|
||||
const [publish, setPublish] = useState(currentFilters.publish);
|
||||
|
||||
const handleChangeStock = useCallback((event: SelectChangeEvent<string[]>) => {
|
||||
const {
|
||||
target: { value },
|
||||
} = event;
|
||||
|
||||
setStock(typeof value === 'string' ? value.split(',') : value);
|
||||
}, []);
|
||||
|
||||
const handleChangePublish = useCallback((event: SelectChangeEvent<string[]>) => {
|
||||
const {
|
||||
target: { value },
|
||||
} = event;
|
||||
|
||||
setPublish(typeof value === 'string' ? value.split(',') : value);
|
||||
}, []);
|
||||
|
||||
const handleFilterStock = useCallback(() => {
|
||||
updateFilters({ stock });
|
||||
}, [updateFilters, stock]);
|
||||
|
||||
const handleFilterPublish = useCallback(() => {
|
||||
updateFilters({ publish });
|
||||
}, [publish, updateFilters]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<FormControl sx={{ flexShrink: 0, width: { xs: 1, md: 200 } }}>
|
||||
<InputLabel htmlFor="filter-stock-select">{t('Stock')}</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={stock}
|
||||
onChange={handleChangeStock}
|
||||
onClose={handleFilterStock}
|
||||
input={<OutlinedInput label="Stock" />}
|
||||
renderValue={(selected) => selected.map((value) => t(value)).join(', ')}
|
||||
inputProps={{ id: 'filter-stock-select' }}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{options.stocks.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<Checkbox
|
||||
disableRipple
|
||||
size="small"
|
||||
checked={stock.includes(option.value)}
|
||||
slotProps={{
|
||||
input: {
|
||||
id: `${option.value}-checkbox`,
|
||||
'aria-label': `${option.label} checkbox`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem
|
||||
onClick={handleFilterStock}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
justifyContent: 'center',
|
||||
fontWeight: theme.typography.button,
|
||||
bgcolor: varAlpha(theme.vars.palette.grey['500Channel'], 0.08),
|
||||
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.16)}`,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{t('Apply')}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl sx={{ flexShrink: 0, width: { xs: 1, md: 200 } }}>
|
||||
<InputLabel htmlFor="filter-publish-select">{t('Publish')}</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={publish}
|
||||
onChange={handleChangePublish}
|
||||
onClose={handleFilterPublish}
|
||||
input={<OutlinedInput label={t('Publish')} />}
|
||||
renderValue={(selected) => selected.map((value) => t(value)).join(', ')}
|
||||
inputProps={{ id: 'filter-publish-select' }}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{options.publishs.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<Checkbox
|
||||
disableRipple
|
||||
size="small"
|
||||
checked={publish.includes(option.value)}
|
||||
slotProps={{
|
||||
input: {
|
||||
id: `${option.value}-checkbox`,
|
||||
'aria-label': `${option.label} checkbox`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<MenuItem
|
||||
disableGutters
|
||||
disableTouchRipple
|
||||
onClick={handleFilterPublish}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
justifyContent: 'center',
|
||||
fontWeight: theme.typography.button,
|
||||
bgcolor: varAlpha(theme.vars.palette.grey['500Channel'], 0.08),
|
||||
border: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.16)}`,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{t('Apply')}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{renderMenuActions()}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
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 { useBoolean } from 'minimal-shared/hooks';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function ConfirmDeleteProductDialog() {
|
||||
const openDialog = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button color="info" variant="outlined" onClick={openDialog.onTrue}>
|
||||
Open alert dialog
|
||||
</Button>
|
||||
|
||||
<Dialog open onClose={openDialog.onFalse}>
|
||||
<DialogTitle>Are you sure delete product ?</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ color: 'text.secondary' }}>
|
||||
Are you sure delete product ?
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={openDialog.onFalse}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button loading variant="contained" onClick={openDialog.onFalse} autoFocus>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
11
03_source/frontend/src/sections/party-event/view/index.ts
Normal file
11
03_source/frontend/src/sections/party-event/view/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './party-event-edit-view';
|
||||
|
||||
export * from './party-event-shop-view';
|
||||
|
||||
export * from './party-event-list-view';
|
||||
|
||||
export * from './party-event-create-view';
|
||||
|
||||
export * from './party-event-details-view';
|
||||
|
||||
export * from './party-event-shop-details-view';
|
@@ -0,0 +1,24 @@
|
||||
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||
import { DashboardContent } from 'src/layouts/dashboard';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { PartyEventNewEditForm } from '../party-event-new-edit-form';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function PartyEventCreateView() {
|
||||
return (
|
||||
<DashboardContent>
|
||||
<CustomBreadcrumbs
|
||||
heading="Create a new party event"
|
||||
links={[
|
||||
{ name: 'Dashboard', href: paths.dashboard.root },
|
||||
{ name: 'Party Event', href: paths.dashboard.product.root },
|
||||
{ name: 'New party event' },
|
||||
]}
|
||||
sx={{ mb: { xs: 3, md: 5 } }}
|
||||
/>
|
||||
|
||||
<PartyEventNewEditForm />
|
||||
</DashboardContent>
|
||||
);
|
||||
}
|
@@ -0,0 +1,186 @@
|
||||
// src/sections/product/view/product-details-view.tsx
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTabs } from 'minimal-shared/hooks';
|
||||
import { varAlpha } from 'minimal-shared/utils';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// import { PRODUCT_PUBLISH_OPTIONS } from 'src/_mock';
|
||||
import { EmptyContent } from 'src/components/empty-content';
|
||||
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 type { IPartyEventItem } from 'src/types/party-event';
|
||||
import { ProductDetailsCarousel } from '../party-event-details-carousel';
|
||||
import { ProductDetailsDescription } from '../party-event-details-description';
|
||||
import { ProductDetailsReview } from '../party-event-details-review';
|
||||
import { PartyEventDetailsSummary } from '../party-event-details-summary';
|
||||
import { ProductDetailsToolbar } from '../party-event-details-toolbar';
|
||||
import { ProductDetailsSkeleton } from '../party-event-skeleton';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const SUMMARY = [
|
||||
{
|
||||
title: '100% original',
|
||||
description: 'Chocolate bar candy canes ice cream toffee cookie halvah.',
|
||||
icon: 'solar:verified-check-bold',
|
||||
},
|
||||
{
|
||||
title: '10 days replacement',
|
||||
description: 'Marshmallow biscuit donut dragée fruitcake wafer.',
|
||||
icon: 'solar:clock-circle-bold',
|
||||
},
|
||||
{
|
||||
title: 'Year warranty',
|
||||
description: 'Cotton candy gingerbread cake I love sugar sweet.',
|
||||
icon: 'solar:shield-check-bold',
|
||||
},
|
||||
] as const;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
partyEvent?: IPartyEventItem;
|
||||
loading?: boolean;
|
||||
error?: any;
|
||||
};
|
||||
|
||||
export function PartyEventDetailsView({ partyEvent, error, loading }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tabs = useTabs('description');
|
||||
|
||||
const [publish, setPublish] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (partyEvent) {
|
||||
setPublish(partyEvent?.publish);
|
||||
}
|
||||
}, [partyEvent]);
|
||||
|
||||
const handleChangePublish = useCallback((newValue: string) => {
|
||||
setPublish(newValue);
|
||||
}, []);
|
||||
|
||||
const PRODUCT_PUBLISH_OPTIONS = [
|
||||
{ value: 'published', label: t('Published') },
|
||||
{ value: 'draft', label: t('Draft') },
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<DashboardContent sx={{ pt: 5 }}>
|
||||
<ProductDetailsSkeleton />
|
||||
</DashboardContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<DashboardContent sx={{ pt: 5 }}>
|
||||
<EmptyContent
|
||||
filled
|
||||
title={t('Party event not found!')}
|
||||
action={
|
||||
<Button
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.partyEvent.root}
|
||||
startIcon={<Iconify width={16} icon="eva:arrow-ios-back-fill" />}
|
||||
sx={{ mt: 3 }}
|
||||
>
|
||||
{t('Back to list')}
|
||||
</Button>
|
||||
}
|
||||
sx={{ py: 10, height: 'auto', flexGrow: 'unset' }}
|
||||
/>
|
||||
</DashboardContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardContent>
|
||||
<ProductDetailsToolbar
|
||||
backHref={paths.dashboard.partyEvent.root}
|
||||
liveHref={paths.partyEvent.details(`${partyEvent?.id}`)}
|
||||
editHref={paths.dashboard.partyEvent.edit(`${partyEvent?.id}`)}
|
||||
publish={publish}
|
||||
onChangePublish={handleChangePublish}
|
||||
publishOptions={PRODUCT_PUBLISH_OPTIONS}
|
||||
/>
|
||||
|
||||
<Grid container spacing={{ xs: 3, md: 5, lg: 8 }}>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 7 }}>
|
||||
<ProductDetailsCarousel images={partyEvent?.images ?? []} />
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6, lg: 5 }}>
|
||||
{partyEvent && <PartyEventDetailsSummary disableActions partyEvent={partyEvent} />}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
gap: 5,
|
||||
my: 10,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' },
|
||||
}}
|
||||
>
|
||||
{SUMMARY.map((item) => (
|
||||
<Box key={item.title} sx={{ textAlign: 'center', px: 5 }}>
|
||||
<Iconify icon={item.icon} width={32} sx={{ color: 'primary.main' }} />
|
||||
|
||||
<Typography variant="subtitle1" sx={{ mb: 1, mt: 2 }}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{item.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Card>
|
||||
<Tabs
|
||||
value={tabs.value}
|
||||
onChange={tabs.onChange}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
px: 3,
|
||||
boxShadow: `inset 0 -2px 0 0 ${varAlpha(theme.vars.palette.grey['500Channel'], 0.08)}`,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{[
|
||||
{ value: 'description', label: 'Description' },
|
||||
{ value: 'reviews', label: `Reviews (${partyEvent?.reviews.length})` },
|
||||
].map((tab) => (
|
||||
<Tab key={tab.value} value={tab.value} label={tab.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
{tabs.value === 'description' && (
|
||||
<ProductDetailsDescription description={partyEvent?.description ?? ''} />
|
||||
)}
|
||||
|
||||
{tabs.value === 'reviews' && (
|
||||
<ProductDetailsReview
|
||||
ratings={partyEvent?.ratings ?? []}
|
||||
reviews={partyEvent?.reviews ?? []}
|
||||
totalRatings={partyEvent?.totalRatings ?? 0}
|
||||
totalReviews={partyEvent?.totalReviews ?? 0}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</DashboardContent>
|
||||
);
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||
import { DashboardContent } from 'src/layouts/dashboard';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
import { PartyEventNewEditForm } from '../party-event-new-edit-form';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
partyEvent?: IPartyEventItem;
|
||||
};
|
||||
|
||||
export function PartyEventEditView({ partyEvent }: Props) {
|
||||
return (
|
||||
<DashboardContent>
|
||||
<CustomBreadcrumbs
|
||||
heading="Edit"
|
||||
backHref={paths.dashboard.partyEvent.root}
|
||||
links={[
|
||||
{ name: 'Dashboard', href: paths.dashboard.root },
|
||||
{ name: 'Party Event', href: paths.dashboard.partyEvent.root },
|
||||
{ name: partyEvent?.name },
|
||||
]}
|
||||
sx={{ mb: { xs: 3, md: 5 } }}
|
||||
/>
|
||||
|
||||
<PartyEventNewEditForm currentPartyEvent={partyEvent} />
|
||||
</DashboardContent>
|
||||
);
|
||||
}
|
@@ -0,0 +1,497 @@
|
||||
// src/sections/party-event/view/party-event-list-view.tsx
|
||||
//
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Link from '@mui/material/Link';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import type {
|
||||
GridActionsCellItemProps,
|
||||
GridColDef,
|
||||
GridColumnVisibilityModel,
|
||||
GridRowSelectionModel,
|
||||
GridSlotProps,
|
||||
} from '@mui/x-data-grid';
|
||||
import {
|
||||
DataGrid,
|
||||
GridActionsCellItem,
|
||||
gridClasses,
|
||||
GridToolbarColumnsButton,
|
||||
GridToolbarContainer,
|
||||
GridToolbarExport,
|
||||
GridToolbarFilterButton,
|
||||
GridToolbarQuickFilter,
|
||||
} from '@mui/x-data-grid';
|
||||
import type { UseSetStateReturn } from 'minimal-shared/hooks';
|
||||
import { useBoolean, useSetState } from 'minimal-shared/hooks';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// import { PRODUCT_STOCK_OPTIONS } from 'src/_mock';
|
||||
import { deletePartyEvent, useGetPartyEvents } from 'src/actions/party-event';
|
||||
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||
import { ConfirmDialog } from 'src/components/custom-dialog';
|
||||
import { EmptyContent } from 'src/components/empty-content';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { toast } from 'src/components/snackbar';
|
||||
import { DashboardContent } from 'src/layouts/dashboard';
|
||||
import { endpoints } from 'src/lib/axios';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { IPartyEventItem, IProductTableFilters } from 'src/types/party-event';
|
||||
import { mutate } from 'swr';
|
||||
import { ProductTableFiltersResult } from '../party-event-table-filters-result';
|
||||
import {
|
||||
RenderCellCreatedAt,
|
||||
RenderCellPrice,
|
||||
RenderCellProduct,
|
||||
RenderCellPublish,
|
||||
RenderCellStock,
|
||||
} from '../party-event-table-row';
|
||||
import { PartyEventTableToolbar } from '../party-event-table-toolbar';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const HIDE_COLUMNS = { category: false };
|
||||
|
||||
const HIDE_COLUMNS_TOGGLABLE = ['category', 'actions'];
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function PartyEventListView() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const confirmDialog = useBoolean();
|
||||
|
||||
const PRODUCT_STOCK_OPTIONS = [
|
||||
{ value: 'in stock', label: t('In stock') },
|
||||
{ value: 'low stock', label: t('Low stock') },
|
||||
{ value: 'out of stock', label: t('Out of stock') },
|
||||
];
|
||||
|
||||
const PUBLISH_OPTIONS = [
|
||||
{ value: 'published', label: t('Published') },
|
||||
{ value: 'draft', label: t('Draft') },
|
||||
];
|
||||
|
||||
const confirmDeleteMultiItemsDialog = useBoolean();
|
||||
|
||||
const confirmDeleteSingleItemDialog = useBoolean();
|
||||
const [idToDelete, setIdToDelete] = useState<string | null>(null);
|
||||
|
||||
const { partyEvents, partyEventsLoading } = useGetPartyEvents();
|
||||
|
||||
const [tableData, setTableData] = useState<IPartyEventItem[]>(partyEvents);
|
||||
const [selectedRowIds, setSelectedRowIds] = useState<GridRowSelectionModel>([]);
|
||||
const [filterButtonEl, setFilterButtonEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const filters = useSetState<IProductTableFilters>({ publish: [], stock: [] });
|
||||
const { state: currentFilters } = filters;
|
||||
|
||||
const [columnVisibilityModel, setColumnVisibilityModel] =
|
||||
useState<GridColumnVisibilityModel>(HIDE_COLUMNS);
|
||||
|
||||
useEffect(() => {
|
||||
if (partyEvents.length) {
|
||||
setTableData(partyEvents);
|
||||
}
|
||||
}, [partyEvents]);
|
||||
|
||||
const canReset = currentFilters.publish.length > 0 || currentFilters.stock.length > 0;
|
||||
|
||||
const dataFiltered = applyFilter({
|
||||
inputData: tableData,
|
||||
filters: currentFilters,
|
||||
});
|
||||
|
||||
const handleDeleteSingleRow = useCallback(async () => {
|
||||
// const deleteRow = tableData.filter((row) => row.id !== id);
|
||||
|
||||
try {
|
||||
if (idToDelete) {
|
||||
await deletePartyEvent(idToDelete);
|
||||
toast.success('Delete success!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Delete failed!');
|
||||
}
|
||||
|
||||
// setTableData(deleteRow);
|
||||
setDeleteInProgress(false);
|
||||
}, [idToDelete, mutate]);
|
||||
|
||||
// NOTE: this is working using example from calendar
|
||||
const handleDeleteRow = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await deletePartyEvent(id);
|
||||
|
||||
// invalidate cache to reload list
|
||||
await mutate(endpoints.partyEvent.list);
|
||||
|
||||
toast.success('Delete success!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
toast.error('Delete error!');
|
||||
}
|
||||
},
|
||||
[tableData]
|
||||
);
|
||||
|
||||
const handleDeleteRows = useCallback(() => {
|
||||
const deleteRows = tableData.filter((row) => !selectedRowIds.includes(row.id));
|
||||
|
||||
toast.success('Delete success!');
|
||||
|
||||
setTableData(deleteRows);
|
||||
}, [selectedRowIds, tableData]);
|
||||
|
||||
const CustomToolbarCallback = useCallback(
|
||||
() => (
|
||||
<CustomToolbar
|
||||
filters={filters}
|
||||
canReset={canReset}
|
||||
selectedRowIds={selectedRowIds}
|
||||
setFilterButtonEl={setFilterButtonEl}
|
||||
filteredResults={dataFiltered.length}
|
||||
onOpenConfirmDeleteRows={confirmDeleteMultiItemsDialog.onTrue}
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[currentFilters, selectedRowIds]
|
||||
);
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{ field: 'category', headerName: t('Category'), filterable: false },
|
||||
{
|
||||
field: 'name',
|
||||
headerName: t('Party Event'),
|
||||
flex: 1,
|
||||
minWidth: 360,
|
||||
hideable: false,
|
||||
renderCell: (params) => (
|
||||
<RenderCellProduct
|
||||
params={params}
|
||||
href={paths.dashboard.partyEvent.details(params.row.id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'createdAt',
|
||||
headerName: t('Create-at'),
|
||||
width: 160,
|
||||
renderCell: (params) => <RenderCellCreatedAt params={params} />,
|
||||
},
|
||||
{
|
||||
field: 'inventoryType',
|
||||
headerName: t('Stock'),
|
||||
width: 160,
|
||||
type: 'singleSelect',
|
||||
valueOptions: PRODUCT_STOCK_OPTIONS,
|
||||
renderCell: (params) => <RenderCellStock params={params} />,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
headerName: t('Price'),
|
||||
width: 140,
|
||||
editable: true,
|
||||
renderCell: (params) => <RenderCellPrice params={params} />,
|
||||
},
|
||||
{
|
||||
field: 'publish',
|
||||
headerName: t('Publish'),
|
||||
width: 110,
|
||||
type: 'singleSelect',
|
||||
editable: true,
|
||||
valueOptions: PUBLISH_OPTIONS,
|
||||
renderCell: (params) => <RenderCellPublish params={params} />,
|
||||
},
|
||||
{
|
||||
type: 'actions',
|
||||
field: 'actions',
|
||||
headerName: ' ',
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
width: 80,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
disableColumnMenu: true,
|
||||
getActions: (params) => [
|
||||
<GridActionsLinkItem
|
||||
showInMenu
|
||||
icon={<Iconify icon="solar:eye-bold" />}
|
||||
label="View"
|
||||
href={paths.dashboard.partyEvent.details(params.row.id)}
|
||||
/>,
|
||||
<GridActionsLinkItem
|
||||
showInMenu
|
||||
icon={<Iconify icon="solar:pen-bold" />}
|
||||
label="Edit"
|
||||
href={paths.dashboard.partyEvent.edit(params.row.id)}
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
showInMenu
|
||||
icon={<Iconify icon="solar:trash-bin-trash-bold" />}
|
||||
label="Delete"
|
||||
onClick={() => {
|
||||
setIdToDelete(params.row.id);
|
||||
confirmDeleteSingleItemDialog.onTrue();
|
||||
}}
|
||||
sx={{ color: 'error.main' }}
|
||||
/>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getTogglableColumns = () =>
|
||||
columns
|
||||
.filter((column) => !HIDE_COLUMNS_TOGGLABLE.includes(column.field))
|
||||
.map((column) => column.field);
|
||||
|
||||
const renderDeleteMultipleItemsConfirmDialog = () => (
|
||||
<ConfirmDialog
|
||||
open={confirmDeleteMultiItemsDialog.value}
|
||||
onClose={confirmDeleteMultiItemsDialog.onFalse}
|
||||
title="Delete multiple party events"
|
||||
content={
|
||||
<>
|
||||
Are you sure want to delete <strong> {selectedRowIds.length} </strong> items?
|
||||
</>
|
||||
}
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
handleDeleteRows();
|
||||
confirmDeleteMultiItemsDialog.onFalse();
|
||||
}}
|
||||
>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const [deleteInProgress, setDeleteInProgress] = useState<boolean>(false);
|
||||
const renderDeleteSingleItemConfirmDialog = () => (
|
||||
<ConfirmDialog
|
||||
open={confirmDeleteSingleItemDialog.value}
|
||||
onClose={confirmDeleteSingleItemDialog.onFalse}
|
||||
title="Delete party event"
|
||||
content={<>Are you sure want to delete item?</>}
|
||||
action={
|
||||
<Button
|
||||
loading={deleteInProgress}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
setDeleteInProgress(true);
|
||||
handleDeleteSingleRow();
|
||||
confirmDeleteSingleItemDialog.onFalse();
|
||||
}}
|
||||
>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<CustomBreadcrumbs
|
||||
heading={t('Party Event List')}
|
||||
links={[
|
||||
{ name: t('Dashboard'), href: paths.dashboard.root },
|
||||
{ name: t('Party Event'), href: paths.dashboard.partyEvent.root },
|
||||
{ name: t('List') },
|
||||
]}
|
||||
action={
|
||||
<Button
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.partyEvent.new}
|
||||
variant="contained"
|
||||
startIcon={<Iconify icon="mingcute:add-line" />}
|
||||
>
|
||||
{t('new-party-event')}
|
||||
</Button>
|
||||
}
|
||||
sx={{ mb: { xs: 3, md: 5 } }}
|
||||
/>
|
||||
|
||||
<Card
|
||||
sx={{
|
||||
minHeight: 640,
|
||||
flexGrow: { md: 1 },
|
||||
display: { md: 'flex' },
|
||||
height: { xs: 800, md: '1px' },
|
||||
flexDirection: { md: 'column' },
|
||||
}}
|
||||
>
|
||||
<DataGrid
|
||||
checkboxSelection
|
||||
disableRowSelectionOnClick
|
||||
rows={dataFiltered}
|
||||
columns={columns}
|
||||
loading={partyEventsLoading}
|
||||
getRowHeight={() => 'auto'}
|
||||
pageSizeOptions={[5, 10, 20, { value: -1, label: 'All' }]}
|
||||
initialState={{ pagination: { paginationModel: { pageSize: 10 } } }}
|
||||
onRowSelectionModelChange={(newSelectionModel) => setSelectedRowIds(newSelectionModel)}
|
||||
columnVisibilityModel={columnVisibilityModel}
|
||||
onColumnVisibilityModelChange={(newModel) => setColumnVisibilityModel(newModel)}
|
||||
slots={{
|
||||
toolbar: CustomToolbarCallback,
|
||||
noRowsOverlay: () => <EmptyContent />,
|
||||
noResultsOverlay: () => <EmptyContent title="No results found" />,
|
||||
}}
|
||||
slotProps={{
|
||||
toolbar: { setFilterButtonEl },
|
||||
panel: { anchorEl: filterButtonEl },
|
||||
columnsManagement: { getTogglableColumns },
|
||||
}}
|
||||
sx={{ [`& .${gridClasses.cell}`]: { alignItems: 'center', display: 'inline-flex' } }}
|
||||
/>
|
||||
</Card>
|
||||
</DashboardContent>
|
||||
|
||||
{renderDeleteMultipleItemsConfirmDialog()}
|
||||
{renderDeleteSingleItemConfirmDialog()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
declare module '@mui/x-data-grid' {
|
||||
interface ToolbarPropsOverrides {
|
||||
setFilterButtonEl: React.Dispatch<React.SetStateAction<HTMLButtonElement | null>>;
|
||||
}
|
||||
}
|
||||
|
||||
type CustomToolbarProps = GridSlotProps['toolbar'] & {
|
||||
canReset: boolean;
|
||||
filteredResults: number;
|
||||
selectedRowIds: GridRowSelectionModel;
|
||||
filters: UseSetStateReturn<IProductTableFilters>;
|
||||
|
||||
onOpenConfirmDeleteRows: () => void;
|
||||
};
|
||||
|
||||
function CustomToolbar({
|
||||
filters,
|
||||
canReset,
|
||||
selectedRowIds,
|
||||
filteredResults,
|
||||
setFilterButtonEl,
|
||||
onOpenConfirmDeleteRows,
|
||||
}: CustomToolbarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const PRODUCT_STOCK_OPTIONS = [
|
||||
{ value: 'in stock', label: t('In stock') },
|
||||
{ value: 'low stock', label: t('Low stock') },
|
||||
{ value: 'out of stock', label: t('Out of stock') },
|
||||
];
|
||||
|
||||
const PUBLISH_OPTIONS = [
|
||||
{ value: 'published', label: t('Published') },
|
||||
{ value: 'draft', label: t('Draft') },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<GridToolbarContainer>
|
||||
<PartyEventTableToolbar
|
||||
filters={filters}
|
||||
options={{ stocks: PRODUCT_STOCK_OPTIONS, publishs: PUBLISH_OPTIONS }}
|
||||
/>
|
||||
|
||||
<GridToolbarQuickFilter />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
gap: 1,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{!!selectedRowIds.length && (
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
startIcon={<Iconify icon="solar:trash-bin-trash-bold" />}
|
||||
onClick={onOpenConfirmDeleteRows}
|
||||
>
|
||||
Delete ({selectedRowIds.length})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<GridToolbarColumnsButton />
|
||||
<GridToolbarFilterButton ref={setFilterButtonEl} />
|
||||
<GridToolbarExport />
|
||||
</Box>
|
||||
</GridToolbarContainer>
|
||||
|
||||
{canReset && (
|
||||
<ProductTableFiltersResult
|
||||
filters={filters}
|
||||
totalResults={filteredResults}
|
||||
sx={{ p: 2.5, pt: 0 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type GridActionsLinkItemProps = Pick<GridActionsCellItemProps, 'icon' | 'label' | 'showInMenu'> & {
|
||||
href: string;
|
||||
sx?: SxProps<Theme>;
|
||||
ref?: React.RefObject<HTMLLIElement | null>;
|
||||
};
|
||||
|
||||
export function GridActionsLinkItem({ ref, href, label, icon, sx }: GridActionsLinkItemProps) {
|
||||
return (
|
||||
<MenuItem ref={ref} sx={sx}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href={href}
|
||||
underline="none"
|
||||
color="inherit"
|
||||
sx={{ width: 1, display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
{label}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type ApplyFilterProps = {
|
||||
inputData: IPartyEventItem[];
|
||||
filters: IProductTableFilters;
|
||||
};
|
||||
|
||||
function applyFilter({ inputData, filters }: ApplyFilterProps) {
|
||||
const { stock, publish } = filters;
|
||||
|
||||
if (stock.length) {
|
||||
inputData = inputData.filter((partyEvent) => stock.includes(partyEvent.inventoryType));
|
||||
}
|
||||
|
||||
if (publish.length) {
|
||||
inputData = inputData.filter((partyEvent) => publish.includes(partyEvent.publish));
|
||||
}
|
||||
|
||||
return inputData;
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import Container from '@mui/material/Container';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTabs } from 'minimal-shared/hooks';
|
||||
import { varAlpha } from 'minimal-shared/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
|
||||
import { EmptyContent } from 'src/components/empty-content';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { IPartyEventItem } from 'src/types/party-event';
|
||||
import { useCheckoutContext } from '../../checkout/context';
|
||||
import { CartIcon } from '../cart-icon';
|
||||
import { ProductDetailsCarousel } from '../party-event-details-carousel';
|
||||
import { ProductDetailsDescription } from '../party-event-details-description';
|
||||
import { ProductDetailsReview } from '../party-event-details-review';
|
||||
import { PartyEventDetailsSummary } from '../party-event-details-summary';
|
||||
import { ProductDetailsSkeleton } from '../party-event-skeleton';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const SUMMARY = [
|
||||
{
|
||||
title: '100% original',
|
||||
description: 'Chocolate bar candy canes ice cream toffee cookie halvah.',
|
||||
icon: 'solar:verified-check-bold',
|
||||
},
|
||||
{
|
||||
title: '10 days replacement',
|
||||
description: 'Marshmallow biscuit donut dragée fruitcake wafer.',
|
||||
icon: 'solar:clock-circle-bold',
|
||||
},
|
||||
{
|
||||
title: 'Year warranty',
|
||||
description: 'Cotton candy gingerbread cake I love sugar sweet.',
|
||||
icon: 'solar:shield-check-bold',
|
||||
},
|
||||
] as const;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
product?: IPartyEventItem;
|
||||
loading?: boolean;
|
||||
error?: any;
|
||||
};
|
||||
|
||||
export function ProductShopDetailsView({ product, error, loading }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { state: checkoutState, onAddToCart } = useCheckoutContext();
|
||||
|
||||
const containerStyles: SxProps<Theme> = {
|
||||
mt: 5,
|
||||
mb: 10,
|
||||
};
|
||||
|
||||
const tabs = useTabs('description');
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Container sx={containerStyles}>
|
||||
<ProductDetailsSkeleton />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Container sx={containerStyles}>
|
||||
<EmptyContent
|
||||
filled
|
||||
title={t('Product not found!')}
|
||||
action={
|
||||
<Button
|
||||
component={RouterLink}
|
||||
href={paths.product.root}
|
||||
startIcon={<Iconify width={16} icon="eva:arrow-ios-back-fill" />}
|
||||
sx={{ mt: 3 }}
|
||||
>
|
||||
Back to list
|
||||
</Button>
|
||||
}
|
||||
sx={{ py: 10 }}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container sx={containerStyles}>
|
||||
<CartIcon totalItems={checkoutState.totalItems} />
|
||||
|
||||
<CustomBreadcrumbs
|
||||
links={[
|
||||
{ name: 'Home', href: '/' },
|
||||
{ name: 'Shop', href: paths.product.root },
|
||||
{ name: product?.name },
|
||||
]}
|
||||
sx={{ mb: 5 }}
|
||||
/>
|
||||
|
||||
<Grid container spacing={{ xs: 3, md: 5, lg: 8 }}>
|
||||
<Grid size={{ xs: 12, md: 6, lg: 7 }}>
|
||||
<ProductDetailsCarousel images={product?.images} />
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6, lg: 5 }}>
|
||||
{product && (
|
||||
<PartyEventDetailsSummary
|
||||
partyEvent={product}
|
||||
items={checkoutState.items}
|
||||
onAddToCart={onAddToCart}
|
||||
disableActions={!product?.available}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box
|
||||
sx={{
|
||||
gap: 5,
|
||||
my: 10,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' },
|
||||
}}
|
||||
>
|
||||
{SUMMARY.map((item) => (
|
||||
<Box key={item.title} sx={{ textAlign: 'center', px: 5 }}>
|
||||
<Iconify icon={item.icon} width={32} sx={{ color: 'primary.main' }} />
|
||||
|
||||
<Typography variant="subtitle1" sx={{ mb: 1, mt: 2 }}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{item.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Card>
|
||||
<Tabs
|
||||
value={tabs.value}
|
||||
onChange={tabs.onChange}
|
||||
sx={[
|
||||
(theme) => ({
|
||||
px: 3,
|
||||
boxShadow: `inset 0 -2px 0 0 ${varAlpha(theme.vars.palette.grey['500Channel'], 0.08)}`,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{[
|
||||
{ value: 'description', label: 'Description' },
|
||||
{ value: 'reviews', label: `Reviews (${product?.reviews.length})` },
|
||||
].map((tab) => (
|
||||
<Tab key={tab.value} value={tab.value} label={tab.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
{tabs.value === 'description' && (
|
||||
<ProductDetailsDescription description={product?.description} />
|
||||
)}
|
||||
|
||||
{tabs.value === 'reviews' && (
|
||||
<ProductDetailsReview
|
||||
ratings={product?.ratings}
|
||||
reviews={product?.reviews}
|
||||
totalRatings={product?.totalRatings}
|
||||
totalReviews={product?.totalReviews}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
@@ -0,0 +1,193 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Container from '@mui/material/Container';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { orderBy } from 'es-toolkit';
|
||||
import { useBoolean, useSetState } from 'minimal-shared/hooks';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
PRODUCT_CATEGORY_OPTIONS,
|
||||
PRODUCT_COLOR_OPTIONS,
|
||||
PRODUCT_GENDER_OPTIONS,
|
||||
PRODUCT_RATING_OPTIONS,
|
||||
PRODUCT_SORT_OPTIONS,
|
||||
} from 'src/_mock';
|
||||
import { EmptyContent } from 'src/components/empty-content';
|
||||
import { paths } from 'src/routes/paths';
|
||||
import type { IPartyEventItem, IProductFilters } from 'src/types/party-event';
|
||||
import { useCheckoutContext } from '../../checkout/context';
|
||||
import { CartIcon } from '../cart-icon';
|
||||
import { ProductFiltersDrawer } from '../party-event-filters-drawer';
|
||||
import { ProductFiltersResult } from '../party-event-filters-result';
|
||||
import { PartyEventList } from '../party-event-list';
|
||||
import { ProductSearch } from '../party-event-search';
|
||||
import { ProductSort } from '../party-event-sort';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
products: IPartyEventItem[];
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export function PartyEventShopView({ products, loading }: Props) {
|
||||
const { state: checkoutState } = useCheckoutContext();
|
||||
|
||||
const openFilters = useBoolean();
|
||||
|
||||
const [sortBy, setSortBy] = useState('featured');
|
||||
|
||||
const filters = useSetState<IProductFilters>({
|
||||
gender: [],
|
||||
colors: [],
|
||||
rating: '',
|
||||
category: 'all',
|
||||
priceRange: [0, 200],
|
||||
});
|
||||
const { state: currentFilters } = filters;
|
||||
|
||||
const dataFiltered = applyFilter({
|
||||
inputData: products,
|
||||
filters: currentFilters,
|
||||
sortBy,
|
||||
});
|
||||
|
||||
const canReset =
|
||||
currentFilters.gender.length > 0 ||
|
||||
currentFilters.colors.length > 0 ||
|
||||
currentFilters.rating !== '' ||
|
||||
currentFilters.category !== 'all' ||
|
||||
currentFilters.priceRange[0] !== 0 ||
|
||||
currentFilters.priceRange[1] !== 200;
|
||||
|
||||
const notFound = !dataFiltered.length && canReset;
|
||||
const isEmpty = !loading && !products.length;
|
||||
|
||||
const renderFilters = () => (
|
||||
<Box
|
||||
sx={{
|
||||
gap: 3,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: { xs: 'flex-end', sm: 'center' },
|
||||
}}
|
||||
>
|
||||
<ProductSearch redirectPath={(id: string) => paths.product.details(id)} />
|
||||
|
||||
<Box sx={{ gap: 1, flexShrink: 0, display: 'flex' }}>
|
||||
<ProductFiltersDrawer
|
||||
filters={filters}
|
||||
canReset={canReset}
|
||||
open={openFilters.value}
|
||||
onOpen={openFilters.onTrue}
|
||||
onClose={openFilters.onFalse}
|
||||
options={{
|
||||
colors: PRODUCT_COLOR_OPTIONS,
|
||||
ratings: PRODUCT_RATING_OPTIONS,
|
||||
genders: PRODUCT_GENDER_OPTIONS,
|
||||
categories: ['all', ...PRODUCT_CATEGORY_OPTIONS],
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProductSort
|
||||
sort={sortBy}
|
||||
onSort={(newValue: string) => setSortBy(newValue)}
|
||||
sortOptions={PRODUCT_SORT_OPTIONS}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const renderResults = () => (
|
||||
<ProductFiltersResult filters={filters} totalResults={dataFiltered.length} />
|
||||
);
|
||||
|
||||
const renderNotFound = () => <EmptyContent filled sx={{ py: 10 }} />;
|
||||
|
||||
return (
|
||||
<Container sx={{ mb: 10 }}>
|
||||
<CartIcon totalItems={checkoutState.totalItems} />
|
||||
|
||||
<Typography variant="h4" sx={{ my: { xs: 3, md: 5 } }}>
|
||||
Shop
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2.5} sx={{ mb: { xs: 3, md: 5 } }}>
|
||||
{renderFilters()}
|
||||
{canReset && renderResults()}
|
||||
</Stack>
|
||||
|
||||
{notFound || isEmpty ? (
|
||||
renderNotFound()
|
||||
) : (
|
||||
<PartyEventList partyEvents={dataFiltered} loading={loading} />
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type ApplyFilterProps = {
|
||||
sortBy: string;
|
||||
filters: IProductFilters;
|
||||
inputData: IPartyEventItem[];
|
||||
};
|
||||
|
||||
function applyFilter({ inputData, filters, sortBy }: ApplyFilterProps) {
|
||||
const { gender, category, colors, priceRange, rating } = filters;
|
||||
|
||||
const min = priceRange[0];
|
||||
const max = priceRange[1];
|
||||
|
||||
// Sort by
|
||||
if (sortBy === 'featured') {
|
||||
inputData = orderBy(inputData, ['totalSold'], ['desc']);
|
||||
}
|
||||
|
||||
if (sortBy === 'newest') {
|
||||
inputData = orderBy(inputData, ['createdAt'], ['desc']);
|
||||
}
|
||||
|
||||
if (sortBy === 'priceDesc') {
|
||||
inputData = orderBy(inputData, ['price'], ['desc']);
|
||||
}
|
||||
|
||||
if (sortBy === 'priceAsc') {
|
||||
inputData = orderBy(inputData, ['price'], ['asc']);
|
||||
}
|
||||
|
||||
// filters
|
||||
if (gender.length) {
|
||||
inputData = inputData.filter((product) => product.gender.some((i) => gender.includes(i)));
|
||||
}
|
||||
|
||||
if (category !== 'all') {
|
||||
inputData = inputData.filter((product) => product.category === category);
|
||||
}
|
||||
|
||||
if (colors.length) {
|
||||
inputData = inputData.filter((product) =>
|
||||
product.colors.some((color) => colors.includes(color))
|
||||
);
|
||||
}
|
||||
|
||||
if (min !== 0 || max !== 200) {
|
||||
inputData = inputData.filter((product) => product.price >= min && product.price <= max);
|
||||
}
|
||||
|
||||
if (rating) {
|
||||
inputData = inputData.filter((product) => {
|
||||
const convertRating = (value: string) => {
|
||||
if (value === 'up4Star') return 4;
|
||||
if (value === 'up3Star') return 3;
|
||||
if (value === 'up2Star') return 2;
|
||||
return 1;
|
||||
};
|
||||
return product.totalRatings > convertRating(rating);
|
||||
});
|
||||
}
|
||||
|
||||
return inputData;
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import type { IOrderCustomer } from 'src/types/party-order';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
customer?: IOrderCustomer;
|
||||
};
|
||||
|
||||
export function PartyOrderDetailsCustomer({ customer }: Props) {
|
||||
return (
|
||||
<>
|
||||
<CardHeader
|
||||
title="Customer info"
|
||||
action={
|
||||
<IconButton>
|
||||
<Iconify icon="solar:pen-bold" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<Box sx={{ p: 3, display: 'flex' }}>
|
||||
<Avatar
|
||||
alt={customer?.name}
|
||||
src={customer?.avatarUrl}
|
||||
sx={{ width: 48, height: 48, mr: 2 }}
|
||||
/>
|
||||
|
||||
<Stack spacing={0.5} sx={{ typography: 'body2', alignItems: 'flex-start' }}>
|
||||
<Typography variant="subtitle2">{customer?.name}</Typography>
|
||||
|
||||
<Box sx={{ color: 'text.secondary' }}>{customer?.email}</Box>
|
||||
|
||||
<div>
|
||||
IP address:
|
||||
<Box component="span" sx={{ color: 'text.secondary', ml: 0.25 }}>
|
||||
{customer?.ipAddress}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
startIcon={<Iconify icon="mingcute:add-line" />}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Add to Blacklist
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user