Restructure volunteer dashboard to FAFC two-column layout
Matches the For Alberta For Canada reference: profile + goal progress
on the left, take-action CTA + action steps on the right. Specifics:
- ProfileCard now embeds the referral link (copy button + code) below
the stats row, eliminating the standalone ReferralCard from the grid.
- ActionCampaignCard slimmed to progress-only: title, description,
progress bar, reward text, and a "Next: {step}" hint. The steps
list is extracted into a new ActionStepsList component.
- ActionStepsList renders as a compact card with kind-prefixed labels
(Email:, Sign:, RSVP:) and Take Action / Mark done buttons, matching
the FAFC action list on the right column.
- Dashboard page uses lg=12/12 for the top two-column section. Falls
back to full-width single column when no campaign and no featured
event exist. Mobile stacks vertically as before. The 3-up middle
row (trainings / my events / activity) and full-width resources grid
remain unchanged.
Bunker Admin
This commit is contained in:
parent
82db26fcef
commit
054902b9f9
@ -1,96 +1,19 @@
|
|||||||
import { useState } from 'react';
|
import { Card, Progress, Typography, Space, Tag, Alert } from 'antd';
|
||||||
import { Card, Progress, List, Button, Typography, Space, Tag, Alert, App } from 'antd';
|
import { TrophyOutlined } from '@ant-design/icons';
|
||||||
import {
|
import type { DashboardActionCampaign } from './types';
|
||||||
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';
|
|
||||||
|
|
||||||
interface ActionCampaignCardProps {
|
interface ActionCampaignCardProps {
|
||||||
campaign: DashboardActionCampaign;
|
campaign: DashboardActionCampaign;
|
||||||
onRefresh: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const KIND_ICONS: Record<ActionStepKind, React.ReactNode> = {
|
export default function ActionCampaignCard({ campaign }: ActionCampaignCardProps) {
|
||||||
WATCH_VIDEO: <VideoCameraOutlined />,
|
|
||||||
SUBMIT_INFLUENCE: <MailOutlined />,
|
|
||||||
SIGN_PETITION: <FileTextOutlined />,
|
|
||||||
RSVP_EVENT: <CalendarOutlined />,
|
|
||||||
SIGNUP_SHIFT: <EnvironmentOutlined />,
|
|
||||||
JOIN_CHALLENGE: <TeamOutlined />,
|
|
||||||
VISIT_LINK: <LinkOutlined />,
|
|
||||||
CUSTOM: <CheckSquareOutlined />,
|
|
||||||
};
|
|
||||||
|
|
||||||
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<string | null>(null);
|
|
||||||
|
|
||||||
const percent = campaign.totalSteps > 0
|
const percent = campaign.totalSteps > 0
|
||||||
? Math.round((campaign.completedSteps / campaign.totalSteps) * 100)
|
? Math.round((campaign.completedSteps / campaign.totalSteps) * 100)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const handleSelfReport = async (step: DashboardActionStep) => {
|
const nextStep = [...campaign.steps]
|
||||||
setCompletingStepId(step.id);
|
.sort((a, b) => a.order - b.order)
|
||||||
try {
|
.find((s) => !s.completed);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -112,11 +35,6 @@ export default function ActionCampaignCard({ campaign, onRefresh }: ActionCampai
|
|||||||
{campaign.description}
|
{campaign.description}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
)}
|
)}
|
||||||
{campaign.rewardText && !campaign.rewardEarned && (
|
|
||||||
<Typography.Paragraph style={{ marginTop: 8, marginBottom: 0, fontSize: 13 }}>
|
|
||||||
<TrophyOutlined /> <strong>Reward:</strong> {campaign.rewardText}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -142,84 +60,17 @@ export default function ActionCampaignCard({ campaign, onRefresh }: ActionCampai
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<List
|
{!campaign.rewardEarned && campaign.rewardText && (
|
||||||
dataSource={[...campaign.steps].sort((a, b) => a.order - b.order)}
|
<Typography.Paragraph style={{ marginBottom: 0, fontSize: 13 }}>
|
||||||
renderItem={(step) => {
|
<TrophyOutlined /> <strong>Reward:</strong> {campaign.rewardText}
|
||||||
const isSelfReport = step.kind === 'CUSTOM' || step.kind === 'VISIT_LINK';
|
</Typography.Paragraph>
|
||||||
const canNavigate = resolveStepLink(step) !== null;
|
)}
|
||||||
return (
|
|
||||||
<List.Item
|
{nextStep && !campaign.rewardEarned && (
|
||||||
style={{
|
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||||
opacity: step.completed ? 0.7 : 1,
|
<strong>Next:</strong> {nextStep.label}
|
||||||
padding: '12px 0',
|
</Typography.Text>
|
||||||
}}
|
)}
|
||||||
actions={[
|
|
||||||
step.completed ? (
|
|
||||||
<Tag color="success" icon={<CheckCircleFilled />}>
|
|
||||||
Completed
|
|
||||||
</Tag>
|
|
||||||
) : isSelfReport ? (
|
|
||||||
<Space size="small">
|
|
||||||
{canNavigate && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
icon={<RightOutlined />}
|
|
||||||
onClick={() => handleNavigate(step)}
|
|
||||||
>
|
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
loading={completingStepId === step.id}
|
|
||||||
onClick={() => handleSelfReport(step)}
|
|
||||||
>
|
|
||||||
Mark as done
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
icon={<RightOutlined />}
|
|
||||||
onClick={() => handleNavigate(step)}
|
|
||||||
disabled={!canNavigate}
|
|
||||||
>
|
|
||||||
Take action
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<List.Item.Meta
|
|
||||||
avatar={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: '50%',
|
|
||||||
background: step.completed ? '#52c41a' : 'rgba(52,152,219,0.2)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
fontSize: 16,
|
|
||||||
color: step.completed ? '#fff' : 'rgba(255,255,255,0.85)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{step.completed ? <CheckCircleFilled /> : KIND_ICONS[step.kind]}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
<span style={{ textDecoration: step.completed ? 'line-through' : 'none' }}>
|
|
||||||
{step.label}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
description={step.description}
|
|
||||||
/>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
187
admin/src/components/volunteer/dashboard/ActionStepsList.tsx
Normal file
187
admin/src/components/volunteer/dashboard/ActionStepsList.tsx
Normal file
@ -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<ActionStepKind, React.ReactNode> = {
|
||||||
|
WATCH_VIDEO: <VideoCameraOutlined />,
|
||||||
|
SUBMIT_INFLUENCE: <MailOutlined />,
|
||||||
|
SIGN_PETITION: <FileTextOutlined />,
|
||||||
|
RSVP_EVENT: <CalendarOutlined />,
|
||||||
|
SIGNUP_SHIFT: <EnvironmentOutlined />,
|
||||||
|
JOIN_CHALLENGE: <TeamOutlined />,
|
||||||
|
VISIT_LINK: <LinkOutlined />,
|
||||||
|
CUSTOM: <CheckSquareOutlined />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const KIND_LABELS: Record<ActionStepKind, string> = {
|
||||||
|
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<string | null>(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 (
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<UnorderedListOutlined />
|
||||||
|
<span>Actions</span>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{campaign.completedSteps} of {campaign.totalSteps}
|
||||||
|
</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>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
icon={<RightOutlined />}
|
||||||
|
onClick={() => handleNavigate(step)}
|
||||||
|
disabled={!canNavigate}
|
||||||
|
>
|
||||||
|
Take Action
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Card, Avatar, Typography, Row, Col, Statistic } from 'antd';
|
import { Card, Avatar, Typography, Row, Col, Statistic, Input, Button, App, Space } from 'antd';
|
||||||
import { TeamOutlined, TrophyOutlined, CalendarOutlined } from '@ant-design/icons';
|
import { TeamOutlined, TrophyOutlined, CalendarOutlined, CopyOutlined } from '@ant-design/icons';
|
||||||
import type { DashboardProfile, DashboardReferral, DashboardPoints, DashboardMyEvent } from './types';
|
import type { DashboardProfile, DashboardReferral, DashboardPoints, DashboardMyEvent } from './types';
|
||||||
|
|
||||||
interface ProfileCardProps {
|
interface ProfileCardProps {
|
||||||
@ -22,6 +22,7 @@ function getInitials(name: string | null, email: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ProfileCard({ profile, referral, points, myEvents }: ProfileCardProps) {
|
export default function ProfileCard({ profile, referral, points, myEvents }: ProfileCardProps) {
|
||||||
|
const { message } = App.useApp();
|
||||||
const initials = getInitials(profile.name, profile.email);
|
const initials = getInitials(profile.name, profile.email);
|
||||||
const displayName = profile.name || profile.email.split('@')[0];
|
const displayName = profile.name || profile.email.split('@')[0];
|
||||||
|
|
||||||
@ -69,6 +70,31 @@ export default function ProfileCard({ profile, referral, points, myEvents }: Pro
|
|||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
{referral.link && (
|
||||||
|
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 8 }}>
|
||||||
|
Your Referral Link
|
||||||
|
</Typography.Text>
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Input
|
||||||
|
value={referral.link}
|
||||||
|
readOnly
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(referral.link!).then(() => message.success('Link copied'));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
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 { ReloadOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import ProfileCard from '@/components/volunteer/dashboard/ProfileCard';
|
import ProfileCard from '@/components/volunteer/dashboard/ProfileCard';
|
||||||
import ReferralCard from '@/components/volunteer/dashboard/ReferralCard';
|
|
||||||
import ActionCampaignCard from '@/components/volunteer/dashboard/ActionCampaignCard';
|
import ActionCampaignCard from '@/components/volunteer/dashboard/ActionCampaignCard';
|
||||||
|
import ActionStepsList from '@/components/volunteer/dashboard/ActionStepsList';
|
||||||
import TakeActionCard from '@/components/volunteer/dashboard/TakeActionCard';
|
import TakeActionCard from '@/components/volunteer/dashboard/TakeActionCard';
|
||||||
import TrainingList from '@/components/volunteer/dashboard/TrainingList';
|
import TrainingList from '@/components/volunteer/dashboard/TrainingList';
|
||||||
import MyEventsCard from '@/components/volunteer/dashboard/MyEventsCard';
|
import MyEventsCard from '@/components/volunteer/dashboard/MyEventsCard';
|
||||||
@ -43,20 +43,11 @@ export default function VolunteerDashboardPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xs={24}>
|
<Col xs={24} lg={12}>
|
||||||
<Card>
|
<Card><Skeleton avatar paragraph={{ rows: 3 }} active /></Card>
|
||||||
<Skeleton avatar paragraph={{ rows: 2 }} active />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} lg={12}>
|
||||||
<Card>
|
<Card><Skeleton paragraph={{ rows: 4 }} active /></Card>
|
||||||
<Skeleton paragraph={{ rows: 4 }} active />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} md={12}>
|
|
||||||
<Card>
|
|
||||||
<Skeleton paragraph={{ rows: 4 }} active />
|
|
||||||
</Card>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
@ -86,6 +77,8 @@ export default function VolunteerDashboardPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasRightColumn = !!data.featuredEvent || !!data.actionCampaign;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Title level={3} style={{ marginBottom: 16 }}>
|
<Typography.Title level={3} style={{ marginBottom: 16 }}>
|
||||||
@ -93,29 +86,35 @@ export default function VolunteerDashboardPage() {
|
|||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col xs={24} md={16}>
|
{/* ── Top section: 2 columns ── */}
|
||||||
<ProfileCard
|
<Col xs={24} lg={hasRightColumn ? 12 : 24}>
|
||||||
profile={data.profile}
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
referral={data.referral}
|
<ProfileCard
|
||||||
points={data.points}
|
profile={data.profile}
|
||||||
myEvents={data.myEvents}
|
referral={data.referral}
|
||||||
/>
|
points={data.points}
|
||||||
</Col>
|
myEvents={data.myEvents}
|
||||||
<Col xs={24} md={8}>
|
/>
|
||||||
<ReferralCard referral={data.referral} />
|
{data.actionCampaign && (
|
||||||
|
<ActionCampaignCard campaign={data.actionCampaign} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{data.actionCampaign && (
|
{hasRightColumn && (
|
||||||
<Col xs={24} md={data.featuredEvent ? 14 : 24}>
|
<Col xs={24} lg={12}>
|
||||||
<ActionCampaignCard campaign={data.actionCampaign} onRefresh={fetchDashboard} />
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
</Col>
|
{data.featuredEvent && (
|
||||||
)}
|
<TakeActionCard event={data.featuredEvent} />
|
||||||
{data.featuredEvent && (
|
)}
|
||||||
<Col xs={24} md={data.actionCampaign ? 10 : 24}>
|
{data.actionCampaign && (
|
||||||
<TakeActionCard event={data.featuredEvent} />
|
<ActionStepsList campaign={data.actionCampaign} onRefresh={fetchDashboard} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Middle section: 3-up ── */}
|
||||||
<Col xs={24} md={8}>
|
<Col xs={24} md={8}>
|
||||||
<TrainingList trainings={data.trainings} onRefresh={fetchDashboard} />
|
<TrainingList trainings={data.trainings} onRefresh={fetchDashboard} />
|
||||||
</Col>
|
</Col>
|
||||||
@ -126,6 +125,7 @@ export default function VolunteerDashboardPage() {
|
|||||||
<ActivityCard points={data.points} />
|
<ActivityCard points={data.points} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
{/* ── Bottom: resources ── */}
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
<ResourcesGrid resources={data.resources} />
|
<ResourcesGrid resources={data.resources} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user