From 7d70b5826b4012e24a879408baf0bfd037ee48f1 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Sat, 26 Apr 2025 09:56:29 +0800 Subject: [PATCH] update, --- .../teachers/mail/[labelId]/SampleThreads.tsx | 86 +++++++++ .../mail/[labelId]/[threadId]/page.tsx | 2 +- .../teachers/mail/[labelId]/layout.tsx | 94 +--------- .../teachers/mail/[labelId]/list/page.tsx | 2 +- .../teachers/mail/[labelId]/page.tsx | 2 +- .../dashboard/teacher/mail/attachments.tsx | 53 ++++++ .../dashboard/teacher/mail/composer.tsx | 119 ++++++++++++ .../dashboard/teacher/mail/label-item.tsx | 100 ++++++++++ .../dashboard/teacher/mail/mail-context.tsx | 79 ++++++++ .../dashboard/teacher/mail/mail-view.tsx | 50 +++++ .../dashboard/teacher/mail/message.tsx | 21 +++ .../dashboard/teacher/mail/reply.tsx | 54 ++++++ .../dashboard/teacher/mail/sidebar.tsx | 172 ++++++++++++++++++ .../dashboard/teacher/mail/thread-item.tsx | 134 ++++++++++++++ .../dashboard/teacher/mail/thread-view.tsx | 170 +++++++++++++++++ .../dashboard/teacher/mail/threads-view.tsx | 163 +++++++++++++++++ .../dashboard/teacher/mail/types.d.ts | 47 +++++ 17 files changed, 1256 insertions(+), 92 deletions(-) create mode 100644 002_source/cms/src/app/dashboard/teachers/mail/[labelId]/SampleThreads.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/attachments.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/composer.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/label-item.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/mail-context.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/mail-view.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/message.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/reply.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/sidebar.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/thread-item.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/thread-view.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/threads-view.tsx create mode 100644 002_source/cms/src/components/dashboard/teacher/mail/types.d.ts diff --git a/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/SampleThreads.tsx b/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/SampleThreads.tsx new file mode 100644 index 0000000..06e870f --- /dev/null +++ b/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/SampleThreads.tsx @@ -0,0 +1,86 @@ +import type { Thread } from '@/components/dashboard/teacher/mail/types'; +import { dayjs } from '@/lib/dayjs'; + +export const SampleThreads = [ + { + id: 'TRD-004', + from: { avatar: '/assets/avatar-9.png', email: 'marcus.finn@domain.com', name: 'Marcus Finn' }, + to: [{ avatar: '/assets/avatar.png', email: 'sofia@devias.io', name: 'Sofia Rivers' }], + subject: 'Website redesign. Interested in collaboration', + message: `Hey there, + +I hope this email finds you well. I'm glad you liked my projects, and I would be happy to provide you with a quote for a similar project. + +Please let me know your requirements and any specific details you have in mind, so I can give you an accurate quote. + +Looking forward to hearing from you soon. + +Best regards, + +Marcus Finn`, + attachments: [ + { + id: 'ATT-001', + name: 'working-sketch.png', + size: '128.5 KB', + type: 'image', + url: '/assets/image-abstract-1.png', + }, + { id: 'ATT-002', name: 'summer-customers.pdf', size: '782.3 KB', type: 'file', url: '#' }, + { + id: 'ATT-003', + name: 'desktop-coffee.png', + size: '568.2 KB', + type: 'image', + url: '/assets/image-minimal-1.png', + }, + ], + folder: 'inbox', + labels: ['work', 'business'], + isImportant: true, + isStarred: false, + isUnread: true, + createdAt: dayjs().subtract(3, 'hour').toDate(), + }, + { + id: 'TRD-003', + to: [{ name: 'Sofia Rivers', avatar: '/assets/avatar.png', email: 'sofia@devias.io' }], + from: { name: 'Miron Vitold', avatar: '/assets/avatar-1.png', email: 'miron.vitold@domain.com' }, + subject: 'Amazing work', + message: `Hey, nice projects! I really liked the one in react. What's your quote on kinda similar project?`, + folder: 'spam', + labels: [], + isImportant: false, + isStarred: true, + isUnread: false, + createdAt: dayjs().subtract(1, 'day').toDate(), + }, + { + id: 'TRD-002', + from: { name: 'Penjani Inyene', avatar: '/assets/avatar-4.png', email: 'penjani.inyene@domain.com' }, + to: [{ name: 'Sofia Rivers', avatar: '/assets/avatar.png', email: 'sofia@devias.io' }], + subject: 'Flight reminder', + message: `Dear Sofia, + +Your flight is coming up soon. Please don't forget to check in for your scheduled flight.`, + folder: 'inbox', + labels: ['business'], + isImportant: false, + isStarred: false, + isUnread: false, + createdAt: dayjs().subtract(2, 'day').toDate(), + }, + { + id: 'TRD-001', + from: { name: 'Carson Darrin', avatar: '/assets/avatar-3.png', email: 'carson.darrin@domain.com' }, + to: [{ name: 'Sofia Rivers', avatar: '/assets/avatar.png', email: 'sofia@devias.io' }], + subject: 'Possible candidates for the position', + message: `My market leading client has another fantastic opportunity for an experienced Software Developer to join them on a heavily remote basis`, + folder: 'trash', + labels: ['personal'], + isImportant: false, + isStarred: false, + isUnread: true, + createdAt: dayjs().subtract(2, 'day').toDate(), + }, +] satisfies Thread[]; diff --git a/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/[threadId]/page.tsx b/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/[threadId]/page.tsx index cf93176..231c33f 100644 --- a/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/[threadId]/page.tsx +++ b/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/[threadId]/page.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import type { Metadata } from 'next'; import { config } from '@/config'; -import { ThreadView } from '@/components/dashboard/mail/thread-view'; +import { ThreadView } from '@/components/dashboard/teacher/mail/thread-view'; export const metadata = { title: `Thread | Mail | Dashboard | ${config.site.name}` } satisfies Metadata; diff --git a/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/layout.tsx b/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/layout.tsx index e159e20..78ac428 100644 --- a/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/layout.tsx +++ b/002_source/cms/src/app/dashboard/teachers/mail/[labelId]/layout.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { dayjs } from '@/lib/dayjs'; -import { MailProvider } from '@/components/dashboard/mail/mail-context'; -import { MailView } from '@/components/dashboard/mail/mail-view'; -import type { Label, Thread } from '@/components/dashboard/mail/types'; +import { MailProvider } from '@/components/dashboard/teacher/mail/mail-context'; +import { MailView } from '@/components/dashboard/teacher/mail/mail-view'; +import type { Label, Thread } from '@/components/dashboard/teacher/mail/types'; +import { SampleThreads } from './SampleThreads'; function filterThreads(threads: Thread[], labelId: string): Thread[] { return threads.filter((thread) => { @@ -40,90 +40,6 @@ const labels = [ { id: 'personal', type: 'custom', name: 'Personal', color: '#FB8A00', unreadCount: 0, totalCount: 1 }, ] satisfies Label[]; -const threads = [ - { - id: 'TRD-004', - from: { avatar: '/assets/avatar-9.png', email: 'marcus.finn@domain.com', name: 'Marcus Finn' }, - to: [{ avatar: '/assets/avatar.png', email: 'sofia@devias.io', name: 'Sofia Rivers' }], - subject: 'Website redesign. Interested in collaboration', - message: `Hey there, - -I hope this email finds you well. I'm glad you liked my projects, and I would be happy to provide you with a quote for a similar project. - -Please let me know your requirements and any specific details you have in mind, so I can give you an accurate quote. - -Looking forward to hearing from you soon. - -Best regards, - -Marcus Finn`, - attachments: [ - { - id: 'ATT-001', - name: 'working-sketch.png', - size: '128.5 KB', - type: 'image', - url: '/assets/image-abstract-1.png', - }, - { id: 'ATT-002', name: 'summer-customers.pdf', size: '782.3 KB', type: 'file', url: '#' }, - { - id: 'ATT-003', - name: 'desktop-coffee.png', - size: '568.2 KB', - type: 'image', - url: '/assets/image-minimal-1.png', - }, - ], - folder: 'inbox', - labels: ['work', 'business'], - isImportant: true, - isStarred: false, - isUnread: true, - createdAt: dayjs().subtract(3, 'hour').toDate(), - }, - { - id: 'TRD-003', - to: [{ name: 'Sofia Rivers', avatar: '/assets/avatar.png', email: 'sofia@devias.io' }], - from: { name: 'Miron Vitold', avatar: '/assets/avatar-1.png', email: 'miron.vitold@domain.com' }, - subject: 'Amazing work', - message: `Hey, nice projects! I really liked the one in react. What's your quote on kinda similar project?`, - folder: 'spam', - labels: [], - isImportant: false, - isStarred: true, - isUnread: false, - createdAt: dayjs().subtract(1, 'day').toDate(), - }, - { - id: 'TRD-002', - from: { name: 'Penjani Inyene', avatar: '/assets/avatar-4.png', email: 'penjani.inyene@domain.com' }, - to: [{ name: 'Sofia Rivers', avatar: '/assets/avatar.png', email: 'sofia@devias.io' }], - subject: 'Flight reminder', - message: `Dear Sofia, - -Your flight is coming up soon. Please don't forget to check in for your scheduled flight.`, - folder: 'inbox', - labels: ['business'], - isImportant: false, - isStarred: false, - isUnread: false, - createdAt: dayjs().subtract(2, 'day').toDate(), - }, - { - id: 'TRD-001', - from: { name: 'Carson Darrin', avatar: '/assets/avatar-3.png', email: 'carson.darrin@domain.com' }, - to: [{ name: 'Sofia Rivers', avatar: '/assets/avatar.png', email: 'sofia@devias.io' }], - subject: 'Possible candidates for the position', - message: `My market leading client has another fantastic opportunity for an experienced Software Developer to join them on a heavily remote basis`, - folder: 'trash', - labels: ['personal'], - isImportant: false, - isStarred: false, - isUnread: true, - createdAt: dayjs().subtract(2, 'day').toDate(), - }, -] satisfies Thread[]; - interface LayoutProps { children: React.ReactNode; params: { labelId: string }; @@ -132,7 +48,7 @@ interface LayoutProps { export default function Layout({ children, params }: LayoutProps): React.JSX.Element { const { labelId } = params; - const filteredThreads = filterThreads(threads, labelId); + const filteredThreads = filterThreads(SampleThreads, labelId); return ( + {count} attachments + + {attachments.map((attachment) => ( + + {attachment.type === 'image' ? : null} + {attachment.type === 'file' ? ( + + + + ) : null} +
+ + {attachment.name} + + + {attachment.size} + +
+
+ ))} +
+
+ +
+ + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/composer.tsx b/002_source/cms/src/components/dashboard/teacher/mail/composer.tsx new file mode 100644 index 0000000..c910475 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/composer.tsx @@ -0,0 +1,119 @@ +'use client'; + +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import Input from '@mui/material/Input'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import { ArrowsInSimple as ArrowsInSimpleIcon } from '@phosphor-icons/react/dist/ssr/ArrowsInSimple'; +import { ArrowsOutSimple as ArrowsOutSimpleIcon } from '@phosphor-icons/react/dist/ssr/ArrowsOutSimple'; +import { Image as ImageIcon } from '@phosphor-icons/react/dist/ssr/Image'; +import { Paperclip as PaperclipIcon } from '@phosphor-icons/react/dist/ssr/Paperclip'; +import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X'; +import type { EditorEvents } from '@tiptap/react'; + +import { TextEditor } from '@/components/core/text-editor/text-editor'; + +export interface ComposerProps { + onClose?: () => void; + open: boolean; +} + +export function Composer({ onClose, open }: ComposerProps): React.JSX.Element | null { + const [isMaximized, setIsMaximized] = React.useState(false); + const [message, setMessage] = React.useState(''); + const [subject, setSubject] = React.useState(''); + const [to, setTo] = React.useState(''); + + const handleSubjectChange = React.useCallback((event: React.ChangeEvent) => { + setSubject(event.target.value); + }, []); + + const handleMessageChange = React.useCallback(({ editor }: EditorEvents['update']) => { + setMessage(editor.getText()); + }, []); + + const handleToChange = React.useCallback((event: React.ChangeEvent) => { + setTo(event.target.value); + }, []); + + if (!open) { + return null; + } + + return ( + + + + New message + + {isMaximized ? ( + { + setIsMaximized(false); + }} + > + + + ) : ( + { + setIsMaximized(true); + }} + > + + + )} + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/label-item.tsx b/002_source/cms/src/components/dashboard/teacher/mail/label-item.tsx new file mode 100644 index 0000000..eaec5a8 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/label-item.tsx @@ -0,0 +1,100 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import type { Icon } from '@phosphor-icons/react/dist/lib/types'; +import { Bookmark as BookmarkIcon } from '@phosphor-icons/react/dist/ssr/Bookmark'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; +import { File as FileIcon } from '@phosphor-icons/react/dist/ssr/File'; +import { PaperPlaneTilt as PaperPlaneTiltIcon } from '@phosphor-icons/react/dist/ssr/PaperPlaneTilt'; +import { Star as StarIcon } from '@phosphor-icons/react/dist/ssr/Star'; +import { Tag as TagIcon } from '@phosphor-icons/react/dist/ssr/Tag'; +import { Trash as TrashIcon } from '@phosphor-icons/react/dist/ssr/Trash'; +import { WarningCircle as WarningCircleIcon } from '@phosphor-icons/react/dist/ssr/WarningCircle'; + +import type { Label } from './types'; + +const systemLabelIcons: Record = { + inbox: EnvelopeSimpleIcon, + sent: PaperPlaneTiltIcon, + trash: TrashIcon, + drafts: FileIcon, + spam: WarningCircleIcon, + starred: StarIcon, + important: BookmarkIcon, +}; + +function getIcon(label: Label): Icon { + if (label.type === 'system') { + return systemLabelIcons[label.id] ?? systemLabelIcons.inbox; + } + + return TagIcon; +} + +function getIconColor(label: Label, active?: boolean): string { + if (label.type === 'custom') { + return label.color ?? 'var(--mui-palette-text-secondary)'; + } + + return active ? 'var(--mui-palette-text-primary)' : 'var(--mui-palette-text-secondary)'; +} + +export interface LabelItemProps { + active?: boolean; + label: Label; + onSelect?: () => void; +} + +export function LabelItem({ active, label, onSelect }: LabelItemProps): React.JSX.Element { + const Icon = getIcon(label); + const iconColor = getIconColor(label, active); + const showUnreadCount = (label.unreadCount ?? 0) > 0; + + return ( + + { + if (event.key === 'Enter' || event.key === ' ') { + onSelect?.(); + } + }} + role="button" + sx={{ + alignItems: 'center', + borderRadius: 1, + color: 'var(--mui-palette-text-secondary)', + cursor: 'pointer', + display: 'flex', + flex: '0 0 auto', + gap: 1, + p: '6px 16px', + textDecoration: 'none', + whiteSpace: 'nowrap', + ...(active && { bgcolor: 'var(--mui-palette-action-selected)', color: 'var(--mui-palette-text-primary)' }), + '&:hover': { + ...(!active && { bgcolor: 'var(--mui-palette-action-hover)', color: 'var(---mui-palette-text-primary)' }), + }, + }} + tabIndex={0} + > + + + + + + {label.name} + + + {showUnreadCount ? ( + + {label.unreadCount} + + ) : null} + + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/mail-context.tsx b/002_source/cms/src/components/dashboard/teacher/mail/mail-context.tsx new file mode 100644 index 0000000..d5c6e0b --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/mail-context.tsx @@ -0,0 +1,79 @@ +'use client'; + +import * as React from 'react'; + +import type { Label, Thread } from './types'; + +function noop(): void { + return undefined; +} + +export interface MailContextValue { + labels: Label[]; + threads: Thread[]; + currentLabelId: string; + openDesktopSidebar: boolean; + setOpenDesktopSidebar: React.Dispatch>; + openMobileSidebar: boolean; + setOpenMobileSidebar: React.Dispatch>; + openCompose: boolean; + setOpenCompose: React.Dispatch>; +} + +export const MailContext = React.createContext({ + labels: [], + threads: [], + currentLabelId: 'inbox', + openDesktopSidebar: true, + setOpenDesktopSidebar: noop, + openMobileSidebar: true, + setOpenMobileSidebar: noop, + openCompose: false, + setOpenCompose: noop, +}); + +export interface MailProviderProps { + children: React.ReactNode; + labels: Label[]; + threads: Thread[]; + currentLabelId: string; +} + +export function MailProvider({ + children, + labels: initialLabels = [], + threads: initialThreads = [], + currentLabelId, +}: MailProviderProps): React.JSX.Element { + const [labels, setLabels] = React.useState([]); + const [threads, setThreads] = React.useState([]); + const [openDesktopSidebar, setOpenDesktopSidebar] = React.useState(true); + const [openMobileSidebar, setOpenMobileSidebar] = React.useState(false); + const [openCompose, setOpenCompose] = React.useState(false); + + React.useEffect((): void => { + setLabels(initialLabels); + }, [initialLabels]); + + React.useEffect((): void => { + setThreads(initialThreads); + }, [initialThreads]); + + return ( + + {children} + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/mail-view.tsx b/002_source/cms/src/components/dashboard/teacher/mail/mail-view.tsx new file mode 100644 index 0000000..02e9a78 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/mail-view.tsx @@ -0,0 +1,50 @@ +'use client'; + +import * as React from 'react'; +import Box from '@mui/material/Box'; + +import { Composer } from './composer'; +import { MailContext } from './mail-context'; +import { Sidebar } from './sidebar'; + +export interface MailViewProps { + children: React.ReactNode; +} + +export function MailView({ children }: MailViewProps): React.JSX.Element { + const { + labels, + currentLabelId, + openDesktopSidebar, + openMobileSidebar, + setOpenMobileSidebar, + openCompose, + setOpenCompose, + } = React.useContext(MailContext); + + return ( + + + { + setOpenMobileSidebar(false); + }} + onCompose={() => { + setOpenCompose(true); + }} + openDesktop={openDesktopSidebar} + openMobile={openMobileSidebar} + /> + {children} + + { + setOpenCompose(false); + }} + open={openCompose} + /> + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/message.tsx b/002_source/cms/src/components/dashboard/teacher/mail/message.tsx new file mode 100644 index 0000000..f0edd50 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/message.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Markdown from 'react-markdown'; + +export interface MessageProps { + content: string; +} + +export function Message({ content }: MessageProps): React.JSX.Element { + return ( + + {content} + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/reply.tsx b/002_source/cms/src/components/dashboard/teacher/mail/reply.tsx new file mode 100644 index 0000000..b4d41d6 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/reply.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Tooltip from '@mui/material/Tooltip'; +import { Image as ImageIcon } from '@phosphor-icons/react/dist/ssr/Image'; +import { Link as LinkIcon } from '@phosphor-icons/react/dist/ssr/Link'; +import { Paperclip as PaperclipIcon } from '@phosphor-icons/react/dist/ssr/Paperclip'; +import { Smiley as SmileyIcon } from '@phosphor-icons/react/dist/ssr/Smiley'; + +import type { User } from '@/types/user'; + +const user = { + id: 'USR-000', + name: 'Sofia Rivers', + avatar: '/assets/avatar.png', + email: 'sofia@devias.io', +} satisfies User; + +export function Reply(): React.JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/sidebar.tsx b/002_source/cms/src/components/dashboard/teacher/mail/sidebar.tsx new file mode 100644 index 0000000..4483854 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/sidebar.tsx @@ -0,0 +1,172 @@ +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import Drawer from '@mui/material/Drawer'; +import IconButton from '@mui/material/IconButton'; +import ListSubheader from '@mui/material/ListSubheader'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus'; +import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X'; + +import { paths } from '@/paths'; +import { useMediaQuery } from '@/hooks/use-media-query'; + +import { LabelItem } from './label-item'; +import type { Label, LabelType } from './types'; + +interface GroupedLabels { + system: Label[]; + custom: Label[]; +} + +function groupLabels(labels: Label[]): GroupedLabels { + const groups: GroupedLabels = { system: [], custom: [] }; + + labels.forEach((label) => { + if (label.type === 'system') { + groups.system.push(label); + } else { + groups.custom.push(label); + } + }); + + return groups; +} + +// A single sidebar component is used, because you might need to have internal state +// shared between both mobile and desktop + +export interface SidebarProps { + currentLabelId?: string; + labels: Label[]; + openDesktop?: boolean; + openMobile?: boolean; + onCloseMobile?: () => void; + onCompose?: () => void; +} + +export function Sidebar({ + currentLabelId, + labels, + onCloseMobile, + onCompose, + openDesktop = false, + openMobile = false, +}: SidebarProps): React.JSX.Element { + const mdUp = useMediaQuery('up', 'md'); + + const content = ( + + ); + + if (mdUp) { + return ( + + {content} + + ); + } + + return ( + + {content} + + ); +} + +export interface SidebarContentProps { + closeOnSelect?: boolean; + currentLabelId?: string; + labels: Label[]; + open?: boolean; + onClose?: () => void; + onCompose?: () => void; +} + +function SidebarContent({ + closeOnSelect, + currentLabelId, + labels, + onClose, + onCompose, +}: SidebarContentProps): React.JSX.Element { + const router = useRouter(); + + const handleLabelSelect = React.useCallback( + (labelId: string) => { + if (closeOnSelect) { + onClose?.(); + } + + const href = paths.dashboard.mail.list(labelId); + + router.push(href); + }, + [router, closeOnSelect, onClose] + ); + + // Maybe use memo + const groupedLabels: GroupedLabels = groupLabels(labels); + + return ( + + + + Mailbox + + + + + + + + + {Object.keys(groupedLabels).map((type) => ( + + {type === 'custom' ? ( + + + Custom Labels + + + ) : null} + + {groupedLabels[type as LabelType].map((label) => ( + { + handleLabelSelect(label.id); + }} + /> + ))} + + + ))} + + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/thread-item.tsx b/002_source/cms/src/components/dashboard/teacher/mail/thread-item.tsx new file mode 100644 index 0000000..e049667 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/thread-item.tsx @@ -0,0 +1,134 @@ +'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 Checkbox from '@mui/material/Checkbox'; +import Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import Stack from '@mui/material/Stack'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import { BookmarkSimple as BookmarkSimpleIcon } from '@phosphor-icons/react/dist/ssr/BookmarkSimple'; +import { Paperclip as PaperclipIcon } from '@phosphor-icons/react/dist/ssr/Paperclip'; +import { Star as StarIcon } from '@phosphor-icons/react/dist/ssr/Star'; + +import { dayjs } from '@/lib/dayjs'; + +import type { Thread } from './types'; + +export interface ThreadItemProps { + href: string; + onDeselect?: () => void; + onSelect?: () => void; + selected: boolean; + thread: Thread; +} + +export function ThreadItem({ thread, onDeselect, onSelect, selected, href }: ThreadItemProps): React.JSX.Element { + const hasAnyAttachments = (thread.attachments ?? []).length > 0; + const hasManyAttachments = (thread.attachments ?? []).length > 1; + + const handleSelectToggle = React.useCallback( + (event: React.ChangeEvent) => { + if (event.target.checked) { + onSelect?.(); + } else { + onDeselect?.(); + } + }, + [onSelect, onDeselect] + ); + + return ( + + + + + + + + + + + + + + + + + + + {thread.from.name} + + + + + + {thread.subject} + + + —{thread.message} + + + {hasAnyAttachments ? ( + + } label={thread.attachments![0].name} size="small" variant="soft" /> + {hasManyAttachments ? : null} + + ) : null} + + + {dayjs(thread.createdAt).format('DD MMM')} + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/thread-view.tsx b/002_source/cms/src/components/dashboard/teacher/mail/thread-view.tsx new file mode 100644 index 0000000..d3e8524 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/thread-view.tsx @@ -0,0 +1,170 @@ +'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 IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import Link from '@mui/material/Link'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft'; +import { CaretLeft as CaretLeftIcon } from '@phosphor-icons/react/dist/ssr/CaretLeft'; +import { CaretRight as CaretRightIcon } from '@phosphor-icons/react/dist/ssr/CaretRight'; +import { Chat as ChatIcon } from '@phosphor-icons/react/dist/ssr/Chat'; +import { Chats as ChatsIcon } from '@phosphor-icons/react/dist/ssr/Chats'; +import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree'; +import { MagnifyingGlass as MagnifyingGlassIcon } from '@phosphor-icons/react/dist/ssr/MagnifyingGlass'; +import { Trash as TrashIcon } from '@phosphor-icons/react/dist/ssr/Trash'; + +import { paths } from '@/paths'; +import { dayjs } from '@/lib/dayjs'; + +import { Attachments } from './attachments'; +import { MailContext } from './mail-context'; +import { Message } from './message'; +import { Reply } from './reply'; +import type { Thread } from './types'; + +/** + * This method is used to get the thread from the context based on the thread ID. + * The thread should be loaded from the API in the page, but for the sake of simplicity we are just using the context. + */ +function useThread(threadId: string): Thread | undefined { + const { threads } = React.useContext(MailContext); + + return threads.find((thread) => thread.id === threadId); +} + +export interface ThreadViewProps { + threadId: string; +} + +export function ThreadView({ threadId }: ThreadViewProps): React.JSX.Element { + const { currentLabelId } = React.useContext(MailContext); + + const thread = useThread(threadId); + + if (!thread) { + return ( + + + Thread not found + + + ); + } + + const backHref = paths.dashboard.mail.list(currentLabelId); + + const hasMessage = Boolean(thread.message); + const hasAttachments = (thread.attachments ?? []).length > 0; + + return ( + + +
+ + + +
+ + + + + } + sx={{ width: '200px' }} + /> + + + + + + + + + + + +
+ + + + + +
+ + {thread.from.name} + {' '} + + {thread.from.email} + + + To:{' '} + {thread.to.map((person) => ( + + {person.email} + + ))} + + {thread.createdAt ? ( + + {dayjs(thread.createdAt).format('MMM D, YYYY hh:mm:ss A')} + + ) : null} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + {thread.subject} + + + {hasMessage ? : null} + {hasAttachments ? : null} + +
+ +
+
+ ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/threads-view.tsx b/002_source/cms/src/components/dashboard/teacher/mail/threads-view.tsx new file mode 100644 index 0000000..2b90699 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/threads-view.tsx @@ -0,0 +1,163 @@ +'use client'; + +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Checkbox from '@mui/material/Checkbox'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Stack from '@mui/material/Stack'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import { ArrowCounterClockwise as ArrowCounterClockwiseIcon } from '@phosphor-icons/react/dist/ssr/ArrowCounterClockwise'; +import { CaretLeft as CaretLeftIcon } from '@phosphor-icons/react/dist/ssr/CaretLeft'; +import { CaretRight as CaretRightIcon } from '@phosphor-icons/react/dist/ssr/CaretRight'; +import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree'; +import { List as ListIcon } from '@phosphor-icons/react/dist/ssr/List'; +import { MagnifyingGlass as MagnifyingGlassIcon } from '@phosphor-icons/react/dist/ssr/MagnifyingGlass'; + +import { paths } from '@/paths'; +import { useMediaQuery } from '@/hooks/use-media-query'; +import { useSelection } from '@/hooks/use-selection'; + +import { MailContext } from './mail-context'; +import { ThreadItem } from './thread-item'; + +export function ThreadsView(): React.JSX.Element { + const { currentLabelId, threads, setOpenDesktopSidebar, setOpenMobileSidebar } = React.useContext(MailContext); + + const mdDown = useMediaQuery('down', 'md'); + + const threadIds = React.useMemo(() => threads.map((thread) => thread.id), [threads]); + const { deselectAll, deselectOne, selectAll, selectOne, selected } = useSelection(threadIds); + + return ( + + +
+ { + if (mdDown) { + setOpenMobileSidebar((prev) => !prev); + } else { + setOpenDesktopSidebar((prev) => !prev); + } + }} + > + + +
+ + + + + } + sx={{ width: '200px' }} + /> + + + + + + + + + + + + + + + + +
+ {threads.length ? ( + + + + 0 && selected.size < threads.length} + onChange={(event: React.ChangeEvent) => { + if (event.target.checked) { + selectAll(); + } else { + deselectAll(); + } + }} + /> + Select all + + + + + + + + + {threads.map((thread) => ( + { + deselectOne(thread.id); + }} + onSelect={() => { + selectOne(thread.id); + }} + selected={selected.has(thread.id)} + thread={thread} + /> + ))} + + + ) : ( + + + + + + + There are no threads + + + + )} +
+ ); +} diff --git a/002_source/cms/src/components/dashboard/teacher/mail/types.d.ts b/002_source/cms/src/components/dashboard/teacher/mail/types.d.ts new file mode 100644 index 0000000..e5eee95 --- /dev/null +++ b/002_source/cms/src/components/dashboard/teacher/mail/types.d.ts @@ -0,0 +1,47 @@ +export interface Attachment { + id: string; + type: 'file' | 'image'; + name?: string; + size?: string; + url?: string; +} + +export interface Sender { + name: string; + avatar?: string; + email: string; +} + +export interface Receiver { + name: string; + avatar?: string; + email: string; +} + +export interface Thread { + id: string; + from: Sender; + to: Receiver[]; + subject: string; + message: string; + attachments?: Attachment[]; + folder: Folder; + labels: string[]; + isImportant: boolean; + isStarred: boolean; + isUnread: boolean; + createdAt: Date; +} + +export type Folder = 'inbox' | 'sent' | 'drafts' | 'spam' | 'trash'; + +export type LabelType = 'system' | 'custom'; + +export interface Label { + id: string; + type: LabelType; + name: string; + color?: string; + unreadCount?: number; + totalCount?: number; +}