Compare commits
38 Commits
7264d643d0
...
develop/de
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3f13cc41bf | ||
cb8643b405 | |||
1157bf0fda | |||
![]() |
2375c144a8 | ||
![]() |
24c62d940c | ||
![]() |
e38e775fee | ||
![]() |
db9d3fbffd | ||
![]() |
368af36acd | ||
d283b8274f | |||
7a8bd60185 | |||
a94ead44ba | |||
50fc385c2b | |||
91b3f53abe | |||
cf0ec8bd64 | |||
59cdf7257b | |||
b8309596be | |||
688e102f2c | |||
4d57d0a5f3 | |||
720838f137 | |||
c92ac33ade | |||
72e478937d | |||
62d8519da5 | |||
8d746f3aa9 | |||
69b2ef59e5 | |||
f9c0deb2e9 | |||
47760fa7b2 | |||
83bd86cc9b | |||
49189a532e | |||
6b917c9fb9 | |||
aa834a43c9 | |||
1775d8c5a3 | |||
779062e247 | |||
60df47fb8d | |||
c87357ff24 | |||
34a7ff7ac9 | |||
3556e77a7c | |||
57e25ef65f | |||
af5040ac6c |
44
001_documentation/Requirements/REQ0021/index.md
Normal file
44
001_documentation/Requirements/REQ0021/index.md
Normal 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 |  |
|
||||||
|
| cms | [https://demo_ls_cms.louislabs.com](https://demo_ls_cms.louislabs.com) | 3011 |  |
|
||||||
|
| mobile | [https://demo_ls_mobile.louislabs.com](https://demo_ls_mobile.louislabs.com) | 3012 |  |
|
||||||
|
| 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
|
@@ -22,3 +22,4 @@
|
|||||||
- [REQ0018: family photo of frameworks](./REQ0018/index.md)
|
- [REQ0018: family photo of frameworks](./REQ0018/index.md)
|
||||||
- [REQ0019: System architecture](./REQ0019/index.md)
|
- [REQ0019: System architecture](./REQ0019/index.md)
|
||||||
- [REQ0020: Mobile login flow](./REQ0020/index.md)
|
- [REQ0020: Mobile login flow](./REQ0020/index.md)
|
||||||
|
- [REQ0021: description](./REQ0021/index.md)
|
||||||
|
2
002_source/.gitignore
vendored
2
002_source/.gitignore
vendored
@@ -2,3 +2,5 @@
|
|||||||
**/*.log
|
**/*.log
|
||||||
**/_archive
|
**/_archive
|
||||||
**/_del
|
**/_del
|
||||||
|
|
||||||
|
**/*copy*
|
||||||
|
40
002_source/cms/.dockerignore
Normal file
40
002_source/cms/.dockerignore
Normal 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
|
@@ -82,7 +82,14 @@ module.exports = {
|
|||||||
'eslintreact/jsx-sort-props': 'off',
|
'eslintreact/jsx-sort-props': 'off',
|
||||||
'react/jsx-sort-props': 'off',
|
'react/jsx-sort-props': 'off',
|
||||||
},
|
},
|
||||||
ignorePatterns: ['**/*del', '**/*bak', '**/*copy.*', '**/*copy*.*'],
|
ignorePatterns: [
|
||||||
|
'**/*.del',
|
||||||
|
'**/*.bak',
|
||||||
|
'**/*copy.*',
|
||||||
|
'**/*copy*.*',
|
||||||
|
'**/*draft*/**',
|
||||||
|
//
|
||||||
|
],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
// override to ignore no-def for `describe`, `it`, and `expect`
|
// override to ignore no-def for `describe`, `it`, and `expect`
|
||||||
|
2
002_source/cms/.gitignore
vendored
2
002_source/cms/.gitignore
vendored
@@ -3,6 +3,8 @@
|
|||||||
**/*log
|
**/*log
|
||||||
**/*tmp
|
**/*tmp
|
||||||
|
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
|
|
||||||
|
17
002_source/cms/Dockerfile
Normal file
17
002_source/cms/Dockerfile
Normal 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"]
|
@@ -10,7 +10,10 @@
|
|||||||
"dev": "next dev -H 0.0.0.0",
|
"dev": "next dev -H 0.0.0.0",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"build:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 15 --exec \"pnpm run 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": "next lint --quiet",
|
||||||
"lint:fix": "next lint --fix",
|
"lint:fix": "next lint --fix",
|
||||||
"lint:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 5 --exec \"pnpm run lint\"",
|
"lint:w": "pnpx nodemon --ext ts,tsx,json,mjs,js,jsx --delay 5 --exec \"pnpm run lint\"",
|
||||||
|
@@ -3,5 +3,11 @@
|
|||||||
set -ex
|
set -ex
|
||||||
|
|
||||||
reset
|
reset
|
||||||
rm -rf .next
|
|
||||||
|
# rm -rf .next
|
||||||
|
|
||||||
|
# pnpm run typecheck
|
||||||
|
# pnpm run lint
|
||||||
|
|
||||||
|
# echo "check ok"
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
18
002_source/cms/scripts/docker/entrypoint.sh
Normal file → Executable file
18
002_source/cms/scripts/docker/entrypoint.sh
Normal file → Executable file
@@ -1,15 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# set -x
|
set -x
|
||||||
|
|
||||||
# nvm use 20
|
while true; do
|
||||||
nvm alias default 18
|
pnpm run start
|
||||||
nvm use default
|
|
||||||
node -v
|
|
||||||
|
|
||||||
npm i -D
|
echo "command restarted"
|
||||||
|
|
||||||
npm run dev
|
sleep 5
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
# pnpm i -D
|
||||||
|
|
||||||
|
# pnpm run start
|
||||||
|
|
||||||
# while true; do
|
# while true; do
|
||||||
# if [ "$NODE_ENV" = "development" ]; then
|
# if [ "$NODE_ENV" = "development" ]; then
|
||||||
|
11
002_source/cms/src/.vscode/settings.json
vendored
11
002_source/cms/src/.vscode/settings.json
vendored
@@ -1,3 +1,10 @@
|
|||||||
{
|
{
|
||||||
"git.ignoreLimitWarning": true
|
"git.ignoreLimitWarning": true,
|
||||||
}
|
"todo-tree.filtering.excludeGlobs": [
|
||||||
|
"**/node_modules/*/**",
|
||||||
|
"**/*.bundle.*",
|
||||||
|
"**/.next/*/**",
|
||||||
|
"**/dist/*/**",
|
||||||
|
"**/build/*/**",
|
||||||
|
],
|
||||||
|
}
|
@@ -1,21 +0,0 @@
|
|||||||
/// <reference types="jest" />
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import Page from '@/app/_helloworld/page';
|
|
||||||
|
|
||||||
// Mock the translation hook
|
|
||||||
jest.mock('react-i18next', () => ({
|
|
||||||
useTranslation: () => ({
|
|
||||||
t: (key: string) => key,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Page', () => {
|
|
||||||
it('renders a heading', () => {
|
|
||||||
render(<Page hello={'world'} />);
|
|
||||||
|
|
||||||
const heading = screen.getByRole('heading', { level: 1 });
|
|
||||||
|
|
||||||
expect(heading).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,9 +0,0 @@
|
|||||||
import { render } from '@testing-library/react';
|
|
||||||
|
|
||||||
// CUT = Component Under Test
|
|
||||||
import CUT from '../components/_helloworld';
|
|
||||||
|
|
||||||
it('renders homepage unchanged', () => {
|
|
||||||
const { container } = render(<CUT />);
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||||
|
|
||||||
|
@@ -28,12 +28,13 @@ import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr
|
|||||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||||
import { RecordModel } from 'pocketbase';
|
import type { RecordModel } from 'pocketbase';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { logger } from '@/lib/default-logger';
|
import { logger } from '@/lib/default-logger';
|
||||||
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
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';
|
||||||
@@ -45,7 +46,7 @@ import { Notifications } from '@/components/dashboard/lesson_category/notificati
|
|||||||
import { Payments } from '@/components/dashboard/lesson_category/payments';
|
import { Payments } from '@/components/dashboard/lesson_category/payments';
|
||||||
import type { Address } from '@/components/dashboard/lesson_category/shipping-address';
|
import type { Address } from '@/components/dashboard/lesson_category/shipping-address';
|
||||||
import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address';
|
import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
@@ -119,39 +120,72 @@ export default function Page(): React.JSX.Element {
|
|||||||
{t('dashboard.lessonCategorys.list.title')}
|
{t('dashboard.lessonCategorys.list.title')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
<Stack
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
|
spacing={3}
|
||||||
|
sx={{ alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={`http://127.0.0.1:8090/api/files/${showLessonCategory.collectionId}/${showLessonCategory.id}/${showLessonCategory.cat_image}`}
|
src={getImageUrlFromFile(
|
||||||
|
showLessonCategory.collectionId,
|
||||||
|
showLessonCategory.id,
|
||||||
|
showLessonCategory.cat_image
|
||||||
|
)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
>
|
>
|
||||||
empty
|
empty
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||||
|
>
|
||||||
<Typography variant="h4">{showLessonCategory.name}</Typography>
|
<Typography variant="h4">{showLessonCategory.name}</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
icon={
|
||||||
|
<CheckCircleIcon
|
||||||
|
color="var(--mui-palette-success-main)"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={showLessonCategory.visible}
|
label={showLessonCategory.visible}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Typography color="text.secondary" variant="body1">
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body1"
|
||||||
|
>
|
||||||
{showLessonCategory.id}
|
{showLessonCategory.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<div>
|
<div>
|
||||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
<Button
|
||||||
|
endIcon={<CaretDownIcon />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
Action
|
Action
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Grid container spacing={4}>
|
<Grid
|
||||||
<Grid lg={4} xs={12}>
|
container
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
lg={4}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
@@ -178,7 +212,16 @@ export default function Page(): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
{ key: 'Customer ID', value: <Chip label={showLessonCategory.id} size="small" variant="soft" /> },
|
{
|
||||||
|
key: 'Customer ID',
|
||||||
|
value: (
|
||||||
|
<Chip
|
||||||
|
label={showLessonCategory.id}
|
||||||
|
size="small"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
{ key: 'Name', value: showLessonCategory.name },
|
{ key: 'Name', value: showLessonCategory.name },
|
||||||
{ key: 'Pos', value: showLessonCategory.pos },
|
{ key: 'Pos', value: showLessonCategory.pos },
|
||||||
{
|
{
|
||||||
@@ -195,9 +238,20 @@ export default function Page(): React.JSX.Element {
|
|||||||
{
|
{
|
||||||
key: 'Quota',
|
key: 'Quota',
|
||||||
value: (
|
value: (
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
<Stack
|
||||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
direction="row"
|
||||||
<Typography color="text.secondary" variant="body2">
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<LinearProgress
|
||||||
|
sx={{ flex: '1 1 auto' }}
|
||||||
|
value={50}
|
||||||
|
variant="determinate"
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
50%
|
50%
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -206,7 +260,11 @@ export default function Page(): React.JSX.Element {
|
|||||||
] satisfies { key: string; value: React.ReactNode }[]
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
).map(
|
).map(
|
||||||
(item): React.JSX.Element => (
|
(item): React.JSX.Element => (
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</PropertyList>
|
</PropertyList>
|
||||||
@@ -223,11 +281,17 @@ export default function Page(): React.JSX.Element {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<div>
|
<div>
|
||||||
<Button color="error" variant="contained">
|
<Button
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
Delete account
|
Delete account
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Typography color="text.secondary" variant="body2">
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
A deleted lesson category cannot be restored. All data will be permanently removed.
|
A deleted lesson category cannot be restored. All data will be permanently removed.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -235,7 +299,10 @@ export default function Page(): React.JSX.Element {
|
|||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid lg={8} xs={12}>
|
<Grid
|
||||||
|
lg={8}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Payments
|
<Payments
|
||||||
ordersValue={2069.48}
|
ordersValue={2069.48}
|
||||||
@@ -282,7 +349,10 @@ export default function Page(): React.JSX.Element {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
action={
|
action={
|
||||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<PencilSimpleIcon />}
|
||||||
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -294,8 +364,14 @@ export default function Page(): React.JSX.Element {
|
|||||||
title={t('billing-details', { ns: 'lesson_category' })}
|
title={t('billing-details', { ns: 'lesson_category' })}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
<Card
|
||||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
sx={{ borderRadius: 1 }}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<PropertyList
|
||||||
|
divider={<Divider />}
|
||||||
|
sx={{ '--PropertyItem-padding': '16px' }}
|
||||||
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
{ key: 'Credit card', value: '**** 4142' },
|
{ key: 'Credit card', value: '**** 4142' },
|
||||||
@@ -307,7 +383,11 @@ export default function Page(): React.JSX.Element {
|
|||||||
] satisfies { key: string; value: React.ReactNode }[]
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
).map(
|
).map(
|
||||||
(item): React.JSX.Element => (
|
(item): React.JSX.Element => (
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</PropertyList>
|
</PropertyList>
|
||||||
@@ -317,7 +397,10 @@ export default function Page(): React.JSX.Element {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
action={
|
action={
|
||||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<PlusIcon />}
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -329,7 +412,10 @@ export default function Page(): React.JSX.Element {
|
|||||||
title={t('shipping-addresses', { ns: 'lesson_category' })}
|
title={t('shipping-addresses', { ns: 'lesson_category' })}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={3}>
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -351,7 +437,11 @@ export default function Page(): React.JSX.Element {
|
|||||||
},
|
},
|
||||||
] satisfies Address[]
|
] satisfies Address[]
|
||||||
).map((address) => (
|
).map((address) => (
|
||||||
<Grid key={address.id} md={6} xs={12}>
|
<Grid
|
||||||
|
key={address.id}
|
||||||
|
md={6}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<ShippingAddress address={address} />
|
<ShippingAddress address={address} />
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
|
@@ -90,8 +90,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isFirstRun.current) {
|
if (!isFirstRun.current) {
|
||||||
isFirstRun.current = true;
|
isFirstRun.current = true;
|
||||||
} else {
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
|
||||||
// reset page number as tab changes
|
// reset page number as tab changes
|
||||||
setLastListOption(listOption);
|
setLastListOption(listOption);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0);
|
||||||
@@ -99,7 +98,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
} else {
|
} else {
|
||||||
void reloadRows();
|
void reloadRows();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
export const LpCategoriesSampleData = [
|
export const LpCategoriesSampleData = [
|
||||||
{
|
{
|
||||||
|
@@ -13,7 +13,7 @@ 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 type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
|
||||||
export default function BasicDetailCard({
|
export default function BasicDetailCard({
|
||||||
lpModel: model,
|
lpModel: model,
|
||||||
|
@@ -10,12 +10,9 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
|||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
|
||||||
function getImageUrlFrRecord(record: CrCategory): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ lpModel }: { lpModel: CrCategory }): React.JSX.Element {
|
export default function SampleTitleCard({ lpModel }: { lpModel: CrCategory }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: CrCategory }): R
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlFrRecord(lpModel)}
|
src={getImageUrlFromFile(lpModel.collectionId, lpModel.id, lpModel.cat_image)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -13,7 +13,7 @@ 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 { LpCategory } from '@/components/dashboard/lp/categories/type';
|
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
export default function BasicDetailCard({
|
export default function BasicDetailCard({
|
||||||
lpModel: model,
|
lpModel: model,
|
||||||
|
@@ -10,11 +10,8 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
|||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
|
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
function getImageUrlFrRecord(record: LpCategory): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
|
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): R
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlFrRecord(lpModel)}
|
src={getImageUrlFromFile(lpModel.collectionId, lpModel.id, lpModel.cat_image)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
export const CrCategoriesSampleData = [
|
export const CrCategoriesSampleData = [
|
||||||
{
|
{
|
||||||
|
@@ -9,14 +9,14 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type { Customer } from '@/components/dashboard/customer/type.d';
|
import type { Customer } from '@/components/dashboard/customer/type.d';
|
||||||
|
|
||||||
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
||||||
|
|
||||||
function getImageUrlFrRecord(record: Customer): string {
|
function getImageUrlFrRecord(record: Customer): string {
|
||||||
// TODO: fix this
|
// TODO: implement getImageUrlFrRecord
|
||||||
// `http://127.0.0.1:8090/api/files/${'record.collectionId'}/${'record.id'}/${'record.cat_image'}`;
|
return 'not implemented';
|
||||||
return 'getImageUrlFrRecord(helloworld)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SampleTitleCard({ lpModel }: { lpModel: Customer }): React.JSX.Element {
|
export default function SampleTitleCard({ lpModel }: { lpModel: Customer }): React.JSX.Element {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||||
|
|
||||||
|
@@ -28,12 +28,13 @@ import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr
|
|||||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||||
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning';
|
||||||
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
||||||
import { RecordModel } from 'pocketbase';
|
import type { RecordModel } from 'pocketbase';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { logger } from '@/lib/default-logger';
|
import { logger } from '@/lib/default-logger';
|
||||||
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
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';
|
||||||
@@ -45,7 +46,7 @@ import { Notifications } from '@/components/dashboard/lesson_category/notificati
|
|||||||
import { Payments } from '@/components/dashboard/lesson_category/payments';
|
import { Payments } from '@/components/dashboard/lesson_category/payments';
|
||||||
import type { Address } from '@/components/dashboard/lesson_category/shipping-address';
|
import type { Address } from '@/components/dashboard/lesson_category/shipping-address';
|
||||||
import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address';
|
import { ShippingAddress } from '@/components/dashboard/lesson_category/shipping-address';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||||
import FormLoading from '@/components/loading';
|
import FormLoading from '@/components/loading';
|
||||||
|
|
||||||
@@ -119,39 +120,72 @@ export default function Page(): React.JSX.Element {
|
|||||||
{t('dashboard.lessonCategorys.list.title')}
|
{t('dashboard.lessonCategorys.list.title')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
<Stack
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
direction={{ xs: 'column', sm: 'row' }}
|
||||||
|
spacing={3}
|
||||||
|
sx={{ alignItems: 'flex-start' }}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={`http://127.0.0.1:8090/api/files/${showLessonCategory.collectionId}/${showLessonCategory.id}/${showLessonCategory.cat_image}`}
|
src={getImageUrlFromFile(
|
||||||
|
showLessonCategory.collectionId,
|
||||||
|
showLessonCategory.id,
|
||||||
|
showLessonCategory.cat_image
|
||||||
|
)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
>
|
>
|
||||||
empty
|
empty
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
|
||||||
|
>
|
||||||
<Typography variant="h4">{showLessonCategory.name}</Typography>
|
<Typography variant="h4">{showLessonCategory.name}</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
icon={<CheckCircleIcon color="var(--mui-palette-success-main)" weight="fill" />}
|
icon={
|
||||||
|
<CheckCircleIcon
|
||||||
|
color="var(--mui-palette-success-main)"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
}
|
||||||
label={showLessonCategory.visible}
|
label={showLessonCategory.visible}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Typography color="text.secondary" variant="body1">
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body1"
|
||||||
|
>
|
||||||
{showLessonCategory.id}
|
{showLessonCategory.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<div>
|
<div>
|
||||||
<Button endIcon={<CaretDownIcon />} variant="contained">
|
<Button
|
||||||
|
endIcon={<CaretDownIcon />}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
Action
|
Action
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Grid container spacing={4}>
|
<Grid
|
||||||
<Grid lg={4} xs={12}>
|
container
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
lg={4}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
@@ -178,7 +212,16 @@ export default function Page(): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
{ key: 'Customer ID', value: <Chip label={showLessonCategory.id} size="small" variant="soft" /> },
|
{
|
||||||
|
key: 'Customer ID',
|
||||||
|
value: (
|
||||||
|
<Chip
|
||||||
|
label={showLessonCategory.id}
|
||||||
|
size="small"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
{ key: 'Name', value: showLessonCategory.name },
|
{ key: 'Name', value: showLessonCategory.name },
|
||||||
{ key: 'Pos', value: showLessonCategory.pos },
|
{ key: 'Pos', value: showLessonCategory.pos },
|
||||||
{
|
{
|
||||||
@@ -195,9 +238,20 @@ export default function Page(): React.JSX.Element {
|
|||||||
{
|
{
|
||||||
key: 'Quota',
|
key: 'Quota',
|
||||||
value: (
|
value: (
|
||||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
<Stack
|
||||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={50} variant="determinate" />
|
direction="row"
|
||||||
<Typography color="text.secondary" variant="body2">
|
spacing={2}
|
||||||
|
sx={{ alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<LinearProgress
|
||||||
|
sx={{ flex: '1 1 auto' }}
|
||||||
|
value={50}
|
||||||
|
variant="determinate"
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
50%
|
50%
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -206,7 +260,11 @@ export default function Page(): React.JSX.Element {
|
|||||||
] satisfies { key: string; value: React.ReactNode }[]
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
).map(
|
).map(
|
||||||
(item): React.JSX.Element => (
|
(item): React.JSX.Element => (
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</PropertyList>
|
</PropertyList>
|
||||||
@@ -223,11 +281,17 @@ export default function Page(): React.JSX.Element {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<div>
|
<div>
|
||||||
<Button color="error" variant="contained">
|
<Button
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
Delete account
|
Delete account
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Typography color="text.secondary" variant="body2">
|
<Typography
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body2"
|
||||||
|
>
|
||||||
A deleted lesson category cannot be restored. All data will be permanently removed.
|
A deleted lesson category cannot be restored. All data will be permanently removed.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -235,7 +299,10 @@ export default function Page(): React.JSX.Element {
|
|||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid lg={8} xs={12}>
|
<Grid
|
||||||
|
lg={8}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Payments
|
<Payments
|
||||||
ordersValue={2069.48}
|
ordersValue={2069.48}
|
||||||
@@ -282,7 +349,10 @@ export default function Page(): React.JSX.Element {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
action={
|
action={
|
||||||
<Button color="secondary" startIcon={<PencilSimpleIcon />}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<PencilSimpleIcon />}
|
||||||
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -294,8 +364,14 @@ export default function Page(): React.JSX.Element {
|
|||||||
title={t('billing-details', { ns: 'lesson_category' })}
|
title={t('billing-details', { ns: 'lesson_category' })}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Card sx={{ borderRadius: 1 }} variant="outlined">
|
<Card
|
||||||
<PropertyList divider={<Divider />} sx={{ '--PropertyItem-padding': '16px' }}>
|
sx={{ borderRadius: 1 }}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<PropertyList
|
||||||
|
divider={<Divider />}
|
||||||
|
sx={{ '--PropertyItem-padding': '16px' }}
|
||||||
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
{ key: 'Credit card', value: '**** 4142' },
|
{ key: 'Credit card', value: '**** 4142' },
|
||||||
@@ -307,7 +383,11 @@ export default function Page(): React.JSX.Element {
|
|||||||
] satisfies { key: string; value: React.ReactNode }[]
|
] satisfies { key: string; value: React.ReactNode }[]
|
||||||
).map(
|
).map(
|
||||||
(item): React.JSX.Element => (
|
(item): React.JSX.Element => (
|
||||||
<PropertyItem key={item.key} name={item.key} value={item.value} />
|
<PropertyItem
|
||||||
|
key={item.key}
|
||||||
|
name={item.key}
|
||||||
|
value={item.value}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</PropertyList>
|
</PropertyList>
|
||||||
@@ -317,7 +397,10 @@ export default function Page(): React.JSX.Element {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
action={
|
action={
|
||||||
<Button color="secondary" startIcon={<PlusIcon />}>
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<PlusIcon />}
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -329,7 +412,10 @@ export default function Page(): React.JSX.Element {
|
|||||||
title={t('shipping-addresses', { ns: 'lesson_category' })}
|
title={t('shipping-addresses', { ns: 'lesson_category' })}
|
||||||
/>
|
/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={3}>
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -351,7 +437,11 @@ export default function Page(): React.JSX.Element {
|
|||||||
},
|
},
|
||||||
] satisfies Address[]
|
] satisfies Address[]
|
||||||
).map((address) => (
|
).map((address) => (
|
||||||
<Grid key={address.id} md={6} xs={12}>
|
<Grid
|
||||||
|
key={address.id}
|
||||||
|
md={6}
|
||||||
|
xs={12}
|
||||||
|
>
|
||||||
<ShippingAddress address={address} />
|
<ShippingAddress address={address} />
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonType } from '@/components/dashboard/lesson_type/lesson-type';
|
import type { LessonType } from '@/components/dashboard/lesson_type/lesson-type';
|
||||||
|
|
||||||
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
|
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonType } from '@/components/dashboard/lesson_type/lesson-type';
|
import type { LessonType } from '@/components/dashboard/lesson_type/lesson-type';
|
||||||
|
|
||||||
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
|
// import type { LessonType } from '@/components/dashboard/lesson_type/ILessonType';
|
||||||
|
|
||||||
|
@@ -88,8 +88,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isFirstRun.current) {
|
if (!isFirstRun.current) {
|
||||||
isFirstRun.current = true;
|
isFirstRun.current = true;
|
||||||
} else {
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
|
||||||
// reset page number as tab changes
|
// reset page number as tab changes
|
||||||
setLastListOption(listOption);
|
setLastListOption(listOption);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0);
|
||||||
@@ -97,7 +96,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
} else {
|
} else {
|
||||||
void reloadRows();
|
void reloadRows();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
export const LpCategoriesSampleData = [
|
export const LpCategoriesSampleData = [
|
||||||
{
|
{
|
||||||
|
@@ -13,7 +13,7 @@ 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 { LpCategory } from '@/components/dashboard/lp/categories/type';
|
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
export default function BasicDetailCard({
|
export default function BasicDetailCard({
|
||||||
lpModel: model,
|
lpModel: model,
|
||||||
|
@@ -10,11 +10,8 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
|||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
|
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
function getImageUrlFrRecord(record: LpCategory): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
|
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): R
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlFrRecord(lpModel)}
|
src={getImageUrlFromFile(lpModel.collectionId, lpModel.id, lpModel.cat_image)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -88,8 +88,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isFirstRun.current) {
|
if (!isFirstRun.current) {
|
||||||
isFirstRun.current = true;
|
isFirstRun.current = true;
|
||||||
} else {
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
|
||||||
// reset page number as tab changes
|
// reset page number as tab changes
|
||||||
setLastListOption(listOption);
|
setLastListOption(listOption);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0);
|
||||||
@@ -97,7 +96,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
} else {
|
} else {
|
||||||
void reloadRows();
|
void reloadRows();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
export const LpCategoriesSampleData = [
|
export const LpCategoriesSampleData = [
|
||||||
{
|
{
|
||||||
|
@@ -13,7 +13,7 @@ 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 { LpCategory } from '@/components/dashboard/lp/categories/type';
|
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
|
|
||||||
export default function BasicDetailCard({
|
export default function BasicDetailCard({
|
||||||
lpModel: model,
|
lpModel: model,
|
||||||
|
@@ -10,11 +10,8 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
|||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { LpCategory } from '@/components/dashboard/lp/categories/type';
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
|
import type { LpCategory } from '@/components/dashboard/lp/categories/type';
|
||||||
function getImageUrlFrRecord(record: LpCategory): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
|
export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: LpCategory }): R
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlFrRecord(lpModel)}
|
src={getImageUrlFromFile(lpModel.collectionId, lpModel.id, lpModel.cat_image)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -86,8 +86,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isFirstRun.current) {
|
if (!isFirstRun.current) {
|
||||||
isFirstRun.current = true;
|
isFirstRun.current = true;
|
||||||
} else {
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
|
||||||
// reset page number as tab changes
|
// reset page number as tab changes
|
||||||
setLastListOption(listOption);
|
setLastListOption(listOption);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0);
|
||||||
@@ -95,7 +94,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
} else {
|
} else {
|
||||||
void reloadRows();
|
void reloadRows();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
export const LpCategoriesSampleData = [
|
export const LpCategoriesSampleData = [
|
||||||
{
|
{
|
||||||
|
@@ -10,12 +10,9 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
|||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
|
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
|
||||||
|
|
||||||
function getImageUrlFrRecord(record: MfCategory): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element {
|
export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): R
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlFrRecord(lpModel)}
|
src={getImageUrlFromFile(lpModel.collectionId, lpModel.id, lpModel.cat_image)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -86,8 +86,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isFirstRun.current) {
|
if (!isFirstRun.current) {
|
||||||
isFirstRun.current = true;
|
isFirstRun.current = true;
|
||||||
} else {
|
} else if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
||||||
if (JSON.stringify(listOption) !== JSON.stringify(lastListOption)) {
|
|
||||||
// reset page number as tab changes
|
// reset page number as tab changes
|
||||||
setLastListOption(listOption);
|
setLastListOption(listOption);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0);
|
||||||
@@ -95,7 +94,6 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
} else {
|
} else {
|
||||||
void reloadRows();
|
void reloadRows();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
export const LpCategoriesSampleData = [
|
export const LpCategoriesSampleData = [
|
||||||
{
|
{
|
||||||
|
@@ -13,7 +13,7 @@ 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 { MfCategory } from '@/components/dashboard/mf/categories/type';
|
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
|
||||||
|
|
||||||
export default function BasicDetailCard({
|
export default function BasicDetailCard({
|
||||||
lpModel: model,
|
lpModel: model,
|
||||||
|
@@ -10,12 +10,9 @@ import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/Caret
|
|||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
|
import type { MfCategory } from '@/components/dashboard/mf/categories/type';
|
||||||
|
|
||||||
function getImageUrlFrRecord(record: MfCategory): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${record.collectionId}/${record.id}/${record.cat_image}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element {
|
export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -28,7 +25,7 @@ export default function SampleTitleCard({ lpModel }: { lpModel: MfCategory }): R
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlFrRecord(lpModel)}
|
src={getImageUrlFromFile(lpModel.collectionId, lpModel.id, lpModel.cat_image)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -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[];
|
||||||
|
@@ -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]);
|
||||||
|
@@ -9,14 +9,10 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
import type { Student } from '@/components/dashboard/student/type.d';
|
import type { Student } from '@/components/dashboard/student/type.d';
|
||||||
|
|
||||||
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
|
||||||
|
|
||||||
function getImageUrlForStudent(student: Student): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${student.collectionId}/${student.id}/${student.avatar}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ studentRecord }: { studentRecord: Student }): React.JSX.Element {
|
export default function SampleTitleCard({ studentRecord }: { studentRecord: Student }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -29,7 +25,7 @@ export default function SampleTitleCard({ studentRecord }: { studentRecord: Stud
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlForStudent(studentRecord)}
|
src={getImageUrlFromFile(studentRecord.collectionId, studentRecord.id, studentRecord.avatar)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -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]);
|
||||||
|
@@ -9,13 +9,9 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Teacher } from '@/components/dashboard/teacher/type.d';
|
|
||||||
|
|
||||||
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
|
import type { Teacher } from '@/components/dashboard/teacher/type.d';
|
||||||
function getImageUrlFrRecord(teacher: Teacher): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${teacher.collectionId}/${teacher.id}/${teacher.avatar}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teacher }): React.JSX.Element {
|
export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teacher }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -29,7 +25,7 @@ export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teac
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlFrRecord(teacherRecord)}
|
src={getImageUrlFromFile(teacherRecord.collectionId, teacherRecord.id, teacherRecord.avatar)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -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);
|
||||||
@@ -96,7 +96,7 @@ export default function Page({ searchParams }: PageProps): React.JSX.Element {
|
|||||||
}, [currentPage, rowsPerPage, listOption]);
|
}, [currentPage, rowsPerPage, listOption]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let tempFilter = [];
|
const tempFilter = [];
|
||||||
let tempSortDir = '';
|
let tempSortDir = '';
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
@@ -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]);
|
||||||
|
@@ -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,
|
||||||
|
@@ -9,13 +9,9 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown';
|
||||||
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Teacher } from '@/components/dashboard/teacher/type.d';
|
|
||||||
|
|
||||||
// import type { CrCategory } from '@/components/dashboard/cr/categories/type';
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
|
import type { Teacher } from '@/components/dashboard/teacher/type.d';
|
||||||
function getImageUrlFrRecord(teacher: Teacher): string {
|
|
||||||
return `http://127.0.0.1:8090/api/files/${teacher.collectionId}/${teacher.id}/${teacher.avatar}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teacher }): React.JSX.Element {
|
export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teacher }): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -29,7 +25,7 @@ export default function SampleTitleCard({ teacherRecord }: { teacherRecord: Teac
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={getImageUrlFrRecord(teacherRecord)}
|
src={getImageUrlFromFile(teacherRecord.collectionId, teacherRecord.id, teacherRecord.avatar)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
>
|
>
|
||||||
{t('empty')}
|
{t('empty')}
|
||||||
|
@@ -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
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
import type { LessonCategory } from '@/components/dashboard/lesson_category/type';
|
||||||
|
|
||||||
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
// import type { LessonCategory } from '@/components/dashboard/lp_categories/type';
|
||||||
|
|
||||||
|
@@ -34,6 +34,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
import { logger } from '@/lib/default-logger';
|
import { logger } from '@/lib/default-logger';
|
||||||
|
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
|
||||||
import { pb } from '@/lib/pb';
|
import { pb } from '@/lib/pb';
|
||||||
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';
|
||||||
@@ -128,7 +129,11 @@ export default function Page(): React.JSX.Element {
|
|||||||
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
sx={{ alignItems: 'center', flex: '1 1 auto' }}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={`http://127.0.0.1:8090/api/files/${showLessonCategory.collectionId}/${showLessonCategory.id}/${showLessonCategory.cat_image}`}
|
src={getImageUrlFromFile(
|
||||||
|
showLessonCategory.collectionId,
|
||||||
|
showLessonCategory.id,
|
||||||
|
showLessonCategory.image
|
||||||
|
)}
|
||||||
sx={{ '--Avatar-size': '64px' }}
|
sx={{ '--Avatar-size': '64px' }}
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
>
|
>
|
||||||
@@ -140,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
|
||||||
@@ -216,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: (
|
||||||
|
@@ -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 { OAuthProvider } from '../OAuthProvider';
|
import { oAuthProviders } from '../o-auth-providers.tsx';
|
||||||
import { oAuthProviders } from '../oAuthProviders';
|
import type { OAuthProvider } from '../OAuthProvider';
|
||||||
|
|
||||||
// interface OAuthProvider {
|
// interface OAuthProvider {
|
||||||
// id: 'google' | 'discord' | 'github';
|
// id: 'google' | 'discord' | 'github';
|
||||||
@@ -146,7 +146,7 @@ export function SignInForm(): React.JSX.Element {
|
|||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Stack
|
<Stack
|
||||||
spacing={2}
|
spacing={2}
|
||||||
direction={'row'}
|
direction="row"
|
||||||
>
|
>
|
||||||
{oAuthProviders.map(
|
{oAuthProviders.map(
|
||||||
(provider): React.JSX.Element => (
|
(provider): React.JSX.Element => (
|
||||||
|
@@ -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();
|
||||||
@@ -195,7 +195,7 @@ export function SignUpForm(): React.JSX.Element {
|
|||||||
<InputLabel>{t('email-address')}:</InputLabel>
|
<InputLabel>{t('email-address')}:</InputLabel>
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
{...field}
|
{...field}
|
||||||
placeholder={t('e.g.') + ' name@example.com'}
|
placeholder={`${t('e.g.')} name@example.com`}
|
||||||
type="email"
|
type="email"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
import { CreateForm, LessonCategory } from './type';
|
import type { CreateForm, LessonCategory } from './type';
|
||||||
|
|
||||||
// import type { CreateForm, LessonCategory } from '../lp_categories/type';
|
// import type { CreateForm, LessonCategory } from '../lp_categories/type';
|
||||||
|
|
||||||
|
@@ -26,7 +26,7 @@ import { Option } from '@/components/core/option';
|
|||||||
|
|
||||||
// import { LessonCategory } from '../lp_categories/type';
|
// import { LessonCategory } from '../lp_categories/type';
|
||||||
import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
|
import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
|
||||||
import { LessonCategory } from './type';
|
import type { LessonCategory } from './type';
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@@ -6,7 +6,7 @@ import * as React from 'react';
|
|||||||
import { useSelection } from '@/hooks/use-selection';
|
import { useSelection } from '@/hooks/use-selection';
|
||||||
import type { Selection } from '@/hooks/use-selection';
|
import type { Selection } from '@/hooks/use-selection';
|
||||||
|
|
||||||
import { LessonCategory } from './type';
|
import type { LessonCategory } from './type';
|
||||||
|
|
||||||
// import type { LessonCategory } from '../lp_categories/type';
|
// import type { LessonCategory } from '../lp_categories/type';
|
||||||
|
|
||||||
|
@@ -99,7 +99,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: {
|
active: {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
import { CrCategory, CreateFormProps } from './type';
|
import type { CrCategory} from './type';
|
||||||
|
import { CreateFormProps } from './type';
|
||||||
|
|
||||||
export const defaultCrCategory: CrCategory = {
|
export const defaultCrCategory: CrCategory = {
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
|
@@ -6,7 +6,7 @@ import * as React from 'react';
|
|||||||
import { useSelection } from '@/hooks/use-selection';
|
import { useSelection } from '@/hooks/use-selection';
|
||||||
import type { Selection } from '@/hooks/use-selection';
|
import type { Selection } from '@/hooks/use-selection';
|
||||||
|
|
||||||
import { CrCategory } from './type';
|
import type { CrCategory } from './type';
|
||||||
|
|
||||||
function noop(): void {
|
function noop(): void {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<CrCateg
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: {
|
active: {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
import { CreateFormProps, CrQuestion } from './type';
|
import type { CrQuestion } from './type';
|
||||||
|
import { CreateFormProps } from './type';
|
||||||
|
|
||||||
export const defaultCrQuestion: CrQuestion = {
|
export const defaultCrQuestion: CrQuestion = {
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
|
@@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core
|
|||||||
import { Option } from '@/components/core/option';
|
import { Option } from '@/components/core/option';
|
||||||
|
|
||||||
import { useCrQuestionsSelection } from './cr-questions-selection-context';
|
import { useCrQuestionsSelection } from './cr-questions-selection-context';
|
||||||
import { CrQuestion } from './type';
|
import type { CrQuestion } from './type';
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<CrQuest
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: {
|
active: {
|
||||||
|
@@ -89,7 +89,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<Custome
|
|||||||
|
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: {
|
active: {
|
||||||
|
@@ -81,7 +81,7 @@ function ErrorDisplay({ message, code, details, severity = 'error' }: PropsError
|
|||||||
{formattedMessage}
|
{formattedMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{details && <ErrorDetails details={details} />}
|
{details ? <ErrorDetails details={details} /> : null}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -133,7 +133,7 @@ export function NotificationsPopover({
|
|||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
color="lightgray"
|
color="lightgray"
|
||||||
variant={'subtitle2'}
|
variant="subtitle2"
|
||||||
>
|
>
|
||||||
{t('list-is-empty')}
|
{t('list-is-empty')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { 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[];
|
||||||
|
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { LoadingButton } from '@mui/lab';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
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 { authClient } from '@/lib/auth/custom/client';
|
||||||
import { logger } from '@/lib/default-logger';
|
import { logger } from '@/lib/default-logger';
|
||||||
@@ -10,39 +14,44 @@ import { useUser } from '@/hooks/use-user';
|
|||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
|
|
||||||
export function CustomSignOut(): React.JSX.Element {
|
export function CustomSignOut(): React.JSX.Element {
|
||||||
|
const { t } = useTranslation('sign_in');
|
||||||
|
|
||||||
const { checkSession } = useUser();
|
const { checkSession } = useUser();
|
||||||
|
const [buttonShowLoading, setButtonShowLoading] = React.useState<boolean>(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSignOut = React.useCallback(async (): Promise<void> => {
|
const handleSignOut = React.useCallback(async (): Promise<void> => {
|
||||||
|
setButtonShowLoading(true);
|
||||||
try {
|
try {
|
||||||
const { error } = await authClient.signOut();
|
const { error } = await authClient.signOut();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Sign out error', 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the auth state
|
// Refresh the auth state
|
||||||
await checkSession?.();
|
await checkSession?.();
|
||||||
|
|
||||||
// UserProvider, for this case, will not refresh the router and we need to do it manually
|
// UserProvider, for this case, will not refresh the router and we need to do it manually
|
||||||
router.refresh();
|
router.refresh();
|
||||||
// After refresh, AuthGuard will handle the redirect
|
// After refresh, AuthGuard will handle the redirect
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Sign out error', 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 (
|
return (
|
||||||
<MenuItem
|
<LoadingButton
|
||||||
component="div"
|
|
||||||
onClick={handleSignOut}
|
onClick={handleSignOut}
|
||||||
sx={{ justifyContent: 'center' }}
|
sx={{ width: '100%' }}
|
||||||
|
variant="text"
|
||||||
|
disabled={buttonShowLoading}
|
||||||
|
loading={buttonShowLoading}
|
||||||
|
startIcon={<SignOutIcon />}
|
||||||
|
color="secondary"
|
||||||
>
|
>
|
||||||
Sign out
|
{t('sign-out')}
|
||||||
</MenuItem>
|
</LoadingButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
@@ -16,15 +16,15 @@ import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
|
|||||||
import type { User } from '@/types/user';
|
import type { User } from '@/types/user';
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import { paths } from '@/paths';
|
import { paths } from '@/paths';
|
||||||
|
import { authClient } from '@/lib/auth/custom/client';
|
||||||
import { AuthStrategy } from '@/lib/auth/strategy';
|
import { AuthStrategy } from '@/lib/auth/strategy';
|
||||||
|
import { logger } from '@/lib/default-logger';
|
||||||
|
|
||||||
import { Auth0SignOut } from './auth0-sign-out';
|
import { Auth0SignOut } from './auth0-sign-out';
|
||||||
import { CognitoSignOut } from './cognito-sign-out';
|
import { CognitoSignOut } from './cognito-sign-out';
|
||||||
import { CustomSignOut } from './custom-sign-out';
|
import { CustomSignOut } from './custom-sign-out';
|
||||||
import { FirebaseSignOut } from './firebase-sign-out';
|
import { FirebaseSignOut } from './firebase-sign-out';
|
||||||
import { SupabaseSignOut } from './supabase-sign-out';
|
import { SupabaseSignOut } from './supabase-sign-out';
|
||||||
import { authClient } from '@/lib/auth/custom/client';
|
|
||||||
import { logger } from '@/lib/default-logger';
|
|
||||||
|
|
||||||
const defaultUser = {
|
const defaultUser = {
|
||||||
id: 'USR-000',
|
id: 'USR-000',
|
||||||
@@ -55,7 +55,8 @@ export function UserPopover({ anchorEl, onClose, open }: UserPopoverProps): Reac
|
|||||||
void loadUserMeta();
|
void loadUserMeta();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!userMeta) return <>loading</>;
|
// NOTE: delay when userMeta is null, used for sign-out
|
||||||
|
if (!userMeta) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
@@ -14,7 +14,7 @@ import { logger } from '@/lib/default-logger';
|
|||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
import { MarkOneAsRead } from '@/db/Notifications/mark-one-as-read';
|
import { MarkOneAsRead } from '@/db/Notifications/mark-one-as-read';
|
||||||
import { getUnreadNotificationsByUserId } from '@/db/Notifications/GetUnreadNotificationsByUserId';
|
import { getUnreadNotificationsByUserId } from '@/db/Notifications/GetUnreadNotificationsByUserId';
|
||||||
import { Notification } from '@/db/Notifications/type';
|
import type { Notification } from '@/db/Notifications/type';
|
||||||
import { useUser } from '@/hooks/use-user';
|
import { useUser } from '@/hooks/use-user';
|
||||||
|
|
||||||
export function NotificationsButton(): React.JSX.Element {
|
export function NotificationsButton(): React.JSX.Element {
|
||||||
|
@@ -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
|
||||||
|
@@ -66,6 +66,7 @@ export function SideNav({ color = 'evident', items = [] }: SideNavProps): React.
|
|||||||
spacing={2}
|
spacing={2}
|
||||||
sx={{ p: 2 }}
|
sx={{ p: 2 }}
|
||||||
>
|
>
|
||||||
|
{/* NOTE: hide logo
|
||||||
<div>
|
<div>
|
||||||
<Box
|
<Box
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
@@ -79,6 +80,7 @@ export function SideNav({ color = 'evident', items = [] }: SideNavProps): React.
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
|
*/}
|
||||||
<WorkspacesSwitch />
|
<WorkspacesSwitch />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box
|
<Box
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
import { CreateForm, LessonCategory } from './type';
|
import type { CreateForm, LessonCategory } from './type';
|
||||||
|
|
||||||
// import type { CreateForm, LessonCategory } from '../lp_categories/type';
|
// import type { CreateForm, LessonCategory } from '../lp_categories/type';
|
||||||
|
|
||||||
|
@@ -26,7 +26,7 @@ import { Option } from '@/components/core/option';
|
|||||||
|
|
||||||
// import { LessonCategory } from '../lp_categories/type';
|
// import { LessonCategory } from '../lp_categories/type';
|
||||||
import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
|
import { useLessonCategoriesSelection } from './lesson-categories-selection-context';
|
||||||
import { LessonCategory } from './type';
|
import type { LessonCategory } from './type';
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@@ -6,7 +6,7 @@ import * as React from 'react';
|
|||||||
import { useSelection } from '@/hooks/use-selection';
|
import { useSelection } from '@/hooks/use-selection';
|
||||||
import type { Selection } from '@/hooks/use-selection';
|
import type { Selection } from '@/hooks/use-selection';
|
||||||
|
|
||||||
import { LessonCategory } from './type';
|
import type { LessonCategory } from './type';
|
||||||
|
|
||||||
// import type { LessonCategory } from '../lp_categories/type';
|
// import type { LessonCategory } from '../lp_categories/type';
|
||||||
|
|
||||||
|
@@ -99,7 +99,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LessonC
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: {
|
active: {
|
||||||
|
@@ -37,7 +37,7 @@ import { pb } from '@/lib/pb';
|
|||||||
import { toast } from '@/components/core/toaster';
|
import { toast } from '@/components/core/toaster';
|
||||||
|
|
||||||
import { LessonTypeCreateFormDefault } from './_constants';
|
import { LessonTypeCreateFormDefault } from './_constants';
|
||||||
import { CreateForm } from './lesson-type';
|
import type { CreateForm } from './lesson-type';
|
||||||
|
|
||||||
const schema = zod.object({
|
const schema = zod.object({
|
||||||
name: zod.string().min(1, 'Name is required').max(255),
|
name: zod.string().min(1, 'Name is required').max(255),
|
||||||
|
@@ -5,7 +5,7 @@ import * as React from 'react';
|
|||||||
import { useSelection } from '@/hooks/use-selection';
|
import { useSelection } from '@/hooks/use-selection';
|
||||||
import type { Selection } from '@/hooks/use-selection';
|
import type { Selection } from '@/hooks/use-selection';
|
||||||
|
|
||||||
import { LessonType } from './lesson-type';
|
import type { LessonType } from './lesson-type';
|
||||||
|
|
||||||
// import { LessonType } from './ILessonType';
|
// import { LessonType } from './ILessonType';
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
import { CreateFormProps, LpCategory } from './type';
|
import type { LpCategory } from './type';
|
||||||
|
import { CreateFormProps } from './type';
|
||||||
|
|
||||||
export const defaultLpCategory: LpCategory = {
|
export const defaultLpCategory: LpCategory = {
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
|
@@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core
|
|||||||
import { Option } from '@/components/core/option';
|
import { Option } from '@/components/core/option';
|
||||||
|
|
||||||
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
||||||
import { LpCategory } from './type';
|
import type { LpCategory } from './type';
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@@ -6,7 +6,7 @@ import * as React from 'react';
|
|||||||
import { useSelection } from '@/hooks/use-selection';
|
import { useSelection } from '@/hooks/use-selection';
|
||||||
import type { Selection } from '@/hooks/use-selection';
|
import type { Selection } from '@/hooks/use-selection';
|
||||||
|
|
||||||
import { LpCategory } from './type';
|
import type { LpCategory } from './type';
|
||||||
|
|
||||||
function noop(): void {
|
function noop(): void {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: {
|
active: {
|
||||||
|
@@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core
|
|||||||
import { Option } from '@/components/core/option';
|
import { Option } from '@/components/core/option';
|
||||||
|
|
||||||
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
import { useLpCategoriesSelection } from './lp-categories-selection-context';
|
||||||
import { LpCategory } from './type';
|
import type { LpCategory } from './type';
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@@ -6,7 +6,7 @@ import * as React from 'react';
|
|||||||
import { useSelection } from '@/hooks/use-selection';
|
import { useSelection } from '@/hooks/use-selection';
|
||||||
import type { Selection } from '@/hooks/use-selection';
|
import type { Selection } from '@/hooks/use-selection';
|
||||||
|
|
||||||
import { LpCategory } from './type';
|
import type { LpCategory } from './type';
|
||||||
|
|
||||||
function noop(): void {
|
function noop(): void {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpCateg
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: {
|
active: {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
import { CreateFormProps, LpQuestion } from './type';
|
import type { LpQuestion } from './type';
|
||||||
|
import { CreateFormProps } from './type';
|
||||||
|
|
||||||
export const defaultLpQuestion: LpQuestion = {
|
export const defaultLpQuestion: LpQuestion = {
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
|
@@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core
|
|||||||
import { Option } from '@/components/core/option';
|
import { Option } from '@/components/core/option';
|
||||||
|
|
||||||
import { useLpQuestionsSelection } from './lp-questions-selection-context';
|
import { useLpQuestionsSelection } from './lp-questions-selection-context';
|
||||||
import { LpQuestion } from './type';
|
import type { LpQuestion } from './type';
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@@ -97,7 +97,7 @@ function columns(handleDeleteClick: (testId: string) => void): ColumnDef<LpQuest
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
formatter: (row): React.JSX.Element => {
|
formatter: (row): React.JSX.Element => {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
active: {
|
active: {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { dayjs } from '@/lib/dayjs';
|
import { dayjs } from '@/lib/dayjs';
|
||||||
|
|
||||||
import { CreateFormProps, MfCategory } from './type';
|
import type { MfCategory } from './type';
|
||||||
|
import { CreateFormProps } from './type';
|
||||||
|
|
||||||
export const defaultMfCategory: MfCategory = {
|
export const defaultMfCategory: MfCategory = {
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
|
@@ -24,7 +24,7 @@ import { FilterButton, FilterPopover, useFilterContext } from '@/components/core
|
|||||||
import { Option } from '@/components/core/option';
|
import { Option } from '@/components/core/option';
|
||||||
|
|
||||||
import { useMfCategoriesSelection } from './mf-categories-selection-context';
|
import { useMfCategoriesSelection } from './mf-categories-selection-context';
|
||||||
import { MfCategory } from './type';
|
import type { MfCategory } from './type';
|
||||||
|
|
||||||
export interface Filters {
|
export interface Filters {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@@ -6,7 +6,7 @@ import * as React from 'react';
|
|||||||
import { useSelection } from '@/hooks/use-selection';
|
import { useSelection } from '@/hooks/use-selection';
|
||||||
import type { Selection } from '@/hooks/use-selection';
|
import type { Selection } from '@/hooks/use-selection';
|
||||||
|
|
||||||
import { MfCategory } from './type';
|
import type { MfCategory } from './type';
|
||||||
|
|
||||||
function noop(): void {
|
function noop(): void {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user