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

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Card, List, Button, Typography, Tag, Space, App } from 'antd'; import { Card, Button, Typography, Tag, Space, App } from 'antd';
import { import {
VideoCameraOutlined, VideoCameraOutlined,
MailOutlined, MailOutlined,
@ -10,8 +10,6 @@ import {
LinkOutlined, LinkOutlined,
CheckSquareOutlined, CheckSquareOutlined,
CheckCircleFilled, CheckCircleFilled,
RightOutlined,
UnorderedListOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
@ -100,88 +98,101 @@ export default function ActionStepsList({ campaign, onRefresh }: ActionStepsList
return ( return (
<Card <Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: 0 },
}}
title={ title={
<Space> <Typography.Text style={{ fontSize: 14, fontWeight: 600 }}>
<UnorderedListOutlined /> {campaign.completedSteps} of {campaign.totalSteps} Actions
<span>Actions</span>
</Space>
}
extra={
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{campaign.completedSteps} of {campaign.totalSteps}
</Typography.Text> </Typography.Text>
} }
styles={{ body: { padding: 0 } }}
> >
<List {sortedSteps.map((step, i) => {
dataSource={sortedSteps} const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK';
renderItem={(step) => { const canNavigate = resolveStepLink(step) !== null;
const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK';
const canNavigate = resolveStepLink(step) !== null; return (
return ( <div
<List.Item key={step.id}
style={{ padding: '10px 20px', opacity: step.completed ? 0.65 : 1 }} style={{
actions={[ display: 'flex',
step.completed ? ( alignItems: 'center',
<Tag color="success" icon={<CheckCircleFilled />}>Completed</Tag> justifyContent: 'space-between',
) : isSelfReport ? ( padding: '12px 20px',
<Space size="small"> borderTop: i > 0 ? '1px solid rgba(255,255,255,0.04)' : undefined,
{canNavigate && ( opacity: step.completed ? 0.55 : 1,
<Button size="small" onClick={() => handleNavigate(step)}>Open</Button> gap: 12,
)} }}
<Button >
size="small" <div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0 }}>
type="primary" <div
loading={completingStepId === step.id} style={{
onClick={() => handleSelfReport(step)} width: 26,
> height: 26,
Mark done borderRadius: '50%',
</Button> background: step.completed ? '#52c41a' : 'rgba(52,152,219,0.15)',
</Space> 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 <Button
size="small" size="small"
type="primary" type="primary"
icon={<RightOutlined />} loading={completingStepId === step.id}
onClick={() => handleNavigate(step)} onClick={() => handleSelfReport(step)}
disabled={!canNavigate}
> >
Take Action Mark done
</Button> </Button>
), </Space>
]} ) : (
> <Button
<List.Item.Meta size="small"
avatar={ type="link"
<div onClick={() => handleNavigate(step)}
style={{ disabled={!canNavigate}
width: 28, style={{ fontWeight: 600 }}
height: 28, >
borderRadius: '50%', Take Action
background: step.completed ? '#52c41a' : 'rgba(52,152,219,0.2)', </Button>
display: 'flex', )}
alignItems: 'center', </div>
justifyContent: 'center', </div>
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>
);
}}
/>
</Card> </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 { TrophyOutlined, StarFilled } from '@ant-design/icons';
import type { DashboardPoints } from './types'; import type { DashboardPoints } from './types';
@ -9,32 +9,36 @@ interface ActivityCardProps {
export default function ActivityCard({ points }: ActivityCardProps) { export default function ActivityCard({ points }: ActivityCardProps) {
return ( return (
<Card <Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: '20px', textAlign: 'center' },
}}
title={ title={
<Space> <span style={{ fontSize: 14, fontWeight: 600 }}>Activity</span>
<TrophyOutlined /> }
<span>My Activity</span> extra={
</Space> <Typography.Text type="warning" style={{ fontSize: 13, fontWeight: 600 }}>
{points.total} pts
</Typography.Text>
} }
styles={{ body: { padding: 20 } }}
> >
<Row gutter={[16, 16]}> <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'baseline', gap: 4, marginBottom: 4 }}>
<Col xs={12}> <TrophyOutlined style={{ fontSize: 24, color: '#faad14' }} />
<Statistic <Typography.Title level={2} style={{ margin: 0, color: '#faad14', fontWeight: 700 }}>
title="Total Points" {points.total}
value={points.total} </Typography.Title>
prefix={<TrophyOutlined style={{ color: '#faad14' }} />} </div>
valueStyle={{ fontSize: 32, fontWeight: 600 }} <Typography.Text type="secondary" style={{ fontSize: 12 }}>
/> points earned
</Col> </Typography.Text>
<Col xs={12}> {points.achievementCount > 0 && (
<Statistic <div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid rgba(255,255,255,0.06)' }}>
title="Achievements" <StarFilled style={{ color: '#f5222d', marginRight: 4 }} />
value={points.achievementCount} <Typography.Text style={{ fontSize: 13 }}>
prefix={<StarFilled style={{ color: '#f5222d' }} />} {points.achievementCount} achievement{points.achievementCount !== 1 ? 's' : ''}
valueStyle={{ fontSize: 32, fontWeight: 600 }} </Typography.Text>
/> </div>
</Col> )}
</Row>
</Card> </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 { CalendarOutlined, TagOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -13,13 +13,16 @@ export default function MyEventsCard({ events }: MyEventsCardProps) {
return ( return (
<Card <Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: 0 },
}}
title={ title={
<Space> <span style={{ fontSize: 14, fontWeight: 600 }}>
<CalendarOutlined /> <CalendarOutlined style={{ marginRight: 8 }} />
<span>My Events</span> My Events
</Space> </span>
} }
styles={{ body: { padding: 0 } }}
> >
{events.length === 0 ? ( {events.length === 0 ? (
<div style={{ padding: 24 }}> <div style={{ padding: 24 }}>
@ -29,39 +32,43 @@ export default function MyEventsCard({ events }: MyEventsCardProps) {
/> />
</div> </div>
) : ( ) : (
<List events.map((event, i) => (
dataSource={events} <div
renderItem={(event) => ( key={event.ticketId}
<List.Item style={{
style={{ padding: '12px 20px' }} display: 'flex',
actions={[ alignItems: 'center',
<Button justifyContent: 'space-between',
size="small" padding: '12px 20px',
type="link" borderTop: i > 0 ? '1px solid rgba(255,255,255,0.04)' : undefined,
onClick={() => navigate(`/event/${event.eventSlug}`)} gap: 12,
> }}
Details >
</Button>, <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 Details
title={event.eventTitle} </Button>
description={ </div>
<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>
)}
/>
)} )}
</Card> </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 { import {
FileTextOutlined, FileTextOutlined,
VideoCameraOutlined, VideoCameraOutlined,
PictureOutlined, PictureOutlined,
DownloadOutlined, DownloadOutlined,
EyeOutlined, EyeOutlined,
FolderOpenOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { mediaApi } from '@/lib/media-api'; import { mediaApi } from '@/lib/media-api';
@ -16,15 +15,9 @@ interface ResourcesGridProps {
} }
const KIND_ICONS = { const KIND_ICONS = {
document: <FileTextOutlined style={{ fontSize: 36 }} />, document: <FileTextOutlined style={{ fontSize: 40, color: 'rgba(255,255,255,0.3)' }} />,
video: <VideoCameraOutlined style={{ fontSize: 36 }} />, video: <VideoCameraOutlined style={{ fontSize: 40, color: 'rgba(255,255,255,0.3)' }} />,
photo: <PictureOutlined style={{ fontSize: 36 }} />, photo: <PictureOutlined style={{ fontSize: 40, color: 'rgba(255,255,255,0.3)' }} />,
};
const KIND_LABELS = {
document: 'Document',
video: 'Video',
photo: 'Photo',
}; };
function resolveDownloadUrl(downloadPath: string): string { function resolveDownloadUrl(downloadPath: string): string {
@ -58,13 +51,13 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
return ( return (
<Card <Card
styles={{
header: { borderBottom: '1px solid rgba(255,255,255,0.06)', padding: '12px 20px', minHeight: 'auto' },
body: { padding: 20 },
}}
title={ title={
<Space> <span style={{ fontSize: 14, fontWeight: 600 }}>Tools & Resources</span>
<FolderOpenOutlined />
<span>Resources</span>
</Space>
} }
styles={{ body: { padding: 20 } }}
> >
{resources.length === 0 ? ( {resources.length === 0 ? (
<Empty description="No resources available yet" image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty description="No resources available yet" image={Empty.PRESENTED_IMAGE_SIMPLE} />
@ -75,15 +68,15 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
<Card <Card
hoverable hoverable
size="small" size="small"
styles={{ body: { padding: 12 } }} styles={{
style={{ height: '100%' }} body: { padding: 0 },
}}
style={{ height: '100%', overflow: 'hidden' }}
> >
<div <div
style={{ style={{
height: 100, height: 120,
marginBottom: 12, background: 'rgba(255,255,255,0.03)',
background: 'rgba(255,255,255,0.05)',
borderRadius: 6,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -100,35 +93,34 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
KIND_ICONS[resource.kind] KIND_ICONS[resource.kind]
)} )}
</div> </div>
<Typography.Text <div style={{ padding: '10px 12px 12px' }}>
strong <Typography.Text
style={{ strong
display: 'block', style={{
fontSize: 13, display: 'block',
marginBottom: 4, fontSize: 13,
overflow: 'hidden', marginBottom: 8,
textOverflow: 'ellipsis', overflow: 'hidden',
whiteSpace: 'nowrap', textOverflow: 'ellipsis',
}} whiteSpace: 'nowrap',
> }}
{resource.title} >
</Typography.Text> {resource.title}
<Typography.Text type="secondary" style={{ fontSize: 11, display: 'block', marginBottom: 8 }}> </Typography.Text>
{KIND_LABELS[resource.kind]} <Button
</Typography.Text> size="small"
<Button block
size="small" icon={resource.kind === 'document' ? <DownloadOutlined /> : <EyeOutlined />}
block onClick={() => handleAction(resource)}
icon={resource.kind === 'document' ? <DownloadOutlined /> : <EyeOutlined />} disabled={
onClick={() => handleAction(resource)} resource.kind === 'document'
disabled={ ? !resource.downloadPath
resource.kind === 'document' : !resource.viewPath
? !resource.downloadPath }
: !resource.viewPath >
} {resource.kind === 'document' ? 'Download' : 'View'}
> </Button>
{resource.kind === 'document' ? 'Download' : 'View'} </div>
</Button>
</Card> </Card>
</Col> </Col>
))} ))}

View File

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

View File

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