diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 9354d0c..46ecd0b 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -15,9 +15,10 @@ import { ConfirmSentEmailPage } from '@containers/auth/ConfirmSentEmailPage'; import { ConfirmRegisteredPage } from '@containers/auth/ConfirmRegisteredPage'; import { DashboardPage } from '@containers/dashboard/DashboardPage'; import { DonorStatsChart } from '@components/DonorStatsChart'; -import SidebarTester from '@containers/dashboard/sidebar/SidebarTester'; -import EditDonationGoalTester from '@components/DonationGoal/EditDonationGoalTester'; +import DashboardOverview from '@containers/dashboard/sidebar/DashboardOverview'; +import { EmailEditor } from './components/EmailComms/EmailEditor'; import { AdminGrowingGoalTester } from '@containers/dashboard/AdminGrowingGoalTester'; +import OverviewPage from '@containers/dashboard/OverviewPage'; const router = createBrowserRouter([ { @@ -37,6 +38,25 @@ const router = createBrowserRouter([ path: '/confirm-registered', element: , }, + { + path: '/overview', + // element: , + children: [ + { + element: , + children: [ + { + path: '', + element: , + }, + { + path: 'email', + element: , + }, + ], + }, + ], + }, { path: '/dashboard', element: , @@ -47,14 +67,6 @@ const router = createBrowserRouter([ }, ], }, - { - path: '/sidebar-test', - element: , - }, - { - path: '/edit-donation-goal-test', - element: , - }, { path: '/test', element: , diff --git a/apps/frontend/src/components/EmailComms/EmailEditor.tsx b/apps/frontend/src/components/EmailComms/EmailEditor.tsx new file mode 100644 index 0000000..1ae9031 --- /dev/null +++ b/apps/frontend/src/components/EmailComms/EmailEditor.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; +import EmailEditorCard from './EmailEditorCard'; +import EmailPreviewPanel from './EmailPreviewPanel'; + +import { + defaultEmails, + defaultSignature, + TAB_CONFIG, + buildSignatureHTML, +} from './types'; +import type { TabId, EmailData, EmailsState, Signature } from './types'; +import { Button } from '../ui/button'; + +export function EmailEditor() { + const [activeTab, setActiveTab] = useState('donation'); + const [emails, setEmails] = useState(defaultEmails); + const [sig, setSig] = useState(defaultSignature); + const [saved, setSaved] = useState(false); + const [sent, setSent] = useState(false); + + const handleEmailChange = ( + tab: TabId, + field: keyof EmailData, + value: string, + ) => { + setEmails((prev) => ({ ...prev, [tab]: { ...prev[tab], [field]: value } })); + }; + + const handleSave = () => { + console.log('[EmailEditor] Save payload:', { + tab: activeTab, + subject: emails[activeTab].subject, + body: emails[activeTab].body, + signature: sig, + }); + setSaved(true); + setTimeout(() => setSaved(false), 2500); + }; + + const handleSend = () => { + const email = emails[activeTab]; + const fullHTML = ` + + ${email.body} + ${buildSignatureHTML(sig)} + + `; + console.log('[EmailEditor] Send payload:', { + to: '[recipient]', + subject: email.subject, + html: fullHTML, + }); + setSent(true); + setTimeout(() => setSent(false), 2500); + }; + + return ( +
+
+ {TAB_CONFIG.map((tab) => ( + + ))} +
+ +
+ + + +
+
+ ); +} diff --git a/apps/frontend/src/components/EmailComms/EmailEditorCard.tsx b/apps/frontend/src/components/EmailComms/EmailEditorCard.tsx new file mode 100644 index 0000000..7d61341 --- /dev/null +++ b/apps/frontend/src/components/EmailComms/EmailEditorCard.tsx @@ -0,0 +1,86 @@ +import type { TabId, EmailData, EmailsState, Signature } from './types'; +import RichTextEditor from './RichTextEditor'; +import { Button } from './../ui/button'; +import SignatureEditorCard from './SignatureEditorCard'; +import { Label } from '../ui/label'; + +type EmailEditorCardProps = { + activeTab: TabId; + emails: EmailsState; + onEmailChange: (tab: TabId, field: keyof EmailData, value: string) => void; + sig: Signature; + onSigChange: (sig: Signature) => void; + saved: boolean; + sent: boolean; + onSave: () => void; + onSend: () => void; +}; + +export default function EmailEditorCard({ + activeTab, + emails, + onEmailChange, + sig, + onSigChange, + saved, + sent, + onSave, + onSend, +}: EmailEditorCardProps) { + const currentEmail = emails[activeTab]; + + return ( +
+
+ + + onEmailChange(activeTab, 'subject', e.target.value)} + className="border border-slate-200 rounded-xl px-4 py-3 text-sm focus:ring-2 focus:ring-emerald-600 outline-none transition" + placeholder="Email subject…" + /> +
+ +
+ + + onEmailChange(activeTab, 'body', html)} + /> +
+ +
+

+ Email Signature +

+ + +
+ +
+ +
+ +
+ + + +
+
+ ); +} diff --git a/apps/frontend/src/components/EmailComms/EmailPreviewPanel.tsx b/apps/frontend/src/components/EmailComms/EmailPreviewPanel.tsx new file mode 100644 index 0000000..946923c --- /dev/null +++ b/apps/frontend/src/components/EmailComms/EmailPreviewPanel.tsx @@ -0,0 +1,41 @@ +import type { EmailTabId, EmailsState, Signature } from './types'; +import { buildSignatureHTML } from './types'; +import FCCEmailHeader from './FCCEmailHeader.svg'; + +interface EmailPreviewPanelProps { + activeTab: EmailTabId; + emails: EmailsState; + sig: Signature; +} + +export default function EmailPreviewPanel({ + activeTab, + emails, + sig, +}: EmailPreviewPanelProps) { + const email = emails[activeTab]; + + return ( +
+

+ Email Preview +

+ +
+ Boston skyline + +
+
+ +
+
+
+
+ ); +} diff --git a/apps/frontend/src/components/EmailComms/FCCEmailHeader.svg b/apps/frontend/src/components/EmailComms/FCCEmailHeader.svg new file mode 100644 index 0000000..3017957 --- /dev/null +++ b/apps/frontend/src/components/EmailComms/FCCEmailHeader.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/frontend/src/components/EmailComms/RichTextEditor.tsx b/apps/frontend/src/components/EmailComms/RichTextEditor.tsx new file mode 100644 index 0000000..971c18c --- /dev/null +++ b/apps/frontend/src/components/EmailComms/RichTextEditor.tsx @@ -0,0 +1,319 @@ +import ToolBarButton from './ToolBarButton'; +import { useCallback } from 'react'; +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Underline from '@tiptap/extension-underline'; +import Link from '@tiptap/extension-link'; +import TextAlign from '@tiptap/extension-text-align'; +import Placeholder from '@tiptap/extension-placeholder'; +import Strike from '@tiptap/extension-strike'; + +export default function RichTextEditor({ + content, + onUpdate, +}: { + content: string; + onUpdate: (html: string) => void; +}) { + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + heading: { levels: [1, 2, 3] }, + bulletList: {}, + orderedList: {}, + bold: {}, + italic: {}, + strike: {}, + }), + Underline, + Link.configure({ openOnClick: false }), + TextAlign.configure({ types: ['heading', 'paragraph'] }), + Placeholder.configure({ placeholder: 'Type your message here…' }), + ], + content, + onUpdate: ({ editor }) => onUpdate(editor.getHTML()), + editorProps: { + attributes: { + class: + 'prose prose-sm max-w-none min-h-[220px] focus:outline-none px-4 py-3 text-slate-700 leading-relaxed', + }, + }, + }); + + const setLink = useCallback(() => { + if (!editor) return; + const prev = editor.getAttributes('link').href ?? ''; + const url = window.prompt('Enter URL', prev); + if (url === null) return; + if (url === '') { + editor.chain().focus().extendMarkRange('link').unsetLink().run(); + return; + } + editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run(); + }, [editor]); + + if (!editor) return null; + + return ( +
+
+ editor.chain().focus().undo().run()} + > + + + + + + + editor.chain().focus().redo().run()} + > + + + + + + +
+ +
+ + + + +
+ +
+ + editor.chain().focus().setTextAlign('left').run()} + > + + + + + + + +
+ + editor.chain().focus().toggleBold().run()} + > + B + + + editor.chain().focus().toggleItalic().run()} + > + I + + + editor.chain().focus().toggleUnderline().run()} + > + U + + + editor.chain().focus().toggleStrike().run()} + > + S + + + editor.chain().focus().toggleCode().run()} + > + + + + + + + editor.chain().focus().toggleCode().run()} + > + + + + + + +
+ + editor.chain().focus().toggleBulletList().run()} + > + + + + + + + + + + + editor.chain().focus().toggleOrderedList().run()} + > + + + 1. + + + + 2. + + + + + + + + + + + +
+ +
+ +
+
+ ); +} diff --git a/apps/frontend/src/components/EmailComms/SignatureEditorCard.tsx b/apps/frontend/src/components/EmailComms/SignatureEditorCard.tsx new file mode 100644 index 0000000..ae25c91 --- /dev/null +++ b/apps/frontend/src/components/EmailComms/SignatureEditorCard.tsx @@ -0,0 +1,35 @@ +import type { Signature } from './types'; +export default function SignatureEditor({ + sig, + onChange, +}: { + sig: Signature; + onChange: (s: Signature) => void; +}) { + const field = (label: string, key: keyof Signature, placeholder: string) => ( +
+ + + onChange({ ...sig, [key]: e.target.value })} + placeholder={placeholder} + className="border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-indigo-400 transition bg-white" + /> +
+ ); + + return ( +
+
+ {field('Full Name', 'name', 'Your name')} + + {field('Position', 'position', 'Your title')} + + {field('Email', 'email', 'you@company.com')} +
+
+ ); +} diff --git a/apps/frontend/src/components/EmailComms/ToolBarButton.tsx b/apps/frontend/src/components/EmailComms/ToolBarButton.tsx new file mode 100644 index 0000000..d81ff5b --- /dev/null +++ b/apps/frontend/src/components/EmailComms/ToolBarButton.tsx @@ -0,0 +1,27 @@ +export default function ToolBarButton({ + onClick, + active, + title, + children, +}: { + onClick: () => void; + active?: boolean; + title: string; + children: React.ReactNode; +}) { + return ( + + ); +} diff --git a/apps/frontend/src/components/EmailComms/types.ts b/apps/frontend/src/components/EmailComms/types.ts new file mode 100644 index 0000000..320e094 --- /dev/null +++ b/apps/frontend/src/components/EmailComms/types.ts @@ -0,0 +1,91 @@ +export type EmailTabId = 'donation' | 'relapsed' | 'mass'; +export type TabId = EmailTabId; + +export type Signature = { + name: string; + position: string; + email: string; + pronouns: string; + phone: string; + website: string; + linkedin: string; + X: string; + facebook: string; +}; + +export type EmailData = { + subject: string; + body: string; +}; + +export type EmailsState = Record; + +export const defaultEmails: EmailsState = { + donation: { + subject: 'Thank You For Your Donation!', + body: '

Dear [Donor Name],

Thank you sincerely for your generous donation of [Amount]. Your support makes a real difference in the lives of those we serve.

Because of donors like you, we can continue our mission to bring positive change to our community.

With gratitude,

', + }, + relapsed: { + subject: "We've Missed You — Come Back!", + body: "

Dear [Donor Name],

It's been a while since we last heard from you, and we wanted to reach out personally.

Your past generosity helped us accomplish so much, and we'd love to have you rejoin our community of supporters. Every contribution — big or small — creates lasting impact.

Warmly,

", + }, + mass: { + subject: 'An Important Update From Us', + body: '

Dear Supporter,

We have an exciting update to share with you regarding our upcoming initiatives. As a valued member of our community, you deserve to hear about this first.

Please read below for full details.

', + }, +}; + +export const defaultSignature: Signature = { + name: 'Mallory Rohig', + position: 'Executive Director', + email: 'filler-email@gmail.com', + pronouns: '(she/her)', + phone: '+1 (555) 234-5678', + website: 'https://fenwaycommunitycenter.org/', + linkedin: 'https://www.linkedin.com/company/fenwaycommunitycenter/', + X: 'https://fenwaycommunitycenter.org/?share=x&nb=1', + facebook: 'https://fenwaycommunitycenter.org/?share=facebook&nb=1', +}; + +export const TAB_CONFIG: { id: TabId; label: string }[] = [ + { id: 'donation', label: 'Donation Response' }, + { id: 'relapsed', label: 'Relapsed Donor Message' }, + { id: 'mass', label: 'Mass Emaill' }, +]; + +export function buildSignatureHTML(sig: Signature): string { + const socialHTML = [ + sig.linkedin + ? `in` + : '', + sig.X + ? `𝕏` + : '', + sig.facebook + ? `` + : '', + ].join(''); + + return ` + + + + + + + + + +
+

We hope you will join us and stay safe.

+
+

${sig.name}

+ +

${sig.position}

+ +

${sig.pronouns}

+ ${sig.website ? `

${sig.website}

` : ''} +
${socialHTML}
+
+ `; +} diff --git a/apps/frontend/src/containers/dashboard/OverviewPage.tsx b/apps/frontend/src/containers/dashboard/OverviewPage.tsx new file mode 100644 index 0000000..716afff --- /dev/null +++ b/apps/frontend/src/containers/dashboard/OverviewPage.tsx @@ -0,0 +1,103 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useAuth } from '../../components/AuthProvider'; +import apiClient from '../../api/apiClient'; +import DonationStatCard from './sidebar/DonationStatCard'; +import { getDisplayName } from '../../utils/user'; +import { PiggyBank, Clock, CalendarDays } from 'lucide-react'; +import welcomeBackground from '../../assets/green-boston-background.png'; + +type DonationStats = { + total: number; + yearToDate: number; + monthToDate: number; +}; + +const defaultStats: DonationStats = { + total: 0, + yearToDate: 0, + monthToDate: 0, +}; + +export default function OverviewPage() { + const { user } = useAuth(); + const [stats, setStats] = useState(defaultStats); + + useEffect(() => { + const fetchStats = async () => { + try { + const response = await apiClient.getDonationStats(); + setStats({ + total: Number(response?.total ?? 0), + yearToDate: Number(response?.yearToDate ?? 0), + monthToDate: Number(response?.monthToDate ?? 0), + }); + } catch { + setStats(defaultStats); + } + }; + + fetchStats(); + }, []); + + const displayUserName = useMemo(() => getDisplayName(user), [user]); + + const formatCurrency = (amount: number) => + new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 0, + }).format(amount); + + return ( +
+
+

+ Donations Raised +

+ +
+
+ + + + + +
+ + +
+
+
+ ); +} diff --git a/apps/frontend/src/containers/dashboard/sidebar/DashboardOverview.tsx b/apps/frontend/src/containers/dashboard/sidebar/DashboardOverview.tsx new file mode 100644 index 0000000..a2530c7 --- /dev/null +++ b/apps/frontend/src/containers/dashboard/sidebar/DashboardOverview.tsx @@ -0,0 +1,16 @@ +import Sidebar from './Sidebar'; +import Header from '../header/Header'; +import { Outlet } from 'react-router-dom'; + +export default function DashboardOverview() { + return ( +
+ +
+
+ + +
+
+ ); +} diff --git a/apps/frontend/src/containers/dashboard/sidebar/Sidebar.tsx b/apps/frontend/src/containers/dashboard/sidebar/Sidebar.tsx index f08548a..3726782 100644 --- a/apps/frontend/src/containers/dashboard/sidebar/Sidebar.tsx +++ b/apps/frontend/src/containers/dashboard/sidebar/Sidebar.tsx @@ -26,27 +26,27 @@ type NavItem = { const navItems: NavItem[] = [ { label: 'Dashboard Overview', - href: '/dashboard', + href: '/overview', icon: Bookmark, }, { label: 'Donation Tracker', - href: '/dashboard/donations', + href: '/overview/donations', icon: ListFilter, }, { label: 'Email Communication', - href: '/dashboard/email', + href: '/overview/email', icon: Mail, }, { label: 'Admin Approval', - href: '/dashboard/approval', + href: '/overview/approval', icon: BadgeCheck, }, { label: 'Settings', - href: '/dashboard/settings', + href: '/overview/settings', icon: Settings, }, ]; @@ -66,7 +66,6 @@ export default function Sidebar({ className }: SidebarProps) { className, )} > - {/*Top Logo Section */}
FCC Logo
- {/*Navigation */}