build cms ok,

This commit is contained in:
2025-05-23 17:16:47 +08:00
parent 50fc385c2b
commit a94ead44ba
41 changed files with 395 additions and 83 deletions

View File

@@ -87,6 +87,7 @@ module.exports = {
'**/*.bak', '**/*.bak',
'**/*copy.*', '**/*copy.*',
'**/*copy*.*', '**/*copy*.*',
'**/*draft*/**',
// //
], ],
overrides: [ overrides: [

View File

@@ -0,0 +1,5 @@
FROM 192.168.10.61:5000/nvm_ubuntu:latest
WORKDIR /app
RUN nvm install 22

View File

@@ -3,5 +3,10 @@
set -ex set -ex
reset reset
rm -rf .next rm -rf .next
# pnpm run typecheck
# pnpm run lint:w
pnpm run build pnpm run build

7
002_source/cms/scripts/docker/entrypoint.sh Normal file → Executable file
View File

@@ -3,13 +3,16 @@
# set -x # set -x
# nvm use 20 # nvm use 20
nvm install 18
nvm alias default 18 nvm alias default 18
nvm use default nvm use default
node -v node -v
npm i -D sleep infinity
npm run dev # pnpm i -D
# pnpm run start
# while true; do # while true; do
# if [ "$NODE_ENV" = "development" ]; then # if [ "$NODE_ENV" = "development" ]; then

View File

@@ -1,8 +1,26 @@
// src/app/dashboard/students/page.tsx // src/app/dashboard/students/page.tsx
'use client'; 'use client';
import type { Student } from '@/db/Students/type.d'; import type { Student } from '@/db/Students/type.d';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
const sampleBillingAddress = {
city: 'string',
country: 'string',
line1: 'string',
line2: 'string',
state: 'string',
zipCode: 'string',
//
id: 'string',
collectionId: 'string',
collectionName: 'string',
updated: 'string',
created: 'string',
};
export const SampleStudents = [ export const SampleStudents = [
{ {
id: 'STU-005', id: 'STU-005',
@@ -13,6 +31,12 @@ export const SampleStudents = [
quota: 50, quota: 50,
status: 'active', status: 'active',
createdAt: dayjs().subtract(1, 'hour').toDate(), createdAt: dayjs().subtract(1, 'hour').toDate(),
billingAddress: sampleBillingAddress,
state: 'active',
timezone: '',
language: '',
currency: '',
collectionId: '',
}, },
{ {
id: 'STU-004', id: 'STU-004',
@@ -23,6 +47,12 @@ export const SampleStudents = [
quota: 100, quota: 100,
status: 'active', status: 'active',
createdAt: dayjs().subtract(3, 'hour').toDate(), createdAt: dayjs().subtract(3, 'hour').toDate(),
billingAddress: sampleBillingAddress,
state: 'active',
timezone: '',
language: '',
currency: '',
collectionId: '',
}, },
{ {
id: 'STU-003', id: 'STU-003',
@@ -33,6 +63,12 @@ export const SampleStudents = [
quota: 10, quota: 10,
status: 'blocked', status: 'blocked',
createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(),
billingAddress: sampleBillingAddress,
state: 'active',
timezone: '',
language: '',
currency: '',
collectionId: '',
}, },
{ {
id: 'STU-002', id: 'STU-002',
@@ -43,6 +79,12 @@ export const SampleStudents = [
quota: 0, quota: 0,
status: 'pending', status: 'pending',
createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(),
billingAddress: sampleBillingAddress,
state: 'active',
timezone: '',
language: '',
currency: '',
collectionId: '',
}, },
{ {
id: 'STU-001', id: 'STU-001',
@@ -53,5 +95,11 @@ export const SampleStudents = [
quota: 50, quota: 50,
status: 'active', status: 'active',
createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(),
billingAddress: sampleBillingAddress,
state: 'active',
timezone: '',
language: '',
currency: '',
collectionId: '',
}, },
] satisfies Student[]; ] satisfies Student[];

View File

@@ -120,12 +120,12 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
tempFilter.push(`phone ~ "%${phone}%"`); tempFilter.push(`phone ~ "%${phone}%"`);
} }
let preFinalListOption = { filter: '' }; let preFinalListOption = { filter: '', sort: '' };
if (tempFilter.length > 0) { if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') }; preFinalListOption.filter = tempFilter.join(' && ');
} }
if (tempSortDir.length > 0) { if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; preFinalListOption.sort = tempSortDir;
} }
setListOption(preFinalListOption); setListOption(preFinalListOption);
}, [sortDir, email, phone, state]); }, [sortDir, email, phone, state]);

View File

@@ -122,12 +122,12 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
tempFilter.push(`phone ~ "%${phone}%"`); tempFilter.push(`phone ~ "%${phone}%"`);
} }
let preFinalListOption = { filter: '' }; const preFinalListOption = { filter: '', sort: '' };
if (tempFilter.length > 0) { if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') }; preFinalListOption.filter = tempFilter.join(' && ');
} }
if (tempSortDir.length > 0) { if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; preFinalListOption.sort = tempSortDir;
} }
setListOption(preFinalListOption); setListOption(preFinalListOption);
}, [sortDir, email, phone, state]); }, [sortDir, email, phone, state]);

View File

@@ -22,7 +22,7 @@ import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
import ErrorDisplay from '@/components/dashboard/error'; import ErrorDisplay from '@/components/dashboard/error';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants'; import { defaultDBUserMeta, defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import type { UserMeta } from '@/components/dashboard/user_meta/type.d'; import type { UserMeta } from '@/components/dashboard/user_meta/type.d';
import { UserMetasFilters } from '@/components/dashboard/user_meta/user-metas-filters'; import { UserMetasFilters } from '@/components/dashboard/user_meta/user-metas-filters';
import { UserMetasPagination } from '@/components/dashboard/user_meta/user-metas-pagination'; import { UserMetasPagination } from '@/components/dashboard/user_meta/user-metas-pagination';
@@ -62,7 +62,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
.getList(currentPage + 1, rowsPerPage, listOption); .getList(currentPage + 1, rowsPerPage, listOption);
const { items, totalItems } = models; const { items, totalItems } = models;
const tempUserMeta: UserMeta[] = items.map((lt) => { const tempUserMeta: UserMeta[] = items.map((lt) => {
return { ...defaultUserMeta, ...lt }; return lt as unknown as UserMeta;
}); });
setUserMetaData(tempUserMeta); setUserMetaData(tempUserMeta);
@@ -115,12 +115,12 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
tempFilter.push(`phone ~ "%${phone}%"`); tempFilter.push(`phone ~ "%${phone}%"`);
} }
let preFinalListOption = { filter: '' }; const preFinalListOption = { filter: '', sort: '' };
if (tempFilter.length > 0) { if (tempFilter.length > 0) {
preFinalListOption = { filter: tempFilter.join(' && ') }; preFinalListOption.filter = tempFilter.join(' && ');
} }
if (tempSortDir.length > 0) { if (tempSortDir.length > 0) {
preFinalListOption = { ...preFinalListOption, sort: tempSortDir }; preFinalListOption.sort = tempSortDir;
} }
setListOption(preFinalListOption); setListOption(preFinalListOption);
}, [sortDir, email, phone, state]); }, [sortDir, email, phone, state]);

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import type { UserMeta } from '@/db/UserMetas/type';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader'; import CardHeader from '@mui/material/CardHeader';
@@ -13,8 +14,9 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item'; import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list'; import { PropertyList } from '@/components/core/property-list';
// import { CrCategory } from '@/components/dashboard/cr/categories/type'; // import { CrCategory } from '@/components/dashboard/cr/categories/type';
import type { UserMeta } from '@/components/dashboard/user_meta/type_move.d'; // import type { UserMeta } from '@/components/dashboard/user_meta/type_move.d';
export default function BasicDetailCard({ export default function BasicDetailCard({
userMeta, userMeta,

View File

@@ -12,6 +12,8 @@ import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard'; import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard'; import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
import { COL_USER_METAS } from '@/constants'; import { COL_USER_METAS } from '@/constants';
import { defaultBillingAddress } from '@/db/billingAddress/constant';
import type { UserMeta } from '@/db/UserMetas/type';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
@@ -20,7 +22,8 @@ import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/Arrow
import type { RecordModel } from 'pocketbase'; import type { RecordModel } from 'pocketbase';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { config } from '@/config'; // TODO: remove me
// import { config } from '@/config';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
@@ -28,7 +31,7 @@ import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error'; import ErrorDisplay from '@/components/dashboard/error';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants'; import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
import { Notifications } from '@/components/dashboard/user_meta/notifications'; import { Notifications } from '@/components/dashboard/user_meta/notifications';
import type { UserMeta } from '@/components/dashboard/user_meta/type_move.d'; // import type { UserMeta } from '@/components/dashboard/user_meta/type_move.d';
import FormLoading from '@/components/loading'; import FormLoading from '@/components/loading';
import BasicDetailCard from './BasicDetailCard'; import BasicDetailCard from './BasicDetailCard';
@@ -109,7 +112,7 @@ export default function Page(): React.JSX.Element {
spacing={3} spacing={3}
sx={{ alignItems: 'flex-start' }} sx={{ alignItems: 'flex-start' }}
> >
<TitleCard teacherRecord={userMeta} /> <TitleCard teacherRecord={{ ...userMeta, billingAddress: defaultBillingAddress }} />
</Stack> </Stack>
</Stack> </Stack>
<Grid <Grid

View File

@@ -145,7 +145,7 @@ export default function Page(): React.JSX.Element {
spacing={2} spacing={2}
sx={{ alignItems: 'center', flexWrap: 'wrap' }} sx={{ alignItems: 'center', flexWrap: 'wrap' }}
> >
<Typography variant="h4">{showLessonCategory.name}</Typography> <Typography variant="h4">{showLessonCategory.word}</Typography>
<Chip <Chip
icon={ icon={
<CheckCircleIcon <CheckCircleIcon
@@ -221,7 +221,7 @@ export default function Page(): React.JSX.Element {
), ),
}, },
{ key: 'word_c', value: showLessonCategory.word_c }, { key: 'word_c', value: showLessonCategory.word_c },
{ key: 'Pos', value: showLessonCategory.pos }, { key: 'Pos', value: showLessonCategory.id },
{ {
key: 'Visible', key: 'Visible',
value: ( value: (

View File

@@ -31,8 +31,8 @@ import { useUser } from '@/hooks/use-user';
import { DynamicLogo } from '@/components/core/logo'; import { DynamicLogo } from '@/components/core/logo';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import { oAuthProviders } from '../o-auth-providers.tsx';
import type { OAuthProvider } from '../OAuthProvider'; import type { OAuthProvider } from '../OAuthProvider';
import { oAuthProviders } from '../oAuthProviders';
// interface OAuthProvider { // interface OAuthProvider {
// id: 'google' | 'discord' | 'github'; // id: 'google' | 'discord' | 'github';

View File

@@ -30,8 +30,8 @@ import { useUser } from '@/hooks/use-user';
import { DynamicLogo } from '@/components/core/logo'; import { DynamicLogo } from '@/components/core/logo';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import { oAuthProviders } from '../o-auth-providers.tsx';
import type { OAuthProvider } from '../OAuthProvider'; import type { OAuthProvider } from '../OAuthProvider';
import { oAuthProviders } from '../oAuthProviders';
export function SignUpForm(): React.JSX.Element { export function SignUpForm(): React.JSX.Element {
const router = useRouter(); const router = useRouter();

View File

@@ -16,16 +16,36 @@ const user = {
name: 'Sofia Rivers', name: 'Sofia Rivers',
avatar: '/assets/avatar.png', avatar: '/assets/avatar.png',
email: 'sofia@devias.io', email: 'sofia@devias.io',
collectionId: '123321',
} satisfies User; } satisfies User;
export function CommentAdd(): React.JSX.Element { export function CommentAdd(): React.JSX.Element {
return ( return (
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}> <Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar src={user.avatar} /> <Avatar src={user.avatar} />
<Stack spacing={3} sx={{ flex: '1 1 auto' }}> <Stack
<OutlinedInput multiline placeholder="Add a comment" rows={3} /> spacing={3}
<Stack direction="row" spacing={3} sx={{ alignItems: 'center', justifyContent: 'space-between' }}> sx={{ flex: '1 1 auto' }}
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}> >
<OutlinedInput
multiline
placeholder="Add a comment"
rows={3}
/>
<Stack
direction="row"
spacing={3}
sx={{ alignItems: 'center', justifyContent: 'space-between' }}
>
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<IconButton sx={{ display: { sm: 'none' } }}> <IconButton sx={{ display: { sm: 'none' } }}>
<PlusIcon /> <PlusIcon />
</IconButton> </IconButton>

View File

@@ -19,6 +19,7 @@ const user = {
name: 'Sofia Rivers', name: 'Sofia Rivers',
avatar: '/assets/avatar.png', avatar: '/assets/avatar.png',
email: 'sofia@devias.io', email: 'sofia@devias.io',
collectionId: '123321',
} satisfies User; } satisfies User;
export interface MessageAddProps { export interface MessageAddProps {
@@ -57,8 +58,15 @@ export function MessageAdd({ disabled = false, onSend }: MessageAddProps): React
); );
return ( return (
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '0 0 auto', px: 3, py: 1 }}> <Stack
<Avatar src={user.avatar} sx={{ display: { xs: 'none', sm: 'inline' } }} /> direction="row"
spacing={2}
sx={{ alignItems: 'center', flex: '0 0 auto', px: 3, py: 1 }}
>
<Avatar
src={user.avatar}
sx={{ display: { xs: 'none', sm: 'inline' } }}
/>
<OutlinedInput <OutlinedInput
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
@@ -67,7 +75,11 @@ export function MessageAdd({ disabled = false, onSend }: MessageAddProps): React
sx={{ flex: '1 1 auto' }} sx={{ flex: '1 1 auto' }}
value={content} value={content}
/> />
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}> <Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center' }}
>
<Tooltip title="Send"> <Tooltip title="Send">
<span> <span>
<IconButton <IconButton
@@ -84,24 +96,40 @@ export function MessageAdd({ disabled = false, onSend }: MessageAddProps): React
</IconButton> </IconButton>
</span> </span>
</Tooltip> </Tooltip>
<Stack direction="row" spacing={1} sx={{ display: { xs: 'none', sm: 'flex' } }}> <Stack
direction="row"
spacing={1}
sx={{ display: { xs: 'none', sm: 'flex' } }}
>
<Tooltip title="Attach photo"> <Tooltip title="Attach photo">
<span> <span>
<IconButton disabled={disabled} edge="end" onClick={handleAttach}> <IconButton
disabled={disabled}
edge="end"
onClick={handleAttach}
>
<CameraIcon /> <CameraIcon />
</IconButton> </IconButton>
</span> </span>
</Tooltip> </Tooltip>
<Tooltip title="Attach file"> <Tooltip title="Attach file">
<span> <span>
<IconButton disabled={disabled} edge="end" onClick={handleAttach}> <IconButton
disabled={disabled}
edge="end"
onClick={handleAttach}
>
<PaperclipIcon /> <PaperclipIcon />
</IconButton> </IconButton>
</span> </span>
</Tooltip> </Tooltip>
</Stack> </Stack>
</Stack> </Stack>
<input hidden ref={fileInputRef} type="file" /> <input
hidden
ref={fileInputRef}
type="file"
/>
</Stack> </Stack>
); );
} }

View File

@@ -17,6 +17,7 @@ const user = {
name: 'Sofia Rivers', name: 'Sofia Rivers',
avatar: '/assets/avatar.png', avatar: '/assets/avatar.png',
email: 'sofia@devias.io', email: 'sofia@devias.io',
collectionId: '123321',
} satisfies User; } satisfies User;
export interface MessageBoxProps { export interface MessageBoxProps {
@@ -38,8 +39,14 @@ export function MessageBox({ message }: MessageBoxProps): React.JSX.Element {
mr: position === 'left' ? 'auto' : 0, mr: position === 'left' ? 'auto' : 0,
}} }}
> >
<Avatar src={message.author.avatar} sx={{ '--Avatar-size': '32px' }} /> <Avatar
<Stack spacing={1} sx={{ flex: '1 1 auto' }}> src={message.author.avatar}
sx={{ '--Avatar-size': '32px' }}
/>
<Stack
spacing={1}
sx={{ flex: '1 1 auto' }}
>
<Card <Card
sx={{ sx={{
px: 2, px: 2,
@@ -52,7 +59,11 @@ export function MessageBox({ message }: MessageBoxProps): React.JSX.Element {
> >
<Stack spacing={1}> <Stack spacing={1}>
<div> <div>
<Link color="inherit" sx={{ cursor: 'pointer' }} variant="subtitle2"> <Link
color="inherit"
sx={{ cursor: 'pointer' }}
variant="subtitle2"
>
{message.author.name} {message.author.name}
</Link> </Link>
</div> </div>
@@ -66,14 +77,21 @@ export function MessageBox({ message }: MessageBoxProps): React.JSX.Element {
/> />
) : null} ) : null}
{message.type === 'text' ? ( {message.type === 'text' ? (
<Typography color="inherit" variant="body1"> <Typography
color="inherit"
variant="body1"
>
{message.content} {message.content}
</Typography> </Typography>
) : null} ) : null}
</Stack> </Stack>
</Card> </Card>
<Box sx={{ display: 'flex', justifyContent: position === 'right' ? 'flex-end' : 'flex-start', px: 2 }}> <Box sx={{ display: 'flex', justifyContent: position === 'right' ? 'flex-end' : 'flex-start', px: 2 }}>
<Typography color="text.secondary" noWrap variant="caption"> <Typography
color="text.secondary"
noWrap
variant="caption"
>
{dayjs(message.createdAt).fromNow()} {dayjs(message.createdAt).fromNow()}
</Typography> </Typography>
</Box> </Box>

View File

@@ -3,6 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { MarkOneAsRead } from '@/db/Notifications/mark-one-as-read';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Badge from '@mui/material/Badge'; import Badge from '@mui/material/Badge';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@@ -25,6 +26,7 @@ import type { NavItemConfig } from '@/types/nav';
import type { NavColor } from '@/types/settings'; import type { NavColor } from '@/types/settings';
import type { User } from '@/types/user'; import type { User } from '@/types/user';
import { paths } from '@/paths'; import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { isNavItemActive } from '@/lib/is-nav-item-active'; import { isNavItemActive } from '@/lib/is-nav-item-active';
import { useDialog } from '@/hooks/use-dialog'; import { useDialog } from '@/hooks/use-dialog';
import { usePopover } from '@/hooks/use-popover'; import { usePopover } from '@/hooks/use-popover';
@@ -33,6 +35,7 @@ import { Dropdown } from '@/components/core/dropdown/dropdown';
import { DropdownPopover } from '@/components/core/dropdown/dropdown-popover'; import { DropdownPopover } from '@/components/core/dropdown/dropdown-popover';
import { DropdownTrigger } from '@/components/core/dropdown/dropdown-trigger'; import { DropdownTrigger } from '@/components/core/dropdown/dropdown-trigger';
import { Logo } from '@/components/core/logo'; import { Logo } from '@/components/core/logo';
import { toast } from '@/components/core/toaster';
import { SearchDialog } from '@/components/dashboard/layout/search-dialog'; import { SearchDialog } from '@/components/dashboard/layout/search-dialog';
import type { ColorScheme } from '@/styles/theme/types'; import type { ColorScheme } from '@/styles/theme/types';
@@ -183,6 +186,29 @@ function SearchButton(): React.JSX.Element {
function NotificationsButton(): React.JSX.Element { function NotificationsButton(): React.JSX.Element {
const popover = usePopover<HTMLButtonElement>(); const popover = usePopover<HTMLButtonElement>();
const [listLength, setListLength] = React.useState<number>(0);
function handleMarkAllAsRead(): void {
// try {
// await MarkOneAsRead(id);
// toast.success('Notification marked as read');
// } catch (error) {
// logger.debug(error);
// toast.error('Something went wrong');
// }
}
function handleRemoveOne(id: string, cb: () => void): void {
MarkOneAsRead(id)
.then(() => {
toast.success('Notification marked as read');
cb();
})
.catch((error) => {
logger.debug(error);
toast.error('Something went wrong');
});
}
return ( return (
<React.Fragment> <React.Fragment>
@@ -190,15 +216,14 @@ function NotificationsButton(): React.JSX.Element {
<Badge <Badge
color="error" color="error"
sx={{ sx={{
'& .MuiBadge-dot': { '& .MuiBadge-badge': {
borderRadius: '50%', height: '20px',
width: '20px',
right: '6px', right: '6px',
top: '6px', top: '6px',
height: '10px',
width: '10px',
}, },
}} }}
variant="dot" badgeContent={listLength}
> >
<IconButton <IconButton
onClick={popover.handleOpen} onClick={popover.handleOpen}
@@ -212,6 +237,9 @@ function NotificationsButton(): React.JSX.Element {
anchorEl={popover.anchorRef.current} anchorEl={popover.anchorRef.current}
onClose={popover.handleClose} onClose={popover.handleClose}
open={popover.open} open={popover.open}
onMarkAllAsRead={handleMarkAllAsRead}
onRemoveOne={handleRemoveOne}
setListLength={setListLength}
/> />
</React.Fragment> </React.Fragment>
); );

View File

@@ -1,6 +1,9 @@
'use client'; 'use client';
import type { Notification } from '@/db/Notifications/type'; import type { Notification } from '@/db/Notifications/type';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
// import type { Notification } from './type.d.tsx'; // import type { Notification } from './type.d.tsx';
export const SampleNotifications = [ export const SampleNotifications = [
@@ -11,6 +14,11 @@ export const SampleNotifications = [
type: 'new_job', type: 'new_job',
author: { id: '0001', collectionId: '0001', name: 'Jie Yan', avatar: '/assets/avatar-8.png' }, author: { id: '0001', collectionId: '0001', name: 'Jie Yan', avatar: '/assets/avatar-8.png' },
job: { title: 'Remote React / React Native Developer' }, job: { title: 'Remote React / React Native Developer' },
//
created: '',
link: '',
NOTI_ID: '',
updated: '',
}, },
{ {
id: 'EV-003', id: 'EV-003',
@@ -19,6 +27,11 @@ export const SampleNotifications = [
type: 'new_job', type: 'new_job',
author: { id: '0001', collectionId: '0001', name: 'Fran Perez', avatar: '/assets/avatar-5.png' }, author: { id: '0001', collectionId: '0001', name: 'Fran Perez', avatar: '/assets/avatar-5.png' },
job: { title: 'Senior Golang Backend Engineer' }, job: { title: 'Senior Golang Backend Engineer' },
//
created: '',
link: '',
NOTI_ID: '',
updated: '',
}, },
{ {
id: 'EV-002', id: 'EV-002',
@@ -27,6 +40,11 @@ export const SampleNotifications = [
type: 'new_feature', type: 'new_feature',
author: { id: '0001', collectionId: '0001', name: 'Fran Perez', avatar: '/assets/avatar-5.png' }, author: { id: '0001', collectionId: '0001', name: 'Fran Perez', avatar: '/assets/avatar-5.png' },
description: 'Logistics management is now available', description: 'Logistics management is now available',
//
created: '',
link: '',
NOTI_ID: '',
updated: '',
}, },
{ {
id: 'EV-001', id: 'EV-001',
@@ -35,5 +53,10 @@ export const SampleNotifications = [
type: 'new_company', type: 'new_company',
author: { id: '0001', collectionId: '002', name: 'Jie Yan', avatar: '/assets/avatar-8.png' }, author: { id: '0001', collectionId: '002', name: 'Jie Yan', avatar: '/assets/avatar-8.png' },
company: { name: 'Stripe' }, company: { name: 'Stripe' },
//
created: '',
link: '',
NOTI_ID: '',
updated: '',
}, },
] satisfies Notification[]; ] satisfies Notification[];

View File

@@ -6,11 +6,12 @@ import Badge from '@mui/material/Badge';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import type { User } from '@/types/user'; import type { User } from '@/types/user';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { usePopover } from '@/hooks/use-popover'; import { usePopover } from '@/hooks/use-popover';
import { useUser } from '@/hooks/use-user';
import { UserPopover } from '../../user-popover/user-popover'; import { UserPopover } from '../../user-popover/user-popover';
import { useUser } from '@/hooks/use-user';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
// import { NotificationsButton } from './notifications-button'; // import { NotificationsButton } from './notifications-button';
// TODO:remove me // TODO:remove me
@@ -50,7 +51,7 @@ export function UserButton(): React.JSX.Element {
}} }}
variant="dot" variant="dot"
> >
<Avatar src={getImageUrlFromFile(user.collectionId, user.id, user.avatar)} /> <Avatar src={getImageUrlFromFile(user.collectionId || '', user.id, user.avatar)} />
</Badge> </Badge>
</Box> </Box>
<UserPopover <UserPopover

View File

@@ -240,7 +240,7 @@ export function AccountDetails(): React.JSX.Element {
</Stack> </Stack>
</Box> </Box>
<Avatar <Avatar
src={getImageUrlFromFile(user.collectionId, user.id, user.avatar)} src={getImageUrlFromFile(user.collectionId || '', user.id, user.avatar)}
sx={{ '--Avatar-size': '100px' }} sx={{ '--Avatar-size': '100px' }}
/> />
</Box> </Box>

View File

@@ -21,7 +21,7 @@ import FormLoading from '@/components/loading';
import ErrorDisplay from '../../error'; import ErrorDisplay from '../../error';
import { NavItem } from './nav-item'; import { NavItem } from './nav-item';
import { navItems } from './navItems'; import { navItems } from './nav-items';
export function SideNav(): React.JSX.Element { export function SideNav(): React.JSX.Element {
const router = useRouter(); const router = useRouter();
@@ -99,7 +99,7 @@ export function SideNav(): React.JSX.Element {
spacing={2} spacing={2}
sx={{ alignItems: 'center' }} sx={{ alignItems: 'center' }}
> >
<Avatar src={getImageUrlFromFile(user.collectionId, user.id, user.avatar)}>{user.name}</Avatar> <Avatar src={getImageUrlFromFile(user.collectionId || '', user.id, user.avatar)}>{user.name}</Avatar>
<div> <div>
<Typography variant="subtitle1">{user.name}</Typography> <Typography variant="subtitle1">{user.name}</Typography>
<Typography <Typography

View File

@@ -18,7 +18,7 @@ import { UsersThree as UsersThreeIcon } from '@phosphor-icons/react/dist/ssr/Use
import type { NavItemConfig } from '@/types/nav'; import type { NavItemConfig } from '@/types/nav';
import { isNavItemActive } from '@/lib/is-nav-item-active'; import { isNavItemActive } from '@/lib/is-nav-item-active';
import { navItems } from './navItems'; import { navItems } from './nav-items';
const icons = { const icons = {
'credit-card': CreditCardIcon, 'credit-card': CreditCardIcon,

View File

@@ -2,7 +2,10 @@
// default variable value for customer // default variable value for customer
// empty valur for customer // empty valur for customer
import { defaultBillingAddress } from '@/db/billingAddress/constant';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import type { Student } from './type.d'; import type { Student } from './type.d';
export const defaultStudent: Student = { export const defaultStudent: Student = {
@@ -14,6 +17,12 @@ export const defaultStudent: Student = {
quota: 0, quota: 0,
status: 'pending', status: 'pending',
createdAt: dayjs().toDate(), createdAt: dayjs().toDate(),
billingAddress: defaultBillingAddress,
state: 'pending',
timezone: '',
language: '',
currency: '',
collectionId: '',
}; };
export const emptyLpCategory: Student = { export const emptyLpCategory: Student = {

View File

@@ -119,6 +119,7 @@ export interface StudentFiltersProps {
filters?: Filters; filters?: Filters;
sortDir?: SortDir; sortDir?: SortDir;
fullData: Student[]; fullData: Student[];
//
} }
// RULES: available filter options for student data // RULES: available filter options for student data
@@ -126,4 +127,5 @@ export interface Filters {
email?: string; email?: string;
phone?: string; phone?: string;
state?: string; state?: string;
status?: string;
} }

View File

@@ -2,7 +2,10 @@
// default variable value for customer // default variable value for customer
// empty valur for customer // empty valur for customer
import { defaultBillingAddress } from '@/db/billingAddress/constant';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import type { Teacher } from './type.d'; import type { Teacher } from './type.d';
export const defaultTeacher: Teacher = { export const defaultTeacher: Teacher = {
@@ -14,6 +17,12 @@ export const defaultTeacher: Teacher = {
quota: 0, quota: 0,
status: 'pending', status: 'pending',
createdAt: dayjs().toDate(), createdAt: dayjs().toDate(),
billingAddress: defaultBillingAddress,
state: 'pending',
// timezone: '',
// language: '',
// currency: '',
collectionId: '',
}; };
export const emptyLpCategory: Teacher = { export const emptyLpCategory: Teacher = {

View File

@@ -181,7 +181,9 @@ export function TeacherEditForm(): React.JSX.Element {
// //
reset({ ...defaultValues, ...result }); reset({ ...defaultValues, ...result });
if (result?.billingAddress !== undefined) {
setBillingAddressId(result.billingAddress.id); setBillingAddressId(result.billingAddress.id);
}
if (result.avatar) { if (result.avatar) {
const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar)); const fetchResult = await fetch(getImageUrlFromFile(result.collectionId, result.id, result.avatar));

View File

@@ -1,5 +1,7 @@
'use client'; 'use client';
import { BillingAddress } from '@/db/billingAddress/type';
// RULES: sorting direction for teacher lists // RULES: sorting direction for teacher lists
export type SortDir = 'asc' | 'desc'; export type SortDir = 'asc' | 'desc';
@@ -14,7 +16,10 @@ export interface Teacher {
email: string; email: string;
phone?: string; phone?: string;
quota: number; quota: number;
//
// billingAddress: BillingAddress[] | [];
billingAddress: BillingAddress | Record<string, never>;
//
// status is obsoleted, replace by state // status is obsoleted, replace by state
status: 'pending' | 'active' | 'blocked'; status: 'pending' | 'active' | 'blocked';
state: 'pending' | 'active' | 'blocked'; state: 'pending' | 'active' | 'blocked';
@@ -44,10 +49,13 @@ export interface CreateFormProps {
timezone: string; timezone: string;
language: string; language: string;
currency: string; currency: string;
// NOTE: fix this to file, change outside world
avatar?: File | null; avatar?: File | null;
// quota?: number; // quota?: number;
state?: 'pending' | 'active' | 'blocked'; state?: 'pending' | 'active' | 'blocked';
meta: Record<string, null>;
// TODO: obsolete
meta?: Record<string, null>;
} }
// RULES: form data structure for editing existing teacher // RULES: form data structure for editing existing teacher

View File

@@ -1,20 +1,50 @@
// RULES: // RULES:
// default variable value for customer // default variable value for customer
// empty valur for customer // empty value for customer
import { defaultBillingAddress } from '@/db/billingAddress/constant';
import type { DBUserMeta, UserMeta } from '@/db/UserMetas/type';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import type { UserMeta } from './type.d'; // import type { UserMeta } from './type.d';
export const defaultUserMeta: UserMeta = { export const defaultUserMeta: UserMeta = {
id: '', id: '',
name: '', name: '',
avatar: undefined, avatar: '',
email: '', email: '',
phone: undefined, phone: '',
quota: 0, quota: 0,
status: 'pending', status: 'pending',
state: 'pending',
collectionId: '',
createdAt: dayjs().toDate(), createdAt: dayjs().toDate(),
//
// billingAddress: defaultBillingAddress,
// state: 'active',
// timezone: '',
// language: '',
// currency: '',
// collectionId: '',
};
export const defaultDBUserMeta: DBUserMeta = {
id: '',
name: '',
avatar: '',
email: '',
phone: '',
quota: 0,
status: 'pending',
created: '1900-01-01',
state: 'active',
timezone: '',
language: '',
currency: '',
collectionId: '',
company: '',
expand: {},
}; };
export const emptyLpCategory: UserMeta = { export const emptyLpCategory: UserMeta = {

View File

@@ -3,6 +3,7 @@
import * as React from 'react'; import * as React from 'react';
import RouterLink from 'next/link'; import RouterLink from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { createTeacher } from '@/db/Teachers/Create';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@@ -26,11 +27,12 @@ import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod'; import { z as zod } from 'zod';
import { paths } from '@/paths'; import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger'; import { logger } from '@/lib/default-logger';
import { base64ToFile } from '@/lib/file-to-base64';
import { Option } from '@/components/core/option'; import { Option } from '@/components/core/option';
import { toast } from '@/components/core/toaster'; import { toast } from '@/components/core/toaster';
import { createTeacher } from '@/db/Teachers/Create'; import type { CreateFormProps } from '@/components/dashboard/teacher/type.d';
import isDevelopment from '@/lib/check-is-development';
function fileToBase64(file: Blob): Promise<string> { function fileToBase64(file: Blob): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -45,6 +47,9 @@ function fileToBase64(file: Blob): Promise<string> {
}); });
} }
// NOTE: face user side
// avatar should be the string inside image box,
// so avatar is string here
const schema = zod.object({ const schema = zod.object({
avatar: zod.string().optional(), avatar: zod.string().optional(),
name: zod.string().min(1, 'Name is required').max(255), name: zod.string().min(1, 'Name is required').max(255),
@@ -101,13 +106,25 @@ export function TeacherCreateForm(): React.JSX.Element {
const onSubmit = React.useCallback( const onSubmit = React.useCallback(
async (values: Values): Promise<void> => { async (values: Values): Promise<void> => {
try { try {
// Use standard create method from db/Customers/Create const temp: CreateFormProps = {
const record = await createTeacher(values); name: values.name,
toast.success('Customer created'); email: values.email,
timezone: values.timezone,
language: values.language,
currency: values.currency,
taxId: values.taxId,
avatar: values.avatar ? await base64ToFile(values.avatar) : null,
state: 'pending',
meta: {},
};
// some convert process here...
const record = await createTeacher(temp);
toast.success('Teacher created');
router.push(paths.dashboard.teachers.details(record.id)); router.push(paths.dashboard.teachers.details(record.id));
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
toast.error('Failed to create customer'); toast.error('Failed to create teacher');
} }
}, },
[router] [router]
@@ -157,7 +174,7 @@ export function TeacherCreateForm(): React.JSX.Element {
}} }}
> >
<Avatar <Avatar
src={avatar} src={''}
sx={{ sx={{
'--Avatar-size': '100px', '--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)', '--Icon-fontSize': 'var(--icon-fontSize-lg)',

View File

@@ -1,7 +1,6 @@
import type { CreateForm, Vocabulary } from './type'; import type { CreateForm, Vocabulary } from './type';
export const defaultVocabulary: Vocabulary = { export const defaultVocabulary: Vocabulary = {
id: 'default-vocabulary-id',
image: undefined, image: undefined,
sound: undefined, sound: undefined,
word: '', word: '',
@@ -11,11 +10,15 @@ export const defaultVocabulary: Vocabulary = {
cat_id: 'default-category-id', cat_id: 'default-category-id',
category: 'default-category', category: 'default-category',
lesson_type_id: 'default-lesson-type-id', lesson_type_id: 'default-lesson-type-id',
visible: 'visible',
//
id: 'default-vocabulary-id',
collectionId: '',
}; };
export const VocabularyCreateFormDefault: CreateForm = { export const VocabularyCreateFormDefault: CreateForm = {
image: undefined, image: null,
sound: undefined, sound: '',
word: '', word: '',
word_c: '', word_c: '',
sample_e: '', sample_e: '',
@@ -23,6 +26,7 @@ export const VocabularyCreateFormDefault: CreateForm = {
cat_id: '', cat_id: '',
category: '', category: '',
lesson_type_id: '', lesson_type_id: '',
visible: 'visible',
}; };
export const emptyVocabulary: Vocabulary = { export const emptyVocabulary: Vocabulary = {

View File

@@ -3,7 +3,7 @@
export interface Vocabulary { export interface Vocabulary {
created?: string; created?: string;
updated?: string; updated?: string;
image: string; image: string | undefined;
sound?: string; sound?: string;
word?: string; word?: string;
word_c?: string; word_c?: string;
@@ -22,6 +22,7 @@ export interface Vocabulary {
// //
}; };
}; };
isEmpty?: boolean;
// //
id: string; id: string;
collectionId: string; collectionId: string;
@@ -62,3 +63,18 @@ export interface EditFormProps {
export interface Helloworld { export interface Helloworld {
helloworld: string; helloworld: string;
} }
// RULES: for use with vocabulary-create-form.tsx
// when you update, please take a look into `vocabulary-create-form.tsx`
export interface CreateForm {
image: File[] | null;
sound: string;
word: string;
word_c: string;
sample_e: string;
sample_c: string;
cat_id: string;
category: string;
lesson_type_id: string;
visible: string;
}

View File

@@ -6,12 +6,16 @@ export const defaultNotification: Notification = {
id: '', id: '',
read: false, read: false,
type: '', type: '',
author: {}, //
job: {}, // TODO: troubleshoot
// author: {},
job: { title: '' },
description: '', description: '',
NOTI_ID: '', NOTI_ID: '',
created: '', created: '',
updated: '', updated: '',
link: '',
createdAt: new Date(),
}; };
export const emptyNotification: Notification = { export const emptyNotification: Notification = {

View File

@@ -16,6 +16,11 @@ export interface Notification {
company?: { name: string }; company?: { name: string };
to_user_id?: User; to_user_id?: User;
link: string; link: string;
//
NOTI_ID: string;
//
created: string;
updated: string;
} }
export interface CreateFormProps { export interface CreateFormProps {

View File

@@ -7,7 +7,7 @@ import type { BillingAddress } from '@/components/dashboard/user_meta/type.d';
export interface DBUserMeta { export interface DBUserMeta {
name: string; name: string;
// //
// NOTE: obslete "avatar" and use "avatar_file" // NOTE: obsolete "avatar" and use "avatar_file"
avatar?: string; avatar?: string;
avatar_file?: string; avatar_file?: string;
// //
@@ -35,20 +35,24 @@ export interface DBUserMeta {
// UserMeta type definitions // UserMeta type definitions
export interface UserMeta { export interface UserMeta {
id: string;
name: string; name: string;
avatar: string; avatar: string;
email: string; email: string;
phone: string; phone: string;
quota: number; quota: number;
state: 'active' | 'blocked' | 'pending';
// TODO: obsolete "status" field
status: 'active' | 'blocked' | 'pending'; status: 'active' | 'blocked' | 'pending';
//
id: string;
collectionId: string;
createdAt: Date; createdAt: Date;
} }
export interface UpdateUserMeta { export interface UpdateUserMeta {
name?: string; name?: string;
// //
// NOTE: obslete "avatar" and use "avatar_file" // NOTE: obsolete "avatar" and use "avatar_file"
// avatar_file?: string; // avatar_file?: string;
avatar: File | null; avatar: File | null;
// //
@@ -57,7 +61,7 @@ export interface UpdateUserMeta {
quota?: number; quota?: number;
company?: string; company?: string;
// //
// relation handle seperately // relation handle separately
// billingAddress: BillingAddress | Record<string, never>; // billingAddress: BillingAddress | Record<string, never>;
// status is obsoleted, replace by state // status is obsoleted, replace by state

View File

@@ -8,9 +8,10 @@
// - Must handle errors gracefully and provide user feedback // - Must handle errors gracefully and provide user feedback
// - Should integrate with the authentication system // - Should integrate with the authentication system
// //
import { pb } from '@/lib/pb';
import { COL_USERS } from '@/constants'; import { COL_USERS } from '@/constants';
import type { User } from '@/types/user'; import type { User } from '@/types/user';
import { pb } from '@/lib/pb';
export async function createUser(userData: Partial<User>): Promise<{ export async function createUser(userData: Partial<User>): Promise<{
data?: User; data?: User;
@@ -18,7 +19,7 @@ export async function createUser(userData: Partial<User>): Promise<{
}> { }> {
try { try {
const data = await pb.collection(COL_USERS).create(userData); const data = await pb.collection(COL_USERS).create(userData);
return { data }; return { data: data as unknown as User };
} catch (error) { } catch (error) {
return { error: error as Error }; return { error: error as Error };
} }

View File

@@ -11,8 +11,8 @@ import { COL_VOCABULARIES } from '@/constants';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
import type { Vocabulary, VocabularyCreate } from './type'; import type { CreateForm, Vocabulary } from './type';
export default function createVocabulary(data: VocabularyCreate): Promise<Vocabulary> { export default function createVocabulary(data: CreateForm): Promise<Vocabulary> {
return pb.collection(COL_VOCABULARIES).create(data); return pb.collection(COL_VOCABULARIES).create(data);
} }

View File

@@ -11,8 +11,8 @@ import { COL_VOCABULARIES } from '@/constants';
import { pb } from '@/lib/pb'; import { pb } from '@/lib/pb';
import type { Vocabularies } from './type'; import type { Vocabulary } from './type';
export default function getAllVocabularies(): Promise<Vocabularies> { export default function getAllVocabularies(): Promise<Vocabulary[]> {
return pb.collection(COL_VOCABULARIES).getFullList(); return pb.collection(COL_VOCABULARIES).getFullList();
} }

View File

@@ -0,0 +1,14 @@
export const defaultBillingAddress = {
city: '',
country: '',
line1: '',
line2: '',
state: '',
zipCode: '',
//
id: '',
collectionId: '',
collectionName: '',
updated: '',
created: '',
};

View File

@@ -1,10 +1,12 @@
export interface User { export interface User {
id: string; id: string;
name?: string; name?: string;
avatar: string;
// comply original superbase example
avatar?: string | undefined;
email?: string; email?: string;
collectionId: string; collectionId?: string;
[key: string]: unknown; [key: string]: unknown;
} }