Compare commits

..

3 Commits

Author SHA1 Message Date
louiscklaw
834f58bde1 update, 2025-05-30 01:14:10 +08:00
louiscklaw
98bc3fe3ce update, 2025-05-30 01:13:54 +08:00
louiscklaw
9f5367e35c init user edit, 2025-05-28 23:17:04 +08:00
74 changed files with 1322 additions and 520 deletions

View File

@@ -37,6 +37,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@faker-js/faker": "^9.8.0",
"@mui/material": "^6.4.8", "@mui/material": "^6.4.8",
"@next-auth/prisma-adapter": "^1.0.7", "@next-auth/prisma-adapter": "^1.0.7",
"@prisma/adapter-pg": "^6.8.2", "@prisma/adapter-pg": "^6.8.2",
@@ -75,4 +76,4 @@
"typescript": "^5.8.2", "typescript": "^5.8.2",
"typescript-eslint": "^8.28.0" "typescript-eslint": "^8.28.0"
} }
} }

View File

@@ -511,7 +511,7 @@ model UserCard {
} }
model UserItem { model UserItem {
id Int @id @default(autoincrement()) id String @id @default(uuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// //
@@ -528,6 +528,9 @@ model UserItem {
avatarUrl String avatarUrl String
phoneNumber String phoneNumber String
isVerified Boolean isVerified Boolean
//
username String
password String
} }
model UserAccountBillingHistory { model UserAccountBillingHistory {

View File

@@ -22,6 +22,7 @@ import { userSeed } from './seeds/user';
import { ProductReview } from './seeds/productReview'; import { ProductReview } from './seeds/productReview';
import { ProductItem } from './seeds/productItem'; import { ProductItem } from './seeds/productItem';
import { FileStore } from './seeds/fileStore'; import { FileStore } from './seeds/fileStore';
import { userItemSeed } from './seeds/userItem';
// //
// import { Blog } from './seeds/blog'; // import { Blog } from './seeds/blog';
// import { Mail } from './seeds/mail'; // import { Mail } from './seeds/mail';
@@ -37,6 +38,7 @@ import { FileStore } from './seeds/fileStore';
await ProductReview; await ProductReview;
await FileStore; await FileStore;
await ProductItem; await ProductItem;
await userItemSeed;
// await Blog; // await Blog;
// await Mail; // await Mail;
// await File; // await File;

View File

@@ -8,8 +8,8 @@ async function user() {
create: { create: {
email: 'alice@prisma.io', email: 'alice@prisma.io',
name: 'Alice', name: 'Alice',
password: 'Aa12345678' password: 'Aa12345678',
} },
}); });
const bob = await prisma.user.upsert({ const bob = await prisma.user.upsert({
@@ -18,8 +18,8 @@ async function user() {
create: { create: {
email: 'bob@prisma.io', email: 'bob@prisma.io',
name: 'Bob', name: 'Bob',
password: 'Aa12345678' password: 'Aa12345678',
} },
}); });
console.log('seed user done'); console.log('seed user done');
} }

View File

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

View File

@@ -7,7 +7,6 @@ import prisma from '../../lib/prisma';
export async function GET(req: NextRequest, res: NextResponse) { export async function GET(req: NextRequest, res: NextResponse) {
try { try {
const users = await prisma.user.findMany(); const users = await prisma.user.findMany();
console.log({ users });
return response({ users }, STATUS.OK); return response({ users }, STATUS.OK);
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,4 @@
###
GET http://localhost:7272/api/helloworld

View File

@@ -24,9 +24,7 @@ export async function GET(req: NextRequest) {
const products = _products(); const products = _products();
// Accept search by name or sku // Accept search by name or sku
const results = products.filter( const results = products.filter(({ name, sku }) => name.toLowerCase().includes(query) || sku?.toLowerCase().includes(query));
({ name, sku }) => name.toLowerCase().includes(query) || sku?.toLowerCase().includes(query)
);
logger('[Product] search-results', results.length); logger('[Product] search-results', results.length);

View File

@@ -0,0 +1,53 @@
// src/app/api/user/createUser/route.ts
//
// PURPOSE:
// create user to db
//
// RULES:
// T.B.A.
//
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/**
***************************************
* POST - create User
***************************************
*/
export async function POST(req: NextRequest) {
// logger('[User] list', users.length);
const { data } = await req.json();
const createForm: CreateUserData = data as unknown as CreateUserData;
try {
const user = await prisma.userItem.create({ data: createForm });
return response({ user }, STATUS.OK);
} catch (error) {
return handleError('User - Create', error);
}
}
type CreateUserData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
username: string;
password: string;
};

View File

@@ -0,0 +1,4 @@
###
POST http://localhost:7272/api/user/createUser

View File

@@ -0,0 +1,47 @@
// src/app/api/product/deleteUser/route.ts
//
// PURPOSE:
// delete product 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 prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* handle Delete Users
*************************************** */
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: userId must exist
const userId = searchParams.get('userId');
if (!userId) {
return response({ message: 'User ID is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: userId confirmed exist, run below
const user = await prisma.userItem.delete({
//
where: { id: userId },
});
if (!user) {
return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
}
logger('[User] details', user.id);
return response({ user }, STATUS.OK);
} catch (error) {
return handleError('User - Get details', error);
}
}

View File

@@ -0,0 +1,3 @@
###
DELETE http://localhost:7272/api/user/deleteUser?userId=3f431e6f-ad05-4d60-9c25-6a7e92a954ad

View File

@@ -0,0 +1,47 @@
// src/app/api/product/details/route.ts
//
// PURPOSE:
// read user 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 prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET User detail
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: userId must exist
const userId = searchParams.get('userId');
if (!userId) {
return response({ message: 'userId is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: userId confirmed exist, run below
const user = await prisma.userItem.findFirst({
// include: { reviews: true },
where: { id: userId },
});
if (!user) {
return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
}
logger('[User] details', user.id);
return response({ user }, STATUS.OK);
} catch (error) {
return handleError('Product - Get details', error);
}
}

View File

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

View File

@@ -0,0 +1,30 @@
// src/app/api/product/image/upload/route.ts
//
// PURPOSE:
// handle upload product image
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
// import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET - Products
*************************************** */
export async function POST(req: NextRequest) {
try {
const { data } = await req.json();
console.log('helloworld');
return response({ hello: 'world' }, STATUS.OK);
} catch (error) {
console.log({ hello: 'world' });
return handleError('Product - store product image', error);
}
}

View File

@@ -0,0 +1,22 @@
// src/app/api/product/list/route.ts
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET - Products
*************************************** */
export async function GET() {
try {
const users = await prisma.userItem.findMany();
logger('[User] list', users.length);
return response({ users }, STATUS.OK);
} catch (error) {
return handleError('Product - Get list', error);
}
}

View File

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

View File

@@ -0,0 +1,115 @@
// src/app/api/product/saveProduct/route.ts
//
// PURPOSE:
// save product to db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/** **************************************
* GET - Products
*************************************** */
export async function POST(req: NextRequest) {
// logger('[Product] list', products.length);
const { searchParams } = req.nextUrl;
const userId = searchParams.get('userId');
// RULES: userId must exist
if (!userId) {
return response({ message: 'Product ID is required!' }, STATUS.BAD_REQUEST);
}
const { data } = await req.json();
try {
const user = await prisma.userItem.updateMany({
where: { id: userId },
data: {
status: data.status,
avatarUrl: data.avatarUrl,
isVerified: data.isVerified,
name: data.name,
email: data.email,
phoneNumber: data.phoneNumber,
country: data.country,
state: data.state,
city: data.city,
address: data.address,
zipCode: data.zipCode,
company: data.company,
role: data.role,
//
username: data.username,
password: data.password,
},
});
return response({ user }, STATUS.OK);
} catch (error) {
console.log({ hello: 'world', data });
return handleError('Product - Get list', error);
}
}
export type IProductItem = {
id: string;
sku: string;
name: string;
code: string;
price: number;
taxes: number;
tags: string[];
sizes: string[];
publish: string;
gender: string[];
coverUrl: string;
images: string[];
colors: string[];
quantity: number;
category: string;
available: number;
totalSold: number;
description: string;
totalRatings: number;
totalReviews: number;
// createdAt: IDateValue;
inventoryType: string;
subDescription: string;
priceSale: number | null;
// reviews: IProductReview[];
newLabel: {
content: string;
enabled: boolean;
};
saleLabel: {
content: string;
enabled: boolean;
};
ratings: {
name: string;
starCount: number;
reviewCount: number;
}[];
};
export type IDateValue = string | number | null;
export type IProductReview = {
id: string;
name: string;
rating: number;
comment: string;
helpful: number;
avatarUrl: string;
postedAt: IDateValue;
isPurchased: boolean;
attachments?: string[];
};

View File

@@ -0,0 +1,3 @@
###
POST http://localhost:7272/api/user/list

View File

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

View File

@@ -411,6 +411,11 @@
"@eslint/core" "^0.12.0" "@eslint/core" "^0.12.0"
levn "^0.4.1" levn "^0.4.1"
"@faker-js/faker@^9.8.0":
version "9.8.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.8.0.tgz#3344284028d1c9dc98dee2479f82939310370d88"
integrity sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==
"@humanfs/core@^0.19.1": "@humanfs/core@^0.19.1":
version "0.19.1" version "0.19.1"
resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz"

View File

@@ -10,7 +10,9 @@ const config = {
printWidth: 100, printWidth: 100,
singleQuote: true, singleQuote: true,
trailingComma: 'es5', trailingComma: 'es5',
plugins: ['@ianvs/prettier-plugin-sort-imports'], plugins: [
// '@ianvs/prettier-plugin-sort-imports'
],
}; };
export default config; export default config;

View File

@@ -0,0 +1,197 @@
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import type { IProductItem } from 'src/types/product';
import { IUserItem } from 'src/types/user';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';
// ----------------------------------------------------------------------
const swrOptions: SWRConfiguration = {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
};
// ----------------------------------------------------------------------
type UsersData = {
users: IUserItem[];
};
export function useGetUsers() {
const url = `http://localhost:7272/api/user/list`;
const { data, isLoading, error, isValidating, mutate } = useSWR<UsersData>(
url,
fetcher,
swrOptions
);
const memoizedValue = useMemo(
() => ({
users: data?.users || [],
usersLoading: isLoading,
usersError: error,
usersValidating: isValidating,
usersEmpty: !isLoading && !isValidating && !data?.users.length,
mutate,
}),
[data?.users, error, isLoading, isValidating, mutate]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type UserData = {
user: IUserItem;
};
export function useGetUser(userId: string) {
const url = userId ? [endpoints.user.details, { params: { userId } }] : '';
const { data, isLoading, error, isValidating } = useSWR<UserData>(url, fetcher, swrOptions);
const memoizedValue = useMemo(
() => ({
user: data?.user,
userLoading: isLoading,
userError: error,
userValidating: isValidating,
}),
[data?.user, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SearchResultsData = {
results: IProductItem[];
};
export function useSearchProducts(query: string) {
const url = query ? [endpoints.product.search, { params: { query } }] : '';
const { data, isLoading, error, isValidating } = useSWR<SearchResultsData>(url, fetcher, {
...swrOptions,
keepPreviousData: true,
});
const memoizedValue = useMemo(
() => ({
searchResults: data?.results || [],
searchLoading: isLoading,
searchError: error,
searchValidating: isValidating,
searchEmpty: !isLoading && !isValidating && !data?.results.length,
}),
[data?.results, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SaveUserData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
username: string;
password: string;
};
export async function saveUser(userId: string, saveUserData: SaveUserData) {
// const url = userId ? [endpoints.user.details, { params: { userId } }] : '';
const res = await axiosInstance.post(
//
`http://localhost:7272/api/user/saveUser?userId=${userId}`,
{
data: saveUserData,
}
);
return res;
}
export async function uploadUserImage(saveUserData: SaveUserData) {
console.log('uploadUserImage ?');
// const url = userId ? [endpoints.user.details, { params: { userId } }] : '';
const res = await axiosInstance.get('http://localhost:7272/api/product/helloworld');
return res;
}
// ----------------------------------------------------------------------
type CreateUserData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
username: string;
password: string;
};
export async function createUser(createUserData: CreateUserData) {
console.log('create product ?');
// const url = productId ? [endpoints.product.details, { params: { productId } }] : '';
const res = await axiosInstance.post('http://localhost:7272/api/user/createUser', {
data: createUserData,
});
return res;
}
// ----------------------------------------------------------------------
type DeleteUserResponse = {
success: boolean;
message?: string;
};
export async function deleteUser(userId: string): Promise<DeleteUserResponse> {
const url = `http://localhost:7272/api/user/deleteUser?userId=${userId}`;
try {
const res = await axiosInstance.delete(url);
return {
success: true,
message: 'User deleted successfully',
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to delete product',
};
}
}

View File

@@ -5,17 +5,13 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import type { ConfirmDialogProps } from './types'; import type { ConfirmDialogProps } from './types';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export function ConfirmDialog({ export function ConfirmDialog({ open, title, action, content, onClose, ...other }: ConfirmDialogProps) {
open, const { t } = useTranslation();
title,
action,
content,
onClose,
...other
}: ConfirmDialogProps) {
return ( return (
<Dialog fullWidth maxWidth="xs" open={open} onClose={onClose} {...other}> <Dialog fullWidth maxWidth="xs" open={open} onClose={onClose} {...other}>
<DialogTitle sx={{ pb: 2 }}>{title}</DialogTitle> <DialogTitle sx={{ pb: 2 }}>{title}</DialogTitle>
@@ -26,7 +22,7 @@ export function ConfirmDialog({
{action} {action}
<Button variant="outlined" color="inherit" onClick={onClose}> <Button variant="outlined" color="inherit" onClick={onClose}>
Cancel {t('Cancel')}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@@ -11,24 +11,20 @@ import { megaMenuClasses } from '../styles';
import { NavUl, NavLi } from './nav-elements'; import { NavUl, NavLi } from './nav-elements';
import type { NavSubItemProps, NavSubListProps } from '../types'; import type { NavSubItemProps, NavSubListProps } from '../types';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export function NavSubList({ data, slotProps, ...other }: NavSubListProps) { export function NavSubList({ data, slotProps, ...other }: NavSubListProps) {
const pathname = usePathname(); const pathname = usePathname();
const { t } = useTranslation();
return ( return (
<> <>
{data?.map((list) => ( {data?.map((list) => (
<NavLi key={list?.subheader ?? list.items[0].title} {...other}> <NavLi key={list?.subheader ?? list.items[0].title} {...other}>
{list?.subheader && ( {list?.subheader && (
<Typography <Typography noWrap component="div" variant="subtitle2" className={megaMenuClasses.subheader} sx={{ mb: 1, ...slotProps?.subheader }}>
noWrap
component="div"
variant="subtitle2"
className={megaMenuClasses.subheader}
sx={{ mb: 1, ...slotProps?.subheader }}
>
{list.subheader} {list.subheader}
</Typography> </Typography>
)} )}

View File

@@ -13,16 +13,8 @@ import { Iconify, iconifyClasses } from '../../iconify';
export type NavSubheaderProps = ListSubheaderProps & { open?: boolean }; export type NavSubheaderProps = ListSubheaderProps & { open?: boolean };
export const NavSubheader = styled(({ open, children, className, ...other }: NavSubheaderProps) => ( export const NavSubheader = styled(({ open, children, className, ...other }: NavSubheaderProps) => (
<ListSubheader <ListSubheader disableSticky component="div" {...other} className={mergeClasses([navSectionClasses.subheader, className])}>
disableSticky <Iconify width={16} icon={open ? 'eva:arrow-ios-downward-fill' : 'eva:arrow-ios-forward-fill'} />
component="div"
{...other}
className={mergeClasses([navSectionClasses.subheader, className])}
>
<Iconify
width={16}
icon={open ? 'eva:arrow-ios-downward-fill' : 'eva:arrow-ios-forward-fill'}
/>
{children} {children}
</ListSubheader> </ListSubheader>
))(({ theme }) => ({ ))(({ theme }) => ({

View File

@@ -27,10 +27,7 @@ export function NavSectionHorizontal({
const cssVars = { ...navSectionCssVars.horizontal(theme), ...overridesVars }; const cssVars = { ...navSectionCssVars.horizontal(theme), ...overridesVars };
return ( return (
<Scrollbar <Scrollbar sx={{ height: 1 }} slotProps={{ contentSx: { height: 1, display: 'flex', alignItems: 'center' } }}>
sx={{ height: 1 }}
slotProps={{ contentSx: { height: 1, display: 'flex', alignItems: 'center' } }}
>
<Nav <Nav
className={mergeClasses([navSectionClasses.horizontal, className])} className={mergeClasses([navSectionClasses.horizontal, className])}
sx={[ sx={[
@@ -66,14 +63,7 @@ export function NavSectionHorizontal({
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
function Group({ function Group({ items, render, cssVars, slotProps, checkPermissions, enabledRootRedirect }: NavGroupProps) {
items,
render,
cssVars,
slotProps,
checkPermissions,
enabledRootRedirect,
}: NavGroupProps) {
return ( return (
<NavLi> <NavLi>
<NavUl sx={{ flexDirection: 'row', gap: 'var(--nav-item-gap)' }}> <NavUl sx={{ flexDirection: 'row', gap: 'var(--nav-item-gap)' }}>

View File

@@ -26,11 +26,7 @@ export function NavSectionMini({
const cssVars = { ...navSectionCssVars.mini(theme), ...overridesVars }; const cssVars = { ...navSectionCssVars.mini(theme), ...overridesVars };
return ( return (
<Nav <Nav className={mergeClasses([navSectionClasses.mini, className])} sx={[{ ...cssVars }, ...(Array.isArray(sx) ? sx : [sx])]} {...other}>
className={mergeClasses([navSectionClasses.mini, className])}
sx={[{ ...cssVars }, ...(Array.isArray(sx) ? sx : [sx])]}
{...other}
>
<NavUl sx={{ flex: '1 1 auto', gap: 'var(--nav-item-gap)' }}> <NavUl sx={{ flex: '1 1 auto', gap: 'var(--nav-item-gap)' }}>
{data.map((group) => ( {data.map((group) => (
<Group <Group
@@ -50,14 +46,7 @@ export function NavSectionMini({
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
function Group({ function Group({ items, render, cssVars, slotProps, checkPermissions, enabledRootRedirect }: NavGroupProps) {
items,
render,
cssVars,
slotProps,
checkPermissions,
enabledRootRedirect,
}: NavGroupProps) {
return ( return (
<NavLi> <NavLi>
<NavUl sx={{ gap: 'var(--nav-item-gap)' }}> <NavUl sx={{ gap: 'var(--nav-item-gap)' }}>

View File

@@ -6,6 +6,7 @@ import { Nav, NavLi, NavSubheader, NavUl } from '../components';
import { navSectionClasses, navSectionCssVars } from '../styles'; import { navSectionClasses, navSectionCssVars } from '../styles';
import type { NavGroupProps, NavSectionProps } from '../types'; import type { NavGroupProps, NavSectionProps } from '../types';
import { NavList } from './nav-list'; import { NavList } from './nav-list';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -25,11 +26,7 @@ export function NavSectionVertical({
const cssVars = { ...navSectionCssVars.vertical(theme), ...overridesVars }; const cssVars = { ...navSectionCssVars.vertical(theme), ...overridesVars };
return ( return (
<Nav <Nav className={mergeClasses([navSectionClasses.vertical, className])} sx={[{ ...cssVars }, ...(Array.isArray(sx) ? sx : [sx])]} {...other}>
className={mergeClasses([navSectionClasses.vertical, className])}
sx={[{ ...cssVars }, ...(Array.isArray(sx) ? sx : [sx])]}
{...other}
>
<NavUl sx={{ flex: '1 1 auto', gap: 'var(--nav-item-gap)' }}> <NavUl sx={{ flex: '1 1 auto', gap: 'var(--nav-item-gap)' }}>
{data.map((group) => ( {data.map((group) => (
<Group <Group
@@ -49,15 +46,9 @@ export function NavSectionVertical({
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
function Group({ function Group({ items, render, subheader, slotProps, checkPermissions, enabledRootRedirect }: NavGroupProps) {
items,
render,
subheader,
slotProps,
checkPermissions,
enabledRootRedirect,
}: NavGroupProps) {
const groupOpen = useBoolean(true); const groupOpen = useBoolean(true);
const { t } = useTranslation();
const renderContent = () => ( const renderContent = () => (
<NavUl sx={{ gap: 'var(--nav-item-gap)' }}> <NavUl sx={{ gap: 'var(--nav-item-gap)' }}>
@@ -79,13 +70,8 @@ function Group({
<NavLi> <NavLi>
{subheader ? ( {subheader ? (
<> <>
<NavSubheader <NavSubheader data-title={subheader} open={groupOpen.value} onClick={groupOpen.onToggle} sx={slotProps?.subheader}>
data-title={subheader} {t(subheader)}
open={groupOpen.value}
onClick={groupOpen.onToggle}
sx={slotProps?.subheader}
>
{subheader}
</NavSubheader> </NavSubheader>
<Collapse in={groupOpen.value}>{renderContent()}</Collapse> <Collapse in={groupOpen.value}>{renderContent()}</Collapse>
</> </>

View File

@@ -11,18 +11,12 @@ import { uploadClasses } from './classes';
import { RejectionFiles } from './components/rejection-files'; import { RejectionFiles } from './components/rejection-files';
import type { UploadProps } from './types'; import type { UploadProps } from './types';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export function UploadAvatar({ export function UploadAvatar({ sx, error, value, disabled, helperText, className, ...other }: UploadProps) {
sx, const { t } = useTranslation();
error,
value,
disabled,
helperText,
className,
...other
}: UploadProps) {
const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({ const { getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
multiple: false, multiple: false,
disabled, disabled,
@@ -44,10 +38,7 @@ export function UploadAvatar({
} }
}, [value]); }, [value]);
const renderPreview = () => const renderPreview = () => hasFile && <Image alt="Avatar" src={preview} sx={{ width: 1, height: 1, borderRadius: '50%' }} />;
hasFile && (
<Image alt="Avatar" src={preview} sx={{ width: 1, height: 1, borderRadius: '50%' }} />
);
const renderPlaceholder = () => ( const renderPlaceholder = () => (
<Box <Box
@@ -85,7 +76,7 @@ export function UploadAvatar({
> >
<Iconify icon="solar:camera-add-bold" width={32} /> <Iconify icon="solar:camera-add-bold" width={32} />
<Typography variant="caption">{hasFile ? 'Update photo' : 'Upload photo'}</Typography> <Typography variant="caption">{hasFile ? t('Update photo') : t('Update photo')}</Typography>
</Box> </Box>
); );

View File

@@ -35,9 +35,7 @@ const flattenNavItems = (navItems: NavItem[], parentGroup?: string): OutputItem[
}; };
export function flattenNavSections(navSections: NavSectionProps['data']): OutputItem[] { export function flattenNavSections(navSections: NavSectionProps['data']): OutputItem[] {
return navSections.flatMap((navSection) => return navSections.flatMap((navSection) => flattenNavItems(navSection.items, navSection.subheader));
flattenNavItems(navSection.items, navSection.subheader)
);
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -50,7 +48,5 @@ type ApplyFilterProps = {
export function applyFilter({ inputData, query }: ApplyFilterProps): OutputItem[] { export function applyFilter({ inputData, query }: ApplyFilterProps): OutputItem[] {
if (!query) return inputData; if (!query) return inputData;
return inputData.filter(({ title, path, group }) => return inputData.filter(({ title, path, group }) => [title, path, group].some((field) => field?.toLowerCase().includes(query.toLowerCase())));
[title, path, group].some((field) => field?.toLowerCase().includes(query.toLowerCase()))
);
} }

View File

@@ -50,13 +50,7 @@ export type DashboardLayoutProps = LayoutBaseProps & {
}; };
}; };
export function DashboardLayout({ export function DashboardLayout({ sx, cssVars, children, slotProps, layoutQuery = 'lg' }: DashboardLayoutProps) {
sx,
cssVars,
children,
slotProps,
layoutQuery = 'lg',
}: DashboardLayoutProps) {
const theme = useTheme(); const theme = useTheme();
const { user } = useMockedUser(); const { user } = useMockedUser();
@@ -73,8 +67,7 @@ export function DashboardLayout({
const isNavHorizontal = settings.state.navLayout === 'horizontal'; const isNavHorizontal = settings.state.navLayout === 'horizontal';
const isNavVertical = isNavMini || settings.state.navLayout === 'vertical'; const isNavVertical = isNavMini || settings.state.navLayout === 'vertical';
const canDisplayItemByRole = (allowedRoles: NavItemProps['allowedRoles']): boolean => const canDisplayItemByRole = (allowedRoles: NavItemProps['allowedRoles']): boolean => !allowedRoles?.includes(user?.role);
!allowedRoles?.includes(user?.role);
const renderHeader = () => { const renderHeader = () => {
const headerSlotProps: HeaderSectionProps['slotProps'] = { const headerSlotProps: HeaderSectionProps['slotProps'] = {
@@ -98,27 +91,13 @@ export function DashboardLayout({
</Alert> </Alert>
), ),
bottomArea: isNavHorizontal ? ( bottomArea: isNavHorizontal ? (
<NavHorizontal <NavHorizontal data={navData} layoutQuery={layoutQuery} cssVars={navVars.section} checkPermissions={canDisplayItemByRole} />
data={navData}
layoutQuery={layoutQuery}
cssVars={navVars.section}
checkPermissions={canDisplayItemByRole}
/>
) : null, ) : null,
leftArea: ( leftArea: (
<> <>
{/** @slot Nav mobile */} {/** @slot Nav mobile */}
<MenuButton <MenuButton onClick={onOpen} sx={{ mr: 1, ml: -1, [theme.breakpoints.up(layoutQuery)]: { display: 'none' } }} />
onClick={onOpen} <NavMobile data={navData} open={open} onClose={onClose} cssVars={navVars.section} checkPermissions={canDisplayItemByRole} />
sx={{ mr: 1, ml: -1, [theme.breakpoints.up(layoutQuery)]: { display: 'none' } }}
/>
<NavMobile
data={navData}
open={open}
onClose={onClose}
cssVars={navVars.section}
checkPermissions={canDisplayItemByRole}
/>
{/** @slot Logo */} {/** @slot Logo */}
{isNavHorizontal && ( {isNavHorizontal && (
@@ -131,15 +110,10 @@ export function DashboardLayout({
)} )}
{/** @slot Divider */} {/** @slot Divider */}
{isNavHorizontal && ( {isNavHorizontal && <VerticalDivider sx={{ [theme.breakpoints.up(layoutQuery)]: { display: 'flex' } }} />}
<VerticalDivider sx={{ [theme.breakpoints.up(layoutQuery)]: { display: 'flex' } }} />
)}
{/** @slot Workspace popover */} {/** @slot Workspace popover */}
<WorkspacesPopover <WorkspacesPopover data={_workspaces} sx={{ ...(isNavHorizontal && { color: 'var(--layout-nav-text-primary-color)' }) }} />
data={_workspaces}
sx={{ ...(isNavHorizontal && { color: 'var(--layout-nav-text-primary-color)' }) }}
/>
</> </>
), ),
rightArea: ( rightArea: (
@@ -184,12 +158,7 @@ export function DashboardLayout({
layoutQuery={layoutQuery} layoutQuery={layoutQuery}
cssVars={navVars.section} cssVars={navVars.section}
checkPermissions={canDisplayItemByRole} checkPermissions={canDisplayItemByRole}
onToggleNav={() => onToggleNav={() => settings.setField('navLayout', settings.state.navLayout === 'vertical' ? 'mini' : 'vertical')}
settings.setField(
'navLayout',
settings.state.navLayout === 'vertical' ? 'mini' : 'vertical'
)
}
/> />
); );

View File

@@ -23,18 +23,7 @@ export type NavVerticalProps = React.ComponentProps<'div'> &
}; };
}; };
export function NavVertical({ export function NavVertical({ sx, data, slots, cssVars, className, isNavMini, onToggleNav, checkPermissions, layoutQuery = 'md', ...other }: NavVerticalProps) {
sx,
data,
slots,
cssVars,
className,
isNavMini,
onToggleNav,
checkPermissions,
layoutQuery = 'md',
...other
}: NavVerticalProps) {
const renderNavVertical = () => ( const renderNavVertical = () => (
<> <>
{slots?.topArea ?? ( {slots?.topArea ?? (
@@ -44,12 +33,7 @@ export function NavVertical({
)} )}
<Scrollbar fillContent> <Scrollbar fillContent>
<NavSectionVertical <NavSectionVertical data={data} cssVars={cssVars} checkPermissions={checkPermissions} sx={{ px: 2, flex: '1 1 auto' }} />
data={data}
cssVars={cssVars}
checkPermissions={checkPermissions}
sx={{ px: 2, flex: '1 1 auto' }}
/>
{slots?.bottomArea ?? <NavUpgrade />} {slots?.bottomArea ?? <NavUpgrade />}
</Scrollbar> </Scrollbar>
@@ -110,22 +94,20 @@ export function NavVertical({
const NavRoot = styled('div', { const NavRoot = styled('div', {
shouldForwardProp: (prop: string) => !['isNavMini', 'layoutQuery', 'sx'].includes(prop), shouldForwardProp: (prop: string) => !['isNavMini', 'layoutQuery', 'sx'].includes(prop),
})<Pick<NavVerticalProps, 'isNavMini' | 'layoutQuery'>>( })<Pick<NavVerticalProps, 'isNavMini' | 'layoutQuery'>>(({ isNavMini, layoutQuery = 'md', theme }) => ({
({ isNavMini, layoutQuery = 'md', theme }) => ({ top: 0,
top: 0, left: 0,
left: 0, height: '100%',
height: '100%', display: 'none',
display: 'none', position: 'fixed',
position: 'fixed', flexDirection: 'column',
flexDirection: 'column', zIndex: 'var(--layout-nav-zIndex)',
zIndex: 'var(--layout-nav-zIndex)', backgroundColor: 'var(--layout-nav-bg)',
backgroundColor: 'var(--layout-nav-bg)', width: isNavMini ? 'var(--layout-nav-mini-width)' : 'var(--layout-nav-vertical-width)',
width: isNavMini ? 'var(--layout-nav-mini-width)' : 'var(--layout-nav-vertical-width)', borderRight: `1px solid var(--layout-nav-border-color, ${varAlpha(theme.vars.palette.grey['500Channel'], 0.12)})`,
borderRight: `1px solid var(--layout-nav-border-color, ${varAlpha(theme.vars.palette.grey['500Channel'], 0.12)})`, transition: theme.transitions.create(['width'], {
transition: theme.transitions.create(['width'], { easing: 'var(--layout-transition-easing)',
easing: 'var(--layout-transition-easing)', duration: 'var(--layout-transition-duration)',
duration: 'var(--layout-transition-duration)', }),
}), [theme.breakpoints.up(layoutQuery)]: { display: 'flex' },
[theme.breakpoints.up(layoutQuery)]: { display: 'flex' }, }));
})
);

View File

@@ -1,13 +1,9 @@
import type { CSSObject } from '@mui/material/styles';
import { varAlpha, mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import ButtonBase from '@mui/material/ButtonBase'; import ButtonBase from '@mui/material/ButtonBase';
import type { CSSObject } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import { mergeClasses, varAlpha } from 'minimal-shared/utils';
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
import { createNavItem, navItemStyles, navSectionClasses } from 'src/components/nav-section'; import { createNavItem, navItemStyles, navSectionClasses } from 'src/components/nav-section';
import type { NavItemProps } from '../types'; import type { NavItemProps } from '../types';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -59,11 +55,7 @@ const shouldForwardProp = (prop: string) => !['open', 'active', 'variant', 'sx']
/** /**
* @slot root * @slot root
*/ */
const ItemRoot = styled(ButtonBase, { shouldForwardProp })<StyledState>(({ const ItemRoot = styled(ButtonBase, { shouldForwardProp })<StyledState>(({ active, open, theme }) => {
active,
open,
theme,
}) => {
const dotTransitions: Record<'in' | 'out', CSSObject> = { const dotTransitions: Record<'in' | 'out', CSSObject> = {
in: { opacity: 0, scale: 0 }, in: { opacity: 0, scale: 0 },
out: { opacity: 1, scale: 1 }, out: { opacity: 1, scale: 1 },

View File

@@ -1,14 +1,11 @@
import { useBoolean } from 'minimal-shared/hooks'; import { useBoolean } from 'minimal-shared/hooks';
import { useRef, useEffect, useCallback } from 'react'; import { isActiveLink, isEqualPath, isExternalLink } from 'minimal-shared/utils';
import { isEqualPath, isActiveLink, isExternalLink } from 'minimal-shared/utils'; import { useCallback, useEffect, useRef } from 'react';
import { usePathname } from 'src/routes/hooks'; import { usePathname } from 'src/routes/hooks';
import { Nav, NavDropdown, NavLi, NavUl } from '../components';
import { NavItem } from './nav-desktop-item';
import { Nav, NavLi, NavUl, NavDropdown } from '../components';
import { NavItemDashboard } from './nav-desktop-item-dashboard';
import type { NavListProps, NavSubListProps } from '../types'; import type { NavListProps, NavSubListProps } from '../types';
import { NavItem } from './nav-desktop-item';
import { NavItemDashboard } from './nav-desktop-item-dashboard';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -109,12 +106,7 @@ function NavSubList({ data, subheader, sx, ...other }: NavSubListProps) {
</NavLi> </NavLi>
) : ( ) : (
<NavLi key={item.title} sx={{ mt: 0.75 }}> <NavLi key={item.title} sx={{ mt: 0.75 }}>
<NavItem <NavItem subItem title={item.title} path={item.path} active={isEqualPath(item.path, pathname)} />
subItem
title={item.title}
path={item.path}
active={isEqualPath(item.path, pathname)}
/>
</NavLi> </NavLi>
) )
)} )}

View File

@@ -1,7 +1,6 @@
import { Nav, NavUl } from '../components'; import { Nav, NavUl } from '../components';
import { NavList } from './nav-desktop-list';
import type { NavMainProps } from '../types'; import type { NavMainProps } from '../types';
import { NavList } from './nav-desktop-list';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -1,13 +1,9 @@
import type { CSSObject } from '@mui/material/styles';
import { varAlpha, mergeClasses } from 'minimal-shared/utils';
import { styled } from '@mui/material/styles';
import ButtonBase from '@mui/material/ButtonBase'; import ButtonBase from '@mui/material/ButtonBase';
import type { CSSObject } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import { mergeClasses, varAlpha } from 'minimal-shared/utils';
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
import { createNavItem, navItemStyles, navSectionClasses } from 'src/components/nav-section'; import { createNavItem, navItemStyles, navSectionClasses } from 'src/components/nav-section';
import type { NavItemProps } from '../types'; import type { NavItemProps } from '../types';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -1,20 +1,14 @@
import { useRef, useCallback } from 'react';
import { useBoolean } from 'minimal-shared/hooks';
import { varAlpha, isActiveLink, isExternalLink } from 'minimal-shared/utils';
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
import { useBoolean } from 'minimal-shared/hooks';
import { paths } from 'src/routes/paths'; import { isActiveLink, isExternalLink, varAlpha } from 'minimal-shared/utils';
import { usePathname } from 'src/routes/hooks'; import { useCallback, useRef } from 'react';
import { CONFIG } from 'src/global-config';
import { navSectionClasses, NavSectionVertical } from 'src/components/nav-section'; import { navSectionClasses, NavSectionVertical } from 'src/components/nav-section';
import { CONFIG } from 'src/global-config';
import { usePathname } from 'src/routes/hooks';
import { paths } from 'src/routes/paths';
import { NavLi } from '../components'; import { NavLi } from '../components';
import { NavItem } from './nav-mobile-item';
import type { NavListProps } from '../types'; import type { NavListProps } from '../types';
import { NavItem } from './nav-mobile-item';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -1,20 +1,15 @@
import { useEffect } from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Drawer from '@mui/material/Drawer'; import Drawer from '@mui/material/Drawer';
import { useEffect } from 'react';
import { paths } from 'src/routes/paths';
import { usePathname } from 'src/routes/hooks';
import { Logo } from 'src/components/logo'; import { Logo } from 'src/components/logo';
import { Scrollbar } from 'src/components/scrollbar'; import { Scrollbar } from 'src/components/scrollbar';
import { usePathname } from 'src/routes/hooks';
import { Nav, NavUl } from '../components'; import { paths } from 'src/routes/paths';
import { NavList } from './nav-mobile-list';
import { SignInButton } from '../../../components/sign-in-button'; import { SignInButton } from '../../../components/sign-in-button';
import { Nav, NavUl } from '../components';
import type { NavMainProps } from '../types'; import type { NavMainProps } from '../types';
import { NavList } from './nav-mobile-list';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -1,5 +1,5 @@
import type { Theme, SxProps } from '@mui/material/styles';
import type { ButtonBaseProps } from '@mui/material/ButtonBase'; import type { ButtonBaseProps } from '@mui/material/ButtonBase';
import type { SxProps, Theme } from '@mui/material/styles';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -1,5 +1,4 @@
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
import type { AccountDrawerProps } from './components/account-drawer'; import type { AccountDrawerProps } from './components/account-drawer';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -2,6 +2,7 @@
import { import {
arSA as arSACore, arSA as arSACore,
frFR as frFRCore, frFR as frFRCore,
jaJP as jaJPCore,
viVN as viVNCore, viVN as viVNCore,
zhCN as zhCNCore, zhCN as zhCNCore,
zhHK as zhHKCore, zhHK as zhHKCore,
@@ -11,6 +12,7 @@ import {
arSD as arSDDataGrid, arSD as arSDDataGrid,
enUS as enUSDataGrid, enUS as enUSDataGrid,
frFR as frFRDataGrid, frFR as frFRDataGrid,
jaJP as jaJPDataGrid,
viVN as viVNDataGrid, viVN as viVNDataGrid,
zhCN as zhCNDataGrid, zhCN as zhCNDataGrid,
zhHK as zhHKDataGrid, zhHK as zhHKDataGrid,
@@ -19,6 +21,7 @@ import {
import { import {
enUS as enUSDate, enUS as enUSDate,
frFR as frFRDate, frFR as frFRDate,
jaJP as jaJPDate,
viVN as viVNDate, viVN as viVNDate,
zhCN as zhCNDate, zhCN as zhCNDate,
zhHK as zhHKDate, zhHK as zhHKDate,
@@ -94,6 +97,16 @@ export const allLangs = [
}, },
}, },
}, },
{
value: 'jp',
label: 'Japanese',
countryCode: 'JP',
adapterLocale: 'ja',
numberFormat: { code: 'ja-JP', currency: 'JPY' },
systemValue: {
components: { ...jaJPCore.components, ...jaJPDate.components, ...jaJPDataGrid.components },
},
},
]; ];
/** /**

View File

@@ -58,5 +58,68 @@
"draft": "草稿", "draft": "草稿",
"Dashboard": "儀表板", "Dashboard": "儀表板",
"Apply": "應用", "Apply": "應用",
"Name": "名稱",
"Phone number": "電話",
"Company": "公司",
"Role": "角色",
"Status": "狀態",
"New user": "新用戶",
"All": "全部",
"Active": "活躍",
"Pending": "等待",
"Banned": "已封鎖",
"Rejected": "已反對",
"Quick update": "快速更新",
"Address": "地址",
"Followers": "追隨者",
"Follower": "追隨者",
"Following": "正在追隨",
"Friends": "朋友圈",
"Gallery": "相集",
"About": "關於用戶",
"Social": "社交媒體",
"Post": "發帖",
"Image/Video": "相/視頻",
"Streaming": "直播",
"Delete": "清除",
"Cancel": "取消",
"Overview": "概覽",
"Management": "管理",
"Quick Edit": "簡易編輯",
"Choose a country": "選擇一個城市",
"Create user": "新用戶",
"State/region": "State/region",
"Misc": "雜項",
"Email verified": "電郵核實",
"Update photo": "上傳相片",
"Create a new user": "創建新用戶",
"Allowed": "Allowed",
"max size of": "max size of",
"Disabling this will automatically send the user a verification email": "Disabling this will automatically send the user a verification email",
"Full name": "Full name",
"Email address": "Email address",
"Country": "Country",
"City": "City",
"Zip/code": "Zip/code",
"Update success": "更新完成",
"Create success": "創建完成",
"Save changes": "儲存變更",
"Product List": "產品列表",
"View": "詳細資料",
"Completed": "已完成",
"Cancelled": "已取消",
"Refunded": "已退款",
"Date ": "日期",
"Order ": "訂單",
"Customer ": "客戶",
"Items ": "項目",
"Start date ": "開始日期",
"End date ": "結束日期",
"Search customer or order number... ": "搜尋客戶或訂單號碼...",
"Print ": "列印",
"Import ": "匯入",
"Export ": "匯出",
"Product not found!": "產品未找到!",
"Back to list": "返回列表",
"hello": "world" "hello": "world"
} }

View File

@@ -0,0 +1,74 @@
{
"demo": {
"lang": "日本語",
"description": "あなたの次のプロジェクトの起点はMUIに基づいています。簡単なカスタマイズで、より速く、より良いアプリケーションを構築することができます。"
},
"new-product": "新製品",
"back": "戻る",
"App": "アプリケーション",
"Ecommerce": "電子商取引",
"Analytics": "分析",
"Banking": "銀行",
"Booking": "予約",
"File": "ファイル",
"Course": "コース",
"User": "ユーザー",
"Product": "製品",
"Order": "注文",
"Invoice": "請求書",
"Blog": "ブログ",
"Job": "仕事",
"Tour": "ツアー",
"manager": "マネージャー",
"Mail": "メール",
"Chat": "チャット",
"Calendar": "カレンダー",
"Kanban": "かんばん",
"Permission": "権限",
"Level": "レベル",
"Disabled": "無効",
"Label": "ラベル",
"Caption": "キャプション",
"Params": "パラメーター",
"link": "リンク",
"Blank": "空白",
"File-manager": "ファイルマネージャー",
"External-link": "外部リンク",
"Profile": "プロフィール",
"Cards": "カード",
"List": "リスト",
"Create": "作成",
"Edit": "編集",
"Account": "アカウント",
"Details": "詳細",
"Create-at": "作成日",
"Category": "カテゴリー",
"Stock": "在庫",
"Price": "価格",
"Publish": "公開",
"in stock": "在庫あり",
"low stock": "在庫少",
"out of stock": "在庫なし",
"In stock": "在庫あり",
"Low stock": "在庫少",
"Out of stock": "在庫なし",
"Published": "公開済み",
"Draft": "下書き",
"published": "公開済み",
"draft": "下書き",
"Dashboard": "ダッシュボード",
"Apply": "適用",
"Name": "名前",
"Phone number": "電話番号",
"Company": "会社",
"Role": "役割",
"Status": "ステータス",
"New user": "新規ユーザー",
"All": "すべて",
"Active": "アクティブ",
"Pending": "保留中",
"Banned": "禁止",
"Rejected": "却下",
"Quick update": "クイックアップデート",
"hello": "world"
}

View File

@@ -0,0 +1,12 @@
{
"app": "アプリケーション",
"job": "仕事",
"user": "ユーザー",
"travel": "旅行",
"invoice": "請求書",
"blog": {
"title": "ブログ",
"caption": "カスタムキーボードショートカットを設定します。"
},
"subheader": "サブヘッダー"
}

View File

@@ -1,7 +1,7 @@
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export const fallbackLng = 'en'; export const fallbackLng = 'en';
export const languages = ['en', 'fr', 'vi', 'cn', 'ar', 'hk']; export const languages = ['en', 'fr', 'vi', 'cn', 'ar', 'hk', 'jp'];
export const defaultNS = 'common'; export const defaultNS = 'common';
export type LanguageValue = (typeof languages)[number]; export type LanguageValue = (typeof languages)[number];

View File

@@ -13,7 +13,6 @@ export default function Page() {
const { id = '' } = useParams(); const { id = '' } = useParams();
const { product } = useGetProduct(id); const { product } = useGetProduct(id);
console.log({ id });
return ( return (
<> <>

View File

@@ -1,9 +1,8 @@
import { useParams } from 'src/routes/hooks'; // import { _userList } from 'src/_mock/_user';
import { CONFIG } from 'src/global-config'; import { CONFIG } from 'src/global-config';
import { _userList } from 'src/_mock/_user'; import { useParams } from 'src/routes/hooks';
import { UserEditView } from 'src/sections/user/view'; import { UserEditView } from 'src/sections/user/view';
import { useGetUser } from 'src/actions/user';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -12,13 +11,15 @@ const metadata = { title: `User edit | Dashboard - ${CONFIG.appName}` };
export default function Page() { export default function Page() {
const { id = '' } = useParams(); const { id = '' } = useParams();
const currentUser = _userList.find((user) => user.id === id); // TODO: remove me
// const currentUser = _userList.find((user) => user.id === id);
const { user } = useGetUser(id);
return ( return (
<> <>
<title>{metadata.title}</title> <title>{metadata.title}</title>
<UserEditView user={currentUser} /> <UserEditView user={user} />
</> </>
); );
} }

View File

@@ -1,5 +1,4 @@
import { CONFIG } from 'src/global-config'; import { CONFIG } from 'src/global-config';
import { UserListView } from 'src/sections/user/view'; import { UserListView } from 'src/sections/user/view';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -1,5 +1,4 @@
import { CONFIG } from 'src/global-config'; import { CONFIG } from 'src/global-config';
import { UserProfileView } from 'src/sections/user/view'; import { UserProfileView } from 'src/sections/user/view';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -63,9 +63,7 @@ export function AccountNotifications({ sx, ...other }: CardProps) {
}); });
const getSelected = (selectedItems: string[], item: string) => const getSelected = (selectedItems: string[], item: string) =>
selectedItems.includes(item) selectedItems.includes(item) ? selectedItems.filter((value) => value !== item) : [...selectedItems, item];
? selectedItems.filter((value) => value !== item)
: [...selectedItems, item];
return ( return (
<Form methods={methods} onSubmit={onSubmit}> <Form methods={methods} onSubmit={onSubmit}>

View File

@@ -49,23 +49,15 @@ import { InvoiceAnalytic } from '../invoice-analytic';
import { InvoiceTableRow } from '../invoice-table-row'; import { InvoiceTableRow } from '../invoice-table-row';
import { InvoiceTableToolbar } from '../invoice-table-toolbar'; import { InvoiceTableToolbar } from '../invoice-table-toolbar';
import { InvoiceTableFiltersResult } from '../invoice-table-filters-result'; import { InvoiceTableFiltersResult } from '../invoice-table-filters-result';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
const TABLE_HEAD: TableHeadCellProps[] = [
{ id: 'invoiceNumber', label: 'Customer' },
{ id: 'createDate', label: 'Create' },
{ id: 'dueDate', label: 'Due' },
{ id: 'price', label: 'Amount' },
{ id: 'sent', label: 'Sent', align: 'center' },
{ id: 'status', label: 'Status' },
{ id: '' },
];
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export function InvoiceListView() { export function InvoiceListView() {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation();
const table = useTable({ defaultOrderBy: 'createDate' }); const table = useTable({ defaultOrderBy: 'createDate' });
@@ -73,6 +65,16 @@ export function InvoiceListView() {
const [tableData, setTableData] = useState<IInvoice[]>(_invoices); const [tableData, setTableData] = useState<IInvoice[]>(_invoices);
const TABLE_HEAD: TableHeadCellProps[] = [
{ id: 'invoiceNumber', label: t('Customer') },
{ id: 'createDate', label: t('Create') },
{ id: 'dueDate', label: t('Due') },
{ id: 'price', label: t('Amount') },
{ id: 'sent', label: t('Sent'), align: 'center' },
{ id: 'status', label: t('Status') },
{ id: '' },
];
const filters = useSetState<IInvoiceTableFilters>({ const filters = useSetState<IInvoiceTableFilters>({
name: '', name: '',
service: [], service: [],

View File

@@ -1,3 +1,4 @@
// src/sections/order/view/order-list-view.tsx
import type { IOrderItem } from 'src/types/order'; import type { IOrderItem } from 'src/types/order';
import { useBoolean, usePopover } from 'minimal-shared/hooks'; import { useBoolean, usePopover } from 'minimal-shared/hooks';
@@ -26,6 +27,7 @@ import { Label } from 'src/components/label';
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
import { ConfirmDialog } from 'src/components/custom-dialog'; import { ConfirmDialog } from 'src/components/custom-dialog';
import { CustomPopover } from 'src/components/custom-popover'; import { CustomPopover } from 'src/components/custom-popover';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -41,6 +43,7 @@ export function OrderTableRow({ row, selected, onSelectRow, onDeleteRow, details
const confirmDialog = useBoolean(); const confirmDialog = useBoolean();
const menuActions = usePopover(); const menuActions = usePopover();
const collapseRow = useBoolean(); const collapseRow = useBoolean();
const { t } = useTranslation();
const renderPrimaryRow = () => ( const renderPrimaryRow = () => (
<TableRow hover selected={selected}> <TableRow hover selected={selected}>
@@ -195,13 +198,13 @@ export function OrderTableRow({ row, selected, onSelectRow, onDeleteRow, details
sx={{ color: 'error.main' }} sx={{ color: 'error.main' }}
> >
<Iconify icon="solar:trash-bin-trash-bold" /> <Iconify icon="solar:trash-bin-trash-bold" />
Delete {t('Delete')}
</MenuItem> </MenuItem>
<li> <li>
<MenuItem component={RouterLink} href={detailsHref} onClick={() => menuActions.onClose()}> <MenuItem component={RouterLink} href={detailsHref} onClick={() => menuActions.onClose()}>
<Iconify icon="solar:eye-bold" /> <Iconify icon="solar:eye-bold" />
View {t('View')}
</MenuItem> </MenuItem>
</li> </li>
</MenuList> </MenuList>

View File

@@ -16,6 +16,7 @@ import { formHelperTextClasses } from '@mui/material/FormHelperText';
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
import { CustomPopover } from 'src/components/custom-popover'; import { CustomPopover } from 'src/components/custom-popover';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -26,6 +27,7 @@ type Props = {
}; };
export function OrderTableToolbar({ filters, onResetPage, dateError }: Props) { export function OrderTableToolbar({ filters, onResetPage, dateError }: Props) {
const { t } = useTranslation();
const menuActions = usePopover(); const menuActions = usePopover();
const { state: currentFilters, setState: updateFilters } = filters; const { state: currentFilters, setState: updateFilters } = filters;
@@ -64,17 +66,17 @@ export function OrderTableToolbar({ filters, onResetPage, dateError }: Props) {
<MenuList> <MenuList>
<MenuItem onClick={() => menuActions.onClose()}> <MenuItem onClick={() => menuActions.onClose()}>
<Iconify icon="solar:printer-minimalistic-bold" /> <Iconify icon="solar:printer-minimalistic-bold" />
Print {t('Print')}
</MenuItem> </MenuItem>
<MenuItem onClick={() => menuActions.onClose()}> <MenuItem onClick={() => menuActions.onClose()}>
<Iconify icon="solar:import-bold" /> <Iconify icon="solar:import-bold" />
Import {t('Import')}
</MenuItem> </MenuItem>
<MenuItem onClick={() => menuActions.onClose()}> <MenuItem onClick={() => menuActions.onClose()}>
<Iconify icon="solar:export-bold" /> <Iconify icon="solar:export-bold" />
Export {t('Export')}
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</CustomPopover> </CustomPopover>
@@ -93,7 +95,7 @@ export function OrderTableToolbar({ filters, onResetPage, dateError }: Props) {
}} }}
> >
<DatePicker <DatePicker
label="Start date" label={t('Start date')}
value={currentFilters.startDate} value={currentFilters.startDate}
onChange={handleFilterStartDate} onChange={handleFilterStartDate}
slotProps={{ textField: { fullWidth: true } }} slotProps={{ textField: { fullWidth: true } }}
@@ -101,7 +103,7 @@ export function OrderTableToolbar({ filters, onResetPage, dateError }: Props) {
/> />
<DatePicker <DatePicker
label="End date" label={t('End date')}
value={currentFilters.endDate} value={currentFilters.endDate}
onChange={handleFilterEndDate} onChange={handleFilterEndDate}
slotProps={{ slotProps={{
@@ -133,7 +135,7 @@ export function OrderTableToolbar({ filters, onResetPage, dateError }: Props) {
fullWidth fullWidth
value={currentFilters.name} value={currentFilters.name}
onChange={handleFilterName} onChange={handleFilterName}
placeholder="Search customer or order number..." placeholder={t('Search customer or order number...')}
slotProps={{ slotProps={{
input: { input: {
startAdornment: ( startAdornment: (

View File

@@ -1,3 +1,5 @@
// src/sections/order/view/order-list-view.tsx
import type { TableHeadCellProps } from 'src/components/table'; import type { TableHeadCellProps } from 'src/components/table';
import type { IOrderItem, IOrderTableFilters } from 'src/types/order'; import type { IOrderItem, IOrderTableFilters } from 'src/types/order';
@@ -43,30 +45,33 @@ import {
import { OrderTableRow } from '../order-table-row'; import { OrderTableRow } from '../order-table-row';
import { OrderTableToolbar } from '../order-table-toolbar'; import { OrderTableToolbar } from '../order-table-toolbar';
import { OrderTableFiltersResult } from '../order-table-filters-result'; import { OrderTableFiltersResult } from '../order-table-filters-result';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
const STATUS_OPTIONS = [{ value: 'all', label: 'All' }, ...ORDER_STATUS_OPTIONS]; const STATUS_OPTIONS = [{ value: 'all', label: 'All' }, ...ORDER_STATUS_OPTIONS];
const TABLE_HEAD: TableHeadCellProps[] = [
{ id: 'orderNumber', label: 'Order', width: 88 },
{ id: 'name', label: 'Customer' },
{ id: 'createdAt', label: 'Date', width: 140 },
{ id: 'totalQuantity', label: 'Items', width: 120, align: 'center' },
{ id: 'totalAmount', label: 'Price', width: 140 },
{ id: 'status', label: 'Status', width: 110 },
{ id: '', width: 88 },
];
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export function OrderListView() { export function OrderListView() {
const { t } = useTranslation();
const table = useTable({ defaultOrderBy: 'orderNumber' }); const table = useTable({ defaultOrderBy: 'orderNumber' });
const confirmDialog = useBoolean(); const confirmDialog = useBoolean();
const [tableData, setTableData] = useState<IOrderItem[]>(_orders); const [tableData, setTableData] = useState<IOrderItem[]>(_orders);
const TABLE_HEAD: TableHeadCellProps[] = [
{ id: 'orderNumber', label: t('Order'), width: 88 },
{ id: 'name', label: t('Customer') },
{ id: 'createdAt', label: t('Date'), width: 140 },
{ id: 'totalQuantity', label: t('Items'), width: 120, align: 'center' },
{ id: 'totalAmount', label: t('Price'), width: 140 },
{ id: 'status', label: t('Status'), width: 110 },
{ id: '', width: 88 },
];
const filters = useSetState<IOrderTableFilters>({ const filters = useSetState<IOrderTableFilters>({
name: '', name: '',
status: 'all', status: 'all',
@@ -143,7 +148,7 @@ export function OrderListView() {
confirmDialog.onFalse(); confirmDialog.onFalse();
}} }}
> >
Delete {t('Delete')}
</Button> </Button>
} }
/> />
@@ -155,9 +160,9 @@ export function OrderListView() {
<CustomBreadcrumbs <CustomBreadcrumbs
heading="List" heading="List"
links={[ links={[
{ name: 'Dashboard', href: paths.dashboard.root }, { name: t('Dashboard'), href: paths.dashboard.root },
{ name: 'Order', href: paths.dashboard.order.root }, { name: t('Order'), href: paths.dashboard.order.root },
{ name: 'List' }, { name: t('List') },
]} ]}
sx={{ mb: { xs: 3, md: 5 } }} sx={{ mb: { xs: 3, md: 5 } }}
/> />
@@ -178,7 +183,7 @@ export function OrderListView() {
key={tab.value} key={tab.value}
iconPosition="end" iconPosition="end"
value={tab.value} value={tab.value}
label={tab.label} label={t(tab.label)}
icon={ icon={
<Label <Label
variant={ variant={
@@ -228,7 +233,7 @@ export function OrderListView() {
) )
} }
action={ action={
<Tooltip title="Delete"> <Tooltip title={t('Delete')}>
<IconButton color="primary" onClick={confirmDialog.onTrue}> <IconButton color="primary" onClick={confirmDialog.onTrue}>
<Iconify icon="solar:trash-bin-trash-bold" /> <Iconify icon="solar:trash-bin-trash-bold" />
</IconButton> </IconButton>

View File

@@ -206,7 +206,6 @@ export function ProductNewEditForm({ currentProduct }: Props) {
if (currentProduct) { if (currentProduct) {
// perform save // perform save
await saveProduct(currentProduct.id, values); await saveProduct(currentProduct.id, values);
} else { } else {
// perform create // perform create
@@ -215,7 +214,8 @@ export function ProductNewEditForm({ currentProduct }: Props) {
toast.success(currentProduct ? 'Update success!' : 'Create success!'); toast.success(currentProduct ? 'Update success!' : 'Create success!');
// router.push(paths.dashboard.product.root); router.push(paths.dashboard.product.root);
// console.info('DATA', updatedData); // console.info('DATA', updatedData);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@@ -1,3 +1,4 @@
// src/sections/product/product-table-row.tsx
import type { GridCellParams } from '@mui/x-data-grid'; import type { GridCellParams } from '@mui/x-data-grid';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@@ -43,7 +44,7 @@ export function RenderCellCreatedAt({ params }: ParamsProps) {
} }
export function RenderCellStock({ params }: ParamsProps) { export function RenderCellStock({ params }: ParamsProps) {
return <>helloworld</> return <>helloworld</>;
return ( return (
<Box sx={{ width: 1, typography: 'caption', color: 'text.secondary' }}> <Box sx={{ width: 1, typography: 'caption', color: 'text.secondary' }}>

View File

@@ -95,14 +95,7 @@ export function ProductTableToolbar({ filters, options }: Props) {
onChange={handleChangeStock} onChange={handleChangeStock}
onClose={handleFilterStock} onClose={handleFilterStock}
input={<OutlinedInput label="Stock" />} input={<OutlinedInput label="Stock" />}
renderValue={(selected) => { renderValue={(selected) => selected.map((value) => t(value)).join(', ')}
return selected
.map((value) => {
console.log(t(value));
return t(value);
})
.join(', ');
}}
inputProps={{ id: 'filter-stock-select' }} inputProps={{ id: 'filter-stock-select' }}
sx={{ textTransform: 'capitalize' }} sx={{ textTransform: 'capitalize' }}
> >

View File

@@ -86,7 +86,7 @@ export function ProductDetailsView({ product, error, loading }: Props) {
<DashboardContent sx={{ pt: 5 }}> <DashboardContent sx={{ pt: 5 }}>
<EmptyContent <EmptyContent
filled filled
title="Product not found!" title={t('Product not found!')}
action={ action={
<Button <Button
component={RouterLink} component={RouterLink}
@@ -94,7 +94,7 @@ export function ProductDetailsView({ product, error, loading }: Props) {
startIcon={<Iconify width={16} icon="eva:arrow-ios-back-fill" />} startIcon={<Iconify width={16} icon="eva:arrow-ios-back-fill" />}
sx={{ mt: 3 }} sx={{ mt: 3 }}
> >
Back to list {t('Back to list')}
</Button> </Button>
} }
sx={{ py: 10, height: 'auto', flexGrow: 'unset' }} sx={{ py: 10, height: 'auto', flexGrow: 'unset' }}

View File

@@ -1,3 +1,4 @@
// src/sections/product/view/product-list-view.tsx
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
@@ -80,6 +81,11 @@ export function ProductListView() {
{ value: 'out of stock', label: t('Out of stock') }, { value: 'out of stock', label: t('Out of stock') },
]; ];
const PUBLISH_OPTIONS = [
{ value: 'published', label: t('Published') },
{ value: 'draft', label: t('Draft') },
];
useEffect(() => { useEffect(() => {
if (products.length) { if (products.length) {
setTableData(products); setTableData(products);
@@ -93,11 +99,6 @@ export function ProductListView() {
filters: currentFilters, filters: currentFilters,
}); });
const PUBLISH_OPTIONS = [
{ value: 'published', label: t('Published') },
{ value: 'draft', label: t('Draft') },
];
const handleDeleteSingleRow = useCallback(async () => { const handleDeleteSingleRow = useCallback(async () => {
// const deleteRow = tableData.filter((row) => row.id !== id); // const deleteRow = tableData.filter((row) => row.id !== id);
@@ -244,7 +245,7 @@ export function ProductListView() {
confirmDeleteMultiItemsDialog.onFalse(); confirmDeleteMultiItemsDialog.onFalse();
}} }}
> >
Delete {t('Delete')}
</Button> </Button>
} }
/> />
@@ -268,7 +269,7 @@ export function ProductListView() {
confirmDeleteSingleItemDialog.onFalse(); confirmDeleteSingleItemDialog.onFalse();
}} }}
> >
Delete {t('Delete')}
</Button> </Button>
} }
/> />
@@ -278,7 +279,7 @@ export function ProductListView() {
<> <>
<DashboardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}> <DashboardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
<CustomBreadcrumbs <CustomBreadcrumbs
heading="List" heading={t('Product List')}
links={[ links={[
{ name: t('Dashboard'), href: paths.dashboard.root }, { name: t('Dashboard'), href: paths.dashboard.root },
{ name: t('Product'), href: paths.dashboard.product.root }, { name: t('Product'), href: paths.dashboard.product.root },

View File

@@ -27,6 +27,7 @@ import { ProductDetailsReview } from '../product-details-review';
import { ProductDetailsSummary } from '../product-details-summary'; import { ProductDetailsSummary } from '../product-details-summary';
import { ProductDetailsCarousel } from '../product-details-carousel'; import { ProductDetailsCarousel } from '../product-details-carousel';
import { ProductDetailsDescription } from '../product-details-description'; import { ProductDetailsDescription } from '../product-details-description';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -57,6 +58,7 @@ type Props = {
}; };
export function ProductShopDetailsView({ product, error, loading }: Props) { export function ProductShopDetailsView({ product, error, loading }: Props) {
const { t } = useTranslation();
const { state: checkoutState, onAddToCart } = useCheckoutContext(); const { state: checkoutState, onAddToCart } = useCheckoutContext();
const containerStyles: SxProps<Theme> = { const containerStyles: SxProps<Theme> = {
@@ -79,7 +81,7 @@ export function ProductShopDetailsView({ product, error, loading }: Props) {
<Container sx={containerStyles}> <Container sx={containerStyles}>
<EmptyContent <EmptyContent
filled filled
title="Product not found!" title={t('Product not found!')}
action={ action={
<Button <Button
component={RouterLink} component={RouterLink}

View File

@@ -21,6 +21,7 @@ import { _socials } from 'src/_mock';
import { Iconify } from 'src/components/iconify'; import { Iconify } from 'src/components/iconify';
import { ProfilePostItem } from './profile-post-item'; import { ProfilePostItem } from './profile-post-item';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -31,6 +32,7 @@ type Props = {
export function ProfileHome({ info, posts }: Props) { export function ProfileHome({ info, posts }: Props) {
const fileRef = useRef<HTMLInputElement>(null); const fileRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const handleAttach = () => { const handleAttach = () => {
if (fileRef.current) { if (fileRef.current) {
@@ -40,21 +42,18 @@ export function ProfileHome({ info, posts }: Props) {
const renderFollows = () => ( const renderFollows = () => (
<Card sx={{ py: 3, textAlign: 'center', typography: 'h4' }}> <Card sx={{ py: 3, textAlign: 'center', typography: 'h4' }}>
<Stack <Stack divider={<Divider orientation="vertical" flexItem sx={{ borderStyle: 'dashed' }} />} sx={{ flexDirection: 'row' }}>
divider={<Divider orientation="vertical" flexItem sx={{ borderStyle: 'dashed' }} />}
sx={{ flexDirection: 'row' }}
>
<Stack sx={{ width: 1 }}> <Stack sx={{ width: 1 }}>
{fNumber(info.totalFollowers)} {fNumber(info.totalFollowers)}
<Box component="span" sx={{ color: 'text.secondary', typography: 'body2' }}> <Box component="span" sx={{ color: 'text.secondary', typography: 'body2' }}>
Follower {t('Follower')}
</Box> </Box>
</Stack> </Stack>
<Stack sx={{ width: 1 }}> <Stack sx={{ width: 1 }}>
{fNumber(info.totalFollowing)} {fNumber(info.totalFollowing)}
<Box component="span" sx={{ color: 'text.secondary', typography: 'body2' }}> <Box component="span" sx={{ color: 'text.secondary', typography: 'body2' }}>
Following {t('Following')}
</Box> </Box>
</Stack> </Stack>
</Stack> </Stack>
@@ -63,7 +62,7 @@ export function ProfileHome({ info, posts }: Props) {
const renderAbout = () => ( const renderAbout = () => (
<Card> <Card>
<CardHeader title="About" /> <CardHeader title={t('About')} />
<Box <Box
sx={{ sx={{
@@ -143,16 +142,16 @@ export function ProfileHome({ info, posts }: Props) {
> >
<Fab size="small" color="inherit" variant="softExtended" onClick={handleAttach}> <Fab size="small" color="inherit" variant="softExtended" onClick={handleAttach}>
<Iconify icon="solar:gallery-wide-bold" width={24} sx={{ color: 'success.main' }} /> <Iconify icon="solar:gallery-wide-bold" width={24} sx={{ color: 'success.main' }} />
Image/Video {t('Image/Video')}
</Fab> </Fab>
<Fab size="small" color="inherit" variant="softExtended"> <Fab size="small" color="inherit" variant="softExtended">
<Iconify icon="solar:videocamera-record-bold" width={24} sx={{ color: 'error.main' }} /> <Iconify icon="solar:videocamera-record-bold" width={24} sx={{ color: 'error.main' }} />
Streaming {t('Streaming')}
</Fab> </Fab>
</Box> </Box>
<Button variant="contained">Post</Button> <Button variant="contained">{t('Post')}</Button>
</Box> </Box>
<input ref={fileRef} type="file" style={{ display: 'none' }} /> <input ref={fileRef} type="file" style={{ display: 'none' }} />
@@ -161,7 +160,7 @@ export function ProfileHome({ info, posts }: Props) {
const renderSocials = () => ( const renderSocials = () => (
<Card> <Card>
<CardHeader title="Social" /> <CardHeader title={t('Social')} />
<Box sx={{ p: 3, gap: 2, display: 'flex', flexDirection: 'column', typography: 'body2' }}> <Box sx={{ p: 3, gap: 2, display: 'flex', flexDirection: 'column', typography: 'body2' }}>
{_socials.map((social) => ( {_socials.map((social) => (

View File

@@ -1,53 +1,53 @@
import type { IUserItem } from 'src/types/user';
import { z as zod } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, Controller } from 'react-hook-form';
import { isValidPhoneNumber } from 'react-phone-number-input/input';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import FormControlLabel from '@mui/material/FormControlLabel';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import FormControlLabel from '@mui/material/FormControlLabel'; import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { paths } from 'src/routes/paths'; import { useTranslation } from 'react-i18next';
import { useRouter } from 'src/routes/hooks'; import { isValidPhoneNumber } from 'react-phone-number-input/input';
import { createUser, deleteUser, saveUser } from 'src/actions/user';
import { fData } from 'src/utils/format-number'; import { Field, Form, schemaHelper } from 'src/components/hook-form';
import { Label } from 'src/components/label'; import { Label } from 'src/components/label';
import { toast } from 'src/components/snackbar'; import { toast } from 'src/components/snackbar';
import { Form, Field, schemaHelper } from 'src/components/hook-form'; import { useRouter } from 'src/routes/hooks';
import { paths } from 'src/routes/paths';
import type { IUserItem } from 'src/types/user';
import { fileToBase64 } from 'src/utils/file-to-base64';
import { fData } from 'src/utils/format-number';
import { z as zod } from 'zod';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export type NewUserSchemaType = zod.infer<typeof NewUserSchema>; export type NewUserSchemaType = zod.infer<typeof NewUserSchema>;
export const NewUserSchema = zod.object({ export const NewUserSchema = zod.object({
avatarUrl: schemaHelper.file({ message: 'Avatar is required!' }),
name: zod.string().min(1, { message: 'Name is required!' }), name: zod.string().min(1, { message: 'Name is required!' }),
city: zod.string().min(1, { message: 'City is required!' }),
role: zod.string().min(1, { message: 'Role is required!' }),
email: zod email: zod
.string() .string()
.min(1, { message: 'Email is required!' }) .min(1, { message: 'Email is required!' })
.email({ message: 'Email must be a valid email address!' }), .email({ message: 'Email must be a valid email address!' }),
phoneNumber: schemaHelper.phoneNumber({ isValid: isValidPhoneNumber }), state: zod.string().min(1, { message: 'State is required!' }),
status: zod.string(),
address: zod.string().min(1, { message: 'Address is required!' }),
country: schemaHelper.nullableInput(zod.string().min(1, { message: 'Country is required!' }), { country: schemaHelper.nullableInput(zod.string().min(1, { message: 'Country is required!' }), {
// message for null value // message for null value
message: 'Country is required!', message: 'Country is required!',
}), }),
address: zod.string().min(1, { message: 'Address is required!' }),
company: zod.string().min(1, { message: 'Company is required!' }),
state: zod.string().min(1, { message: 'State is required!' }),
city: zod.string().min(1, { message: 'City is required!' }),
role: zod.string().min(1, { message: 'Role is required!' }),
zipCode: zod.string().min(1, { message: 'Zip code is required!' }), zipCode: zod.string().min(1, { message: 'Zip code is required!' }),
// Not required company: zod.string().min(1, { message: 'Company is required!' }),
status: zod.string(), avatarUrl: schemaHelper.file({ message: 'Avatar is required!' }),
phoneNumber: schemaHelper.phoneNumber({ isValid: isValidPhoneNumber }),
isVerified: zod.boolean(), isVerified: zod.boolean(),
username: zod.string(),
password: zod.string(),
}); });
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -57,22 +57,27 @@ type Props = {
}; };
export function UserNewEditForm({ currentUser }: Props) { export function UserNewEditForm({ currentUser }: Props) {
const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const defaultValues: NewUserSchemaType = { const defaultValues: NewUserSchemaType = {
status: '', status: '',
avatarUrl: null, avatarUrl: null,
isVerified: true, isVerified: true,
name: '', name: '新用戶名字',
email: '', email: 'user@123.com',
phoneNumber: '', phoneNumber: '+85291234567',
country: '', country: 'Hong Kong',
state: '', state: 'HK',
city: '', city: 'hong kong',
address: '', address: 'Kwun Tong, Sau Mau Ping',
zipCode: '', zipCode: '00000',
company: '', company: 'test company',
role: '', role: 'user',
//
username: '',
password: '',
}; };
const methods = useForm<NewUserSchemaType>({ const methods = useForm<NewUserSchemaType>({
@@ -87,18 +92,50 @@ export function UserNewEditForm({ currentUser }: Props) {
watch, watch,
control, control,
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { errors, isSubmitting },
} = methods; } = methods;
const values = watch(); const values = watch();
const onSubmit = handleSubmit(async (data) => { const [disableDeleteUserButton, setDisableDeleteUserButton] = useState<boolean>(false);
const handleDeleteUserClick = async () => {
setDisableDeleteUserButton(true);
try {
if (currentUser) {
await deleteUser(currentUser.id);
toast.success(t('user deleted'));
router.push(paths.dashboard.user.list);
}
} catch (error) {
console.error(error);
}
setDisableDeleteUserButton(false);
};
const onSubmit = handleSubmit(async (data: any) => {
try { try {
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
reset(); reset();
toast.success(currentUser ? 'Update success!' : 'Create success!');
const temp: any = data.avatarUrl;
if (temp instanceof File) {
data.avatarUrl = await fileToBase64(temp);
}
if (currentUser) {
// perform save
await saveUser(currentUser.id, data);
} else {
// perform create
await createUser(data);
}
toast.success(currentUser ? t('Update success!') : t('Create success!'));
router.push(paths.dashboard.user.list); router.push(paths.dashboard.user.list);
console.info('DATA', data);
// console.info('DATA', data);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@@ -137,8 +174,8 @@ export function UserNewEditForm({ currentUser }: Props) {
color: 'text.disabled', color: 'text.disabled',
}} }}
> >
Allowed *.jpeg, *.jpg, *.png, *.gif {t('Allowed')} *.jpeg, *.jpg, *.png, *.gif
<br /> max size of {fData(3145728)} <br /> {t('max size of')} {fData(3145728)}
</Typography> </Typography>
} }
/> />
@@ -165,10 +202,10 @@ export function UserNewEditForm({ currentUser }: Props) {
label={ label={
<> <>
<Typography variant="subtitle2" sx={{ mb: 0.5 }}> <Typography variant="subtitle2" sx={{ mb: 0.5 }}>
Banned {t('Banned')}
</Typography> </Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}> <Typography variant="body2" sx={{ color: 'text.secondary' }}>
Apply disable account {t('Apply disable account')}
</Typography> </Typography>
</> </>
} }
@@ -187,10 +224,10 @@ export function UserNewEditForm({ currentUser }: Props) {
label={ label={
<> <>
<Typography variant="subtitle2" sx={{ mb: 0.5 }}> <Typography variant="subtitle2" sx={{ mb: 0.5 }}>
Email verified {t('Email verified')}
</Typography> </Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}> <Typography variant="body2" sx={{ color: 'text.secondary' }}>
Disabling this will automatically send the user a verification email {t('Disabling this will automatically send the user a verification email')}
</Typography> </Typography>
</> </>
} }
@@ -199,8 +236,14 @@ export function UserNewEditForm({ currentUser }: Props) {
{currentUser && ( {currentUser && (
<Stack sx={{ mt: 3, alignItems: 'center', justifyContent: 'center' }}> <Stack sx={{ mt: 3, alignItems: 'center', justifyContent: 'center' }}>
<Button variant="soft" color="error"> <Button
Delete user disabled={disableDeleteUserButton}
loading={disableDeleteUserButton}
variant="soft"
color="error"
onClick={handleDeleteUserClick}
>
{t('Delete user')}
</Button> </Button>
</Stack> </Stack>
)} )}
@@ -217,32 +260,33 @@ export function UserNewEditForm({ currentUser }: Props) {
gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)' }, gridTemplateColumns: { xs: 'repeat(1, 1fr)', sm: 'repeat(2, 1fr)' },
}} }}
> >
<Field.Text name="name" label="Full name" /> <Field.Text name="name" label={t('Full name')} />
<Field.Text name="email" label="Email address" /> <Field.Text name="email" label={t('Email address')} />
<Field.Phone <Field.Phone name="phoneNumber" label={t('Phone number')} country="HK" />
name="phoneNumber"
label="Phone number"
country={!currentUser ? 'DE' : undefined}
/>
<Field.CountrySelect <Field.CountrySelect
fullWidth fullWidth
name="country" name="country"
label="Country" label={t('Country')}
placeholder="Choose a country" placeholder={t('Choose a country')}
/> />
<Field.Text name="state" label="State/region" /> <Field.Text name="state" label={t('State/region')} />
<Field.Text name="city" label="City" /> <Field.Text name="city" label={t('City')} />
<Field.Text name="address" label="Address" /> <Field.Text name="address" label={t('Address')} />
<Field.Text name="zipCode" label="Zip/code" /> <Field.Text name="zipCode" label={t('Zip/code')} />
<Field.Text name="company" label="Company" /> <Field.Text name="company" label={t('Company')} />
<Field.Text name="role" label="Role" /> <Field.Text name="role" label={t('Role')} />
</Box> </Box>
<Stack sx={{ mt: 3, alignItems: 'flex-end' }}> <Stack sx={{ mt: 3, alignItems: 'flex-end' }}>
<Button type="submit" variant="contained" loading={isSubmitting}> <Button
{!currentUser ? 'Create user' : 'Save changes'} disabled={isSubmitting}
loading={isSubmitting}
type="submit"
variant="contained"
>
{!currentUser ? t('Create user') : t('Save changes')}
</Button> </Button>
</Stack> </Stack>
</Card> </Card>

View File

@@ -1,23 +1,20 @@
import type { IUserItem } from 'src/types/user';
import { z as zod } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { isValidPhoneNumber } from 'react-phone-number-input/input';
import Box from '@mui/material/Box';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import MenuItem from '@mui/material/MenuItem';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MenuItem from '@mui/material/MenuItem';
import { useForm } from 'react-hook-form';
import { isValidPhoneNumber } from 'react-phone-number-input/input';
import { USER_STATUS_OPTIONS } from 'src/_mock'; import { USER_STATUS_OPTIONS } from 'src/_mock';
import { Field, Form, schemaHelper } from 'src/components/hook-form';
import { toast } from 'src/components/snackbar'; import { toast } from 'src/components/snackbar';
import { Form, Field, schemaHelper } from 'src/components/hook-form'; import { useTranslate } from 'src/locales';
import type { IUserItem } from 'src/types/user';
import { z as zod } from 'zod';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -53,6 +50,8 @@ type Props = {
}; };
export function UserQuickEditForm({ currentUser, open, onClose }: Props) { export function UserQuickEditForm({ currentUser, open, onClose }: Props) {
const { t } = useTranslate();
const defaultValues: UserQuickEditSchemaType = { const defaultValues: UserQuickEditSchemaType = {
name: '', name: '',
email: '', email: '',
@@ -113,7 +112,7 @@ export function UserQuickEditForm({ currentUser, open, onClose }: Props) {
}, },
}} }}
> >
<DialogTitle>Quick update</DialogTitle> <DialogTitle>{t('Quick update')}</DialogTitle>
<Form methods={methods} onSubmit={onSubmit}> <Form methods={methods} onSubmit={onSubmit}>
<DialogContent> <DialogContent>

View File

@@ -1,28 +1,25 @@
import type { IUserItem } from 'src/types/user';
import { useBoolean, usePopover } from 'minimal-shared/hooks';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Tooltip from '@mui/material/Tooltip'; import Box from '@mui/material/Box';
import MenuList from '@mui/material/MenuList'; import Button from '@mui/material/Button';
import MenuItem from '@mui/material/MenuItem';
import TableRow from '@mui/material/TableRow';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import TableCell from '@mui/material/TableCell';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Link from '@mui/material/Link';
import { RouterLink } from 'src/routes/components'; import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import { Label } from 'src/components/label'; import Stack from '@mui/material/Stack';
import { Iconify } from 'src/components/iconify'; import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import Tooltip from '@mui/material/Tooltip';
import { useBoolean, usePopover } from 'minimal-shared/hooks';
import { useTranslation } from 'react-i18next';
import { ConfirmDialog } from 'src/components/custom-dialog'; import { ConfirmDialog } from 'src/components/custom-dialog';
import { CustomPopover } from 'src/components/custom-popover'; import { CustomPopover } from 'src/components/custom-popover';
import { Iconify } from 'src/components/iconify';
import { Label } from 'src/components/label';
import { RouterLink } from 'src/routes/components';
import type { IUserItem } from 'src/types/user';
import { UserQuickEditForm } from './user-quick-edit-form'; import { UserQuickEditForm } from './user-quick-edit-form';
import { useState } from 'react';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -38,6 +35,7 @@ export function UserTableRow({ row, selected, editHref, onSelectRow, onDeleteRow
const menuActions = usePopover(); const menuActions = usePopover();
const confirmDialog = useBoolean(); const confirmDialog = useBoolean();
const quickEditForm = useBoolean(); const quickEditForm = useBoolean();
const { t } = useTranslation();
const renderQuickEditForm = () => ( const renderQuickEditForm = () => (
<UserQuickEditForm <UserQuickEditForm
@@ -58,7 +56,7 @@ export function UserTableRow({ row, selected, editHref, onSelectRow, onDeleteRow
<li> <li>
<MenuItem component={RouterLink} href={editHref} onClick={() => menuActions.onClose()}> <MenuItem component={RouterLink} href={editHref} onClick={() => menuActions.onClose()}>
<Iconify icon="solar:pen-bold" /> <Iconify icon="solar:pen-bold" />
Edit {t('Edit')}
</MenuItem> </MenuItem>
</li> </li>
@@ -70,21 +68,31 @@ export function UserTableRow({ row, selected, editHref, onSelectRow, onDeleteRow
sx={{ color: 'error.main' }} sx={{ color: 'error.main' }}
> >
<Iconify icon="solar:trash-bin-trash-bold" /> <Iconify icon="solar:trash-bin-trash-bold" />
Delete {t('Delete')}
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</CustomPopover> </CustomPopover>
); );
const [disableDeleteButton, setDisableDeleteButton] = useState<boolean>(false);
const renderConfirmDialog = () => ( const renderConfirmDialog = () => (
<ConfirmDialog <ConfirmDialog
open={confirmDialog.value} open={confirmDialog.value}
onClose={confirmDialog.onFalse} onClose={confirmDialog.onFalse}
title="Delete" title={t('Delete')}
content="Are you sure want to delete?" content={t('Are you sure want to delete user?')}
action={ action={
<Button variant="contained" color="error" onClick={onDeleteRow}> <Button
Delete disabled={disableDeleteButton}
loading={disableDeleteButton}
variant="contained"
color="error"
onClick={() => {
setDisableDeleteButton(true);
onDeleteRow();
}}
>
{t('Delete')}
</Button> </Button>
} }
/> />
@@ -148,7 +156,7 @@ export function UserTableRow({ row, selected, editHref, onSelectRow, onDeleteRow
<TableCell> <TableCell>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title="Quick Edit" placement="top" arrow> <Tooltip title={t('Quick Edit')} placement="top" arrow>
<IconButton <IconButton
color={quickEditForm.value ? 'inherit' : 'default'} color={quickEditForm.value ? 'inherit' : 'default'}
onClick={quickEditForm.onTrue} onClick={quickEditForm.onTrue}

View File

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

View File

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

View File

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

View File

@@ -1,69 +1,78 @@
import type { TableHeadCellProps } from 'src/components/table'; // src/sections/user/view/user-list-view.tsx
import type { IUserItem, IUserTableFilters } from 'src/types/user';
import { useState, useCallback } from 'react';
import { varAlpha } from 'minimal-shared/utils';
import { useBoolean, useSetState } from 'minimal-shared/hooks';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Card from '@mui/material/Card';
import Table from '@mui/material/Table';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip'; import Card from '@mui/material/Card';
import TableBody from '@mui/material/TableBody';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Tab from '@mui/material/Tab';
import { paths } from 'src/routes/paths'; import Table from '@mui/material/Table';
import { RouterLink } from 'src/routes/components'; import TableBody from '@mui/material/TableBody';
import Tabs from '@mui/material/Tabs';
import { DashboardContent } from 'src/layouts/dashboard'; import Tooltip from '@mui/material/Tooltip';
import { useBoolean, useSetState } from 'minimal-shared/hooks';
import { varAlpha } from 'minimal-shared/utils';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { _roles, _userList, USER_STATUS_OPTIONS } from 'src/_mock'; import { _roles, _userList, USER_STATUS_OPTIONS } from 'src/_mock';
import { deleteUser, useGetUsers } from 'src/actions/user';
import { Label } from 'src/components/label';
import { toast } from 'src/components/snackbar';
import { Iconify } from 'src/components/iconify';
import { Scrollbar } from 'src/components/scrollbar';
import { ConfirmDialog } from 'src/components/custom-dialog';
import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs'; import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
import { ConfirmDialog } from 'src/components/custom-dialog';
import { Iconify } from 'src/components/iconify';
import { Label } from 'src/components/label';
import { Scrollbar } from 'src/components/scrollbar';
import { toast } from 'src/components/snackbar';
import type { TableHeadCellProps } from 'src/components/table';
import { import {
useTable,
emptyRows, emptyRows,
rowInPage,
TableNoData,
getComparator, getComparator,
rowInPage,
TableEmptyRows, TableEmptyRows,
TableHeadCustom, TableHeadCustom,
TableSelectedAction, TableNoData,
TablePaginationCustom, TablePaginationCustom,
TableSelectedAction,
useTable,
} from 'src/components/table'; } from 'src/components/table';
import { DashboardContent } from 'src/layouts/dashboard';
import { RouterLink } from 'src/routes/components';
import { paths } from 'src/routes/paths';
import type { IUserItem, IUserTableFilters } from 'src/types/user';
import { UserTableFiltersResult } from '../user-table-filters-result';
import { UserTableRow } from '../user-table-row'; import { UserTableRow } from '../user-table-row';
import { UserTableToolbar } from '../user-table-toolbar'; import { UserTableToolbar } from '../user-table-toolbar';
import { UserTableFiltersResult } from '../user-table-filters-result'; import { Router } from 'react-router';
import { useRouter } from 'src/routes/hooks';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
const STATUS_OPTIONS = [{ value: 'all', label: 'All' }, ...USER_STATUS_OPTIONS]; const STATUS_OPTIONS = [{ value: 'all', label: 'All' }, ...USER_STATUS_OPTIONS];
const TABLE_HEAD: TableHeadCellProps[] = [
{ id: 'name', label: 'Name' },
{ id: 'phoneNumber', label: 'Phone number', width: 180 },
{ id: 'company', label: 'Company', width: 220 },
{ id: 'role', label: 'Role', width: 180 },
{ id: 'status', label: 'Status', width: 100 },
{ id: '', width: 88 },
];
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export function UserListView() { export function UserListView() {
const { t } = useTranslation();
const router = useRouter();
const TABLE_HEAD: TableHeadCellProps[] = [
{ id: 'name', label: t('Name') },
{ id: 'phoneNumber', label: t('Phone number'), width: 180 },
{ id: 'company', label: t('Company'), width: 220 },
{ id: 'role', label: t('Role'), width: 180 },
{ id: 'status', label: t('Status'), width: 100 },
{ id: '', width: 88 },
];
const { users, mutate } = useGetUsers();
const [processNewUser, setProcessNewUser] = useState<boolean>(false);
const table = useTable(); const table = useTable();
const confirmDialog = useBoolean(); const confirmDialog = useBoolean();
const [tableData, setTableData] = useState<IUserItem[]>(_userList); const [tableData, setTableData] = useState<IUserItem[]>([]);
useEffect(() => {
setTableData(users);
}, [users]);
const filters = useSetState<IUserTableFilters>({ name: '', role: [], status: 'all' }); const filters = useSetState<IUserTableFilters>({ name: '', role: [], status: 'all' });
const { state: currentFilters, setState: updateFilters } = filters; const { state: currentFilters, setState: updateFilters } = filters;
@@ -82,16 +91,22 @@ export function UserListView() {
const notFound = (!dataFiltered.length && canReset) || !dataFiltered.length; const notFound = (!dataFiltered.length && canReset) || !dataFiltered.length;
const handleDeleteRow = useCallback( const handleDeleteRow = useCallback(
(id: string) => { async (id: string) => {
const deleteRow = tableData.filter((row) => row.id !== id); // const deleteRow = tableData.filter((row) => row.id !== id);
// toast.success('Delete success!');
// setTableData(deleteRow);
// table.onUpdatePageDeleteRow(dataInPage.length);
toast.success('Delete success!'); try {
await deleteUser(id);
setTableData(deleteRow); toast.success('Delete success!');
mutate();
table.onUpdatePageDeleteRow(dataInPage.length); } catch (error) {
console.error(error);
toast.error('Delete failed!');
}
}, },
[dataInPage.length, table, tableData] [table, tableData, mutate]
); );
const handleDeleteRows = useCallback(() => { const handleDeleteRows = useCallback(() => {
@@ -116,7 +131,7 @@ export function UserListView() {
<ConfirmDialog <ConfirmDialog
open={confirmDialog.value} open={confirmDialog.value}
onClose={confirmDialog.onFalse} onClose={confirmDialog.onFalse}
title="Delete" title={t('Delete')}
content={ content={
<> <>
Are you sure want to delete <strong> {table.selected.length} </strong> items? Are you sure want to delete <strong> {table.selected.length} </strong> items?
@@ -128,33 +143,44 @@ export function UserListView() {
color="error" color="error"
onClick={() => { onClick={() => {
handleDeleteRows(); handleDeleteRows();
confirmDialog.onFalse(); // confirmDialog.onFalse();
}} }}
> >
Delete {t('Delete')}
</Button> </Button>
} }
/> />
); );
useEffect(() => {
mutate();
}, []);
return ( return (
<> <>
<DashboardContent> <DashboardContent>
<CustomBreadcrumbs <CustomBreadcrumbs
heading="List" heading="List"
links={[ links={[
{ name: 'Dashboard', href: paths.dashboard.root }, //
{ name: 'User', href: paths.dashboard.user.root }, { name: t('Dashboard'), href: paths.dashboard.root },
{ name: 'List' }, { name: t('User'), href: paths.dashboard.user.root },
{ name: t('List') },
]} ]}
action={ action={
<Button <Button
component={RouterLink} disabled={processNewUser}
href={paths.dashboard.user.new} loading={processNewUser}
// component={RouterLink}
// href={paths.dashboard.user.new}
variant="contained" variant="contained"
startIcon={<Iconify icon="mingcute:add-line" />} startIcon={<Iconify icon="mingcute:add-line" />}
onClick={() => {
setProcessNewUser(true);
router.push(paths.dashboard.user.new);
}}
> >
New user {t('New user')}
</Button> </Button>
} }
sx={{ mb: { xs: 3, md: 5 } }} sx={{ mb: { xs: 3, md: 5 } }}
@@ -176,7 +202,7 @@ export function UserListView() {
key={tab.value} key={tab.value}
iconPosition="end" iconPosition="end"
value={tab.value} value={tab.value}
label={tab.label} label={t(tab.label)}
icon={ icon={
<Label <Label
variant={ variant={

View File

@@ -22,6 +22,7 @@ import { ProfileCover } from '../profile-cover';
import { ProfileFriends } from '../profile-friends'; import { ProfileFriends } from '../profile-friends';
import { ProfileGallery } from '../profile-gallery'; import { ProfileGallery } from '../profile-gallery';
import { ProfileFollowers } from '../profile-followers'; import { ProfileFollowers } from '../profile-followers';
import { useTranslation } from 'react-i18next';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -53,6 +54,8 @@ const NAV_ITEMS = [
const TAB_PARAM = 'tab'; const TAB_PARAM = 'tab';
export function UserProfileView() { export function UserProfileView() {
const { t } = useTranslation();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const selectedTab = searchParams.get(TAB_PARAM) ?? ''; const selectedTab = searchParams.get(TAB_PARAM) ?? '';
@@ -74,21 +77,12 @@ export function UserProfileView() {
<DashboardContent> <DashboardContent>
<CustomBreadcrumbs <CustomBreadcrumbs
heading="Profile" heading="Profile"
links={[ links={[{ name: t('Dashboard'), href: paths.dashboard.root }, { name: t('User'), href: paths.dashboard.user.root }, { name: user?.displayName }]}
{ name: 'Dashboard', href: paths.dashboard.root },
{ name: 'User', href: paths.dashboard.user.root },
{ name: user?.displayName },
]}
sx={{ mb: { xs: 3, md: 5 } }} sx={{ mb: { xs: 3, md: 5 } }}
/> />
<Card sx={{ mb: 3, height: 290 }}> <Card sx={{ mb: 3, height: 290 }}>
<ProfileCover <ProfileCover role={_userAbout.role} name={user?.displayName} avatarUrl={user?.photoURL} coverUrl={_userAbout.coverUrl} />
role={_userAbout.role}
name={user?.displayName}
avatarUrl={user?.photoURL}
coverUrl={_userAbout.coverUrl}
/>
<Box <Box
sx={{ sx={{
@@ -109,7 +103,7 @@ export function UserProfileView() {
key={tab.value} key={tab.value}
value={tab.value} value={tab.value}
icon={tab.icon} icon={tab.icon}
label={tab.label} label={t(tab.label)}
href={createRedirectPath(pathname, tab.value)} href={createRedirectPath(pathname, tab.value)}
/> />
))} ))}
@@ -121,13 +115,7 @@ export function UserProfileView() {
{selectedTab === 'followers' && <ProfileFollowers followers={_userFollowers} />} {selectedTab === 'followers' && <ProfileFollowers followers={_userFollowers} />}
{selectedTab === 'friends' && ( {selectedTab === 'friends' && <ProfileFriends friends={_userFriends} searchFriends={searchFriends} onSearchFriends={handleSearchFriends} />}
<ProfileFriends
friends={_userFriends}
searchFriends={searchFriends}
onSearchFriends={handleSearchFriends}
/>
)}
{selectedTab === 'gallery' && <ProfileGallery gallery={_userGallery} />} {selectedTab === 'gallery' && <ProfileGallery gallery={_userGallery} />}
</DashboardContent> </DashboardContent>

View File

@@ -34,6 +34,11 @@ if (navigator_language.startsWith('sc')) primaryFont = 'Noto Sans SC Variable';
// TODO: not tested, T.B.C. // TODO: not tested, T.B.C.
if (navigator_language.startsWith('jp')) primaryFont = 'Noto Sans JP Variable'; if (navigator_language.startsWith('jp')) primaryFont = 'Noto Sans JP Variable';
console.log({
navigator_language,
primaryFont,
});
export const themeConfig: ThemeConfig = { export const themeConfig: ThemeConfig = {
/** ************************************** /** **************************************
* Base * Base

View File

@@ -89,6 +89,9 @@ export type IUserItem = {
avatarUrl: string; avatarUrl: string;
phoneNumber: string; phoneNumber: string;
isVerified: boolean; isVerified: boolean;
//
username: string;
password: string;
}; };
export type IUserAccountBillingHistory = { export type IUserAccountBillingHistory = {

View File

@@ -33,4 +33,7 @@ export default defineConfig({
}, },
server: { port: PORT, host: true }, server: { port: PORT, host: true },
preview: { port: PORT, host: true }, preview: { port: PORT, host: true },
build: {
sourcemap: true,
},
}); });