Compare commits

...

36 Commits

Author SHA1 Message Date
louiscklaw
3f13cc41bf ```update Enh 2025-05-24 08:51:32 +08:00
cb8643b405 ``update Add Docker build and deployment workflow for CMS and Ionic Mobile, update docker-compose.yml with new service images, and enhance documentation with strategy diagram and endpoint table`` 2025-05-24 08:21:33 +08:00
1157bf0fda ``update Update service address table formatting and placeholders for REQ0021`` 2025-05-24 08:15:44 +08:00
louiscklaw
2375c144a8 ``update Add build status message and server restart log in CMS entrypoint script`` 2025-05-24 06:11:33 +08:00
louiscklaw
24c62d940c ``update Migrate from custom Node image to official Node 22 image, install pnpm, update Dockerfile and entrypoint script`` 2025-05-24 06:02:46 +08:00
louiscklaw
e38e775fee ``update Simplify ENTRYPOINT syntax in PocketBase Dockerfile`` 2025-05-24 06:02:35 +08:00
louiscklaw
db9d3fbffd ``update Update port mappings for PocketBase, CMS, and Ionic Mobile services as per REQ0021`` 2025-05-24 05:48:45 +08:00
louiscklaw
368af36acd ``update Fix CPU resource limits format in docker-compose.db.yml and clean up dc.sh`` 2025-05-24 05:48:45 +08:00
d283b8274f update ticket, 2025-05-24 05:43:53 +08:00
7a8bd60185 update docker config, 2025-05-24 05:41:35 +08:00
a94ead44ba build cms ok, 2025-05-23 17:16:47 +08:00
50fc385c2b ``update Add todo-tree filter exclusions for node_modules, bundles, Next.js build folders, and distribution artifacts`` 2025-05-17 18:12:32 +08:00
91b3f53abe ``add Add database guidelines document outlining db driver structure and collection mappings`` 2025-05-17 18:12:19 +08:00
cf0ec8bd64 ``update Change logout behavior back to route navigation after sign-out instead of full page reload `` 2025-05-17 10:02:35 +00:00
59cdf7257b ```
update Implement standardized documentation for all database modules, including purpose, rules, and requirements for each CRUD operation; refactor existing comments to follow new documentation standard
```
2025-05-17 10:02:05 +00:00
b8309596be update example for ai, 2025-05-17 11:20:33 +08:00
688e102f2c ``delete Remove database schema definitions and hidden field count retrieval functionality`` 2025-05-17 09:41:36 +08:00
4d57d0a5f3 ``update Change logout behavior from route navigation to full page reload after sign-out `` 2025-05-16 23:41:22 +08:00
720838f137 Merge branch 'develop/ionic_mobile/i18n/trunk' into develop/ionic_mobile/login-flow/trunk 2025-05-16 23:38:56 +08:00
c92ac33ade ```
replace setting tab button with conditional rendering based on user authentication status, showing avatar if available
```
2025-05-16 23:37:39 +08:00
72e478937d ``update Add QR code generation feature with dynamic sizing and styling, implement screen-width-based conditional rendering, update i18n translations, and adjust context providers structure`` 2025-05-16 22:51:33 +08:00
62d8519da5 ``delete Remove i18n configuration file and English translation resources; likely replaced by a JSON-based or externalized translation management approach`` 2025-05-16 22:50:24 +08:00
8d746f3aa9 ``update Refactor navigation URLs by replacing hardcoded LESSON_LINK with Paths.LESSON_LINK across App, RouteConfig, and multiple components; remove redundant LESSON_LINK from constants`` 2025-05-16 22:00:04 +08:00
69b2ef59e5 ``update Upgrade dependencies including i18next, react-i18next, eslint, and testing-library; adjust peer dependencies and add new ESLint plugins`` 2025-05-16 21:59:52 +08:00
f9c0deb2e9 ``add Add CMS project code-workspace file to define multi-root workspace including documentation and AI workspace directories`` 2025-05-16 21:57:52 +08:00
47760fa7b2 ``add Add code-workspace files for documentation, ionic_mobile, and test projects to define multi-root workspace configurations`` 2025-05-16 21:57:41 +08:00
83bd86cc9b ``update Add .env.example file with PocketBase URL configuration for development environment `` 2025-05-16 17:47:33 +08:00
49189a532e ```
update Add development environment configuration, I18n support, route adjustments, and various hooks refactoring
```
2025-05-16 17:47:05 +08:00
6b917c9fb9 ``update Add error constant for empty PocketBase URL and validate it during app initialization `` 2025-05-16 15:56:56 +08:00
aa834a43c9 ``update Add platform-specific back button for Android, replace header layout, use LoadingSpinner, and refactor imports in Lesson page component`` 2025-05-16 15:56:46 +08:00
1775d8c5a3 ``update Remove unused components, imports, and refactor file URL generation in WordPage component`` 2025-05-16 15:56:24 +08:00
779062e247 ``update Replace getStudentAvatar with getStudentAvatarUrl for avatar URL generation in student profile`` 2025-05-16 15:56:19 +08:00
60df47fb8d ``update Extract PocketBase URL to constant and refactor related functions to use it for dynamic file URL generation`` 2025-05-16 15:55:47 +08:00
c87357ff24 ``update Remove unused import path suffix and comment out unused useRequireAuth hook`` 2025-05-16 15:55:30 +08:00
34a7ff7ac9 ``refactor Rename LoadingScreen component to LoadingSpinner and refactor structure for better reusability`` 2025-05-16 15:55:07 +08:00
3556e77a7c ```
add Implement custom sign-out functionality with loading state and error handling
```
2025-05-16 13:26:17 +08:00
250 changed files with 4015 additions and 4974 deletions

View File

@@ -0,0 +1,44 @@
---
tags: docker
---
# description
docker and public endpoint
## strategy
```mermaid
graph LR
A["next build"]
B["docker build"]
A --> B --> deploy --> run
```
1. build on develop machine
1. docker build on develop machine (include source code)
1. push image to production machine
1. run image to production machine
## endpoint table
| function | address | | status |
| -------- | ---------------------------------------------------------------------------- | ---- | ---------------------------------------------------- |
| DB | [https://demo_ls_db.louislabs.com](https://demo_ls_db.louislabs.com) | 3013 | ![](https://status.iamon99.com/api/badge/108/uptime) |
| cms | [https://demo_ls_cms.louislabs.com](https://demo_ls_cms.louislabs.com) | 3011 | ![](https://status.iamon99.com/api/badge/109/uptime) |
| mobile | [https://demo_ls_mobile.louislabs.com](https://demo_ls_mobile.louislabs.com) | 3012 | ![](https://status.iamon99.com/api/badge/110/uptime) |
| T.B.A. | T.B.A. | 3014 | |
| T.B.A. | T.B.A. | 3015 | |
| T.B.A. | T.B.A. | 3016 | |
| T.B.A. | T.B.A. | 3017 | |
| T.B.A. | T.B.A. | 3018 | |
| T.B.A. | T.B.A. | 3019 | |
| T.B.A. | T.B.A. | 3010 | |
## status panel
[https://status.iamon99.com/status/demo-ls](https://status.iamon99.com/status/demo-ls)
## TODO
pending separated ssl certificate, currently using shared affine.louislabs.com

View File

@@ -22,3 +22,4 @@
- [REQ0018: family photo of frameworks](./REQ0018/index.md)
- [REQ0019: System architecture](./REQ0019/index.md)
- [REQ0020: Mobile login flow](./REQ0020/index.md)
- [REQ0021: description](./REQ0021/index.md)

View File

@@ -2,3 +2,5 @@
**/*.log
**/_archive
**/_del
**/*copy*

View File

@@ -0,0 +1,40 @@
# Ignore all files and directories by default
-
# Except for the following:
!.dockerignore
!.gitignore
!.env.example
!.git/
!.env
!.vscode/
!.idea/
# Node.js specific
node_modules/
# Build artifacts
dist/
build/
# Logs
logs/
\*.log
# Environment files
.env\*
# Cache files
\*.cache
# OS generated files
.DS_Store
Thumbs.db

View File

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

View File

@@ -3,6 +3,8 @@
**/*log
**/*tmp
.pnpm-store
.env
.env.production

17
002_source/cms/Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
# Use official Node 18 base image
FROM node:22-slim
# Install pnpm globally
RUN npm install -g pnpm
# Copy your application code (optional, comment out if not needed)
COPY . /app
RUN cd /app && pnpm i
RUN cd /app && pnpm run build
# Set working directory
WORKDIR /app
# Default command (optional)
CMD ["./scripts/docker/entrypoint.sh"]

View File

@@ -10,7 +10,10 @@
"dev": "next dev -H 0.0.0.0",
"build": "next build",
"build:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 15 --exec \"pnpm run build\"",
"start": "next start",
"docker:push": "docker push 192.168.10.61:5000/demo_ls_cms ",
"docker:build": "docker build -t 192.168.10.61:5000/demo_ls_cms .",
"build:docker": "pnpm run build && pnpm run docker:build && pnpm run docker:push",
"start": "next start -H 0.0.0.0",
"lint": "next lint --quiet",
"lint:fix": "next lint --fix",
"lint:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 5 --exec \"pnpm run lint\"",
@@ -117,4 +120,4 @@
"protobufjs"
]
}
}
}

View File

@@ -3,5 +3,11 @@
set -ex
reset
rm -rf .next
# rm -rf .next
# pnpm run typecheck
# pnpm run lint
# echo "check ok"
pnpm run build

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

@@ -1,15 +1,19 @@
#!/usr/bin/env bash
# set -x
set -x
# nvm use 20
nvm alias default 18
nvm use default
node -v
while true; do
pnpm run start
npm i -D
echo "command restarted"
npm run dev
sleep 5
done
# pnpm i -D
# pnpm run start
# while true; do
# if [ "$NODE_ENV" = "development" ]; then

View File

@@ -1,3 +1,10 @@
{
"git.ignoreLimitWarning": true
}
"git.ignoreLimitWarning": true,
"todo-tree.filtering.excludeGlobs": [
"**/node_modules/*/**",
"**/*.bundle.*",
"**/.next/*/**",
"**/dist/*/**",
"**/build/*/**",
],
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
'use client';
import * as React from 'react';
import type { UserMeta } from '@/db/UserMetas/type';
import Avatar from '@mui/material/Avatar';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
@@ -13,8 +14,9 @@ import { useTranslation } from 'react-i18next';
import { PropertyItem } from '@/components/core/property-item';
import { PropertyList } from '@/components/core/property-list';
// 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({
userMeta,

View File

@@ -12,6 +12,8 @@ import { SampleNotifications } from '@/app/dashboard/Sample/Notifications';
import SamplePaymentCard from '@/app/dashboard/Sample/PaymentCard';
import SampleSecurityCard from '@/app/dashboard/Sample/SecurityCard';
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 Link from '@mui/material/Link';
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 { useTranslation } from 'react-i18next';
import { config } from '@/config';
// TODO: remove me
// import { config } from '@/config';
import { paths } from '@/paths';
import { logger } from '@/lib/default-logger';
import { pb } from '@/lib/pb';
@@ -28,7 +31,7 @@ import { toast } from '@/components/core/toaster';
import ErrorDisplay from '@/components/dashboard/error';
import { defaultUserMeta } from '@/components/dashboard/user_meta/_constants';
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 BasicDetailCard from './BasicDetailCard';
@@ -109,7 +112,7 @@ export default function Page(): React.JSX.Element {
spacing={3}
sx={{ alignItems: 'flex-start' }}
>
<TitleCard teacherRecord={userMeta} />
<TitleCard teacherRecord={{ ...userMeta, billingAddress: defaultBillingAddress }} />
</Stack>
</Stack>
<Grid

View File

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

View File

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

View File

@@ -30,8 +30,8 @@ import { useUser } from '@/hooks/use-user';
import { DynamicLogo } from '@/components/core/logo';
import { toast } from '@/components/core/toaster';
import { oAuthProviders } from '../o-auth-providers.tsx';
import type { OAuthProvider } from '../OAuthProvider';
import { oAuthProviders } from '../oAuthProviders';
export function SignUpForm(): React.JSX.Element {
const router = useRouter();
@@ -195,7 +195,7 @@ export function SignUpForm(): React.JSX.Element {
<InputLabel>{t('email-address')}:</InputLabel>
<OutlinedInput
{...field}
placeholder={`${t('e.g.') } name@example.com`}
placeholder={`${t('e.g.')} name@example.com`}
type="email"
disabled={isPending}
/>

View File

@@ -16,16 +16,36 @@ const user = {
name: 'Sofia Rivers',
avatar: '/assets/avatar.png',
email: 'sofia@devias.io',
collectionId: '123321',
} satisfies User;
export function CommentAdd(): React.JSX.Element {
return (
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar src={user.avatar} />
<Stack spacing={3} sx={{ flex: '1 1 auto' }}>
<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' }}>
<Stack
spacing={3}
sx={{ flex: '1 1 auto' }}
>
<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' } }}>
<PlusIcon />
</IconButton>

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,11 @@
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { LoadingButton } from '@mui/lab';
import { Button } from '@mui/material';
import MenuItem from '@mui/material/MenuItem';
import { SignOut as SignOutIcon } from '@phosphor-icons/react/dist/ssr/SignOut';
import { useTranslation } from 'react-i18next';
import { authClient } from '@/lib/auth/custom/client';
import { logger } from '@/lib/default-logger';
@@ -10,39 +14,44 @@ import { useUser } from '@/hooks/use-user';
import { toast } from '@/components/core/toaster';
export function CustomSignOut(): React.JSX.Element {
const { t } = useTranslation('sign_in');
const { checkSession } = useUser();
const [buttonShowLoading, setButtonShowLoading] = React.useState<boolean>(false);
const router = useRouter();
const handleSignOut = React.useCallback(async (): Promise<void> => {
setButtonShowLoading(true);
try {
const { error } = await authClient.signOut();
if (error) {
logger.error('Sign out error', error);
toast.error('Something went wrong, unable to sign out');
toast.error(t('something-went-wrong-unable-to-sign-out'));
return;
}
// Refresh the auth state
await checkSession?.();
// UserProvider, for this case, will not refresh the router and we need to do it manually
router.refresh();
// After refresh, AuthGuard will handle the redirect
} catch (err) {
logger.error('Sign out error', err);
toast.error('Something went wrong, unable to sign out');
toast.error(t('something-went-wrong-unable-to-sign-out'));
}
}, [checkSession, router]);
}, [checkSession, router, t]);
return (
<MenuItem
component="div"
<LoadingButton
onClick={handleSignOut}
sx={{ justifyContent: 'center' }}
sx={{ width: '100%' }}
variant="text"
disabled={buttonShowLoading}
loading={buttonShowLoading}
startIcon={<SignOutIcon />}
color="secondary"
>
Sign out
</MenuItem>
{t('sign-out')}
</LoadingButton>
);
}

View File

@@ -16,15 +16,15 @@ import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
import type { User } from '@/types/user';
import { config } from '@/config';
import { paths } from '@/paths';
import { authClient } from '@/lib/auth/custom/client';
import { AuthStrategy } from '@/lib/auth/strategy';
import { logger } from '@/lib/default-logger';
import { Auth0SignOut } from './auth0-sign-out';
import { CognitoSignOut } from './cognito-sign-out';
import { CustomSignOut } from './custom-sign-out';
import { FirebaseSignOut } from './firebase-sign-out';
import { SupabaseSignOut } from './supabase-sign-out';
import { authClient } from '@/lib/auth/custom/client';
import { logger } from '@/lib/default-logger';
const defaultUser = {
id: 'USR-000',
@@ -55,7 +55,8 @@ export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): Reac
void loadUserMeta();
}, []);
if (!userMeta) return <>loading</>;
// NOTE: delay when userMeta is null, used for sign-out
if (!userMeta) return <></>;
return (
<Popover

View File

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

View File

@@ -66,6 +66,7 @@ export function SideNav({ color = 'evident', items = [] }: SideNavProps): React.
spacing={2}
sx={{ p: 2 }}
>
{/* NOTE: hide logo
<div>
<Box
component={RouterLink}
@@ -79,6 +80,7 @@ export function SideNav({ color = 'evident', items = [] }: SideNavProps): React.
/>
</Box>
</div>
*/}
<WorkspacesSwitch />
</Stack>
<Box

View File

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

View File

@@ -21,7 +21,7 @@ import FormLoading from '@/components/loading';
import ErrorDisplay from '../../error';
import { NavItem } from './nav-item';
import { navItems } from './navItems';
import { navItems } from './nav-items';
export function SideNav(): React.JSX.Element {
const router = useRouter();
@@ -99,7 +99,7 @@ export function SideNav(): React.JSX.Element {
spacing={2}
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>
<Typography variant="subtitle1">{user.name}</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 { isNavItemActive } from '@/lib/is-nav-item-active';
import { navItems } from './navItems';
import { navItems } from './nav-items';
const icons = {
'credit-card': CreditCardIcon,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,50 @@
// RULES:
// 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 type { UserMeta } from './type.d';
// import type { UserMeta } from './type.d';
export const defaultUserMeta: UserMeta = {
id: '',
name: '',
avatar: undefined,
avatar: '',
email: '',
phone: undefined,
phone: '',
quota: 0,
status: 'pending',
state: 'pending',
collectionId: '',
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 = {

View File

@@ -3,6 +3,7 @@
import * as React from 'react';
import RouterLink from 'next/link';
import { useRouter } from 'next/navigation';
import { createTeacher } from '@/db/Teachers/Create';
import { zodResolver } from '@hookform/resolvers/zod';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
@@ -26,11 +27,12 @@ import { Controller, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
import { paths } from '@/paths';
import isDevelopment from '@/lib/check-is-development';
import { logger } from '@/lib/default-logger';
import { base64ToFile } from '@/lib/file-to-base64';
import { Option } from '@/components/core/option';
import { toast } from '@/components/core/toaster';
import { createTeacher } from '@/db/Teachers/Create';
import isDevelopment from '@/lib/check-is-development';
import type { CreateFormProps } from '@/components/dashboard/teacher/type.d';
function fileToBase64(file: Blob): Promise<string> {
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({
avatar: zod.string().optional(),
name: zod.string().min(1, 'Name is required').max(255),
@@ -101,13 +106,25 @@ export function TeacherCreateForm(): React.JSX.Element {
const onSubmit = React.useCallback(
async (values: Values): Promise<void> => {
try {
// Use standard create method from db/Customers/Create
const record = await createTeacher(values);
toast.success('Customer created');
const temp: CreateFormProps = {
name: values.name,
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));
} catch (err) {
logger.error(err);
toast.error('Failed to create customer');
toast.error('Failed to create teacher');
}
},
[router]
@@ -157,7 +174,7 @@ export function TeacherCreateForm(): React.JSX.Element {
}}
>
<Avatar
src={avatar}
src={''}
sx={{
'--Avatar-size': '100px',
'--Icon-fontSize': 'var(--icon-fontSize-lg)',

View File

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

View File

@@ -3,7 +3,7 @@
export interface Vocabulary {
created?: string;
updated?: string;
image: string;
image: string | undefined;
sound?: string;
word?: string;
word_c?: string;
@@ -22,6 +22,7 @@ export interface Vocabulary {
//
};
};
isEmpty?: boolean;
//
id: string;
collectionId: string;
@@ -62,3 +63,18 @@ export interface EditFormProps {
export interface Helloworld {
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

@@ -1,6 +1,11 @@
// api method for crate customer record
// PURPOSE:
// Create new customer record
// REQ0006
//
// RULES:
// TBA
// error handled by caller
// contain definition to collection only
//
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import type { CreateFormProps } from '@/components/dashboard/customer/type.d';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Delete customer record
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get count of active customers
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get all customer records
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get total count of all customers
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get count of blocked customers
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get customer record by ID
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get count of pending customers
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { COL_USER_METAS } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Sample hello world function
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
export function helloCustomer(): string {
return 'Hello from Customers module!';
}

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Update customer information
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,11 @@
// PURPOSE:
// Create new lesson category in the database
//
// RULES:
// 1. Must provide data matching CreateFormProps type
// 2. Uses PocketBase collection API
// 3. Returns Promise<RecordModel>
import { COL_LESSON_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,11 @@
// PURPOSE:
// Delete specified lesson category from database
//
// RULES:
// 1. Must provide valid category ID
// 2. Uses PocketBase collection API
// 3. Returns Promise<boolean> indicating deletion success
import { COL_LESSON_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,11 @@
// PURPOSE:
// Retrieve all lesson categories from database
//
// RULES:
// 1. Uses PocketBase getFullList API
// 2. Returns Promise<RecordModel[]> with all categories
// 3. Accepts no parameters
import { COL_LESSON_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,6 +1,11 @@
// PURPOSE:
// Get total count of all lesson categories
//
// RULES:
// error handled by caller
// contain definition to collection only
// 1. Uses PocketBase getList API
// 2. Returns Promise<number> representing total count
// 3. Errors handled by caller
// 4. Contains collection definition only
import { COL_LESSON_CATEGORIES } from '@/constants';

View File

@@ -1,3 +1,11 @@
// PURPOSE:
// Retrieve single lesson category by ID
//
// RULES:
// 1. Must provide valid category ID
// 2. Uses PocketBase getOne API
// 3. Returns Promise<RecordModel> with requested category
import { COL_LESSON_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,4 +1,13 @@
// PURPOSE:
// Get count of lesson categories marked as hidden
// REQ0006
//
// RULES:
// 1. Uses PocketBase getList API with visible="hidden" filter
// 2. Returns Promise<number> with count of hidden categories
// 3. Maximum 9999 records returned
// 4. Returns 0 on error
import { COL_LESSON_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,4 +1,13 @@
// PURPOSE:
// Get count of lesson categories marked as visible
// REQ0006
//
// RULES:
// 1. Uses PocketBase getList API with visible="visible" filter
// 2. Returns Promise<number> with count of visible categories
// 3. Maximum 9999 records returned
// 4. Returns 0 on error
import { COL_LESSON_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,10 @@
// PURPOSE:
// Example/test function that returns "helloworld" string
//
// RULES:
// 1. Takes no parameters
// 2. Always returns fixed string "helloworld"
function Helloworld(): string {
return 'helloworld';
}

View File

@@ -1,5 +1,10 @@
// PURPOSE:
// List all lesson categories with expanded relations
//
// RULES:
// 1. Uses PocketBase getFullList API
// 2. Expands cat_id relations
// 3. Returns Promise<LessonCategory[]> with all categories
import { COL_LESSON_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,11 @@
// PURPOSE:
// Update existing lesson category with new data
//
// RULES:
// 1. Requires valid category ID and update data
// 2. Uses PocketBase collection update API
// 3. Returns Promise<RecordModel> with updated category
import { COL_LESSON_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,13 @@
//
// PURPOSE:
// Create new lesson type in PocketBase
//
// RULES:
// - Must validate using CreateForm type
// - Uses COL_LESSON_TYPES collection
// - Returns Promise with created record
//
import { COL_LESSON_TYPES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,7 +1,25 @@
//
// PURPOSE:
// Delete lesson type record from PocketBase
//
// RULES:
// - Calls pb.collection().delete()
// - Returns Promise<{success: boolean, error?: string}>
// - Basic error handling
//
import { COL_LESSON_TYPES } from '@/constants';
import { pb } from '@/lib/pb';
export default function deleteLessonType(id: string): Promise<boolean> {
return pb.collection(COL_LESSON_TYPES).delete(id);
export default async function deleteLessonType(id: string): Promise<{success: boolean, error?: string}> {
try {
await pb.collection(COL_LESSON_TYPES).delete(id);
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}

View File

@@ -1,3 +1,12 @@
//
// PURPOSE:
// Get all lesson type records from PocketBase
//
// RULES:
// - Uses pb.collection().getFullList()
// - Returns Promise<RecordModel[]>
// - No pagination or filtering implemented
//
import { COL_LESSON_TYPES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,4 +1,13 @@
//
// PURPOSE:
// Get total count of lesson types from PocketBase
// REQ0006
//
// RULES:
// - Uses pb.collection().getList() with large page size
// - Returns Promise<number> with total count
// - No filter support implemented
//
import { COL_LESSON_TYPES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,12 @@
//
// PURPOSE:
// Get lesson type by ID from PocketBase
//
// RULES:
// - Simply calls pb.collection().getOne()
// - Returns Promise<RecordModel>
// - Throws error if record not found
//
import { COL_LESSON_TYPES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,4 +1,14 @@
//
// PURPOSE:
// Get count of hidden lesson types
// REQ0006
//
// RULES:
// - Must clearly define "hidden" status
// - Must cache results
// - Must return accurate count
//
import { COL_LESSON_TYPES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,4 +1,15 @@
//
// PURPOSE:
// Get count of visible lesson types from PocketBase
// REQ0006
//
// RULES:
// - Uses filter: visible = "visible"
// - Returns Promise<number> with count
// - Returns 0 on error
// - No caching implemented
//
import { COL_LESSON_TYPES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,13 @@
//
// PURPOSE:
// Example/test file
//
// RULES:
// - For development/testing only
// - Should not contain business logic
// - Can be modified/deleted anytime
//
function Helloworld(): string {
return 'helloworld';
}

View File

@@ -1,3 +1,13 @@
//
// PURPOSE:
// Update lesson type in PocketBase
//
// RULES:
// - Simply calls pb.collection().update()
// - Uses CreateForm type for validation
// - Returns Promise<RecordModel>
// - No additional validation or logging
//
import { COL_LESSON_TYPES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,6 +1,11 @@
// api method for create notification record
// PURPOSE:
// Create new notification record
//
// RULES:
// TBA
// 1. Uses COL_NOTIFICATIONS collection
// 2. Requires NotificationFormProps type data
// 3. Returns Promise<RecordModel> with created record
// 4. Errors handled by caller
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
import type { NotificationFormProps } from '@/components/dashboard/notification/type.d';

View File

@@ -1,6 +1,11 @@
// api method for delete notification record
// PURPOSE:
// Delete notification record by ID
//
// RULES:
// TBA
// 1. Uses COL_NOTIFICATIONS collection
// 2. Requires valid notification ID string
// 3. Returns Promise<boolean> indicating success
// 4. Errors handled by caller
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';

View File

@@ -1,3 +1,11 @@
// PURPOSE:
// Count active notification records
//
// RULES:
// 1. Uses COL_CUSTOMERS collection
// 2. Filters records with status="active"
// 3. Returns Promise<number> with count
// 4. Errors handled by caller
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,6 +1,12 @@
// api method for get all notification records
// PURPOSE:
// Get all notification records
//
// RULES:
// TBA
// 1. Uses COL_NOTIFICATIONS collection
// 2. Returns Promise<RecordModel[]> with all records
// 3. Accepts optional options parameter
// 4. Uses getFullList API
// 5. Errors handled by caller
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get total count of notification records
//
// RULES:
// 1. Uses COL_NOTIFICATIONS collection
// 2. Returns Promise<number> with total count
// 3. Uses getList API
// 4. Errors handled by caller
//
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';

View File

@@ -1,3 +1,11 @@
// PURPOSE:
// Count blocked notification records
//
// RULES:
// 1. Uses COL_CUSTOMERS collection
// 2. Filters records with status="blocked"
// 3. Returns Promise<number> with count
// 4. Errors handled by caller
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,6 +1,12 @@
// api method for get notification by id
// PURPOSE:
// - Provides functionality to retrieve a single notification by ID
// - Returns complete notification details for specified ID
//
// RULES:
// TBA
// - Must validate input ID format
// - Should handle cases where notification is not found
// - Must respect data privacy and only return authorized fields
// - Should optimize query performance for single record retrieval
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,6 +1,12 @@
// PURPOSE:
// Get notifications for specific user ID
//
// RULES:
// api method for get notifications by user id
// 1. Uses COL_NOTIFICATIONS collection
// 2. Requires valid user ID string
// 3. Returns Promise<Notification[]> with user's notifications
// 4. Expands author and to_user_id relations
// 5. Sorts by created date (newest first)
import { COL_NOTIFICATIONS } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,11 @@
// PURPOSE:
// Count pending customer notifications
//
// RULES:
// 1. Uses COL_CUSTOMERS collection
// 2. Filters records with status="pending"
// 3. Returns Promise<number> with count
// 4. Errors handled by caller
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,6 +1,13 @@
// PURPOSE:
// Get all unread notifications for specified user ID
//
// RULES:
// api method for get notifications by user id
// 1. Uses COL_NOTIFICATIONS collection
// 2. Filters by to_user_id and read=false status
// 3. Expands author and to_user_id relations
// 4. Sorts by created date (newest first)
// 5. Returns Promise<Notification[]>
// 6. Disables caching for real-time results
import { COL_NOTIFICATIONS } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,10 @@
// PURPOSE:
// Example/Test component for notifications
//
// RULES:
// 1. Returns fixed string for testing
// 2. Not used in production
// 3. Follows project coding standards
export function helloCustomer(): string {
return 'Hello from Customers module!';
}

View File

@@ -1,6 +1,12 @@
// api method for update notification record
// PURPOSE:
// Update notification record
//
// RULES:
// TBA
// 1. Uses COL_NOTIFICATIONS collection
// 2. Requires valid notification ID string
// 3. Accepts Partial<NotificationFormProps> update data
// 4. Returns Promise<RecordModel> with updated record
// 5. Errors handled by caller
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

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

View File

@@ -1,6 +1,12 @@
// api method for update notification record
// PURPOSE:
// Mark single notification as read
//
// RULES:
// TBA
// 1. Uses COL_NOTIFICATIONS collection
// 2. Requires valid notification ID string
// 3. Updates read status to true
// 4. Returns Promise<RecordModel> with updated record
// 5. Errors handled by caller
import { COL_NOTIFICATIONS } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

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

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Create new Quiz CR Category record
//
// RULES:
// 1. Uses COL_QUIZ_CR_CATEGORIES collection
// 2. Requires CreateFormProps type data
// 3. Returns Promise<RecordModel> with created record
// 4. Errors handled by caller
// 5. Uses pb.collection().create()
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Delete Quiz CR Category record
//
// RULES:
// 1. Uses COL_QUIZ_CR_CATEGORIES collection
// 2. Requires valid record ID string
// 3. Returns Promise<boolean> indicating success
// 4. Errors handled by caller
// 5. Uses pb.collection().delete()
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get all Quiz CR Category records
//
// RULES:
// 1. Uses COL_QUIZ_CR_CATEGORIES collection
// 2. Returns Promise<RecordModel[]> with all records
// 3. Accepts no parameters
// 4. Uses getFullList API
// 5. Errors handled by caller
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get total count of Quiz CR Category records
//
// RULES:
// 1. Uses COL_QUIZ_CR_CATEGORIES collection
// 2. Returns Promise<number> with total count
// 3. Limits to first 9999 records
// 4. Errors handled by caller
// 5. Uses getList API
// REQ0006
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get Quiz CR Category record by ID
//
// RULES:
// 1. Uses COL_QUIZ_CR_CATEGORIES collection
// 2. Requires valid record ID string
// 3. Returns Promise<RecordModel> with requested record
// 4. Errors handled by caller
// 5. Uses getOne API
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get count of hidden Quiz CR Category records
//
// RULES:
// 1. Uses COL_QUIZ_CR_CATEGORIES collection
// 2. Filters records with visible="hidden"
// 3. Returns Promise<number> with count
// 4. Limits to first 9999 records
// 5. Returns 0 on error
// REQ0006
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
import { pb } from '@/lib/pb';

View File

@@ -1,3 +1,12 @@
// PURPOSE:
// Get count of visible Quiz CR Category records
//
// RULES:
// 1. Uses COL_QUIZ_CR_CATEGORIES collection
// 2. Filters records with visible="visible"
// 3. Returns Promise<number> with count
// 4. Limits to first 9999 records
// 5. Returns 0 on error
// REQ0006
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';

View File

@@ -1,3 +1,13 @@
// PURPOSE:
// Update Quiz CR Category record
//
// RULES:
// 1. Uses COL_QUIZ_CR_CATEGORIES collection
// 2. Requires valid record ID string
// 3. Accepts CreateFormProps update data
// 4. Returns Promise<RecordModel> with updated record
// 5. Errors handled by caller
// 6. Uses pb.collection().update()
import { COL_QUIZ_CR_CATEGORIES } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,13 @@
//
// PURPOSE:
// Create new QuizCRQuestions record
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { COL_QUIZ_CR_QUESTIONS } from '@/constants';
import type { RecordModel } from 'pocketbase';

View File

@@ -1,3 +1,13 @@
//
// PURPOSE:
// Delete specified QuizCRQuestions record
// REQ0006
//
// RULES:
// error handled by caller
// contain definition to collection only
//
import { COL_QUIZ_CR_QUESTIONS } from '@/constants';
import { pb } from '@/lib/pb';

Some files were not shown because too many files have changed in this diff Show More