Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
43da91e
feat: redesign campaign info, wip campaign stats, remove unused compo…
KirillKirill Mar 13, 2026
a26cc4e
fix: address feedback from copilot
KirillKirill Mar 13, 2026
445e632
feat: add a few widgets, minor styles improvements
KirillKirill Mar 19, 2026
be33b74
fix: reinstall dependencies
KirillKirill Mar 20, 2026
8cd6d62
chore: minor changes of the widgets
KirillKirill Mar 20, 2026
6d4cf3f
feat: rework join campaign flow; store all joined campaigns in context
KirillKirill Mar 25, 2026
63591d2
chore: show join button at the bottom on mobile; overlay logic if use…
KirillKirill Mar 26, 2026
318282c
feat: apply some new designs highlighting most important props
KirillKirill Mar 30, 2026
68237d2
feat: add leaderboard query logic
KirillKirill Apr 2, 2026
c0bfcf0
feat: add cycle info section, misc styles changes
KirillKirill Apr 2, 2026
e9f141d
chore: adjust loading states for campaign details child components; m…
KirillKirill Apr 3, 2026
49fdf69
fix: remove unused imports
KirillKirill Apr 6, 2026
3060da2
chore: add new fields according to the backend changes
KirillKirill Apr 7, 2026
990121a
fix: revert totalParticipants
KirillKirill Apr 7, 2026
3f7a3b1
chore: minor ui changes, add the total generated widget
KirillKirill Apr 8, 2026
0cef570
feat: complete user widget on details section; utilize cancellation_a…
KirillKirill Apr 8, 2026
a95970f
fix: show user rank, if user's presented in leaderboard
KirillKirill Apr 8, 2026
8996639
[Campaign Launcher UI] Leaderboard (#835)
KirillKirill Apr 8, 2026
5ace5dd
feat: add the cancel campaign logic
KirillKirill Apr 9, 2026
663677b
feat: add joinedAt block
KirillKirill Apr 9, 2026
438820c
feat: add campaign results section; a minor ui adjustments
KirillKirill Apr 9, 2026
3eb98b1
feat: special treatment of threshold campaign's cycle info
KirillKirill Apr 9, 2026
ec4c79e
fix: a minor cosmetic change
KirillKirill Apr 9, 2026
aaa0607
chore: address feedback
KirillKirill Apr 10, 2026
914715d
refactor: normalize address and remove toLowerCase; improve the cycle…
KirillKirill Apr 10, 2026
027aa85
fix: merge conflict
KirillKirill Apr 13, 2026
fbc6106
chore: add tooltip to campaign start_date and end_date
KirillKirill Apr 13, 2026
74b5d3a
feat: add estimated reward to the leaderboard; a minor styles changes
KirillKirill Apr 16, 2026
0203cb7
feat: add tooltip for the individual reward widget
KirillKirill Apr 16, 2026
8fd7f92
chore: manually change campaign status after cancellation request
KirillKirill Apr 20, 2026
c892d9c
fix: minor change
KirillKirill Apr 20, 2026
43ce0f6
fix: simplify counting of total cycles
KirillKirill Apr 20, 2026
d99ed3f
fix: remove dollar sign from target in the leaderboard
KirillKirill Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions campaign-launcher/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,11 @@
"@tanstack/react-query": "^5.90.21",
"@walletconnect/ethereum-provider": "^2.23.5",
"axios": "^1.13.2",
"chart.js": "^4.5.1",
"chartjs-plugin-annotation": "^3.1.0",
"dayjs": "^1.11.19",
"ethers": "~6.16.0",
"jwt-decode": "^4.0.0",
"notistack": "^3.0.2",
"react": "^19.2.5",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.5",
"react-hook-form": "^7.68.0",
"react-number-format": "^5.4.4",
Expand Down
11 changes: 11 additions & 0 deletions campaign-launcher/client/src/api/recordingApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
UserProgress,
CheckCampaignJoinStatusResponse,
JoinedCampaignsResponse,
LeaderboardResponseDto,
} from '@/types';
import { HttpClient, HttpError } from '@/utils/HttpClient';
import type { TokenData, TokenManager } from '@/utils/TokenManager';
Expand Down Expand Up @@ -219,4 +220,14 @@ export class RecordingApiClient extends HttpClient {

return response || null;
}

async getLeaderboard(
chain_id: ChainId,
campaign_address: string
): Promise<LeaderboardResponseDto> {
const response = await this.get<LeaderboardResponseDto>(
`/campaigns/${chain_id}-${campaign_address}/leaderboard`
);
return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ const AddressLink: FC<Props> = ({ address, chainId, size }) => {
size === 'small' ? '12px' : size === 'medium' ? '14px' : '16px',
color: 'text.primary',
textDecoration: 'underline',
textDecorationStyle: 'dotted',
textDecorationThickness: '12%',
fontWeight: 600,
}}
>
Expand Down
259 changes: 196 additions & 63 deletions campaign-launcher/client/src/components/CampaignInfo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,136 +1,269 @@
import { useState, type FC } from 'react';
import { type FC } from 'react';

import { Box, Button, Skeleton, Stack, Typography } from '@mui/material';
import {
Box,
Divider as MuiDivider,
Skeleton,
Stack,
styled,
Typography,
} from '@mui/material';

import CampaignAddress from '@/components/CampaignAddress';
import CampaignStatusLabel from '@/components/CampaignStatusLabel';
import CampaignTypeLabel from '@/components/CampaignTypeLabel';
import CustomTooltip from '@/components/CustomTooltip';
import JoinCampaignButton from '@/components/JoinCampaignButton';
import ChartModal from '@/components/modals/ChartModal';
import { useIsMobile } from '@/hooks/useBreakpoints';
import { CalendarIcon } from '@/icons';
import type { CampaignDetails, CampaignJoinStatus } from '@/types';
import { useActiveAccount } from '@/providers/ActiveAccountProvider';
import type { CampaignDetails } from '@/types';
import { getChainIcon, getNetworkName } from '@/utils';
import dayjs from '@/utils/dayjs';

const formatDate = (dateString: string): string => {
return dayjs(dateString).format('D MMM YYYY');
return dayjs(dateString).format('Do MMM YYYY');
};

const formatTime = (dateString: string): string => {
const date = dayjs(dateString);
return date.format('HH:mm [GMT]Z');
};

const formatJoinTime = (dateString: string): string => {
return dayjs(dateString).format('HH:mm');
};

const DividerStyled = styled(MuiDivider)({
borderColor: 'rgba(255, 255, 255, 0.3)',
height: 16,
alignSelf: 'center',
});

type Props = {
campaign: CampaignDetails | null | undefined;
isCampaignLoading: boolean;
joinStatus?: CampaignJoinStatus;
joinedAt?: string;
isOngoingCampaign: boolean;
isJoined: boolean;
joinedAt: string | undefined;
isJoinStatusLoading: boolean;
};

const CampaignInfo: FC<Props> = ({ campaign, isCampaignLoading }) => {
const [isChartModalOpen, setIsChartModalOpen] = useState(false);

const CampaignInfo: FC<Props> = ({
campaign,
isCampaignLoading,
isOngoingCampaign,
isJoined,
joinedAt,
isJoinStatusLoading,
}) => {
const isMobile = useIsMobile();
const { activeAddress } = useActiveAccount();

const isHosted = campaign?.launcher === activeAddress;

if (isCampaignLoading) {
if (!isMobile) return null;
if (isMobile) {
return (
<Stack mx={-2} px={2} pb={4} gap={2} borderBottom="1px solid #473C74">
<Skeleton variant="text" width="100%" height={32} />
<Skeleton variant="text" width="100%" height={48} />
{isJoined && (
<Skeleton variant="rectangular" width="100%" height={39} />
)}
</Stack>
);
}

return (
<Stack gap={3} width="100%">
<Skeleton variant="text" width="100%" height={36} />
<Skeleton variant="text" width="100%" height={24} />
<Skeleton variant="text" width="100%" height={24} />
<Stack gap={3.5}>
<Skeleton variant="text" width="100%" height={42} />
<Skeleton variant="text" width="100%" height={32} />
{isJoined && (
<Skeleton variant="rectangular" width="100%" height={39} />
)}
</Stack>
);
}

if (!campaign) return null;

const oracleFee =
campaign.exchange_oracle_fee_percent +
campaign.recording_oracle_fee_percent +
campaign.reputation_oracle_fee_percent;

return (
<Box
display="flex"
alignItems={{ xs: 'flex-start', md: 'center' }}
flexDirection={{ xs: 'column', md: 'row' }}
height={{ xs: 'auto', md: '40px' }}
gap={{ xs: 3, md: 4 }}
width="100%"
<Stack
mx={{ xs: -2, md: 0 }}
px={{ xs: 2, md: 0 }}
pb={{ xs: 4, md: 0 }}
gap={{ xs: 2, md: 3.5 }}
borderBottom={{ xs: '1px solid #473C74', md: 'none' }}
>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
width={{ xs: '100%', md: 'auto' }}
gap={1}
order={1}
gap={2}
height={{ xs: 'auto', md: '42px' }}
>
<CampaignTypeLabel campaignType={campaign.type} />
<CampaignStatusLabel
campaignStatus={campaign.status}
startDate={campaign.start_date}
endDate={campaign.end_date}
/>
{isMobile && <JoinCampaignButton campaign={campaign} />}
<Typography
variant="h6"
color="white"
fontWeight={{ xs: 500, md: 600 }}
>
Campaign Details
</Typography>
<Box display="flex" alignItems="center" gap={3}>
<CampaignStatusLabel
campaignStatus={campaign.status}
startDate={campaign.start_date}
endDate={campaign.end_date}
/>
{!isMobile && <JoinCampaignButton campaign={campaign} />}
</Box>
</Box>
<Box order={{ xs: 3, md: 2 }}>
<Box
display="flex"
flexWrap="wrap"
alignItems="center"
columnGap={1.5}
rowGap={1}
>
<Box display="flex" alignItems="center" gap={1}>
<Box
display="flex"
alignItems="center"
justifyContent="center"
width={{ xs: 24, md: 32 }}
height={{ xs: 24, md: 32 }}
borderRadius="100%"
bgcolor="#3a2e6f"
sx={{ '& > svg': { fontSize: { xs: '12px', md: '16px' } } }}
>
{getChainIcon(campaign.chain_id)}
</Box>
<Typography
color={isMobile ? 'text.primary' : 'white'}
fontSize={{ xs: 14, md: 20 }}
fontWeight={500}
lineHeight="100%"
letterSpacing={0}
textTransform="uppercase"
>
{getNetworkName(campaign.chain_id)?.slice(0, 3)}
</Typography>
</Box>
<DividerStyled orientation="vertical" flexItem />
<CampaignAddress
address={campaign.address}
chainId={campaign.chain_id}
size={isMobile ? 'medium' : 'large'}
withCopy
/>
</Box>
<Box display="flex" alignItems="center" gap={3} order={{ xs: 2, md: 3 }}>
<Box display="flex" alignItems="center" gap={1}>
<CalendarIcon />
<DividerStyled orientation="vertical" flexItem />
{(isJoined || isHosted) && (
<>
<Typography
color="error.main"
fontSize={{ xs: 14, md: 20 }}
fontWeight={500}
lineHeight="100%"
letterSpacing={0}
textTransform="uppercase"
>
{isJoined ? 'Joined' : 'Hosted'}
</Typography>
<DividerStyled orientation="vertical" flexItem />
</>
)}
<Box display="flex" alignItems="center" gap={0.75}>
<CustomTooltip
arrow
placement="top"
title={formatTime(campaign.start_date)}
>
<Typography variant="subtitle2" borderBottom="1px dashed">
<Typography
fontSize={{ xs: 14, md: 20 }}
fontWeight={500}
lineHeight="100%"
sx={{
textDecoration: 'underline',
textDecorationStyle: 'dotted',
textDecorationThickness: '12%',
}}
>
{formatDate(campaign.start_date)}
</Typography>
</CustomTooltip>
<Typography component="span" variant="subtitle2">
-
<Typography
component="span"
color="error.main"
fontSize={{ xs: 14, md: 20 }}
fontWeight={500}
lineHeight="100%"
>
&gt;
</Typography>
<CustomTooltip
arrow
placement="top"
title={formatTime(campaign.end_date)}
>
<Typography variant="subtitle2" borderBottom="1px dashed">
<Typography
fontSize={{ xs: 14, md: 20 }}
fontWeight={500}
lineHeight="100%"
sx={{
textDecoration: 'underline',
textDecorationStyle: 'dotted',
textDecorationThickness: '12%',
}}
>
{formatDate(campaign.end_date)}
</Typography>
</CustomTooltip>
</Box>
<CustomTooltip
arrow
title={getNetworkName(campaign.chain_id) || 'Unknown Network'}
placement="top"
<DividerStyled orientation="vertical" flexItem />
<Typography
fontSize={{ xs: 14, md: 20 }}
fontWeight={500}
lineHeight="100%"
letterSpacing={0}
>
<Box display="flex">{getChainIcon(campaign.chain_id)}</Box>
</CustomTooltip>
{oracleFee}% Oracle fees
</Typography>
</Box>
{!isMobile && (
<Box ml={{ xs: 0, md: 'auto' }} order={4}>
<Button
variant="outlined"
size="medium"
onClick={() => setIsChartModalOpen(true)}
{!isJoinStatusLoading && joinedAt && isOngoingCampaign && (
<Box
display="flex"
alignItems="center"
gap={2}
justifyContent="space-between"
px={2}
py={1}
bgcolor="rgba(212, 207, 255, 0.15)"
borderRadius="8px"
border="1px solid rgba(255, 255, 255, 0.07)"
>
<Typography
color="#a496c2"
fontSize={12}
fontWeight={600}
lineHeight="150%"
letterSpacing="1.5px"
textTransform="uppercase"
>
Paid Amount Chart
</Button>
<ChartModal
open={isChartModalOpen}
onClose={() => setIsChartModalOpen(false)}
campaign={campaign}
/>
Joined at
</Typography>
<Typography fontSize={14} fontWeight={500} lineHeight="150%">
{' '}
{formatDate(joinedAt)}
{', '}
{formatJoinTime(joinedAt)}
</Typography>
</Box>
)}
</Box>
</Stack>
);
};

Expand Down
Loading