diff --git a/admin/src/components/volunteer/dashboard/ActionCampaignCard.tsx b/admin/src/components/volunteer/dashboard/ActionCampaignCard.tsx index 8767c8a8..345b3e92 100644 --- a/admin/src/components/volunteer/dashboard/ActionCampaignCard.tsx +++ b/admin/src/components/volunteer/dashboard/ActionCampaignCard.tsx @@ -1,96 +1,19 @@ -import { useState } from 'react'; -import { Card, Progress, List, Button, Typography, Space, Tag, Alert, App } from 'antd'; -import { - VideoCameraOutlined, - MailOutlined, - FileTextOutlined, - CalendarOutlined, - EnvironmentOutlined, - TeamOutlined, - LinkOutlined, - CheckSquareOutlined, - CheckCircleFilled, - TrophyOutlined, - RightOutlined, -} from '@ant-design/icons'; -import { useNavigate } from 'react-router-dom'; -import { api } from '@/lib/api'; -import type { DashboardActionCampaign, DashboardActionStep, ActionStepKind } from './types'; +import { Card, Progress, Typography, Space, Tag, Alert } from 'antd'; +import { TrophyOutlined } from '@ant-design/icons'; +import type { DashboardActionCampaign } from './types'; interface ActionCampaignCardProps { campaign: DashboardActionCampaign; - onRefresh: () => void; } -const KIND_ICONS: Record = { - WATCH_VIDEO: , - SUBMIT_INFLUENCE: , - SIGN_PETITION: , - RSVP_EVENT: , - SIGNUP_SHIFT: , - JOIN_CHALLENGE: , - VISIT_LINK: , - CUSTOM: , -}; - -function resolveStepLink(step: DashboardActionStep): { to: string; external: boolean } | null { - if (step.targetUrl) { - const external = /^https?:\/\//i.test(step.targetUrl); - return { to: step.targetUrl, external }; - } - if (!step.targetId) return null; - switch (step.kind) { - case 'WATCH_VIDEO': - return { to: `/gallery/watch/${step.targetId}`, external: false }; - case 'SUBMIT_INFLUENCE': - return { to: `/campaign/${step.targetId}`, external: false }; - case 'SIGN_PETITION': - return { to: `/petition/${step.targetId}`, external: false }; - case 'RSVP_EVENT': - return { to: `/event/${step.targetId}`, external: false }; - case 'SIGNUP_SHIFT': - return { to: `/volunteer/shifts?shiftId=${step.targetId}`, external: false }; - case 'JOIN_CHALLENGE': - return { to: `/volunteer/challenges/${step.targetId}`, external: false }; - default: - return null; - } -} - -export default function ActionCampaignCard({ campaign, onRefresh }: ActionCampaignCardProps) { - const navigate = useNavigate(); - const { message } = App.useApp(); - const [completingStepId, setCompletingStepId] = useState(null); - +export default function ActionCampaignCard({ campaign }: ActionCampaignCardProps) { const percent = campaign.totalSteps > 0 ? Math.round((campaign.completedSteps / campaign.totalSteps) * 100) : 0; - const handleSelfReport = async (step: DashboardActionStep) => { - setCompletingStepId(step.id); - try { - await api.post(`/action-campaigns/${campaign.slug}/steps/${step.id}/complete`); - message.success('Step marked as done'); - onRefresh(); - } catch { - message.error('Failed to mark step as done'); - } finally { - setCompletingStepId(null); - } - }; - - const handleNavigate = (step: DashboardActionStep) => { - const link = resolveStepLink(step); - if (!link) { - message.info('No link configured for this step'); - return; - } - if (link.external) { - window.open(link.to, '_blank', 'noopener,noreferrer'); - } else { - navigate(link.to); - } - }; + const nextStep = [...campaign.steps] + .sort((a, b) => a.order - b.order) + .find((s) => !s.completed); return ( )} - {campaign.rewardText && !campaign.rewardEarned && ( - - Reward: {campaign.rewardText} - - )}
@@ -142,84 +60,17 @@ export default function ActionCampaignCard({ campaign, onRefresh }: ActionCampai /> )} - a.order - b.order)} - renderItem={(step) => { - const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK'; - const canNavigate = resolveStepLink(step) !== null; - return ( - }> - Completed - - ) : isSelfReport ? ( - - {canNavigate && ( - - )} - - - ) : ( - - ), - ]} - > - - {step.completed ? : KIND_ICONS[step.kind]} -
- } - title={ - - {step.label} - - } - description={step.description} - /> - - ); - }} - /> + {!campaign.rewardEarned && campaign.rewardText && ( + + Reward: {campaign.rewardText} + + )} + + {nextStep && !campaign.rewardEarned && ( + + Next: {nextStep.label} + + )}
); diff --git a/admin/src/components/volunteer/dashboard/ActionStepsList.tsx b/admin/src/components/volunteer/dashboard/ActionStepsList.tsx new file mode 100644 index 00000000..7dac8482 --- /dev/null +++ b/admin/src/components/volunteer/dashboard/ActionStepsList.tsx @@ -0,0 +1,187 @@ +import { useState } from 'react'; +import { Card, List, Button, Typography, Tag, Space, App } from 'antd'; +import { + VideoCameraOutlined, + MailOutlined, + FileTextOutlined, + CalendarOutlined, + EnvironmentOutlined, + TeamOutlined, + LinkOutlined, + CheckSquareOutlined, + CheckCircleFilled, + RightOutlined, + UnorderedListOutlined, +} from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import { api } from '@/lib/api'; +import type { DashboardActionCampaign, DashboardActionStep, ActionStepKind } from './types'; + +interface ActionStepsListProps { + campaign: DashboardActionCampaign; + onRefresh: () => void; +} + +const KIND_ICONS: Record = { + WATCH_VIDEO: , + SUBMIT_INFLUENCE: , + SIGN_PETITION: , + RSVP_EVENT: , + SIGNUP_SHIFT: , + JOIN_CHALLENGE: , + VISIT_LINK: , + CUSTOM: , +}; + +const KIND_LABELS: Record = { + WATCH_VIDEO: 'Watch', + SUBMIT_INFLUENCE: 'Email', + SIGN_PETITION: 'Sign', + RSVP_EVENT: 'RSVP', + SIGNUP_SHIFT: 'Shift', + JOIN_CHALLENGE: 'Join', + VISIT_LINK: 'Visit', + CUSTOM: 'Action', +}; + +function resolveStepLink(step: DashboardActionStep): { to: string; external: boolean } | null { + if (step.targetUrl) { + const external = /^https?:\/\//i.test(step.targetUrl); + return { to: step.targetUrl, external }; + } + if (!step.targetId) return null; + switch (step.kind) { + case 'WATCH_VIDEO': + return { to: `/gallery/watch/${step.targetId}`, external: false }; + case 'SUBMIT_INFLUENCE': + return { to: `/campaign/${step.targetId}`, external: false }; + case 'SIGN_PETITION': + return { to: `/petition/${step.targetId}`, external: false }; + case 'RSVP_EVENT': + return { to: `/event/${step.targetId}`, external: false }; + case 'SIGNUP_SHIFT': + return { to: `/volunteer/shifts?shiftId=${step.targetId}`, external: false }; + case 'JOIN_CHALLENGE': + return { to: `/volunteer/challenges/${step.targetId}`, external: false }; + default: + return null; + } +} + +export default function ActionStepsList({ campaign, onRefresh }: ActionStepsListProps) { + const navigate = useNavigate(); + const { message } = App.useApp(); + const [completingStepId, setCompletingStepId] = useState(null); + + const handleSelfReport = async (step: DashboardActionStep) => { + setCompletingStepId(step.id); + try { + await api.post(`/action-campaigns/${campaign.slug}/steps/${step.id}/complete`); + message.success('Step marked as done'); + onRefresh(); + } catch { + message.error('Failed to mark step as done'); + } finally { + setCompletingStepId(null); + } + }; + + const handleNavigate = (step: DashboardActionStep) => { + const link = resolveStepLink(step); + if (!link) return; + if (link.external) { + window.open(link.to, '_blank', 'noopener,noreferrer'); + } else { + navigate(link.to); + } + }; + + const sortedSteps = [...campaign.steps].sort((a, b) => a.order - b.order); + + return ( + + + Actions + + } + extra={ + + {campaign.completedSteps} of {campaign.totalSteps} + + } + styles={{ body: { padding: 0 } }} + > + { + const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK'; + const canNavigate = resolveStepLink(step) !== null; + return ( + }>Completed + ) : isSelfReport ? ( + + {canNavigate && ( + + )} + + + ) : ( + + ), + ]} + > + + {step.completed ? : KIND_ICONS[step.kind]} + + } + title={ + + + {KIND_LABELS[step.kind]}: + {' '} + {step.label} + + } + /> + + ); + }} + /> + + ); +} diff --git a/admin/src/components/volunteer/dashboard/ProfileCard.tsx b/admin/src/components/volunteer/dashboard/ProfileCard.tsx index 10de5a8c..7fd397e8 100644 --- a/admin/src/components/volunteer/dashboard/ProfileCard.tsx +++ b/admin/src/components/volunteer/dashboard/ProfileCard.tsx @@ -1,5 +1,5 @@ -import { Card, Avatar, Typography, Row, Col, Statistic } from 'antd'; -import { TeamOutlined, TrophyOutlined, CalendarOutlined } from '@ant-design/icons'; +import { Card, Avatar, Typography, Row, Col, Statistic, Input, Button, App, Space } from 'antd'; +import { TeamOutlined, TrophyOutlined, CalendarOutlined, CopyOutlined } from '@ant-design/icons'; import type { DashboardProfile, DashboardReferral, DashboardPoints, DashboardMyEvent } from './types'; interface ProfileCardProps { @@ -22,6 +22,7 @@ function getInitials(name: string | null, email: string): string { } export default function ProfileCard({ profile, referral, points, myEvents }: ProfileCardProps) { + const { message } = App.useApp(); const initials = getInitials(profile.name, profile.email); const displayName = profile.name || profile.email.split('@')[0]; @@ -69,6 +70,31 @@ export default function ProfileCard({ profile, referral, points, myEvents }: Pro + {referral.link && ( +
+ + Your Referral Link + + + (e.target as HTMLInputElement).select()} + /> + + +
+ )} ); } diff --git a/admin/src/pages/volunteer/VolunteerDashboardPage.tsx b/admin/src/pages/volunteer/VolunteerDashboardPage.tsx index c67ae4ee..2b0af1a6 100644 --- a/admin/src/pages/volunteer/VolunteerDashboardPage.tsx +++ b/admin/src/pages/volunteer/VolunteerDashboardPage.tsx @@ -1,11 +1,11 @@ import { useEffect, useState, useCallback } from 'react'; -import { Row, Col, Skeleton, Result, Button, Typography, Card, Empty } from 'antd'; +import { Row, Col, Skeleton, Result, Button, Typography, Card, Empty, Space } from 'antd'; import { ReloadOutlined } from '@ant-design/icons'; import axios from 'axios'; import { api } from '@/lib/api'; import ProfileCard from '@/components/volunteer/dashboard/ProfileCard'; -import ReferralCard from '@/components/volunteer/dashboard/ReferralCard'; import ActionCampaignCard from '@/components/volunteer/dashboard/ActionCampaignCard'; +import ActionStepsList from '@/components/volunteer/dashboard/ActionStepsList'; import TakeActionCard from '@/components/volunteer/dashboard/TakeActionCard'; import TrainingList from '@/components/volunteer/dashboard/TrainingList'; import MyEventsCard from '@/components/volunteer/dashboard/MyEventsCard'; @@ -43,20 +43,11 @@ export default function VolunteerDashboardPage() { return (
- - - - + + - - - - - - - - - + +
@@ -86,6 +77,8 @@ export default function VolunteerDashboardPage() { ); } + const hasRightColumn = !!data.featuredEvent || !!data.actionCampaign; + return (
@@ -93,29 +86,35 @@ export default function VolunteerDashboardPage() { - - - - - + {/* ── Top section: 2 columns ── */} + + + + {data.actionCampaign && ( + + )} + - {data.actionCampaign && ( - - - - )} - {data.featuredEvent && ( - - + {hasRightColumn && ( + + + {data.featuredEvent && ( + + )} + {data.actionCampaign && ( + + )} + )} + {/* ── Middle section: 3-up ── */} @@ -126,6 +125,7 @@ export default function VolunteerDashboardPage() { + {/* ── Bottom: resources ── */}