From 64d631d8905175d963147b70e48a42a6c416db54 Mon Sep 17 00:00:00 2001 From: kayela Date: Thu, 29 Jan 2026 18:12:48 -0600 Subject: [PATCH 1/6] Members -removed un used selection options. Profile added back button --- src/pages/Profile.js | 27 +++++++++++++++++++-------- src/pages/admin/AdminMembers.js | 2 -- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/pages/Profile.js b/src/pages/Profile.js index 236fe4e..7852fab 100644 --- a/src/pages/Profile.js +++ b/src/pages/Profile.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { useAuth } from '../context/AuthContext'; +import { useNavigate } from 'react-router-dom'; import api from '../utils/api'; import { Card } from '../components/ui/card'; import { Button } from '../components/ui/button'; @@ -9,7 +10,7 @@ import { Textarea } from '../components/ui/textarea'; import { toast } from 'sonner'; import Navbar from '../components/Navbar'; import MemberFooter from '../components/MemberFooter'; -import { User, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react'; +import { User, ArrowLeft, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react'; import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar'; import ChangePasswordDialog from '../components/ChangePasswordDialog'; import TransactionHistory from '../components/TransactionHistory'; @@ -27,6 +28,7 @@ const Profile = () => { const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] }); const [transactionsLoading, setTransactionsLoading] = useState(true); + const navigate = useNavigate(); const [formData, setFormData] = useState({ // Personal Information first_name: '', @@ -244,13 +246,22 @@ const Profile = () => {
-
-

- My Profile -

-

- Update your personal information below. -

+
+
+

+ My Profile +

+

+ Update your personal information below. +

+
+ {/* Todo: functional back button */} +
diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js index a4b4e7a..71455cf 100644 --- a/src/pages/admin/AdminMembers.js +++ b/src/pages/admin/AdminMembers.js @@ -323,11 +323,9 @@ const AdminMembers = () => { Active Payment Pending Pending Validation - Pre-Validated Inactive Canceled Expired - Abandoned
From 27d5c48805c9bd029d14ec65ed1a18f251d44495 Mon Sep 17 00:00:00 2001 From: kayela Date: Thu, 29 Jan 2026 18:36:13 -0600 Subject: [PATCH 2/6] Componentized subscription table --- src/components/admin/SubscriptionsTable.jsx | 319 ++++++++++++++++++++ src/pages/admin/AdminSubscriptions.js | 291 ++---------------- 2 files changed, 337 insertions(+), 273 deletions(-) create mode 100644 src/components/admin/SubscriptionsTable.jsx diff --git a/src/components/admin/SubscriptionsTable.jsx b/src/components/admin/SubscriptionsTable.jsx new file mode 100644 index 0000000..e3881fe --- /dev/null +++ b/src/components/admin/SubscriptionsTable.jsx @@ -0,0 +1,319 @@ +import React from 'react'; +import { Button } from '../ui/button'; +import StatusBadge from '../StatusBadge'; +import { + ChevronDown, + ChevronUp, + Edit, + XCircle, + CreditCard, + Info, + ExternalLink, + Copy +} from 'lucide-react'; + +const HEADER_CELLS = [ + { label: 'Member', align: 'text-left' }, + { label: 'Plan', align: 'text-left' }, + { label: 'Status', align: 'text-left' }, + { label: 'Period', align: 'text-left' }, + { label: 'Base Fee', align: 'text-right' }, + { label: 'Donation', align: 'text-right' }, + { label: 'Total', align: 'text-right' }, + { label: 'Details', align: 'text-center' }, + { label: 'Actions', align: 'text-center' } +]; + +const HeaderCell = ({ align, children }) => ( + + {children} + +); + +const SubscriptionRow = ({ + sub, + isExpanded, + onToggle, + onEdit, + onCancel, + hasPermission, + formatDate, + formatDateTime, + formatPrice, + copyToClipboard +}) => ( + <> + + +
+ {sub.user.first_name} {sub.user.last_name} +
+
+ {sub.user.email} +
+ + +
+ {sub.plan.name} +
+
+ {sub.plan.billing_cycle} +
+ + + + + +
+
{formatDate(sub.start_date)}
+
to {formatDate(sub.end_date)}
+
+ + + {formatPrice(sub.base_subscription_cents || 0)} + + + {formatPrice(sub.donation_cents || 0)} + + + {formatPrice(sub.amount_paid_cents || 0)} + + + + + +
+ {hasPermission('subscriptions.edit') && ( + + )} + {sub.status === 'active' && hasPermission('subscriptions.cancel') && ( + + )} +
+ + + + {isExpanded && ( + + +
+

+ Transaction Details +

+
+
+
+ + Payment Information +
+
+ {sub.payment_completed_at && ( +
+ Payment Date: + {formatDateTime(sub.payment_completed_at)} +
+ )} + {sub.payment_method && ( +
+ Payment Method: + {sub.payment_method} +
+ )} + {sub.card_brand && sub.card_last4 && ( +
+ Card: + {sub.card_brand} ****{sub.card_last4} +
+ )} +
+
+ +
+
+ + Stripe Transaction IDs +
+
+ {sub.stripe_payment_intent_id && ( +
+ Payment Intent: +
+ + {sub.stripe_payment_intent_id.substring(0, 20)}... + + +
+
+ )} + {sub.stripe_charge_id && ( +
+ Charge ID: +
+ + {sub.stripe_charge_id.substring(0, 20)}... + + +
+
+ )} + {sub.stripe_subscription_id && ( +
+ Subscription ID: +
+ + {sub.stripe_subscription_id.substring(0, 20)}... + + +
+
+ )} + {sub.stripe_invoice_id && ( +
+ Invoice ID: +
+ + {sub.stripe_invoice_id.substring(0, 20)}... + + +
+
+ )} + {sub.stripe_customer_id && ( +
+ Customer ID: +
+ + {sub.stripe_customer_id.substring(0, 20)}... + + +
+
+ )} + {sub.stripe_receipt_url && ( +
+ Receipt: + +
+ )} +
+
+
+
+ + + )} + +); + +const SubscriptionsTable = ({ + subscriptions, + expandedRows, + onToggleRowExpansion, + onEdit, + onCancel, + hasPermission, + formatDate, + formatDateTime, + formatPrice, + copyToClipboard +}) => ( + + + + {HEADER_CELLS.map((cell) => ( + + {cell.label} + + ))} + + + + {subscriptions.length > 0 ? ( + subscriptions.map((sub) => ( + onToggleRowExpansion(sub.id)} + onEdit={onEdit} + onCancel={onCancel} + hasPermission={hasPermission} + formatDate={formatDate} + formatDateTime={formatDateTime} + formatPrice={formatPrice} + copyToClipboard={copyToClipboard} + /> + )) + ) : ( + + + + )} + +
+ No subscriptions found +
+); + +export default SubscriptionsTable; diff --git a/src/pages/admin/AdminSubscriptions.js b/src/pages/admin/AdminSubscriptions.js index 7915282..f5e5756 100644 --- a/src/pages/admin/AdminSubscriptions.js +++ b/src/pages/admin/AdminSubscriptions.js @@ -35,11 +35,14 @@ import { FileDown, AlertTriangle, Info, + Repeat, ChevronDown, ChevronUp, + + + ExternalLink, - Copy, - Repeat + Copy } from 'lucide-react'; import { DropdownMenu, @@ -49,6 +52,7 @@ import { } from '../../components/ui/dropdown-menu'; import StatusBadge from '@/components/StatusBadge'; import CreateSubscriptionDialog from '@/components/CreateSubscriptionDialog'; +import SubscriptionsTable from '@/components/admin/SubscriptionsTable'; const AdminSubscriptions = () => { const { hasPermission } = useAuth(); @@ -590,277 +594,18 @@ Proceed with activation?`; {/* Desktop Table View */}
- - - - - - - - - - - - - - - - {filteredSubscriptions.length > 0 ? ( - filteredSubscriptions.map((sub) => { - const isExpanded = expandedRows.has(sub.id); - return ( - - - - - - - - - - - - - {/* Expandable Details Row */} - {isExpanded && ( - - - - )} - - ); - }) - ) : ( - - - - )} - -
- Member - - Plan - - Status - - Period - - Base Fee - - Donation - - Total - - Details - - Actions -
-
- {sub.user.first_name} {sub.user.last_name} -
-
- {sub.user.email} -
-
-
- {sub.plan.name} -
-
- {sub.plan.billing_cycle} -
-
- - - -
-
{formatDate(sub.start_date)}
-
to {formatDate(sub.end_date)}
-
-
- {formatPrice(sub.base_subscription_cents || 0)} - - {formatPrice(sub.donation_cents || 0)} - - {formatPrice(sub.amount_paid_cents || 0)} - - - -
- {hasPermission('subscriptions.edit') && ( - - )} - {sub.status === 'active' && hasPermission('subscriptions.cancel') && ( - - )} -
-
-
-

- Transaction Details -

-
- {/* Payment Information */} -
-
- - Payment Information -
-
- {sub.payment_completed_at && ( -
- Payment Date: - {formatDateTime(sub.payment_completed_at)} -
- )} - {sub.payment_method && ( -
- Payment Method: - {sub.payment_method} -
- )} - {sub.card_brand && sub.card_last4 && ( -
- Card: - {sub.card_brand} ****{sub.card_last4} -
- )} -
-
- - {/* Stripe Transaction IDs */} -
-
- - Stripe Transaction IDs -
-
- {sub.stripe_payment_intent_id && ( -
- Payment Intent: -
- - {sub.stripe_payment_intent_id.substring(0, 20)}... - - -
-
- )} - {sub.stripe_charge_id && ( -
- Charge ID: -
- - {sub.stripe_charge_id.substring(0, 20)}... - - -
-
- )} - {sub.stripe_subscription_id && ( -
- Subscription ID: -
- - {sub.stripe_subscription_id.substring(0, 20)}... - - -
-
- )} - {sub.stripe_invoice_id && ( -
- Invoice ID: -
- - {sub.stripe_invoice_id.substring(0, 20)}... - - -
-
- )} - {sub.stripe_customer_id && ( -
- Customer ID: -
- - {sub.stripe_customer_id.substring(0, 20)}... - - -
-
- )} - {sub.stripe_receipt_url && ( -
- Receipt: - -
- )} -
-
-
-
-
- No subscriptions found -
+
From de719d9d6900eadb01420fc711cc58d10704fe6b Mon Sep 17 00:00:00 2001 From: kayela Date: Thu, 29 Jan 2026 18:46:16 -0600 Subject: [PATCH 3/6] refactor subscriptionsTable.jsx --- src/components/admin/SubscriptionsTable.jsx | 72 ++++++++++++++------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/src/components/admin/SubscriptionsTable.jsx b/src/components/admin/SubscriptionsTable.jsx index e3881fe..e12adbc 100644 --- a/src/components/admin/SubscriptionsTable.jsx +++ b/src/components/admin/SubscriptionsTable.jsx @@ -33,6 +33,17 @@ const HeaderCell = ({ align, children }) => ( ); +const TableCell = ({ align = 'text-left', className = '', style, children, ...props }) => ( + + {children} + +); + + const SubscriptionRow = ({ sub, isExpanded, @@ -47,41 +58,53 @@ const SubscriptionRow = ({ }) => ( <> - +
{sub.user.first_name} {sub.user.last_name}
{sub.user.email}
- - +
+
{sub.plan.name}
{sub.plan.billing_cycle}
- - +
+ - - + +
{formatDate(sub.start_date)}
to {formatDate(sub.end_date)}
- - +
+ {formatPrice(sub.base_subscription_cents || 0)} - - + + {formatPrice(sub.donation_cents || 0)} - - + + {formatPrice(sub.amount_paid_cents || 0)} - - + + - - + +
{hasPermission('subscriptions.edit') && (
- +
{isExpanded && ( - +

Transaction Details @@ -260,7 +283,7 @@ const SubscriptionRow = ({

- + )} @@ -307,9 +330,14 @@ const SubscriptionsTable = ({ )) ) : ( - + No subscriptions found - + )} From d5152609b6ff03cb44a1ee9d02034ee56c0408ff Mon Sep 17 00:00:00 2001 From: kayela Date: Thu, 29 Jan 2026 19:37:41 -0600 Subject: [PATCH 4/6] Donation status badge upates, admin validation tootips --- src/components/StatusBadge.js | 8 +- src/components/ui/table.jsx | 87 +++++++++++--------- src/pages/admin/AdminDonations.js | 121 ++++++++++++---------------- src/pages/admin/AdminValidations.js | 72 +++++++++++------ 4 files changed, 156 insertions(+), 132 deletions(-) diff --git a/src/components/StatusBadge.js b/src/components/StatusBadge.js index 89ce768..f021d8c 100644 --- a/src/components/StatusBadge.js +++ b/src/components/StatusBadge.js @@ -6,7 +6,6 @@ const STATUS_BADGE_CONFIG = { //status-based badges pending_email: { label: 'Pending Email', variant: 'orange2' }, pending_validation: { label: 'Pending Validation', variant: 'gray' }, - pre_validated: { label: 'Pre-Validated', variant: 'green' }, payment_pending: { label: 'Payment Pending', variant: 'orange' }, active: { label: 'Active', variant: 'green' }, inactive: { label: 'Inactive', variant: 'gray2' }, @@ -23,7 +22,12 @@ const STATUS_BADGE_CONFIG = { admin: { label: 'Admin', variant: 'purple' }, moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }, staff: { label: 'Staff', variant: 'gray' }, - media: { label: 'Media', variant: 'gray2' } + media: { label: 'Media', variant: 'gray2' }, + + //donation badges + pending: { label: 'Payment Pending', variant: 'orange' }, + completed: { label: 'Completed', variant: 'green' }, + failed: { label: 'Failed', className: 'bg-red-100 text-red-700' } }; //todo: make shield icon dynamic based on status diff --git a/src/components/ui/table.jsx b/src/components/ui/table.jsx index 07443a1..07164fb 100644 --- a/src/components/ui/table.jsx +++ b/src/components/ui/table.jsx @@ -1,78 +1,91 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Table = React.forwardRef(({ className, ...props }, ref) => (
- +
-)) -Table.displayName = "Table" +)); +Table.displayName = "Table"; const TableHeader = React.forwardRef(({ className, ...props }, ref) => ( - -)) -TableHeader.displayName = "TableHeader" + +)); +TableHeader.displayName = "TableHeader"; const TableBody = React.forwardRef(({ className, ...props }, ref) => ( -)) -TableBody.displayName = "TableBody" + {...props} + /> +)); +TableBody.displayName = "TableBody"; const TableFooter = React.forwardRef(({ className, ...props }, ref) => ( tr]:last:border-b-0", className)} - {...props} /> -)) -TableFooter.displayName = "TableFooter" + className={cn( + "border-t border-[var(--neutral-800)] font-medium [&>tr]:last:border-b-0", + className, + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; const TableRow = React.forwardRef(({ className, ...props }, ref) => ( -)) -TableRow.displayName = "TableRow" + {...props} + /> +)); +TableRow.displayName = "TableRow"; const TableHead = React.forwardRef(({ className, ...props }, ref) => (
[role=checkbox]]:translate-y-[2px]", - className + "p-4 text-left align-middle font-semibold text-[var(--purple-ink)] [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", + className, )} - {...props} /> -)) -TableHead.displayName = "TableHead" + {...props} + /> +)); +TableHead.displayName = "TableHead"; const TableCell = React.forwardRef(({ className, ...props }, ref) => ( [role=checkbox]]:translate-y-[2px]", - className + "p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] ", + className, )} - {...props} /> -)) -TableCell.displayName = "TableCell" + {...props} + /> +)); +TableCell.displayName = "TableCell"; const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
-)) -TableCaption.displayName = "TableCaption" + {...props} + /> +)); +TableCaption.displayName = "TableCaption"; export { Table, @@ -83,4 +96,4 @@ export { TableRow, TableCell, TableCaption, -} +}; diff --git a/src/pages/admin/AdminDonations.js b/src/pages/admin/AdminDonations.js index 4e53094..beb4da5 100644 --- a/src/pages/admin/AdminDonations.js +++ b/src/pages/admin/AdminDonations.js @@ -3,6 +3,7 @@ import { useAuth } from '../../context/AuthContext'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; import { Input } from '../../components/ui/input'; +import StatusBadge from '@/components/StatusBadge'; import { Select, SelectContent, @@ -17,6 +18,14 @@ import { DropdownMenuTrigger, } from '../../components/ui/dropdown-menu'; import { Badge } from '../../components/ui/badge'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '../../components/ui/table'; import api from '../../utils/api'; import { toast } from 'sonner'; import { @@ -184,15 +193,8 @@ const AdminDonations = () => { toast.error('Failed to copy to clipboard'); } }; - - const getStatusBadgeVariant = (status) => { - const variants = { - completed: 'default', - pending: 'secondary', - failed: 'destructive' - }; - return variants[status] || 'outline'; - }; + /* + */ const getTypeBadgeColor = (type) => { return type === 'member' ? 'bg-[var(--green-light)]' : 'bg-brand-purple '; @@ -392,51 +394,37 @@ const AdminDonations = () => { {/* Donations Table */}
- - - - - - - - - - - - - +
- Donor - - Type - - Amount - - Status - - Date - - Payment Method - - Details -
+ + + Donor + Type + Amount + Status + Date + Payment Method + Details + + + {filteredDonations.length === 0 ? ( - - - + + ) : ( filteredDonations.map((donation) => { const isExpanded = expandedRows.has(donation.id); return ( - - - - - - - - - - {/* Expandable Details Row */} + + {isExpanded && ( - - - + + )} ); }) )} - -
+ +

{donations.length === 0 ? 'No donations yet' : 'No donations match your filters'}

-
+ +

{donation.donor_name || 'Anonymous'} @@ -445,39 +433,37 @@ const AdminDonations = () => { {donation.donor_email || 'No email'}

-
+ + {donation.donation_type === 'member' ? 'Member' : 'Public'} - + +

{donation.amount}

-
- - {donation.status.charAt(0).toUpperCase() + donation.status.slice(1)} - - + + + + +
{formatDate(donation.created_at)}
-
-

+ + +

{donation.payment_method || 'N/A'}

-
+ + -
+ +

Transaction Details @@ -601,15 +586,15 @@ const AdminDonations = () => {

-
+ +
diff --git a/src/pages/admin/AdminValidations.js b/src/pages/admin/AdminValidations.js index c1882f9..0d17fc5 100644 --- a/src/pages/admin/AdminValidations.js +++ b/src/pages/admin/AdminValidations.js @@ -18,6 +18,12 @@ import { TableRow, TableCell, } from '../../components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '../../components/ui/tooltip'; import { Pagination, PaginationContent, @@ -417,7 +423,7 @@ const AdminValidations = () => { - + handleSort('first_name')} @@ -482,8 +488,8 @@ const AdminValidations = () => { onValueChange={(action) => handleActionSelect(user, action)} disabled={actionLoading === user.id || resendLoading === user.id} > - - + + {user.status === 'rejected' ? ( @@ -521,29 +527,45 @@ const AdminValidations = () => { )} - {/* view registration */} - + + {/* view registration */} + + + + + + View registration + + - {/* reject */} - {hasPermission('users.approve') && ( - - )} + {/* reject */} + {hasPermission('users.approve') && ( + + + + + + Reject user + + + )} + From f70a133e180120f812d23774b3ed3f03561e8d03 Mon Sep 17 00:00:00 2001 From: kayela Date: Thu, 29 Jan 2026 20:49:13 -0600 Subject: [PATCH 5/6] feat: tabs layout for edit profile --- src/pages/Profile.js | 1048 +++++++++++++++++++++++------------------- 1 file changed, 572 insertions(+), 476 deletions(-) diff --git a/src/pages/Profile.js b/src/pages/Profile.js index 7852fab..a56965f 100644 --- a/src/pages/Profile.js +++ b/src/pages/Profile.js @@ -1,6 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; import { useAuth } from '../context/AuthContext'; -import { useNavigate } from 'react-router-dom'; import api from '../utils/api'; import { Card } from '../components/ui/card'; import { Button } from '../components/ui/button'; @@ -9,11 +8,10 @@ import { Label } from '../components/ui/label'; import { Textarea } from '../components/ui/textarea'; import { toast } from 'sonner'; import Navbar from '../components/Navbar'; -import MemberFooter from '../components/MemberFooter'; -import { User, ArrowLeft, Save, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2 } from 'lucide-react'; +import { User, Lock, Heart, Users, Mail, BookUser, Camera, Upload, Trash2, Eye, CreditCard, Handshake, ArrowLeft } from 'lucide-react'; import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar'; import ChangePasswordDialog from '../components/ChangePasswordDialog'; -import TransactionHistory from '../components/TransactionHistory'; +import { useNavigate } from 'react-router-dom'; const Profile = () => { const { user } = useAuth(); @@ -24,13 +22,13 @@ const Profile = () => { const [previewImage, setPreviewImage] = useState(null); const [uploadingPhoto, setUploadingPhoto] = useState(false); const fileInputRef = useRef(null); - const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); // Default 50MB - const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); // Default 50MB in bytes - const [transactions, setTransactions] = useState({ subscriptions: [], donations: [] }); - const [transactionsLoading, setTransactionsLoading] = useState(true); + const [maxFileSizeMB, setMaxFileSizeMB] = useState(50); + const [maxFileSizeBytes, setMaxFileSizeBytes] = useState(52428800); + const [activeTab, setActiveTab] = useState('account'); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [initialFormData, setInitialFormData] = useState(null); const navigate = useNavigate(); const [formData, setFormData] = useState({ - // Personal Information first_name: '', last_name: '', phone: '', @@ -38,19 +36,15 @@ const Profile = () => { city: '', state: '', zipcode: '', - // Partner Information partner_first_name: '', partner_last_name: '', partner_is_member: false, partner_plan_to_become_member: false, - // Newsletter Preferences newsletter_publish_name: false, newsletter_publish_photo: false, newsletter_publish_birthday: false, newsletter_publish_none: false, - // Volunteer Interests (array) volunteer_interests: [], - // Member Directory Settings show_in_directory: false, directory_email: '', directory_bio: '', @@ -63,9 +57,16 @@ const Profile = () => { useEffect(() => { fetchConfig(); fetchProfile(); - fetchTransactions(); }, []); + // Track unsaved changes + useEffect(() => { + if (initialFormData) { + const hasChanges = JSON.stringify(formData) !== JSON.stringify(initialFormData); + setHasUnsavedChanges(hasChanges); + } + }, [formData, initialFormData]); + const fetchConfig = async () => { try { const response = await api.get('/config'); @@ -73,7 +74,6 @@ const Profile = () => { setMaxFileSizeBytes(response.data.max_file_size_bytes); } catch (error) { console.error('Failed to fetch config, using defaults:', error); - // Keep default values if fetch fails } }; @@ -83,8 +83,7 @@ const Profile = () => { setProfileData(response.data); setProfilePhotoUrl(response.data.profile_photo_url); setPreviewImage(response.data.profile_photo_url); - setFormData({ - // Personal Information + const newFormData = { first_name: response.data.first_name || '', last_name: response.data.last_name || '', phone: response.data.phone || '', @@ -92,19 +91,15 @@ const Profile = () => { city: response.data.city || '', state: response.data.state || '', zipcode: response.data.zipcode || '', - // Partner Information partner_first_name: response.data.partner_first_name || '', partner_last_name: response.data.partner_last_name || '', partner_is_member: response.data.partner_is_member || false, partner_plan_to_become_member: response.data.partner_plan_to_become_member || false, - // Newsletter Preferences newsletter_publish_name: response.data.newsletter_publish_name || false, newsletter_publish_photo: response.data.newsletter_publish_photo || false, newsletter_publish_birthday: response.data.newsletter_publish_birthday || false, newsletter_publish_none: response.data.newsletter_publish_none || false, - // Volunteer Interests volunteer_interests: response.data.volunteer_interests || [], - // Member Directory Settings show_in_directory: response.data.show_in_directory || false, directory_email: response.data.directory_email || '', directory_bio: response.data.directory_bio || '', @@ -112,25 +107,14 @@ const Profile = () => { directory_phone: response.data.directory_phone || '', directory_dob: response.data.directory_dob || '', directory_partner_name: response.data.directory_partner_name || '' - }); + }; + setFormData(newFormData); + setInitialFormData(newFormData); } catch (error) { toast.error('Failed to load profile'); } }; - const fetchTransactions = async () => { - try { - setTransactionsLoading(true); - const response = await api.get('/members/transactions'); - setTransactions(response.data); - } catch (error) { - console.error('Failed to load transactions:', error); - // Don't show error toast - transactions are optional - } finally { - setTransactionsLoading(false); - } - }; - const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); @@ -150,7 +134,6 @@ const Profile = () => { })); }; - // Volunteer interest options const volunteerOptions = [ 'Event Planning', 'Social Media', @@ -168,13 +151,11 @@ const Profile = () => { const file = e.target.files[0]; if (!file) return; - // Validate file type if (!file.type.startsWith('image/')) { toast.error('Please select an image file'); return; } - // Validate file size if (file.size > maxFileSizeBytes) { toast.error(`File size must be less than ${maxFileSizeMB}MB`); return; @@ -222,6 +203,8 @@ const Profile = () => { try { await api.put('/users/profile', formData); toast.success('Profile updated successfully!'); + setInitialFormData(formData); + setHasUnsavedChanges(false); fetchProfile(); } catch (error) { toast.error('Failed to update profile'); @@ -230,510 +213,623 @@ const Profile = () => { } }; + const tabs = [ + { id: 'account', label: 'Account & Privacy', shortLabel: 'Account', icon: Lock }, + { id: 'bio', label: 'My Bio & Directory', shortLabel: 'Bio & Directory', icon: User }, + { id: 'engagement', label: 'Engagement', shortLabel: 'Engagement', icon: Handshake } + ]; + if (!profileData) { return ( -
+
-

Loading profile...

+

Loading profile...

); } - return ( -
- + // Account & Privacy Tab Content + const AccountPrivacyContent = () => ( +
-
-
-
-

- My Profile -

-

- Update your personal information below. -

+ +
+

Account & Privacy

+
+
+

Login Email

+

{profileData.email}

+
+ +
+
+

Password

+

••••••••

- {/* Todo: functional back button */}
- - {/* Read-only Information */} -
-

- - Account Information -

-
-
-

Email

-

{profileData.email}

-
-
-

Status

-

{profileData.status.replace('_', ' ')}

-
-
-

Role

-

{profileData.role}

-
-
-

Date of Birth

-

- {new Date(profileData.date_of_birth).toLocaleDateString()} -

-
-
+
+

Membership Status

+

+ {profileData.status?.replace('_', ' ') || 'Active'} +

+
-
+
+
+

Payment Method

+
+ +
+
+ +
+ +
+ ); + + // My Bio & Directory Tab Content + const BioDirectoryContent = () => ( + +
+

My Bio & Directory

+
+ + {/* Profile Photo Section */} +
+

+ + Profile Photo +

+
+ + + + {profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)} + + + +
+ + + + + {profilePhotoUrl && ( -
+ )} + +

+ Max {maxFileSizeMB}MB +

+
+
- {/* Profile Photo Section */} -
-

- - Profile Photo -

-
- - - - {profileData?.first_name?.charAt(0)}{profileData?.last_name?.charAt(0)} - - + {/* Personal Information */} +
+

+ Personal Information +

-
- - - - - {profilePhotoUrl && ( - - )} - -

- Upload a profile photo (Max {maxFileSizeMB}MB) -

-
-
+
+
+ +
+
+ + +
+
- {/* Editable Form */} -
-

- Personal Information -

+
+ + +
-
-
- - -
-
- - -
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Member Directory Settings */} +
+

+ + Member Directory Settings +

+

+ Control your visibility and information in the member directory. +

+ +
+ + +
+ + {formData.show_in_directory && ( +
+
+ +
- + +