Visual polish of volunteer dashboard components to match FAFC reference

TakeActionCard: replaced standard Card with a bold red gradient panel
(linear-gradient #e74c3c → #c0392b) with white text, uppercase header,
and a white CTA button — matches the FAFC "Take Action" visual weight.

ActionCampaignCard: tightened padding, thicker progress bar (10px),
compact card header with gold trophy icon, and a "Next:" hint with
the upcoming step label separated by a subtle divider.

ActionStepsList: replaced Antd List with manual flex rows for tighter
control. Each row shows a kind prefix label (Email:, Sign:, RSVP:)
in muted text above the step label. "Take Action" buttons are type=link
for visual lightness. Completed steps show a compact "Done" tag.

TrainingList: added count badge ("2 Upcoming" in green) in the card
header extra slot. Each row uses a format tag ("In Person" / "Virtual")
matching FAFC's colored badges. RSVP buttons use danger variant (red)
to match FAFC's prominent red RSVP buttons.

ActivityCard: centered hero layout with large gold point number and
"points earned" subtitle. Achievement count only shown when > 0,
separated by a subtle divider.

MyEventsCard: consistent row layout with flex instead of Antd List.
Details button uses type=link for visual consistency with the steps
list.

ResourcesGrid: renamed to "Tools & Resources". Resource cards have
taller thumbnails (120px), cleaner padding, removed the type label
to reduce clutter — the icon in the placeholder already communicates
the type.

All cards now use consistent header styling (14px bold, 12px padding,
subtle 6% white bottom border) for visual rhythm across the page.

Bunker Admin
This commit is contained in:
bunker-admin 2026-04-11 21:35:58 -06:00
parent 054902b9f9
commit 29d1f3998a
7 changed files with 354 additions and 326 deletions

View File

@ -1,4 +1,4 @@
import { Card, Progress, Typography, Space, Tag, Alert } from 'antd';
import { Card, Progress, Typography, Tag, Alert } from 'antd';
import { TrophyOutlined } from '@ant-design/icons';
import type { DashboardActionCampaign } from './types';
@ -17,61 +17,65 @@ export default function ActionCampaignCard({ campaign }: ActionCampaignCardProps
return (
<Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: '16px 20px' },
}}
title={
<Space>
<TrophyOutlined />
<span>Your Goal</span>
</Space>
<span style={{ fontSize: 14, fontWeight: 600 }}>
<TrophyOutlined style={{ marginRight: 8, color: '#faad14' }} />
Your Goal
</span>
}
styles={{ body: { padding: 20 } }}
>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<Typography.Title level={4} style={{ margin: 0 }}>
{campaign.title}
</Typography.Title>
{campaign.description && (
<Typography.Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0, fontSize: 13 }}>
{campaign.description}
</Typography.Paragraph>
)}
</div>
<Typography.Title level={5} style={{ margin: '0 0 4px' }}>
{campaign.title}
</Typography.Title>
{campaign.description && (
<Typography.Text type="secondary" style={{ fontSize: 13, display: 'block', marginBottom: 12 }}>
{campaign.description}
</Typography.Text>
)}
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<Typography.Text style={{ fontSize: 13 }}>
Progress: {campaign.completedSteps} of {campaign.totalSteps} complete
</Typography.Text>
{campaign.rewardEarned && <Tag color="gold">Reward Earned!</Tag>}
</div>
<Progress
percent={percent}
status={campaign.rewardEarned ? 'success' : 'active'}
showInfo={false}
/>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{campaign.completedSteps} of {campaign.totalSteps} complete
</Typography.Text>
{campaign.rewardEarned && <Tag color="gold" style={{ margin: 0 }}>Reward Earned!</Tag>}
</div>
<Progress
percent={percent}
status={campaign.rewardEarned ? 'success' : 'active'}
showInfo={false}
strokeWidth={10}
style={{ marginBottom: 0 }}
/>
{campaign.rewardEarned && campaign.rewardText && (
<Alert
type="success"
showIcon
message="You've earned your reward!"
description={campaign.rewardText}
/>
)}
{campaign.rewardEarned && campaign.rewardText && (
<Alert
type="success"
showIcon
message="You've earned your reward!"
description={campaign.rewardText}
style={{ marginTop: 12 }}
/>
)}
{!campaign.rewardEarned && campaign.rewardText && (
<Typography.Paragraph style={{ marginBottom: 0, fontSize: 13 }}>
<TrophyOutlined /> <strong>Reward:</strong> {campaign.rewardText}
</Typography.Paragraph>
)}
{!campaign.rewardEarned && campaign.rewardText && (
<Typography.Text style={{ fontSize: 12, display: 'block', marginTop: 10, color: 'rgba(255,255,255,0.5)' }}>
<TrophyOutlined style={{ marginRight: 4 }} />
{campaign.rewardText}
</Typography.Text>
)}
{nextStep && !campaign.rewardEarned && (
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
<strong>Next:</strong> {nextStep.label}
{nextStep && !campaign.rewardEarned && (
<div style={{ marginTop: 10, paddingTop: 10, borderTop: '1px solid rgba(255,255,255,0.06)' }}>
<Typography.Text style={{ fontSize: 13 }}>
<strong>Next:</strong>{' '}
<Typography.Text type="secondary" style={{ fontSize: 13 }}>{nextStep.label}</Typography.Text>
</Typography.Text>
)}
</Space>
</div>
)}
</Card>
);
}

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import { Card, List, Button, Typography, Tag, Space, App } from 'antd';
import { Card, Button, Typography, Tag, Space, App } from 'antd';
import {
VideoCameraOutlined,
MailOutlined,
@ -10,8 +10,6 @@ import {
LinkOutlined,
CheckSquareOutlined,
CheckCircleFilled,
RightOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { api } from '@/lib/api';
@ -100,88 +98,101 @@ export default function ActionStepsList({ campaign, onRefresh }: ActionStepsList
return (
<Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: 0 },
}}
title={
<Space>
<UnorderedListOutlined />
<span>Actions</span>
</Space>
}
extra={
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{campaign.completedSteps} of {campaign.totalSteps}
<Typography.Text style={{ fontSize: 14, fontWeight: 600 }}>
{campaign.completedSteps} of {campaign.totalSteps} Actions
</Typography.Text>
}
styles={{ body: { padding: 0 } }}
>
<List
dataSource={sortedSteps}
renderItem={(step) => {
const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK';
const canNavigate = resolveStepLink(step) !== null;
return (
<List.Item
style={{ padding: '10px 20px', opacity: step.completed ? 0.65 : 1 }}
actions={[
step.completed ? (
<Tag color="success" icon={<CheckCircleFilled />}>Completed</Tag>
) : isSelfReport ? (
<Space size="small">
{canNavigate && (
<Button size="small" onClick={() => handleNavigate(step)}>Open</Button>
)}
<Button
size="small"
type="primary"
loading={completingStepId === step.id}
onClick={() => handleSelfReport(step)}
>
Mark done
</Button>
</Space>
) : (
{sortedSteps.map((step, i) => {
const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK';
const canNavigate = resolveStepLink(step) !== null;
return (
<div
key={step.id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 20px',
borderTop: i > 0 ? '1px solid rgba(255,255,255,0.04)' : undefined,
opacity: step.completed ? 0.55 : 1,
gap: 12,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0 }}>
<div
style={{
width: 26,
height: 26,
borderRadius: '50%',
background: step.completed ? '#52c41a' : 'rgba(52,152,219,0.15)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
flexShrink: 0,
color: step.completed ? '#fff' : 'rgba(255,255,255,0.7)',
}}
>
{step.completed ? <CheckCircleFilled /> : KIND_ICONS[step.kind]}
</div>
<div style={{ minWidth: 0 }}>
<Typography.Text strong style={{ fontSize: 12, color: 'rgba(255,255,255,0.45)', display: 'block' }}>
{KIND_LABELS[step.kind]}
</Typography.Text>
<Typography.Text
style={{
fontSize: 13,
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
textDecoration: step.completed ? 'line-through' : 'none',
}}
>
{step.label}
</Typography.Text>
</div>
</div>
<div style={{ flexShrink: 0 }}>
{step.completed ? (
<Tag color="success" style={{ margin: 0 }}>Done</Tag>
) : isSelfReport ? (
<Space size={4}>
{canNavigate && (
<Button size="small" type="text" onClick={() => handleNavigate(step)}>Open</Button>
)}
<Button
size="small"
type="primary"
icon={<RightOutlined />}
onClick={() => handleNavigate(step)}
disabled={!canNavigate}
loading={completingStepId === step.id}
onClick={() => handleSelfReport(step)}
>
Take Action
Mark done
</Button>
),
]}
>
<List.Item.Meta
avatar={
<div
style={{
width: 28,
height: 28,
borderRadius: '50%',
background: step.completed ? '#52c41a' : 'rgba(52,152,219,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 14,
color: step.completed ? '#fff' : 'rgba(255,255,255,0.85)',
}}
>
{step.completed ? <CheckCircleFilled /> : KIND_ICONS[step.kind]}
</div>
}
title={
<span style={{ fontSize: 13 }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{KIND_LABELS[step.kind]}:
</Typography.Text>{' '}
{step.label}
</span>
}
/>
</List.Item>
);
}}
/>
</Space>
) : (
<Button
size="small"
type="link"
onClick={() => handleNavigate(step)}
disabled={!canNavigate}
style={{ fontWeight: 600 }}
>
Take Action
</Button>
)}
</div>
</div>
);
})}
</Card>
);
}

View File

@ -1,4 +1,4 @@
import { Card, Row, Col, Statistic, Space } from 'antd';
import { Card, Typography } from 'antd';
import { TrophyOutlined, StarFilled } from '@ant-design/icons';
import type { DashboardPoints } from './types';
@ -9,32 +9,36 @@ interface ActivityCardProps {
export default function ActivityCard({ points }: ActivityCardProps) {
return (
<Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: '20px', textAlign: 'center' },
}}
title={
<Space>
<TrophyOutlined />
<span>My Activity</span>
</Space>
<span style={{ fontSize: 14, fontWeight: 600 }}>Activity</span>
}
extra={
<Typography.Text type="warning" style={{ fontSize: 13, fontWeight: 600 }}>
{points.total} pts
</Typography.Text>
}
styles={{ body: { padding: 20 } }}
>
<Row gutter={[16, 16]}>
<Col xs={12}>
<Statistic
title="Total Points"
value={points.total}
prefix={<TrophyOutlined style={{ color: '#faad14' }} />}
valueStyle={{ fontSize: 32, fontWeight: 600 }}
/>
</Col>
<Col xs={12}>
<Statistic
title="Achievements"
value={points.achievementCount}
prefix={<StarFilled style={{ color: '#f5222d' }} />}
valueStyle={{ fontSize: 32, fontWeight: 600 }}
/>
</Col>
</Row>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'baseline', gap: 4, marginBottom: 4 }}>
<TrophyOutlined style={{ fontSize: 24, color: '#faad14' }} />
<Typography.Title level={2} style={{ margin: 0, color: '#faad14', fontWeight: 700 }}>
{points.total}
</Typography.Title>
</div>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
points earned
</Typography.Text>
{points.achievementCount > 0 && (
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid rgba(255,255,255,0.06)' }}>
<StarFilled style={{ color: '#f5222d', marginRight: 4 }} />
<Typography.Text style={{ fontSize: 13 }}>
{points.achievementCount} achievement{points.achievementCount !== 1 ? 's' : ''}
</Typography.Text>
</div>
)}
</Card>
);
}

View File

@ -1,4 +1,4 @@
import { Card, List, Button, Typography, Space, Empty, Tag } from 'antd';
import { Card, Button, Typography, Empty, Tag } from 'antd';
import { CalendarOutlined, TagOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
@ -13,13 +13,16 @@ export default function MyEventsCard({ events }: MyEventsCardProps) {
return (
<Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: 0 },
}}
title={
<Space>
<CalendarOutlined />
<span>My Events</span>
</Space>
<span style={{ fontSize: 14, fontWeight: 600 }}>
<CalendarOutlined style={{ marginRight: 8 }} />
My Events
</span>
}
styles={{ body: { padding: 0 } }}
>
{events.length === 0 ? (
<div style={{ padding: 24 }}>
@ -29,39 +32,43 @@ export default function MyEventsCard({ events }: MyEventsCardProps) {
/>
</div>
) : (
<List
dataSource={events}
renderItem={(event) => (
<List.Item
style={{ padding: '12px 20px' }}
actions={[
<Button
size="small"
type="link"
onClick={() => navigate(`/event/${event.eventSlug}`)}
>
Details
</Button>,
]}
events.map((event, i) => (
<div
key={event.ticketId}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 20px',
borderTop: i > 0 ? '1px solid rgba(255,255,255,0.04)' : undefined,
gap: 12,
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<Typography.Text strong style={{ fontSize: 13, display: 'block' }}>
{event.eventTitle}
</Typography.Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 2 }}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{dayjs(event.eventDate).format('MMM D, YYYY')}
</Typography.Text>
{event.tierName && (
<Tag icon={<TagOutlined />} color="blue" style={{ margin: 0, fontSize: 11, lineHeight: '18px', padding: '0 6px' }}>
{event.tierName}
</Tag>
)}
</div>
</div>
<Button
size="small"
type="link"
onClick={() => navigate(`/event/${event.eventSlug}`)}
style={{ fontWeight: 600 }}
>
<List.Item.Meta
title={event.eventTitle}
description={
<Space size={8} wrap>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{dayjs(event.eventDate).format('MMM D, YYYY')}
</Typography.Text>
{event.tierName && (
<Tag icon={<TagOutlined />} color="blue">
{event.tierName}
</Tag>
)}
</Space>
}
/>
</List.Item>
)}
/>
Details
</Button>
</div>
))
)}
</Card>
);

View File

@ -1,11 +1,10 @@
import { Card, Row, Col, Button, Typography, Empty, Space } from 'antd';
import { Card, Row, Col, Button, Typography, Empty } from 'antd';
import {
FileTextOutlined,
VideoCameraOutlined,
PictureOutlined,
DownloadOutlined,
EyeOutlined,
FolderOpenOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { mediaApi } from '@/lib/media-api';
@ -16,15 +15,9 @@ interface ResourcesGridProps {
}
const KIND_ICONS = {
document: <FileTextOutlined style={{ fontSize: 36 }} />,
video: <VideoCameraOutlined style={{ fontSize: 36 }} />,
photo: <PictureOutlined style={{ fontSize: 36 }} />,
};
const KIND_LABELS = {
document: 'Document',
video: 'Video',
photo: 'Photo',
document: <FileTextOutlined style={{ fontSize: 40, color: 'rgba(255,255,255,0.3)' }} />,
video: <VideoCameraOutlined style={{ fontSize: 40, color: 'rgba(255,255,255,0.3)' }} />,
photo: <PictureOutlined style={{ fontSize: 40, color: 'rgba(255,255,255,0.3)' }} />,
};
function resolveDownloadUrl(downloadPath: string): string {
@ -58,13 +51,13 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
return (
<Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: 20 },
}}
title={
<Space>
<FolderOpenOutlined />
<span>Resources</span>
</Space>
<span style={{ fontSize: 14, fontWeight: 600 }}>Tools & Resources</span>
}
styles={{ body: { padding: 20 } }}
>
{resources.length === 0 ? (
<Empty description="No resources available yet" image={Empty.PRESENTED_IMAGE_SIMPLE} />
@ -75,15 +68,15 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
<Card
hoverable
size="small"
styles={{ body: { padding: 12 } }}
style={{ height: '100%' }}
styles={{
body: { padding: 0 },
}}
style={{ height: '100%', overflow: 'hidden' }}
>
<div
style={{
height: 100,
marginBottom: 12,
background: 'rgba(255,255,255,0.05)',
borderRadius: 6,
height: 120,
background: 'rgba(255,255,255,0.03)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@ -100,35 +93,34 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
KIND_ICONS[resource.kind]
)}
</div>
<Typography.Text
strong
style={{
display: 'block',
fontSize: 13,
marginBottom: 4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{resource.title}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 11, display: 'block', marginBottom: 8 }}>
{KIND_LABELS[resource.kind]}
</Typography.Text>
<Button
size="small"
block
icon={resource.kind === 'document' ? <DownloadOutlined /> : <EyeOutlined />}
onClick={() => handleAction(resource)}
disabled={
resource.kind === 'document'
? !resource.downloadPath
: !resource.viewPath
}
>
{resource.kind === 'document' ? 'Download' : 'View'}
</Button>
<div style={{ padding: '10px 12px 12px' }}>
<Typography.Text
strong
style={{
display: 'block',
fontSize: 13,
marginBottom: 8,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{resource.title}
</Typography.Text>
<Button
size="small"
block
icon={resource.kind === 'document' ? <DownloadOutlined /> : <EyeOutlined />}
onClick={() => handleAction(resource)}
disabled={
resource.kind === 'document'
? !resource.downloadPath
: !resource.viewPath
}
>
{resource.kind === 'document' ? 'Download' : 'View'}
</Button>
</div>
</Card>
</Col>
))}

View File

@ -1,5 +1,5 @@
import { Card, Button, Typography, Space } from 'antd';
import { FireOutlined, CalendarOutlined, EnvironmentOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Typography } from 'antd';
import { CalendarOutlined, EnvironmentOutlined, RightOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import type { DashboardFeaturedEvent } from './types';
@ -11,55 +11,52 @@ interface TakeActionCardProps {
export default function TakeActionCard({ event }: TakeActionCardProps) {
const navigate = useNavigate();
const handleClick = () => {
navigate(`/event/${event.slug}`);
};
return (
<Card
title={
<Space>
<FireOutlined style={{ color: '#ff4d4f' }} />
<span>Take Action</span>
</Space>
}
styles={{ body: { padding: 0 } }}
style={{ overflow: 'hidden' }}
<div
style={{
borderRadius: 8,
overflow: 'hidden',
background: 'linear-gradient(135deg, #e74c3c 0%, #c0392b 60%, #a93226 100%)',
position: 'relative',
}}
>
{event.coverImageUrl && (
<div
style={{
height: 140,
background: `url(${event.coverImageUrl}) center/cover`,
borderBottom: '1px solid rgba(255,255,255,0.1)',
}}
/>
)}
<div style={{ padding: 20 }}>
<Typography.Title level={4} style={{ margin: '0 0 8px' }}>
<div style={{ padding: '12px 20px', borderBottom: '1px solid rgba(255,255,255,0.15)' }}>
<Typography.Text strong style={{ color: '#fff', fontSize: 14, letterSpacing: 0.5, textTransform: 'uppercase' }}>
Take Action
</Typography.Text>
</div>
<div style={{ padding: '20px 20px 24px' }}>
<Typography.Title level={4} style={{ color: '#fff', margin: '0 0 12px', lineHeight: 1.3 }}>
{event.title}
</Typography.Title>
<Space direction="vertical" size="small" style={{ width: '100%', marginBottom: 16 }}>
<Typography.Text type="secondary">
<CalendarOutlined /> {dayjs(event.date).format('MMM D, YYYY')} at {event.startTime}
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 20 }}>
<Typography.Text style={{ color: 'rgba(255,255,255,0.85)', fontSize: 13 }}>
<CalendarOutlined style={{ marginRight: 6 }} />
{dayjs(event.date).format('ddd, MMM D, YYYY')} at {event.startTime}
</Typography.Text>
{event.venueName && (
<Typography.Text type="secondary">
<EnvironmentOutlined /> {event.venueName}
<Typography.Text style={{ color: 'rgba(255,255,255,0.85)', fontSize: 13 }}>
<EnvironmentOutlined style={{ marginRight: 6 }} />
{event.venueName}
</Typography.Text>
)}
</Space>
</div>
<Button
type="primary"
danger
type="default"
size="large"
block
icon={<RightOutlined />}
onClick={handleClick}
onClick={() => navigate(`/event/${event.slug}`)}
style={{
background: '#fff',
color: '#c0392b',
fontWeight: 600,
border: 'none',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
}}
>
Take Action
</Button>
</div>
</Card>
</div>
);
}

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { Card, List, Button, Typography, Tag, Space, Empty, App } from 'antd';
import { ReadOutlined, CalendarOutlined, ClockCircleOutlined, EnvironmentOutlined, CheckOutlined } from '@ant-design/icons';
import { Card, Button, Typography, Tag, Empty, App } from 'antd';
import { ReadOutlined, CheckOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { api } from '@/lib/api';
import type { DashboardTraining } from './types';
@ -27,66 +27,79 @@ export default function TrainingList({ trainings, onRefresh }: TrainingListProps
}
};
const upcoming = trainings.filter((t) => new Date(t.date) >= new Date());
return (
<Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: 0 },
}}
title={
<Space>
<ReadOutlined />
<span>Upcoming Trainings</span>
</Space>
<span style={{ fontSize: 14, fontWeight: 600 }}>
<ReadOutlined style={{ marginRight: 8 }} />
Volunteer Training
</span>
}
extra={
upcoming.length > 0 ? (
<Typography.Text type="success" style={{ fontSize: 13, fontWeight: 600 }}>
{upcoming.length} Upcoming
</Typography.Text>
) : null
}
styles={{ body: { padding: 0 } }}
>
{trainings.length === 0 ? (
<div style={{ padding: 24 }}>
<Empty description="No upcoming trainings" image={Empty.PRESENTED_IMAGE_SIMPLE} />
</div>
) : (
<List
dataSource={trainings}
renderItem={(training) => {
const full = training.maxVolunteers > 0 && training.currentVolunteers >= training.maxVolunteers;
return (
<List.Item
style={{ padding: '12px 20px' }}
actions={[
training.isSignedUp ? (
<Tag color="success" icon={<CheckOutlined />}>Signed up</Tag>
) : (
<Button
size="small"
type="primary"
loading={signingUpId === training.id}
disabled={full}
onClick={() => handleSignup(training)}
>
{full ? 'Full' : 'RSVP'}
</Button>
),
]}
>
<List.Item.Meta
title={training.title}
description={
<Space direction="vertical" size={2}>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
<CalendarOutlined /> {dayjs(training.date).format('MMM D, YYYY')}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
<ClockCircleOutlined /> {training.startTime} {training.endTime}
</Typography.Text>
{training.location && (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
<EnvironmentOutlined /> {training.location}
</Typography.Text>
)}
</Space>
}
/>
</List.Item>
);
}}
/>
trainings.map((training, i) => {
const full = training.maxVolunteers > 0 && training.currentVolunteers >= training.maxVolunteers;
return (
<div
key={training.id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 20px',
borderTop: i > 0 ? '1px solid rgba(255,255,255,0.04)' : undefined,
gap: 12,
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 2 }}>
<Tag color="volcano" style={{ margin: 0, fontSize: 11, lineHeight: '18px', padding: '0 6px' }}>
{training.location ? 'In Person' : 'Virtual'}
</Tag>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{dayjs(training.date).format('ddd, MMM D')} &middot; {training.startTime} - {training.endTime}
</Typography.Text>
</div>
<Typography.Text strong style={{ fontSize: 13 }}>
{training.title}
</Typography.Text>
</div>
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
{training.isSignedUp ? (
<Tag color="success" icon={<CheckOutlined />} style={{ margin: 0 }}>Signed up</Tag>
) : (
<Button
size="small"
type="primary"
danger
loading={signingUpId === training.id}
disabled={full}
onClick={() => handleSignup(training)}
>
{full ? 'Full' : 'RSVP'}
</Button>
)}
</div>
</div>
);
})
)}
</Card>
);