From ba2138275bf0ac07daab2192da2d4c3d0f0d57c1 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Sat, 19 Apr 2025 03:25:12 +0800 Subject: [PATCH] update, --- 002_source/cms/public/locales/dev/common.json | 3 +- .../cms/public/locales/dev/translation.json | 4 + .../Categories/[customerId]/page.tsx | 308 ++ .../Categories/create/page.tsx | 48 + .../ListeningPractice/Categories/page.tsx | 255 ++ .../Questions/[customerId]/page.tsx | 308 ++ .../Questions/create/page.tsx | 48 + .../ListeningPractice/Questions/page.tsx | 255 ++ .../lesson_categories/[cat_id]/page.tsx | 2 +- .../dashboard/students/[customerId]/page.tsx | 308 ++ .../app/dashboard/students/create/page.tsx | 48 + .../cms/src/app/dashboard/students/page.tsx | 255 ++ .../dashboard/teachers/[customerId]/page.tsx | 308 ++ .../app/dashboard/teachers/create/page.tsx | 48 + .../cms/src/app/dashboard/teachers/page.tsx | 255 ++ .../src/components/dashboard/layout/config.ts | 120 +- .../student/customer-create-form.tsx | 398 +++ .../dashboard/student/customers-filters.tsx | 241 ++ .../student/customers-pagination.tsx | 31 + .../student/customers-selection-context.tsx | 43 + .../dashboard/student/customers-table.tsx | 139 + .../dashboard/student/helloworld.tsx | 3 + .../dashboard/student/notifications.tsx | 101 + .../components/dashboard/student/payments.tsx | 138 + .../dashboard/student/shipping-address.tsx | 46 + .../teacher/customer-create-form.tsx | 398 +++ .../dashboard/teacher/customers-filters.tsx | 241 ++ .../teacher/customers-pagination.tsx | 31 + .../teacher/customers-selection-context.tsx | 43 + .../dashboard/teacher/customers-table.tsx | 139 + .../dashboard/teacher/helloworld.tsx | 3 + .../dashboard/teacher/notifications.tsx | 101 + .../components/dashboard/teacher/payments.tsx | 138 + .../dashboard/teacher/shipping-address.tsx | 46 + 002_source/cms/src/db/DB_AI_GUIDELINE.MD | 12 +- .../cms/src/db/LessonTypes/Helloworld.tsx | 5 + 002_source/cms/src/db/schema.json | 2651 +++++++++++++++++ 002_source/cms/src/paths.ts | 62 +- 38 files changed, 7510 insertions(+), 73 deletions(-) create mode 100644 002_source/cms/src/app/dashboard/ListeningPractice/Categories/[customerId]/page.tsx create mode 100644 002_source/cms/src/app/dashboard/ListeningPractice/Categories/create/page.tsx create mode 100644 002_source/cms/src/app/dashboard/ListeningPractice/Categories/page.tsx create mode 100644 002_source/cms/src/app/dashboard/ListeningPractice/Questions/[customerId]/page.tsx create mode 100644 002_source/cms/src/app/dashboard/ListeningPractice/Questions/create/page.tsx create mode 100644 002_source/cms/src/app/dashboard/ListeningPractice/Questions/page.tsx create mode 100644 002_source/cms/src/app/dashboard/students/[customerId]/page.tsx create mode 100644 002_source/cms/src/app/dashboard/students/create/page.tsx create mode 100644 002_source/cms/src/app/dashboard/students/page.tsx create mode 100644 002_source/cms/src/app/dashboard/teachers/[customerId]/page.tsx create mode 100644 002_source/cms/src/app/dashboard/teachers/create/page.tsx create mode 100644 002_source/cms/src/app/dashboard/teachers/page.tsx create mode 100644 002_source/cms/src/components/dashboard/student/customer-create-form.tsx create mode 100644 002_source/cms/src/components/dashboard/student/customers-filters.tsx create mode 100644 002_source/cms/src/components/dashboard/student/customers-pagination.tsx create mode 100644 002_source/cms/src/components/dashboard/student/customers-selection-context.tsx create mode 100644 002_source/cms/src/components/dashboard/student/customers-table.tsx create mode 100644 002_source/cms/src/components/dashboard/student/helloworld.tsx create mode 100644 002_source/cms/src/components/dashboard/student/notifications.tsx create mode 100644 002_source/cms/src/components/dashboard/student/payments.tsx create mode 100644 002_source/cms/src/components/dashboard/student/shipping-address.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/customer-create-form.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/customers-filters.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/customers-pagination.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/customers-selection-context.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/customers-table.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/helloworld.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/notifications.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/payments.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/shipping-address.tsx create mode 100644 002_source/cms/src/db/LessonTypes/Helloworld.tsx create mode 100644 002_source/cms/src/db/schema.json diff --git a/002_source/cms/public/locales/dev/common.json b/002_source/cms/public/locales/dev/common.json index 0735b9c..4ba7d1e 100644 --- a/002_source/cms/public/locales/dev/common.json +++ b/002_source/cms/public/locales/dev/common.json @@ -130,5 +130,6 @@ "unable-to-process-request": "無法處理您的請求", "detailed-error-information": "詳細錯誤資訊" }, - "name-is-required": "名稱為必填" + "name-is-required": "名稱為必填", + "listening-practice": "聽講練習" } \ No newline at end of file diff --git a/002_source/cms/public/locales/dev/translation.json b/002_source/cms/public/locales/dev/translation.json index 8ecdb10..a57c1f2 100644 --- a/002_source/cms/public/locales/dev/translation.json +++ b/002_source/cms/public/locales/dev/translation.json @@ -166,6 +166,10 @@ "Delete": "刪除", "All": "全部", "categories": "課程分類", + "listening-practice": "聽講練習", + "matching-frenzy": "配對練習", + "connective-revision": "連接詞練習", + "word-count": "字數", "loading": "載入中", "Notifications": "通知", "Mark all as read": "標記所有為已讀", diff --git a/002_source/cms/src/app/dashboard/ListeningPractice/Categories/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/ListeningPractice/Categories/[customerId]/page.tsx new file mode 100644 index 0000000..edf064e --- /dev/null +++ b/002_source/cms/src/app/dashboard/ListeningPractice/Categories/[customerId]/page.tsx @@ -0,0 +1,308 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; +import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; +import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; + +import { config } from '@/config'; +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +import { Notifications } from '@/components/dashboard/customer/notifications'; +import { Payments } from '@/components/dashboard/customer/payments'; +import type { Address } from '@/components/dashboard/customer/shipping-address'; +import { ShippingAddress } from '@/components/dashboard/customer/shipping-address'; + +export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + +
+ + + Customers + +
+ + + + MV + +
+ + Miron Vitold + } + label="Active" + size="small" + variant="outlined" + /> + + + miron.vitold@domain.com + +
+
+
+ +
+
+
+ + + + + + + + } + avatar={ + + + + } + title="Basic details" + /> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > + {( + [ + { key: 'Customer ID', value: }, + { key: 'Name', value: 'Miron Vitold' }, + { key: 'Email', value: 'miron.vitold@domain.com' }, + { key: 'Phone', value: '(425) 434-5535' }, + { key: 'Company', value: 'Devias IO' }, + { + key: 'Quota', + value: ( + + + + 50% + + + ), + }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + + } + title="Security" + /> + + +
+ +
+ + A deleted customer cannot be restored. All data will be permanently removed. + +
+
+
+
+
+ + + + + }> + Edit + + } + avatar={ + + + + } + title="Billing details" + /> + + + } sx={{ '--PropertyItem-padding': '16px' }}> + {( + [ + { key: 'Credit card', value: '**** 4142' }, + { key: 'Country', value: 'United States' }, + { key: 'State', value: 'Michigan' }, + { key: 'City', value: 'Southfield' }, + { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, + { key: 'Tax ID', value: 'EU87956621' }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + }> + Add + + } + avatar={ + + + + } + title="Shipping addresses" + /> + + + {( + [ + { + id: 'ADR-001', + country: 'United States', + state: 'Michigan', + city: 'Lansing', + zipCode: '48933', + street: '480 Haven Lane', + primary: true, + }, + { + id: 'ADR-002', + country: 'United States', + state: 'Missouri', + city: 'Springfield', + zipCode: '65804', + street: '4807 Lighthouse Drive', + }, + ] satisfies Address[] + ).map((address) => ( + + + + ))} + + + + + + +
+
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/ListeningPractice/Categories/create/page.tsx b/002_source/cms/src/app/dashboard/ListeningPractice/Categories/create/page.tsx new file mode 100644 index 0000000..a0460ab --- /dev/null +++ b/002_source/cms/src/app/dashboard/ListeningPractice/Categories/create/page.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; + +import { config } from '@/config'; +import { paths } from '@/paths'; +import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form'; + +export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + +
+ + + Customers + +
+
+ Create customer +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/ListeningPractice/Categories/page.tsx b/002_source/cms/src/app/dashboard/ListeningPractice/Categories/page.tsx new file mode 100644 index 0000000..552f8dd --- /dev/null +++ b/002_source/cms/src/app/dashboard/ListeningPractice/Categories/page.tsx @@ -0,0 +1,255 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; + +import { config } from '@/config'; +import { dayjs } from '@/lib/dayjs'; +import { CustomersFilters } from '@/components/dashboard/customer/customers-filters'; +import type { Filters } from '@/components/dashboard/customer/customers-filters'; +import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination'; +import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context'; +import { CustomersTable } from '@/components/dashboard/customer/customers-table'; +import type { Customer } from '@/components/dashboard/customer/customers-table'; + +export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +const customers = [ + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, +] satisfies Customer[]; + +interface PageProps { + searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; +} + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { email, phone, sortDir, status } = searchParams; + + const sortedCustomers = applySort(customers, sortDir); + const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); + + return ( + + + + + Customers + + + + + + + + + + + + + + + + + + + ); +} + +// Sorting and filtering has to be done on the server. + +function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] { + return row.sort((a, b) => { + if (sortDir === 'asc') { + return a.createdAt.getTime() - b.createdAt.getTime(); + } + + return b.createdAt.getTime() - a.createdAt.getTime(); + }); +} + +function applyFilters(row: Customer[], { email, phone, status }: Filters): Customer[] { + return row.filter((item) => { + if (email) { + if (!item.email?.toLowerCase().includes(email.toLowerCase())) { + return false; + } + } + + if (phone) { + if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { + return false; + } + } + + if (status) { + if (item.status !== status) { + return false; + } + } + + return true; + }); +} diff --git a/002_source/cms/src/app/dashboard/ListeningPractice/Questions/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/ListeningPractice/Questions/[customerId]/page.tsx new file mode 100644 index 0000000..edf064e --- /dev/null +++ b/002_source/cms/src/app/dashboard/ListeningPractice/Questions/[customerId]/page.tsx @@ -0,0 +1,308 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; +import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; +import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; + +import { config } from '@/config'; +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +import { Notifications } from '@/components/dashboard/customer/notifications'; +import { Payments } from '@/components/dashboard/customer/payments'; +import type { Address } from '@/components/dashboard/customer/shipping-address'; +import { ShippingAddress } from '@/components/dashboard/customer/shipping-address'; + +export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + +
+ + + Customers + +
+ + + + MV + +
+ + Miron Vitold + } + label="Active" + size="small" + variant="outlined" + /> + + + miron.vitold@domain.com + +
+
+
+ +
+
+
+ + + + + + + + } + avatar={ + + + + } + title="Basic details" + /> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > + {( + [ + { key: 'Customer ID', value: }, + { key: 'Name', value: 'Miron Vitold' }, + { key: 'Email', value: 'miron.vitold@domain.com' }, + { key: 'Phone', value: '(425) 434-5535' }, + { key: 'Company', value: 'Devias IO' }, + { + key: 'Quota', + value: ( + + + + 50% + + + ), + }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + + } + title="Security" + /> + + +
+ +
+ + A deleted customer cannot be restored. All data will be permanently removed. + +
+
+
+
+
+ + + + + }> + Edit + + } + avatar={ + + + + } + title="Billing details" + /> + + + } sx={{ '--PropertyItem-padding': '16px' }}> + {( + [ + { key: 'Credit card', value: '**** 4142' }, + { key: 'Country', value: 'United States' }, + { key: 'State', value: 'Michigan' }, + { key: 'City', value: 'Southfield' }, + { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, + { key: 'Tax ID', value: 'EU87956621' }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + }> + Add + + } + avatar={ + + + + } + title="Shipping addresses" + /> + + + {( + [ + { + id: 'ADR-001', + country: 'United States', + state: 'Michigan', + city: 'Lansing', + zipCode: '48933', + street: '480 Haven Lane', + primary: true, + }, + { + id: 'ADR-002', + country: 'United States', + state: 'Missouri', + city: 'Springfield', + zipCode: '65804', + street: '4807 Lighthouse Drive', + }, + ] satisfies Address[] + ).map((address) => ( + + + + ))} + + + + + + +
+
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/ListeningPractice/Questions/create/page.tsx b/002_source/cms/src/app/dashboard/ListeningPractice/Questions/create/page.tsx new file mode 100644 index 0000000..a0460ab --- /dev/null +++ b/002_source/cms/src/app/dashboard/ListeningPractice/Questions/create/page.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; + +import { config } from '@/config'; +import { paths } from '@/paths'; +import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form'; + +export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + +
+ + + Customers + +
+
+ Create customer +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/ListeningPractice/Questions/page.tsx b/002_source/cms/src/app/dashboard/ListeningPractice/Questions/page.tsx new file mode 100644 index 0000000..552f8dd --- /dev/null +++ b/002_source/cms/src/app/dashboard/ListeningPractice/Questions/page.tsx @@ -0,0 +1,255 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; + +import { config } from '@/config'; +import { dayjs } from '@/lib/dayjs'; +import { CustomersFilters } from '@/components/dashboard/customer/customers-filters'; +import type { Filters } from '@/components/dashboard/customer/customers-filters'; +import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination'; +import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context'; +import { CustomersTable } from '@/components/dashboard/customer/customers-table'; +import type { Customer } from '@/components/dashboard/customer/customers-table'; + +export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +const customers = [ + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, +] satisfies Customer[]; + +interface PageProps { + searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; +} + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { email, phone, sortDir, status } = searchParams; + + const sortedCustomers = applySort(customers, sortDir); + const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); + + return ( + + + + + Customers + + + + + + + + + + + + + + + + + + + ); +} + +// Sorting and filtering has to be done on the server. + +function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] { + return row.sort((a, b) => { + if (sortDir === 'asc') { + return a.createdAt.getTime() - b.createdAt.getTime(); + } + + return b.createdAt.getTime() - a.createdAt.getTime(); + }); +} + +function applyFilters(row: Customer[], { email, phone, status }: Filters): Customer[] { + return row.filter((item) => { + if (email) { + if (!item.email?.toLowerCase().includes(email.toLowerCase())) { + return false; + } + } + + if (phone) { + if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { + return false; + } + } + + if (status) { + if (item.status !== status) { + return false; + } + } + + return true; + }); +} diff --git a/002_source/cms/src/app/dashboard/lesson_categories/[cat_id]/page.tsx b/002_source/cms/src/app/dashboard/lesson_categories/[cat_id]/page.tsx index 6711445..dcdcaca 100644 --- a/002_source/cms/src/app/dashboard/lesson_categories/[cat_id]/page.tsx +++ b/002_source/cms/src/app/dashboard/lesson_categories/[cat_id]/page.tsx @@ -51,7 +51,7 @@ import FormLoading from '@/components/loading'; // export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; export default function Page(): React.JSX.Element { - const { t } = useTranslation(['common']); + const { t } = useTranslation(); const router = useRouter(); // const { cat_id: catId } = useParams<{ cat_id: string }>(); diff --git a/002_source/cms/src/app/dashboard/students/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/students/[customerId]/page.tsx new file mode 100644 index 0000000..edf064e --- /dev/null +++ b/002_source/cms/src/app/dashboard/students/[customerId]/page.tsx @@ -0,0 +1,308 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; +import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; +import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; + +import { config } from '@/config'; +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +import { Notifications } from '@/components/dashboard/customer/notifications'; +import { Payments } from '@/components/dashboard/customer/payments'; +import type { Address } from '@/components/dashboard/customer/shipping-address'; +import { ShippingAddress } from '@/components/dashboard/customer/shipping-address'; + +export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + +
+ + + Customers + +
+ + + + MV + +
+ + Miron Vitold + } + label="Active" + size="small" + variant="outlined" + /> + + + miron.vitold@domain.com + +
+
+
+ +
+
+
+ + + + + + + + } + avatar={ + + + + } + title="Basic details" + /> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > + {( + [ + { key: 'Customer ID', value: }, + { key: 'Name', value: 'Miron Vitold' }, + { key: 'Email', value: 'miron.vitold@domain.com' }, + { key: 'Phone', value: '(425) 434-5535' }, + { key: 'Company', value: 'Devias IO' }, + { + key: 'Quota', + value: ( + + + + 50% + + + ), + }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + + } + title="Security" + /> + + +
+ +
+ + A deleted customer cannot be restored. All data will be permanently removed. + +
+
+
+
+
+ + + + + }> + Edit + + } + avatar={ + + + + } + title="Billing details" + /> + + + } sx={{ '--PropertyItem-padding': '16px' }}> + {( + [ + { key: 'Credit card', value: '**** 4142' }, + { key: 'Country', value: 'United States' }, + { key: 'State', value: 'Michigan' }, + { key: 'City', value: 'Southfield' }, + { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, + { key: 'Tax ID', value: 'EU87956621' }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + }> + Add + + } + avatar={ + + + + } + title="Shipping addresses" + /> + + + {( + [ + { + id: 'ADR-001', + country: 'United States', + state: 'Michigan', + city: 'Lansing', + zipCode: '48933', + street: '480 Haven Lane', + primary: true, + }, + { + id: 'ADR-002', + country: 'United States', + state: 'Missouri', + city: 'Springfield', + zipCode: '65804', + street: '4807 Lighthouse Drive', + }, + ] satisfies Address[] + ).map((address) => ( + + + + ))} + + + + + + +
+
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/students/create/page.tsx b/002_source/cms/src/app/dashboard/students/create/page.tsx new file mode 100644 index 0000000..a0460ab --- /dev/null +++ b/002_source/cms/src/app/dashboard/students/create/page.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; + +import { config } from '@/config'; +import { paths } from '@/paths'; +import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form'; + +export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + +
+ + + Customers + +
+
+ Create customer +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/students/page.tsx b/002_source/cms/src/app/dashboard/students/page.tsx new file mode 100644 index 0000000..552f8dd --- /dev/null +++ b/002_source/cms/src/app/dashboard/students/page.tsx @@ -0,0 +1,255 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; + +import { config } from '@/config'; +import { dayjs } from '@/lib/dayjs'; +import { CustomersFilters } from '@/components/dashboard/customer/customers-filters'; +import type { Filters } from '@/components/dashboard/customer/customers-filters'; +import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination'; +import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context'; +import { CustomersTable } from '@/components/dashboard/customer/customers-table'; +import type { Customer } from '@/components/dashboard/customer/customers-table'; + +export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +const customers = [ + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, +] satisfies Customer[]; + +interface PageProps { + searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; +} + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { email, phone, sortDir, status } = searchParams; + + const sortedCustomers = applySort(customers, sortDir); + const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); + + return ( + + + + + Customers + + + + + + + + + + + + + + + + + + + ); +} + +// Sorting and filtering has to be done on the server. + +function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] { + return row.sort((a, b) => { + if (sortDir === 'asc') { + return a.createdAt.getTime() - b.createdAt.getTime(); + } + + return b.createdAt.getTime() - a.createdAt.getTime(); + }); +} + +function applyFilters(row: Customer[], { email, phone, status }: Filters): Customer[] { + return row.filter((item) => { + if (email) { + if (!item.email?.toLowerCase().includes(email.toLowerCase())) { + return false; + } + } + + if (phone) { + if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { + return false; + } + } + + if (status) { + if (item.status !== status) { + return false; + } + } + + return true; + }); +} diff --git a/002_source/cms/src/app/dashboard/teachers/[customerId]/page.tsx b/002_source/cms/src/app/dashboard/teachers/[customerId]/page.tsx new file mode 100644 index 0000000..edf064e --- /dev/null +++ b/002_source/cms/src/app/dashboard/teachers/[customerId]/page.tsx @@ -0,0 +1,308 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { CreditCard as CreditCardIcon } from '@phosphor-icons/react/dist/ssr/CreditCard'; +import { House as HouseIcon } from '@phosphor-icons/react/dist/ssr/House'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShieldWarning as ShieldWarningIcon } from '@phosphor-icons/react/dist/ssr/ShieldWarning'; +import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User'; + +import { config } from '@/config'; +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { PropertyItem } from '@/components/core/property-item'; +import { PropertyList } from '@/components/core/property-list'; +import { Notifications } from '@/components/dashboard/customer/notifications'; +import { Payments } from '@/components/dashboard/customer/payments'; +import type { Address } from '@/components/dashboard/customer/shipping-address'; +import { ShippingAddress } from '@/components/dashboard/customer/shipping-address'; + +export const metadata = { title: `Details | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + +
+ + + Customers + +
+ + + + MV + +
+ + Miron Vitold + } + label="Active" + size="small" + variant="outlined" + /> + + + miron.vitold@domain.com + +
+
+
+ +
+
+
+ + + + + + + + } + avatar={ + + + + } + title="Basic details" + /> + } + orientation="vertical" + sx={{ '--PropertyItem-padding': '12px 24px' }} + > + {( + [ + { key: 'Customer ID', value: }, + { key: 'Name', value: 'Miron Vitold' }, + { key: 'Email', value: 'miron.vitold@domain.com' }, + { key: 'Phone', value: '(425) 434-5535' }, + { key: 'Company', value: 'Devias IO' }, + { + key: 'Quota', + value: ( + + + + 50% + + + ), + }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + + } + title="Security" + /> + + +
+ +
+ + A deleted customer cannot be restored. All data will be permanently removed. + +
+
+
+
+
+ + + + + }> + Edit + + } + avatar={ + + + + } + title="Billing details" + /> + + + } sx={{ '--PropertyItem-padding': '16px' }}> + {( + [ + { key: 'Credit card', value: '**** 4142' }, + { key: 'Country', value: 'United States' }, + { key: 'State', value: 'Michigan' }, + { key: 'City', value: 'Southfield' }, + { key: 'Address', value: '1721 Bartlett Avenue, 48034' }, + { key: 'Tax ID', value: 'EU87956621' }, + ] satisfies { key: string; value: React.ReactNode }[] + ).map( + (item): React.JSX.Element => ( + + ) + )} + + + + + + }> + Add + + } + avatar={ + + + + } + title="Shipping addresses" + /> + + + {( + [ + { + id: 'ADR-001', + country: 'United States', + state: 'Michigan', + city: 'Lansing', + zipCode: '48933', + street: '480 Haven Lane', + primary: true, + }, + { + id: 'ADR-002', + country: 'United States', + state: 'Missouri', + city: 'Springfield', + zipCode: '65804', + street: '4807 Lighthouse Drive', + }, + ] satisfies Address[] + ).map((address) => ( + + + + ))} + + + + + + +
+
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/teachers/create/page.tsx b/002_source/cms/src/app/dashboard/teachers/create/page.tsx new file mode 100644 index 0000000..a0460ab --- /dev/null +++ b/002_source/cms/src/app/dashboard/teachers/create/page.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import RouterLink from 'next/link'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; + +import { config } from '@/config'; +import { paths } from '@/paths'; +import { CustomerCreateForm } from '@/components/dashboard/customer/customer-create-form'; + +export const metadata = { title: `Create | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +export default function Page(): React.JSX.Element { + return ( + + + +
+ + + Customers + +
+
+ Create customer +
+
+ +
+
+ ); +} diff --git a/002_source/cms/src/app/dashboard/teachers/page.tsx b/002_source/cms/src/app/dashboard/teachers/page.tsx new file mode 100644 index 0000000..552f8dd --- /dev/null +++ b/002_source/cms/src/app/dashboard/teachers/page.tsx @@ -0,0 +1,255 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; + +import { config } from '@/config'; +import { dayjs } from '@/lib/dayjs'; +import { CustomersFilters } from '@/components/dashboard/customer/customers-filters'; +import type { Filters } from '@/components/dashboard/customer/customers-filters'; +import { CustomersPagination } from '@/components/dashboard/customer/customers-pagination'; +import { CustomersSelectionProvider } from '@/components/dashboard/customer/customers-selection-context'; +import { CustomersTable } from '@/components/dashboard/customer/customers-table'; +import type { Customer } from '@/components/dashboard/customer/customers-table'; + +export const metadata = { title: `List | Customers | Dashboard | ${config.site.name}` } satisfies Metadata; + +const customers = [ + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, + { + id: 'USR-005', + name: 'Fran Perez', + avatar: '/assets/avatar-5.png', + email: 'fran.perez@domain.com', + phone: '(815) 704-0045', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(1, 'hour').toDate(), + }, + { + id: 'USR-004', + name: 'Penjani Inyene', + avatar: '/assets/avatar-4.png', + email: 'penjani.inyene@domain.com', + phone: '(803) 937-8925', + quota: 100, + status: 'active', + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'USR-003', + name: 'Carson Darrin', + avatar: '/assets/avatar-3.png', + email: 'carson.darrin@domain.com', + phone: '(715) 278-5041', + quota: 10, + status: 'blocked', + createdAt: dayjs().subtract(1, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-002', + name: 'Siegbert Gottfried', + avatar: '/assets/avatar-2.png', + email: 'siegbert.gottfried@domain.com', + phone: '(603) 766-0431', + quota: 0, + status: 'pending', + createdAt: dayjs().subtract(7, 'hour').subtract(1, 'day').toDate(), + }, + { + id: 'USR-001', + name: 'Miron Vitold', + avatar: '/assets/avatar-1.png', + email: 'miron.vitold@domain.com', + phone: '(425) 434-5535', + quota: 50, + status: 'active', + createdAt: dayjs().subtract(2, 'hour').subtract(2, 'day').toDate(), + }, +] satisfies Customer[]; + +interface PageProps { + searchParams: { email?: string; phone?: string; sortDir?: 'asc' | 'desc'; status?: string }; +} + +export default function Page({ searchParams }: PageProps): React.JSX.Element { + const { email, phone, sortDir, status } = searchParams; + + const sortedCustomers = applySort(customers, sortDir); + const filteredCustomers = applyFilters(sortedCustomers, { email, phone, status }); + + return ( + + + + + Customers + + + + + + + + + + + + + + + + + + + ); +} + +// Sorting and filtering has to be done on the server. + +function applySort(row: Customer[], sortDir: 'asc' | 'desc' | undefined): Customer[] { + return row.sort((a, b) => { + if (sortDir === 'asc') { + return a.createdAt.getTime() - b.createdAt.getTime(); + } + + return b.createdAt.getTime() - a.createdAt.getTime(); + }); +} + +function applyFilters(row: Customer[], { email, phone, status }: Filters): Customer[] { + return row.filter((item) => { + if (email) { + if (!item.email?.toLowerCase().includes(email.toLowerCase())) { + return false; + } + } + + if (phone) { + if (!item.phone?.toLowerCase().includes(phone.toLowerCase())) { + return false; + } + } + + if (status) { + if (item.status !== status) { + return false; + } + } + + return true; + }); +} diff --git a/002_source/cms/src/components/dashboard/layout/config.ts b/002_source/cms/src/components/dashboard/layout/config.ts index 1f69921..2354895 100644 --- a/002_source/cms/src/components/dashboard/layout/config.ts +++ b/002_source/cms/src/components/dashboard/layout/config.ts @@ -62,90 +62,94 @@ export const layoutConfig = { ], }, { - key: 'quiz_listenings', - title: 'listenings', + key: 'quiz_lp', + title: 'listening-practice', icon: 'users', items: [ { - key: 'quiz_listening_categories', - title: 'question_category', - icon: 'align-left', - items: [ - { key: 'quiz_listenings', title: 'List', href: paths.dashboard.customers.list }, - { key: 'quiz_listenings:create', title: 'Create', href: paths.dashboard.customers.create }, - ], - }, - { - key: 'quiz_listening_questions', - title: 'question_list', - icon: 'align-left', - items: [ - { key: 'quiz_listenings', title: 'List', href: paths.dashboard.customers.list }, - { key: 'quiz_listenings:create', title: 'Create', href: paths.dashboard.customers.create }, - ], - }, - ], - }, - { - key: 'quiz_matching_frenzy', - title: 'Matching Frenzy', - icon: 'users', - items: [ - { - key: 'quiz_mf_categories', + key: 'quiz_lp', title: 'categories', - icon: 'align-left', - items: [ - { key: 'quiz_matching_frenzy', title: 'List', href: paths.dashboard.customers.list }, - { key: 'quiz_matching_frenzy:create', title: 'Create', href: paths.dashboard.customers.create }, - ], + href: paths.dashboard.lp_categories.list, + // }, { - key: 'quiz_mf_questions', + key: 'quiz_lp', title: 'questions', - icon: 'align-left', - items: [ - { key: 'quiz_matching_frenzy', title: 'List', href: paths.dashboard.customers.list }, - { key: 'quiz_matching_frenzy:create', title: 'Create', href: paths.dashboard.customers.create }, - ], + href: paths.dashboard.lp_questions.list, + // }, ], }, { - key: 'quiz_Connective_revision', - title: 'Connective revision', + key: 'quiz_mf', + title: 'matching-frenzy', icon: 'users', items: [ { - key: 'quiz_cr_categories', + key: 'quiz_mf', title: 'categories', - icon: 'align-left', - items: [ - { key: 'quiz_Connective_revision', title: 'List', href: paths.dashboard.customers.list }, - { key: 'quiz_Connective_revision:create', title: 'Create', href: paths.dashboard.customers.create }, - ], + href: paths.dashboard.mf_categories.list, + // }, { - key: 'quiz_cr_questions', + key: 'quiz_mf', title: 'questions', - icon: 'align-left', - items: [ - { key: 'quiz_Connective_revision', title: 'List', href: paths.dashboard.customers.list }, - { key: 'quiz_Connective_revision:create', title: 'Create', href: paths.dashboard.customers.create }, - ], + href: paths.dashboard.mf_questions.list, + // }, ], }, { - key: 'customers', - title: 'Customers', + key: 'quiz_cr', + title: 'connective-revision', icon: 'users', items: [ - { key: 'customers', title: 'List customers', href: paths.dashboard.customers.list }, - { key: 'customers:create', title: 'Create customer', href: paths.dashboard.customers.create }, - { key: 'customers:details', title: 'Customer details', href: paths.dashboard.customers.details('1') }, + { + key: 'quiz_cr', + title: 'categories', + href: paths.dashboard.cr_categories.list, + // + }, + { + key: 'quiz_cr', + title: 'questions', + href: paths.dashboard.cr_questions.list, + // + }, ], }, + + { + key: 'teachers', + title: 'Teachers', + icon: 'users', + items: [ + { key: 'teachers', title: 'List teachers', href: paths.dashboard.teachers.list }, + { key: 'teachers:create', title: 'Create teacher', href: paths.dashboard.teachers.create }, + { key: 'teachers:details', title: 'Teacher details', href: paths.dashboard.teachers.details('1') }, + ], + }, + { + key: 'students', + title: 'Students', + icon: 'users', + items: [ + { key: 'students', title: 'List students', href: paths.dashboard.students.list }, + { key: 'students:create', title: 'Create student', href: paths.dashboard.students.create }, + { key: 'students:details', title: 'Student details', href: paths.dashboard.students.details('1') }, + ], + }, + // { + // key: 'customers', + // title: 'Customers', + // icon: 'users', + // items: [ + // { key: 'customers', title: 'List customers', href: paths.dashboard.customers.list }, + // { key: 'customers:create', title: 'Create customer', href: paths.dashboard.customers.create }, + // { key: 'customers:details', title: 'Customer details', href: paths.dashboard.customers.details('1') }, + // ], + // }, + // // { // key: 'products', // title: 'Products', diff --git a/002_source/cms/src/components/dashboard/student/customer-create-form.tsx b/002_source/cms/src/components/dashboard/student/customer-create-form.tsx new file mode 100644 index 0000000..7be8fc8 --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/customer-create-form.tsx @@ -0,0 +1,398 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { zodResolver } from '@hookform/resolvers/zod'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Checkbox from '@mui/material/Checkbox'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +import { Controller, useForm } from 'react-hook-form'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { Option } from '@/components/core/option'; +import { toast } from '@/components/core/toaster'; + +function fileToBase64(file: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.onerror = () => { + reject(new Error('Error converting file to base64')); + }; + }); +} + +const schema = zod.object({ + avatar: zod.string().optional(), + name: zod.string().min(1, 'Name is required').max(255), + email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), + phone: zod.string().min(1, 'Phone is required').max(15), + company: zod.string().max(255), + billingAddress: zod.object({ + country: zod.string().min(1, 'Country is required').max(255), + state: zod.string().min(1, 'State is required').max(255), + city: zod.string().min(1, 'City is required').max(255), + zipCode: zod.string().min(1, 'Zip code is required').max(255), + line1: zod.string().min(1, 'Street line 1 is required').max(255), + line2: zod.string().max(255).optional(), + }), + taxId: zod.string().max(255).optional(), + timezone: zod.string().min(1, 'Timezone is required').max(255), + language: zod.string().min(1, 'Language is required').max(255), + currency: zod.string().min(1, 'Currency is required').max(255), +}); + +type Values = zod.infer; + +const defaultValues = { + avatar: '', + name: '', + email: '', + phone: '', + company: '', + billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, + taxId: '', + timezone: 'new_york', + language: 'en', + currency: 'USD', +} satisfies Values; + +export function CustomerCreateForm(): React.JSX.Element { + const router = useRouter(); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (_: Values): Promise => { + try { + // Make API request + toast.success('Customer updated'); + router.push(paths.dashboard.customers.details('1')); + } catch (err) { + logger.error(err); + toast.error('Something went wrong!'); + } + }, + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + return ( +
+ + + } spacing={4}> + + Account information + + + + + + + + + + Avatar + Min 400x400px, PNG or JPEG + + + + + + + ( + + Name + + {errors.name ? {errors.name.message} : null} + + )} + /> + + + ( + + Email address + + {errors.email ? {errors.email.message} : null} + + )} + /> + + + ( + + Phone number + + {errors.phone ? {errors.phone.message} : null} + + )} + /> + + + ( + + Company + + {errors.company ? {errors.company.message} : null} + + )} + /> + + + + + Billing information + + + ( + + Country + + {errors.billingAddress?.country ? ( + {errors.billingAddress?.country?.message} + ) : null} + + )} + /> + + + ( + + State + + {errors.billingAddress?.state ? ( + {errors.billingAddress?.state?.message} + ) : null} + + )} + /> + + + ( + + City + + {errors.billingAddress?.city ? ( + {errors.billingAddress?.city?.message} + ) : null} + + )} + /> + + + ( + + Zip code + + {errors.billingAddress?.zipCode ? ( + {errors.billingAddress?.zipCode?.message} + ) : null} + + )} + /> + + + ( + + Address + + {errors.billingAddress?.line1 ? ( + {errors.billingAddress?.line1?.message} + ) : null} + + )} + /> + + + ( + + Tax ID + + {errors.taxId ? {errors.taxId.message} : null} + + )} + /> + + + + + Shipping information + } label="Same as billing address" /> + + + Additional information + + + ( + + Timezone + + {errors.timezone ? {errors.timezone.message} : null} + + )} + /> + + + ( + + Language + + {errors.language ? {errors.language.message} : null} + + )} + /> + + + ( + + Currency + + {errors.currency ? {errors.currency.message} : null} + + )} + /> + + + + + + + + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/student/customers-filters.tsx b/002_source/cms/src/components/dashboard/student/customers-filters.tsx new file mode 100644 index 0000000..1567e3b --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/customers-filters.tsx @@ -0,0 +1,241 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useCustomersSelection } from './customers-selection-context'; + +// The tabs should be generated using API data. +const tabs = [ + { label: 'All', value: '', count: 5 }, + { label: 'Active', value: 'active', count: 3 }, + { label: 'Pending', value: 'pending', count: 1 }, + { label: 'Blocked', value: 'blocked', count: 1 }, +] as const; + +export interface Filters { + email?: string; + phone?: string; + status?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface CustomersFiltersProps { + filters?: Filters; + sortDir?: SortDir; +} + +export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element { + const { email, phone, status } = filters; + + const router = useRouter(); + + const selection = useCustomersSelection(); + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + const hasFilters = status || email || phone; + + return ( +
+ + {tabs.map((tab) => ( + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleEmailChange(value as string); + }} + onFilterDelete={() => { + handleEmailChange(); + }} + popover={} + value={email} + /> + { + handlePhoneChange(value as string); + }} + onFilterDelete={() => { + handlePhoneChange(); + }} + popover={} + value={phone} + /> + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} selected + + + + ) : null} + + +
+ ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/student/customers-pagination.tsx b/002_source/cms/src/components/dashboard/student/customers-pagination.tsx new file mode 100644 index 0000000..ab01272 --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/customers-pagination.tsx @@ -0,0 +1,31 @@ +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface CustomersPaginationProps { + count: number; + page: number; +} + +export function CustomersPagination({ count, page }: CustomersPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + + return ( + + ); +} diff --git a/002_source/cms/src/components/dashboard/student/customers-selection-context.tsx b/002_source/cms/src/components/dashboard/student/customers-selection-context.tsx new file mode 100644 index 0000000..023dbc0 --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/customers-selection-context.tsx @@ -0,0 +1,43 @@ +'use client'; + +import * as React from 'react'; + +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import type { Customer } from './customers-table'; + +function noop(): void { + return undefined; +} + +export interface CustomersSelectionContextValue extends Selection {} + +export const CustomersSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface CustomersSelectionProviderProps { + children: React.ReactNode; + customers: Customer[]; +} + +export function CustomersSelectionProvider({ + children, + customers = [], +}: CustomersSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => customers.map((customer) => customer.id), [customers]); + const selection = useSelection(customerIds); + + return {children}; +} + +export function useCustomersSelection(): CustomersSelectionContextValue { + return React.useContext(CustomersSelectionContext); +} diff --git a/002_source/cms/src/components/dashboard/student/customers-table.tsx b/002_source/cms/src/components/dashboard/student/customers-table.tsx new file mode 100644 index 0000000..bf9b01a --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/customers-table.tsx @@ -0,0 +1,139 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import { useCustomersSelection } from './customers-selection-context'; + +export interface Customer { + id: string; + name: string; + avatar?: string; + email: string; + phone?: string; + quota: number; + status: 'pending' | 'active' | 'blocked'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {' '} +
+ + {row.name} + + + {row.email} + +
+
+ ), + name: 'Name', + width: '250px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + name: 'Quota', + width: '250px', + }, + { field: 'phone', name: 'Phone number', width: '150px' }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY h:mm A'); + }, + name: 'Created at', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + active: { label: 'Active', icon: }, + blocked: { label: 'Blocked', icon: }, + pending: { label: 'Pending', icon: }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ; + }, + name: 'Status', + width: '150px', + }, + { + formatter: (): React.JSX.Element => ( + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface CustomersTableProps { + rows: Customer[]; +} + +export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element { + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); + + return ( + + + columns={columns} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + No customers found + + + ) : null} + + ); +} diff --git a/002_source/cms/src/components/dashboard/student/helloworld.tsx b/002_source/cms/src/components/dashboard/student/helloworld.tsx new file mode 100644 index 0000000..3989cb1 --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/helloworld.tsx @@ -0,0 +1,3 @@ +const helloworld = 'helloworld'; + +export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/student/notifications.tsx b/002_source/cms/src/components/dashboard/student/notifications.tsx new file mode 100644 index 0000000..a6c16bd --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/notifications.tsx @@ -0,0 +1,101 @@ +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/student/payments.tsx b/002_source/cms/src/components/dashboard/student/payments.tsx new file mode 100644 index 0000000..0420d32 --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/payments.tsx @@ -0,0 +1,138 @@ +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/student/shipping-address.tsx b/002_source/cms/src/components/dashboard/student/shipping-address.tsx new file mode 100644 index 0000000..8793e5c --- /dev/null +++ b/002_source/cms/src/components/dashboard/student/shipping-address.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/customer-create-form.tsx b/002_source/cms/src/components/dashboard/teacher/customer-create-form.tsx new file mode 100644 index 0000000..7be8fc8 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/customer-create-form.tsx @@ -0,0 +1,398 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { zodResolver } from '@hookform/resolvers/zod'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Checkbox from '@mui/material/Checkbox'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Grid from '@mui/material/Unstable_Grid2'; +import { Camera as CameraIcon } from '@phosphor-icons/react/dist/ssr/Camera'; +import { Controller, useForm } from 'react-hook-form'; +import { z as zod } from 'zod'; + +import { paths } from '@/paths'; +import { logger } from '@/lib/default-logger'; +import { Option } from '@/components/core/option'; +import { toast } from '@/components/core/toaster'; + +function fileToBase64(file: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.onerror = () => { + reject(new Error('Error converting file to base64')); + }; + }); +} + +const schema = zod.object({ + avatar: zod.string().optional(), + name: zod.string().min(1, 'Name is required').max(255), + email: zod.string().email('Must be a valid email').min(1, 'Email is required').max(255), + phone: zod.string().min(1, 'Phone is required').max(15), + company: zod.string().max(255), + billingAddress: zod.object({ + country: zod.string().min(1, 'Country is required').max(255), + state: zod.string().min(1, 'State is required').max(255), + city: zod.string().min(1, 'City is required').max(255), + zipCode: zod.string().min(1, 'Zip code is required').max(255), + line1: zod.string().min(1, 'Street line 1 is required').max(255), + line2: zod.string().max(255).optional(), + }), + taxId: zod.string().max(255).optional(), + timezone: zod.string().min(1, 'Timezone is required').max(255), + language: zod.string().min(1, 'Language is required').max(255), + currency: zod.string().min(1, 'Currency is required').max(255), +}); + +type Values = zod.infer; + +const defaultValues = { + avatar: '', + name: '', + email: '', + phone: '', + company: '', + billingAddress: { country: '', state: '', city: '', zipCode: '', line1: '', line2: '' }, + taxId: '', + timezone: 'new_york', + language: 'en', + currency: 'USD', +} satisfies Values; + +export function CustomerCreateForm(): React.JSX.Element { + const router = useRouter(); + + const { + control, + handleSubmit, + formState: { errors }, + setValue, + watch, + } = useForm({ defaultValues, resolver: zodResolver(schema) }); + + const onSubmit = React.useCallback( + async (_: Values): Promise => { + try { + // Make API request + toast.success('Customer updated'); + router.push(paths.dashboard.customers.details('1')); + } catch (err) { + logger.error(err); + toast.error('Something went wrong!'); + } + }, + [router] + ); + + const avatarInputRef = React.useRef(null); + const avatar = watch('avatar'); + + const handleAvatarChange = React.useCallback( + async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + + if (file) { + const url = await fileToBase64(file); + setValue('avatar', url); + } + }, + [setValue] + ); + + return ( +
+ + + } spacing={4}> + + Account information + + + + + + + + + + Avatar + Min 400x400px, PNG or JPEG + + + + + + + ( + + Name + + {errors.name ? {errors.name.message} : null} + + )} + /> + + + ( + + Email address + + {errors.email ? {errors.email.message} : null} + + )} + /> + + + ( + + Phone number + + {errors.phone ? {errors.phone.message} : null} + + )} + /> + + + ( + + Company + + {errors.company ? {errors.company.message} : null} + + )} + /> + + + + + Billing information + + + ( + + Country + + {errors.billingAddress?.country ? ( + {errors.billingAddress?.country?.message} + ) : null} + + )} + /> + + + ( + + State + + {errors.billingAddress?.state ? ( + {errors.billingAddress?.state?.message} + ) : null} + + )} + /> + + + ( + + City + + {errors.billingAddress?.city ? ( + {errors.billingAddress?.city?.message} + ) : null} + + )} + /> + + + ( + + Zip code + + {errors.billingAddress?.zipCode ? ( + {errors.billingAddress?.zipCode?.message} + ) : null} + + )} + /> + + + ( + + Address + + {errors.billingAddress?.line1 ? ( + {errors.billingAddress?.line1?.message} + ) : null} + + )} + /> + + + ( + + Tax ID + + {errors.taxId ? {errors.taxId.message} : null} + + )} + /> + + + + + Shipping information + } label="Same as billing address" /> + + + Additional information + + + ( + + Timezone + + {errors.timezone ? {errors.timezone.message} : null} + + )} + /> + + + ( + + Language + + {errors.language ? {errors.language.message} : null} + + )} + /> + + + ( + + Currency + + {errors.currency ? {errors.currency.message} : null} + + )} + /> + + + + + + + + + + +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/customers-filters.tsx b/002_source/cms/src/components/dashboard/teacher/customers-filters.tsx new file mode 100644 index 0000000..1567e3b --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/customers-filters.tsx @@ -0,0 +1,241 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Select from '@mui/material/Select'; +import type { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import Typography from '@mui/material/Typography'; + +import { paths } from '@/paths'; +import { FilterButton, FilterPopover, useFilterContext } from '@/components/core/filter-button'; +import { Option } from '@/components/core/option'; + +import { useCustomersSelection } from './customers-selection-context'; + +// The tabs should be generated using API data. +const tabs = [ + { label: 'All', value: '', count: 5 }, + { label: 'Active', value: 'active', count: 3 }, + { label: 'Pending', value: 'pending', count: 1 }, + { label: 'Blocked', value: 'blocked', count: 1 }, +] as const; + +export interface Filters { + email?: string; + phone?: string; + status?: string; +} + +export type SortDir = 'asc' | 'desc'; + +export interface CustomersFiltersProps { + filters?: Filters; + sortDir?: SortDir; +} + +export function CustomersFilters({ filters = {}, sortDir = 'desc' }: CustomersFiltersProps): React.JSX.Element { + const { email, phone, status } = filters; + + const router = useRouter(); + + const selection = useCustomersSelection(); + + const updateSearchParams = React.useCallback( + (newFilters: Filters, newSortDir: SortDir): void => { + const searchParams = new URLSearchParams(); + + if (newSortDir === 'asc') { + searchParams.set('sortDir', newSortDir); + } + + if (newFilters.status) { + searchParams.set('status', newFilters.status); + } + + if (newFilters.email) { + searchParams.set('email', newFilters.email); + } + + if (newFilters.phone) { + searchParams.set('phone', newFilters.phone); + } + + router.push(`${paths.dashboard.customers.list}?${searchParams.toString()}`); + }, + [router] + ); + + const handleClearFilters = React.useCallback(() => { + updateSearchParams({}, sortDir); + }, [updateSearchParams, sortDir]); + + const handleStatusChange = React.useCallback( + (_: React.SyntheticEvent, value: string) => { + updateSearchParams({ ...filters, status: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleEmailChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, email: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handlePhoneChange = React.useCallback( + (value?: string) => { + updateSearchParams({ ...filters, phone: value }, sortDir); + }, + [updateSearchParams, filters, sortDir] + ); + + const handleSortChange = React.useCallback( + (event: SelectChangeEvent) => { + updateSearchParams(filters, event.target.value as SortDir); + }, + [updateSearchParams, filters] + ); + + const hasFilters = status || email || phone; + + return ( +
+ + {tabs.map((tab) => ( + } + iconPosition="end" + key={tab.value} + label={tab.label} + sx={{ minHeight: 'auto' }} + tabIndex={0} + value={tab.value} + /> + ))} + + + + + { + handleEmailChange(value as string); + }} + onFilterDelete={() => { + handleEmailChange(); + }} + popover={} + value={email} + /> + { + handlePhoneChange(value as string); + }} + onFilterDelete={() => { + handlePhoneChange(); + }} + popover={} + value={phone} + /> + {hasFilters ? : null} + + {selection.selectedAny ? ( + + + {selection.selected.size} selected + + + + ) : null} + + +
+ ); +} + +function EmailFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} + +function PhoneFilterPopover(): React.JSX.Element { + const { anchorEl, onApply, onClose, open, value: initialValue } = useFilterContext(); + const [value, setValue] = React.useState(''); + + React.useEffect(() => { + setValue((initialValue as string | undefined) ?? ''); + }, [initialValue]); + + return ( + + + { + setValue(event.target.value); + }} + onKeyUp={(event) => { + if (event.key === 'Enter') { + onApply(value); + } + }} + value={value} + /> + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/customers-pagination.tsx b/002_source/cms/src/components/dashboard/teacher/customers-pagination.tsx new file mode 100644 index 0000000..ab01272 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/customers-pagination.tsx @@ -0,0 +1,31 @@ +'use client'; + +import * as React from 'react'; +import TablePagination from '@mui/material/TablePagination'; + +function noop(): void { + return undefined; +} + +interface CustomersPaginationProps { + count: number; + page: number; +} + +export function CustomersPagination({ count, page }: CustomersPaginationProps): React.JSX.Element { + // You should implement the pagination using a similar logic as the filters. + // Note that when page change, you should keep the filter search params. + + return ( + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/customers-selection-context.tsx b/002_source/cms/src/components/dashboard/teacher/customers-selection-context.tsx new file mode 100644 index 0000000..023dbc0 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/customers-selection-context.tsx @@ -0,0 +1,43 @@ +'use client'; + +import * as React from 'react'; + +import { useSelection } from '@/hooks/use-selection'; +import type { Selection } from '@/hooks/use-selection'; + +import type { Customer } from './customers-table'; + +function noop(): void { + return undefined; +} + +export interface CustomersSelectionContextValue extends Selection {} + +export const CustomersSelectionContext = React.createContext({ + deselectAll: noop, + deselectOne: noop, + selectAll: noop, + selectOne: noop, + selected: new Set(), + selectedAny: false, + selectedAll: false, +}); + +interface CustomersSelectionProviderProps { + children: React.ReactNode; + customers: Customer[]; +} + +export function CustomersSelectionProvider({ + children, + customers = [], +}: CustomersSelectionProviderProps): React.JSX.Element { + const customerIds = React.useMemo(() => customers.map((customer) => customer.id), [customers]); + const selection = useSelection(customerIds); + + return {children}; +} + +export function useCustomersSelection(): CustomersSelectionContextValue { + return React.useContext(CustomersSelectionContext); +} diff --git a/002_source/cms/src/components/dashboard/teacher/customers-table.tsx b/002_source/cms/src/components/dashboard/teacher/customers-table.tsx new file mode 100644 index 0000000..bf9b01a --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/customers-table.tsx @@ -0,0 +1,139 @@ +'use client'; + +import * as React from 'react'; +import RouterLink from 'next/link'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import LinearProgress from '@mui/material/LinearProgress'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { CheckCircle as CheckCircleIcon } from '@phosphor-icons/react/dist/ssr/CheckCircle'; +import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock'; +import { Minus as MinusIcon } from '@phosphor-icons/react/dist/ssr/Minus'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; + +import { useCustomersSelection } from './customers-selection-context'; + +export interface Customer { + id: string; + name: string; + avatar?: string; + email: string; + phone?: string; + quota: number; + status: 'pending' | 'active' | 'blocked'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {' '} +
+ + {row.name} + + + {row.email} + +
+
+ ), + name: 'Name', + width: '250px', + }, + { + formatter: (row): React.JSX.Element => ( + + + + {new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(row.quota / 100)} + + + ), + name: 'Quota', + width: '250px', + }, + { field: 'phone', name: 'Phone number', width: '150px' }, + { + formatter(row) { + return dayjs(row.createdAt).format('MMM D, YYYY h:mm A'); + }, + name: 'Created at', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + active: { label: 'Active', icon: }, + blocked: { label: 'Blocked', icon: }, + pending: { label: 'Pending', icon: }, + } as const; + const { label, icon } = mapping[row.status] ?? { label: 'Unknown', icon: null }; + + return ; + }, + name: 'Status', + width: '150px', + }, + { + formatter: (): React.JSX.Element => ( + + + + ), + name: 'Actions', + hideName: true, + width: '100px', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface CustomersTableProps { + rows: Customer[]; +} + +export function CustomersTable({ rows }: CustomersTableProps): React.JSX.Element { + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useCustomersSelection(); + + return ( + + + columns={columns} + onDeselectAll={deselectAll} + onDeselectOne={(_, row) => { + deselectOne(row.id); + }} + onSelectAll={selectAll} + onSelectOne={(_, row) => { + selectOne(row.id); + }} + rows={rows} + selectable + selected={selected} + /> + {!rows.length ? ( + + + No customers found + + + ) : null} + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/helloworld.tsx b/002_source/cms/src/components/dashboard/teacher/helloworld.tsx new file mode 100644 index 0000000..3989cb1 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/helloworld.tsx @@ -0,0 +1,3 @@ +const helloworld = 'helloworld'; + +export { helloworld }; diff --git a/002_source/cms/src/components/dashboard/teacher/notifications.tsx b/002_source/cms/src/components/dashboard/teacher/notifications.tsx new file mode 100644 index 0000000..a6c16bd --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/notifications.tsx @@ -0,0 +1,101 @@ +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Select from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; + +import { dayjs } from '@/lib/dayjs'; +import { DataTable } from '@/components/core/data-table'; +import type { ColumnDef } from '@/components/core/data-table'; +import { Option } from '@/components/core/option'; + +export interface Notification { + id: string; + type: string; + status: 'delivered' | 'pending' | 'failed'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {row.type} + + ), + name: 'Type', + width: '300px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + delivered: { label: 'Delivered', color: 'success' }, + pending: { label: 'Pending', color: 'warning' }, + failed: { label: 'Failed', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface NotificationsProps { + notifications: Notification[]; +} + +export function Notifications({ notifications }: NotificationsProps): React.JSX.Element { + return ( + + + + + } + title="Notifications" + /> + + + + +
+ +
+
+ + + columns={columns} rows={notifications} /> + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/payments.tsx b/002_source/cms/src/components/dashboard/teacher/payments.tsx new file mode 100644 index 0000000..0420d32 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/payments.tsx @@ -0,0 +1,138 @@ +'use client'; + +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import CardHeader from '@mui/material/CardHeader'; +import Chip from '@mui/material/Chip'; +import Divider from '@mui/material/Divider'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { ShoppingCartSimple as ShoppingCartSimpleIcon } from '@phosphor-icons/react/dist/ssr/ShoppingCartSimple'; + +import { dayjs } from '@/lib/dayjs'; +import type { ColumnDef } from '@/components/core/data-table'; +import { DataTable } from '@/components/core/data-table'; + +export interface Payment { + currency: string; + amount: number; + invoiceId: string; + status: 'pending' | 'completed' | 'canceled' | 'refunded'; + createdAt: Date; +} + +const columns = [ + { + formatter: (row): React.JSX.Element => ( + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: row.currency }).format(row.amount)} + + ), + name: 'Amount', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + const mapping = { + pending: { label: 'Pending', color: 'warning' }, + completed: { label: 'Completed', color: 'success' }, + canceled: { label: 'Canceled', color: 'error' }, + refunded: { label: 'Refunded', color: 'error' }, + } as const; + const { label, color } = mapping[row.status] ?? { label: 'Unknown', color: 'secondary' }; + + return ; + }, + name: 'Status', + width: '200px', + }, + { + formatter: (row): React.JSX.Element => { + return {row.invoiceId}; + }, + name: 'Invoice ID', + width: '150px', + }, + { + formatter: (row): React.JSX.Element => ( + + {dayjs(row.createdAt).format('MMM D, YYYY hh:mm A')} + + ), + name: 'Date', + align: 'right', + }, +] satisfies ColumnDef[]; + +export interface PaymentsProps { + ordersValue: number; + payments: Payment[]; + refundsValue: number; + totalOrders: number; +} + +export function Payments({ ordersValue, payments = [], refundsValue, totalOrders }: PaymentsProps): React.JSX.Element { + return ( + + }> + Create Payment + + } + avatar={ + + + + } + title="Payments" + /> + + + + } + spacing={3} + sx={{ justifyContent: 'space-between', p: 2 }} + > +
+ + Total orders + + {new Intl.NumberFormat('en-US').format(totalOrders)} +
+
+ + Orders value + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(ordersValue)} + +
+
+ + Refunds + + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(refundsValue)} + +
+
+
+ + + columns={columns} rows={payments} /> + + +
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/shipping-address.tsx b/002_source/cms/src/components/dashboard/teacher/shipping-address.tsx new file mode 100644 index 0000000..8793e5c --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/shipping-address.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PencilSimple as PencilSimpleIcon } from '@phosphor-icons/react/dist/ssr/PencilSimple'; + +export interface Address { + id: string; + country: string; + state: string; + city: string; + zipCode: string; + street: string; + primary?: boolean; +} + +export interface ShippingAddressProps { + address: Address; +} + +export function ShippingAddress({ address }: ShippingAddressProps): React.ReactElement { + return ( + + + + + {address.street}, +
+ {address.city}, {address.state}, {address.country}, +
+ {address.zipCode} +
+ + {address.primary ? : } + + +
+
+
+ ); +} diff --git a/002_source/cms/src/db/DB_AI_GUIDELINE.MD b/002_source/cms/src/db/DB_AI_GUIDELINE.MD index 111eed4..d977bef 100644 --- a/002_source/cms/src/db/DB_AI_GUIDELINE.MD +++ b/002_source/cms/src/db/DB_AI_GUIDELINE.MD @@ -4,19 +4,25 @@ please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/001_documentation/Requirements/REQ0006/schema.dbml` +please read `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/schema.json` + please look into the md files in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/_AI_GUIDELINE` please read, remember and link up the ideas, i will tell you the task afterwards --- -working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` - clone `GetVisibleCount.tsx` and `GetHiddenCount.tsx` from `LessonTypes` to `LessonCategories` and update it please draft `GetHiddenCount.tsx` for COL_LESSON_TYPES and `status = hidden` -pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `UserMetas` and update the content +well done !, please proceed to another request + +working directory: `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db` + +according information from `schema.json`, get the collection of `Students` + +pleaes clone the `tsx` files from `LessonTypes` and `LessonCategories` to `Students` and update the content when you draft coding, review file and append with `.tsx.draft` diff --git a/002_source/cms/src/db/LessonTypes/Helloworld.tsx b/002_source/cms/src/db/LessonTypes/Helloworld.tsx new file mode 100644 index 0000000..ff49bd5 --- /dev/null +++ b/002_source/cms/src/db/LessonTypes/Helloworld.tsx @@ -0,0 +1,5 @@ +function Helloworld(): string { + return 'helloworld'; +} + +export { Helloworld }; diff --git a/002_source/cms/src/db/schema.json b/002_source/cms/src/db/schema.json new file mode 100644 index 0000000..296e20f --- /dev/null +++ b/002_source/cms/src/db/schema.json @@ -0,0 +1,2651 @@ +[ + { + "id": "pbc_3142635823", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_superusers", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey_pbc_3142635823` ON `_superusers` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email_pbc_3142635823` ON `_superusers` (`email`) WHERE `email` != ''" + ], + "system": true, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "", + "username": "", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 86400 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "_pb_users_auth_", + "listRule": "id = @request.auth.id", + "viewRule": "id = @request.auth.id", + "createRule": "", + "updateRule": "id = @request.auth.id", + "deleteRule": "id = @request.auth.id", + "name": "users", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 0, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": true, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 255, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)", + "CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''" + ], + "system": false, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "mappedFields": { + "id": "", + "name": "name", + "username": "", + "avatarURL": "avatar" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 604800 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 259200 + }, + "fileToken": { + "duration": 180 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_1430376151", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Categories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2034676914", + "max": 0, + "min": 0, + "name": "cat_image_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2739402623", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation3455582614", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1196309394", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "LessonsCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1137421714", + "max": 0, + "min": 0, + "name": "cat_image_url", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation3455582614", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "convertURLs": false, + "hidden": false, + "id": "editor1843675174", + "maxSize": 0, + "name": "description", + "presentable": false, + "required": false, + "system": false, + "type": "editor" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1156222427", + "max": 0, + "min": 0, + "name": "remarks", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2328411368", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "LessonsTypes", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2363381545", + "max": 0, + "min": 0, + "name": "type", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2058414169", + "max": 0, + "min": 0, + "name": "visible", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "date1542800728", + "max": "", + "min": "", + "name": "field", + "presentable": false, + "required": false, + "system": false, + "type": "date" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_4061499106", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizCRCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3141885671", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizCRQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2416551515", + "max": 0, + "min": 0, + "name": "question_fh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2814132303", + "max": 0, + "min": 0, + "name": "question_sh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1249130051", + "max": 0, + "min": 0, + "name": "modal_ans", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_4061499106", + "hidden": false, + "id": "relation1827623476", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "json3493198471", + "maxSize": 0, + "name": "options", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3571292172", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2034676914", + "max": 0, + "min": 0, + "name": "cat_image", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_96745150", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizConnectives", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2416551515", + "max": 0, + "min": 0, + "name": "question_fh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2814132303", + "max": 0, + "min": 0, + "name": "question_sh", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1249130051", + "max": 0, + "min": 0, + "name": "modal_ans", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_342761728", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_342761728", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizConnectivesCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3639453778", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizLPCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_742947356", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizLPQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3639453778", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2511066072", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizListenings", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3571292172", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_84667061", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizMFCategories", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1125157303", + "max": 0, + "min": 0, + "name": "cat_name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file2034676914", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "cat_image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "number2161764012", + "max": null, + "min": null, + "name": "pos", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "hidden": false, + "id": "json3915970527", + "maxSize": 0, + "name": "init_answer", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_3346420851", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "QuizMFQuestions", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_84667061", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2936646783", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "QuizMatchings", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_3571292172", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 999, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1305841361", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "UserMetas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4192936109", + "max": 0, + "min": 0, + "name": "helloworld", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "json3622966325", + "maxSize": 0, + "name": "meta", + "presentable": false, + "required": false, + "system": false, + "type": "json" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2809058197", + "maxSelect": 1, + "minSelect": 0, + "name": "user_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2744374011", + "max": 0, + "min": 0, + "name": "state", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "file376926767", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1466534506", + "max": 0, + "min": 0, + "name": "role", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_1638686383", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "Vocabularies", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "file3309110367", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "image", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "hidden": false, + "id": "file4170105732", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "sound", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3287381265", + "max": 0, + "min": 0, + "name": "word", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3690399444", + "max": 0, + "min": 0, + "name": "word_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text412313404", + "max": 0, + "min": 0, + "name": "sample_e", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4059087369", + "max": 0, + "min": 0, + "name": "sample_c", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1430376151", + "hidden": false, + "id": "relation3870140739", + "maxSelect": 1, + "minSelect": 0, + "name": "cat_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text105650625", + "max": 0, + "min": 0, + "name": "category", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_2328411368", + "hidden": false, + "id": "relation808508980", + "maxSelect": 1, + "minSelect": 0, + "name": "lesson_type_id", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_4275539003", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_authOrigins", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text4228609354", + "max": 0, + "min": 0, + "name": "fingerprint", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_authOrigins_unique_pairs` ON `_authOrigins` (collectionRef, recordRef, fingerprint)" + ], + "system": true + }, + { + "id": "pbc_2281828961", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "name": "_externalAuths", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2462348188", + "max": 0, + "min": 0, + "name": "provider", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1044722854", + "max": 0, + "min": 0, + "name": "providerId", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `idx_externalAuths_record_provider` ON `_externalAuths` (collectionRef, recordRef, provider)", + "CREATE UNIQUE INDEX `idx_externalAuths_collection_provider` ON `_externalAuths` (collectionRef, provider, providerId)" + ], + "system": true + }, + { + "id": "pbc_2279338944", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_mfas", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1582905952", + "max": 0, + "min": 0, + "name": "method", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX `idx_mfas_collectionRef_recordRef` ON `_mfas` (collectionRef,recordRef)" + ], + "system": true + }, + { + "id": "pbc_1638494021", + "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId", + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "_otps", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text455797646", + "max": 0, + "min": 0, + "name": "collectionRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text127846527", + "max": 0, + "min": 0, + "name": "recordRef", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 8, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 0, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "", + "hidden": true, + "id": "text3866985172", + "max": 0, + "min": 0, + "name": "sentTo", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": true, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": true, + "type": "autodate" + } + ], + "indexes": [ + "CREATE INDEX `idx_otps_collectionRef_recordRef` ON `_otps` (collectionRef, recordRef)" + ], + "system": true + }, + { + "id": "pbc_123408445", + "listRule": "", + "viewRule": "", + "createRule": "", + "updateRule": "", + "deleteRule": "", + "name": "helloworlds", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text907060870", + "max": 0, + "min": 0, + "name": "hello", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "system": false + }, + { + "id": "pbc_2109205374", + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "name": "t1", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text907060870", + "max": 0, + "min": 0, + "name": "hello", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "file2313559263", + "maxSelect": 1, + "maxSize": 0, + "mimeTypes": [], + "name": "test_file", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + } + ], + "indexes": [], + "system": false + } +] \ No newline at end of file diff --git a/002_source/cms/src/paths.ts b/002_source/cms/src/paths.ts index 7bb9de1..59eabc0 100644 --- a/002_source/cms/src/paths.ts +++ b/002_source/cms/src/paths.ts @@ -79,25 +79,67 @@ export const paths = { lesson_types: { list: '/dashboard/lesson_types', create: '/dashboard/lesson_types/create', - details: (typeId: string) => `/dashboard/lesson_types/${typeId}`, - edit: (typeId: string) => `/dashboard/lesson_types/edit/${typeId}`, + details: (id: string) => `/dashboard/lesson_types/${id}`, + edit: (id: string) => `/dashboard/lesson_types/edit/${id}`, }, lesson_categories: { list: '/dashboard/lesson_categories', create: '/dashboard/lesson_categories/create', - details: (catId: string) => `/dashboard/lesson_categories/${catId}`, - edit: (catId: string) => `/dashboard/lesson_categories/edit/${catId}`, + details: (id: string) => `/dashboard/lesson_categories/${id}`, + edit: (id: string) => `/dashboard/lesson_categories/edit/${id}`, }, - customers: { - list: '/dashboard/customers', - create: '/dashboard/customers/create', - details: (customerId: string) => `/dashboard/customers/${customerId}`, + lp_categories: { + list: '/dashboard/lp_categories', + create: '/dashboard/lp_categories/create', + details: (id: string) => `/dashboard/lp_categories/${id}`, + edit: (id: string) => `/dashboard/lp_categories/edit/${id}`, + }, + lp_questions: { + list: '/dashboard/lp_questions', + create: '/dashboard/lp_questions/create', + details: (id: string) => `/dashboard/lp_questions/${id}`, + edit: (id: string) => `/dashboard/lp_questions/edit/${id}`, + }, + mf_categories: { + list: '/dashboard/mf_categories', + create: '/dashboard/mf_categories/create', + details: (id: string) => `/dashboard/mf_categories/${id}`, + edit: (id: string) => `/dashboard/mf_categories/edit/${id}`, + }, + mf_questions: { + list: '/dashboard/mf_questions', + create: '/dashboard/mf_questions/create', + details: (id: string) => `/dashboard/mf_questions/${id}`, + edit: (id: string) => `/dashboard/mf_questions/edit/${id}`, + }, + cr_categories: { + list: '/dashboard/cr_categories', + create: '/dashboard/cr_categories/create', + details: (id: string) => `/dashboard/cr_categories/${id}`, + edit: (id: string) => `/dashboard/cr_categories/edit/${id}`, + }, + cr_questions: { + list: '/dashboard/cr_questions', + create: '/dashboard/cr_questions/create', + details: (id: string) => `/dashboard/cr_questions/${id}`, + edit: (id: string) => `/dashboard/cr_questions/edit/${id}`, + }, + teachers: { + list: '/dashboard/teachers', + create: '/dashboard/teachers/create', + details: (id: string) => `/dashboard/teachers/${id}`, + edit: (id: string) => `/dashboard/teachers/edit/${id}`, }, students: { list: '/dashboard/students', create: '/dashboard/students/create', - details: (studentId: string) => `/dashboard/students/${studentId}`, - edit: (studentId: string) => `/dashboard/students/edit/${studentId}`, + details: (id: string) => `/dashboard/students/${id}`, + edit: (id: string) => `/dashboard/students/edit/${id}`, + }, + customers: { + list: '/dashboard/customers', + create: '/dashboard/customers/create', + details: (id: string) => `/dashboard/customers/${id}`, }, eCommerce: '/dashboard/e-commerce', fileStorage: '/dashboard/file-storage',