init user edit,

This commit is contained in:
louiscklaw
2025-05-28 23:17:04 +08:00
parent db805f23b6
commit 9f5367e35c
39 changed files with 975 additions and 167 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

@@ -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,97 @@
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: 0 },
update: {},
create: {
name: `${firstName} ${lastName}`,
city: '',
role: '',
email: `${username}@${SEED_EMAIL_DOMAIN}`,
state: '',
status: '',
address: '',
country: '',
zipCode: '',
company: '',
avatarUrl: '',
phoneNumber: '',
isVerified: true,
//
username,
password: await generateHash('Abc1234!'),
},
});
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();
const alice = await prisma.userItem.upsert({
where: { id: i },
update: {},
create: {
name: randomFaker.person.fullName(),
city: randomFaker.location.city(),
role: 'user',
email: randomFaker.internet.email(),
state: randomFaker.location.state(),
status: '',
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 };

View File

@@ -0,0 +1,75 @@
// src/app/api/product/createProduct/route.ts
//
// PURPOSE:
// create product 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 - Products
*************************************** */
export async function POST(req: NextRequest) {
// logger('[Product] list', products.length);
const { data } = await req.json();
const createForm: CreateProductData = data as unknown as CreateProductData;
console.log({ createForm });
try {
console.log({ data });
await prisma.productItem.create({ data: createForm });
return response({ hello: 'world' }, STATUS.OK);
} catch (error) {
console.log({ hello: 'world', data });
return handleError('Product - Create', error);
}
}
type CreateProductData = {
// 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;
inventoryType: string;
subDescription: string;
priceSale: number;
newLabel: {
content: string;
enabled: boolean;
};
saleLabel: {
content: string;
enabled: boolean;
};
// ratings: {
// name: string;
// starCount: number;
// reviewCount: number;
// }[];
};

View File

@@ -0,0 +1,44 @@
// src/app/api/product/deleteProduct/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 Products
*************************************** */
export async function DELETE(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: productId must exist
const productId = searchParams.get('productId');
if (!productId) {
return response({ message: 'Product ID is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: productId confirmed exist, run below
const product = await prisma.productItem.delete({ where: { id: productId } });
if (!product) {
return response({ message: 'Product not found!' }, STATUS.NOT_FOUND);
}
logger('[Product] details', product.id);
return response({ product }, STATUS.OK);
} catch (error) {
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,3 @@
###
DELETE http://localhost:7272/api/product/deleteProduct?productId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b06

View File

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

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,4 @@
###
GET http://localhost:7272/api/user/list

View File

@@ -0,0 +1,129 @@
// 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 { data } = await req.json();
try {
const products = await prisma.productItem.updateMany({
data: {
name: data.name,
sku: data.sku,
code: data.code,
price: data.price,
taxes: data.taxes,
tags: data.tags,
sizes: data.sizes,
publish: data.publish,
gender: data.gender,
coverUrl: data.coverUrl,
images: data.images,
colors: data.colors,
quantity: data.quantity,
category: data.category,
available: data.available,
totalSold: data.totalSold,
description: data.description,
totalRatings: data.totalRatings,
totalReviews: data.totalReviews,
inventoryType: data.inventoryType,
subDescription: data.subDescription,
priceSale: data.priceSale,
//
newLabel: {
content: data.newLabel?.content || '',
enabled: data.newLabel?.enabled ?? false,
},
saleLabel: {
content: data.saleLabel?.content || '',
enabled: data.saleLabel?.enabled ?? false,
},
ratings: {
set: data.ratings.map((rating: { name: string; starCount: number; reviewCount: number }) => ({
name: rating.name,
starCount: rating.starCount,
reviewCount: rating.reviewCount,
})),
},
},
where: { id: data.id },
});
return response({ hello: 'world', data }, 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,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

@@ -0,0 +1,236 @@
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 UserData = {
users: IUserItem[];
};
export function useGetUsers() {
// const url = endpoints.user.list;
const url = `http://localhost:7272/api/user/list`;
const { data, isLoading, error, isValidating, mutate } = useSWR<UserData>(
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 ProductData = {
product: IProductItem;
};
export function useGetProduct(productId: string) {
const url = productId ? [endpoints.product.details, { params: { productId } }] : '';
const { data, isLoading, error, isValidating } = useSWR<ProductData>(url, fetcher, swrOptions);
const memoizedValue = useMemo(
() => ({
product: data?.product,
productLoading: isLoading,
productError: error,
productValidating: isValidating,
}),
[data?.product, 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 SaveProductData = {
// id: string;
sku: string;
name: string;
code: string;
price: number | null;
taxes: number | null;
tags: string[];
sizes: string[];
// publish: string;
gender: string[];
// coverUrl: string;
images: (string | File)[];
colors: string[];
quantity: number | null;
category: string;
// available: number;
// totalSold: number;
description: string;
// totalRatings: number;
// totalReviews: number;
// inventoryType: string;
subDescription: string;
priceSale: number | null;
newLabel: {
content: string;
enabled: boolean;
};
saleLabel: {
content: string;
enabled: boolean;
};
// ratings: {
// name: string;
// starCount: number;
// reviewCount: number;
// }[];
};
export async function saveProduct(productId: string, saveProductData: SaveProductData) {
console.log('save product ?');
// const url = productId ? [endpoints.product.details, { params: { productId } }] : '';
const res = await axiosInstance.post('http://localhost:7272/api/product/saveProduct', {
data: saveProductData,
});
return res;
}
export async function uploadProductImage(saveProductData: SaveProductData) {
console.log('save product ?');
// const url = productId ? [endpoints.product.details, { params: { productId } }] : '';
const res = await axiosInstance.get('http://localhost:7272/api/product/helloworld');
return res;
}
// ----------------------------------------------------------------------
type CreateProductData = {
// id: string;
sku: string;
name: string;
code: string;
price: number | null;
taxes: number | null;
tags: string[];
sizes: string[];
publish: string;
gender: string[];
coverUrl: string;
images: (string | File)[];
colors: string[];
quantity: number | null;
category: string;
available: number;
totalSold: number;
description: string;
totalRatings: number;
totalReviews: number;
inventoryType: string;
subDescription: string;
priceSale: number | null;
newLabel: {
content: string;
enabled: boolean;
};
saleLabel: {
content: string;
enabled: boolean;
};
// ratings: {
// name: string;
// starCount: number;
// reviewCount: number;
// }[];
};
export async function createProduct(createProductData: CreateProductData) {
console.log('create product ?');
// const url = productId ? [endpoints.product.details, { params: { productId } }] : '';
const res = await axiosInstance.post('http://localhost:7272/api/product/createProduct', {
data: createProductData,
});
return res;
}
// ----------------------------------------------------------------------
type DeleteProductResponse = {
success: boolean;
message?: string;
};
export async function deleteUser(productId: string): Promise<DeleteProductResponse> {
const url = `http://localhost:7272/api/product/deleteProduct?productId=${productId}`;
try {
const res = await axiosInstance.delete(url);
console.log({ res });
return {
success: true,
message: 'Product deleted successfully',
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to delete product',
};
}
}

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,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';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

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,18 @@
"draft": "草稿", "draft": "草稿",
"Dashboard": "儀表板", "Dashboard": "儀表板",
"Apply": "應用", "Apply": "應用",
"Name": "名稱",
"Phone number": "電話",
"Company": "公司",
"Role": "角色",
"Status": "狀態",
"New user": "新用戶",
"All": "全部",
"Active": "活躍",
"Pending": "等待",
"Banned": "已封鎖",
"Rejected": "已反對",
"Quick update": "快速更新",
"Address": "地址",
"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

@@ -1,8 +1,6 @@
import { useParams } from 'src/routes/hooks';
import { CONFIG } from 'src/global-config';
import { _userList } from 'src/_mock/_user'; import { _userList } from 'src/_mock/_user';
import { CONFIG } from 'src/global-config';
import { useParams } from 'src/routes/hooks';
import { UserEditView } from 'src/sections/user/view'; import { UserEditView } from 'src/sections/user/view';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

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

@@ -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

@@ -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';

View File

@@ -1,27 +1,23 @@
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 { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { paths } from 'src/routes/paths'; import { isValidPhoneNumber } from 'react-phone-number-input/input';
import { useRouter } from 'src/routes/hooks'; import { Field, Form, schemaHelper } from 'src/components/hook-form';
import { fData } from 'src/utils/format-number';
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 { fData } from 'src/utils/format-number';
import { z as zod } from 'zod';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -57,6 +53,8 @@ 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 = {
@@ -234,7 +232,7 @@ export function UserNewEditForm({ currentUser }: Props) {
<Field.Text name="state" label="State/region" /> <Field.Text name="state" label="State/region" />
<Field.Text name="city" label="City" /> <Field.Text name="city" label="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="Zip/code" />
<Field.Text name="company" label="Company" /> <Field.Text name="company" label="Company" />
<Field.Text name="role" label="Role" /> <Field.Text name="role" label="Role" />

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,27 +1,23 @@
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';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -38,6 +34,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
@@ -148,7 +145,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

@@ -1,11 +1,7 @@
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';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -1,70 +1,75 @@
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 { 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';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
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 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 table = useTable(); const table = useTable();
const confirmDialog = useBoolean(); const confirmDialog = useBoolean();
const [tableData, setTableData] = useState<IUserItem[]>(_userList); const [tableData, setTableData] = useState<IUserItem[]>(_userList);
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;
@@ -131,7 +136,7 @@ export function UserListView() {
confirmDialog.onFalse(); confirmDialog.onFalse();
}} }}
> >
Delete {t('Delete')}
</Button> </Button>
} }
/> />
@@ -154,7 +159,7 @@ export function UserListView() {
variant="contained" variant="contained"
startIcon={<Iconify icon="mingcute:add-line" />} startIcon={<Iconify icon="mingcute:add-line" />}
> >
New user {t('New user')}
</Button> </Button>
} }
sx={{ mb: { xs: 3, md: 5 } }} sx={{ mb: { xs: 3, md: 5 } }}
@@ -176,7 +181,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={