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,39 +17,39 @@ 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>
<Typography.Title level={4} style={{ margin: 0 }}>
{campaign.title} {campaign.title}
</Typography.Title> </Typography.Title>
{campaign.description && ( {campaign.description && (
<Typography.Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0, fontSize: 13 }}> <Typography.Text type="secondary" style={{ fontSize: 13, display: 'block', marginBottom: 12 }}>
{campaign.description} {campaign.description}
</Typography.Paragraph>
)}
</div>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<Typography.Text style={{ fontSize: 13 }}>
Progress: {campaign.completedSteps} of {campaign.totalSteps} complete
</Typography.Text> </Typography.Text>
{campaign.rewardEarned && <Tag color="gold">Reward Earned!</Tag>} )}
<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> </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
@ -57,21 +57,25 @@ export default function ActionCampaignCard({ campaign }: ActionCampaignCardProps
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,34 +98,76 @@ 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}
renderItem={(step) => {
const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK'; const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK';
const canNavigate = resolveStepLink(step) !== null; const canNavigate = resolveStepLink(step) !== null;
return ( return (
<List.Item <div
style={{ padding: '10px 20px', opacity: step.completed ? 0.65 : 1 }} key={step.id}
actions={[ style={{
step.completed ? ( display: 'flex',
<Tag color="success" icon={<CheckCircleFilled />}>Completed</Tag> 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 ? ( ) : isSelfReport ? (
<Space size="small"> <Space size={4}>
{canNavigate && ( {canNavigate && (
<Button size="small" onClick={() => handleNavigate(step)}>Open</Button> <Button size="small" type="text" onClick={() => handleNavigate(step)}>Open</Button>
)} )}
<Button <Button
size="small" size="small"
@ -141,47 +181,18 @@ export default function ActionStepsList({ campaign, onRefresh }: ActionStepsList
) : ( ) : (
<Button <Button
size="small" size="small"
type="primary" type="link"
icon={<RightOutlined />}
onClick={() => handleNavigate(step)} onClick={() => handleNavigate(step)}
disabled={!canNavigate} disabled={!canNavigate}
style={{ fontWeight: 600 }}
> >
Take Action Take Action
</Button> </Button>
), )}
]} </div>
>
<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> </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 <div style={{ flex: 1, minWidth: 0 }}>
</Button>, <Typography.Text strong style={{ fontSize: 13, display: 'block' }}>
]} {event.eventTitle}
> </Typography.Text>
<List.Item.Meta <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 2 }}>
title={event.eventTitle}
description={
<Space size={8} wrap>
<Typography.Text type="secondary" style={{ fontSize: 12 }}> <Typography.Text type="secondary" style={{ fontSize: 12 }}>
{dayjs(event.eventDate).format('MMM D, YYYY')} {dayjs(event.eventDate).format('MMM D, YYYY')}
</Typography.Text> </Typography.Text>
{event.tierName && ( {event.tierName && (
<Tag icon={<TagOutlined />} color="blue"> <Tag icon={<TagOutlined />} color="blue" style={{ margin: 0, fontSize: 11, lineHeight: '18px', padding: '0 6px' }}>
{event.tierName} {event.tierName}
</Tag> </Tag>
)} )}
</Space> </div>
} </div>
/> <Button
</List.Item> size="small"
)} type="link"
/> onClick={() => navigate(`/event/${event.eventSlug}`)}
style={{ fontWeight: 600 }}
>
Details
</Button>
</div>
))
)} )}
</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,12 +93,13 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
KIND_ICONS[resource.kind] KIND_ICONS[resource.kind]
)} )}
</div> </div>
<div style={{ padding: '10px 12px 12px' }}>
<Typography.Text <Typography.Text
strong strong
style={{ style={{
display: 'block', display: 'block',
fontSize: 13, fontSize: 13,
marginBottom: 4, marginBottom: 8,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@ -113,9 +107,6 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
> >
{resource.title} {resource.title}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 11, display: 'block', marginBottom: 8 }}>
{KIND_LABELS[resource.kind]}
</Typography.Text>
<Button <Button
size="small" size="small"
block block
@ -129,6 +120,7 @@ export default function ResourcesGrid({ resources }: ResourcesGridProps) {
> >
{resource.kind === 'document' ? 'Download' : 'View'} {resource.kind === 'document' ? 'Download' : 'View'}
</Button> </Button>
</div>
</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
title={
<Space>
<FireOutlined style={{ color: '#ff4d4f' }} />
<span>Take Action</span>
</Space>
}
styles={{ body: { padding: 0 } }}
style={{ overflow: 'hidden' }}
>
{event.coverImageUrl && (
<div <div
style={{ style={{
height: 140, borderRadius: 8,
background: `url(${event.coverImageUrl}) center/cover`, overflow: 'hidden',
borderBottom: '1px solid rgba(255,255,255,0.1)', background: 'linear-gradient(135deg, #e74c3c 0%, #c0392b 60%, #a93226 100%)',
position: 'relative',
}} }}
/> >
)} <div style={{ padding: '12px 20px', borderBottom: '1px solid rgba(255,255,255,0.15)' }}>
<div style={{ padding: 20 }}> <Typography.Text strong style={{ color: '#fff', fontSize: 14, letterSpacing: 0.5, textTransform: 'uppercase' }}>
<Typography.Title level={4} style={{ margin: '0 0 8px' }}> 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} {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}
renderItem={(training) => {
const full = training.maxVolunteers > 0 && training.currentVolunteers >= training.maxVolunteers; const full = training.maxVolunteers > 0 && training.currentVolunteers >= training.maxVolunteers;
return ( return (
<List.Item <div
style={{ padding: '12px 20px' }} key={training.id}
actions={[ style={{
training.isSignedUp ? ( display: 'flex',
<Tag color="success" icon={<CheckOutlined />}>Signed up</Tag> 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 <Button
size="small" size="small"
type="primary" type="primary"
danger
loading={signingUpId === training.id} loading={signingUpId === training.id}
disabled={full} disabled={full}
onClick={() => handleSignup(training)} onClick={() => handleSignup(training)}
> >
{full ? 'Full' : 'RSVP'} {full ? 'Full' : 'RSVP'}
</Button> </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> </div>
} </div>
/>
</List.Item>
); );
}} })
/>
)} )}
</Card> </Card>
); );