Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
629ed85
feat: redesign campaign info, wip campaign stats, remove unused compo…
KirillKirill Mar 13, 2026
c69d1b5
fix: address feedback from copilot
KirillKirill Mar 13, 2026
fe9cb57
feat: add a few widgets, minor styles improvements
KirillKirill Mar 19, 2026
f838bb1
fix: reinstall dependencies
KirillKirill Mar 20, 2026
e37fead
chore: minor changes of the widgets
KirillKirill Mar 20, 2026
d788b14
feat: rework join campaign flow; store all joined campaigns in context
KirillKirill Mar 25, 2026
e29636c
chore: show join button at the bottom on mobile; overlay logic if use…
KirillKirill Mar 26, 2026
768d8b1
feat: apply some new designs highlighting most important props
KirillKirill Mar 30, 2026
6c5de8b
feat: add leaderboard query logic
KirillKirill Apr 2, 2026
7e54680
feat: add cycle info section, misc styles changes
KirillKirill Apr 2, 2026
394e1e0
chore: adjust loading states for campaign details child components; m…
KirillKirill Apr 3, 2026
11759c0
fix: remove unused imports
KirillKirill Apr 6, 2026
1db74d7
chore: add new fields according to the backend changes
KirillKirill Apr 7, 2026
02177a8
fix: revert totalParticipants
KirillKirill Apr 7, 2026
8b272e9
chore: minor ui changes, add the total generated widget
KirillKirill Apr 8, 2026
fc006e8
feat: complete user widget on details section; utilize cancellation_a…
KirillKirill Apr 8, 2026
4b5234d
fix: show user rank, if user's presented in leaderboard
KirillKirill Apr 8, 2026
84fa347
[Campaign Launcher UI] Leaderboard (#835)
KirillKirill Apr 8, 2026
fc31671
feat: add the cancel campaign logic
KirillKirill Apr 9, 2026
9242378
feat: add joinedAt block
KirillKirill Apr 9, 2026
a42483e
feat: add campaign results section; a minor ui adjustments
KirillKirill Apr 9, 2026
c288dde
feat: special treatment of threshold campaign's cycle info
KirillKirill Apr 9, 2026
92e4e11
fix: a minor cosmetic change
KirillKirill Apr 9, 2026
4c7510f
chore: address feedback
KirillKirill Apr 10, 2026
6e7a8f5
refactor: normalize address and remove toLowerCase; improve the cycle…
KirillKirill Apr 10, 2026
bb998a1
fix: merge conflict
KirillKirill Apr 13, 2026
fe1bb50
chore: add tooltip to campaign start_date and end_date
KirillKirill Apr 13, 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.4",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.4",
"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
257 changes: 195 additions & 62 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;
isJoined: boolean;
joinedAt: string | undefined;
isJoinStatusLoading: boolean;
};

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

const CampaignInfo: FC<Props> = ({
campaign,
isCampaignLoading,
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 }}>
<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={1}>
<CalendarIcon />
<CustomTooltip
arrow
placement="top"
title={formatTime(campaign.start_date)}
>
<Typography variant="subtitle2" borderBottom="1px dashed">
<Typography
color="white"
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
color="white"
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 && (
<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