Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions apps/admin/src/components/season-config-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const SeasonConfigSchema = Schema.Struct({
}),
),
enableAutomaticRefill: Schema.Boolean,
claimingEnabled: Schema.Boolean,
});

type SeasonConfig = typeof SeasonConfigSchema.Type;
Expand All @@ -73,6 +74,7 @@ const FormValuesSchema = Schema.Struct({
rewardsResourceAddress: Schema.String,
adminBadgeResourceAddress: Schema.String,
enableAutomaticRefill: Schema.Boolean,
claimingEnabled: Schema.Boolean,
});

type FormValues = typeof FormValuesSchema.Type;
Expand Down Expand Up @@ -113,6 +115,7 @@ const ConfigToFormValuesSchema = Schema.transform(
Option.getOrElse(() => ''),
),
enableAutomaticRefill: config.enableAutomaticRefill,
claimingEnabled: config.claimingEnabled,
}),
encode: (formValues) => ({
seasonRewardComponentAddress:
Expand All @@ -139,6 +142,7 @@ const ConfigToFormValuesSchema = Schema.transform(
}
: null,
enableAutomaticRefill: formValues.enableAutomaticRefill,
claimingEnabled: formValues.claimingEnabled,
}),
},
);
Expand Down Expand Up @@ -239,6 +243,7 @@ const SeasonConfigFormContent = ({
: null,
adminBadge,
enableAutomaticRefill: value.enableAutomaticRefill,
claimingEnabled: value.claimingEnabled,
},
});
},
Expand Down Expand Up @@ -484,6 +489,31 @@ const SeasonConfigFormContent = ({
</form.Field>
</div>

<div className="space-y-4 rounded-lg border p-4">
<h4 className="font-medium text-sm">Claiming</h4>

<form.Field name="claimingEnabled">
{(field) => (
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor={field.name}>Claiming Enabled</Label>
<p className="text-muted-foreground text-sm">
When disabled, users will no longer be able to claim
rewards for this season. The frontend will show that the
claiming period has ended.
</p>
</div>
<Switch
id={field.name}
checked={field.state.value}
onCheckedChange={(checked) => field.handleChange(checked)}
disabled={isSubmitting}
/>
</div>
)}
</form.Field>
</div>

<KillSwitchSection seasonId={seasonId} />

<div className="flex justify-end">
Expand Down
77 changes: 52 additions & 25 deletions apps/incentives/src/app/dashboard/season-reward/[seasonId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { TRPCClientError } from '@trpc/client';
import BigNumber from 'bignumber.js';
import { Array as A, Option, pipe } from 'effect';
import { AnimatePresence } from 'framer-motion';
import { Clock } from 'lucide-react';
import { Ban, Clock } from 'lucide-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { type Amount, SeasonId } from 'shared/brandedTypes';
import { toast } from 'sonner';
import { Card } from '~/components/ui/card';
import { EmptyState } from '~/components/ui/empty-state';
import { Skeleton } from '~/components/ui/skeleton';
import { useClaimingEnabled } from '~/lib/hooks/useClaimingEnabled';
import { api, type RouterOutputs } from '~/trpc/react';
import { SeasonRewardCard } from '../components/season-reward-card';
import { SlideIn } from '../helpers/slideIn';
Expand All @@ -35,6 +37,9 @@ export default function SeasonRewardDetailPage() {
api.season.getSeasons.useQuery();
const brandedSeasonId = SeasonId.make(seasonId);

const { claimingEnabled, isLoading: claimingStatusLoading } =
useClaimingEnabled(seasonId);

// Check if vester component is properly configured
const {
data: vesterInfo,
Expand Down Expand Up @@ -213,31 +218,52 @@ export default function SeasonRewardDetailPage() {
className="min-w-0 flex flex-col gap-2 overflow-hidden p-6 sm:col-span-5"
>
<div className="flex flex-col gap-4">
<AnimatePresence mode="wait">
{!selectedAccount ? (
<SlideIn key="account-selection">
<SelectAccount
seasonName={season?.name}
onSelectAccount={setSelectedAccount}
hasPendingClaim={hasPendingClaim || isAwaitingClaim}
isFullyClaimed={isFullyClaimed}
lastClaimAt={lastClaimAt}
/>
</SlideIn>
) : (
<SlideIn key="claim-form">
<RequestClaimForm
seasonName={season?.name}
selectedAccount={selectedAccount}
onClearAccount={() => setSelectedAccount(undefined)}
availableAmount={remainingAmount}
onRequestClaim={handleClaimSeasonReward}
/>
</SlideIn>
)}
</AnimatePresence>
{claimingStatusLoading ? (
<div className="space-y-4 py-4">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</div>
) : !claimingEnabled ? (
<div className="flex flex-col items-center gap-3 py-8 text-center">
<Ban className="h-10 w-10 text-white/40" />
<div>
<p className="font-medium text-lg text-white">
Claiming Period Ended
</p>
<p className="mt-1 text-sm text-white/60">
The claiming period for this season has ended. Rewards can
no longer be claimed.
</p>
</div>
</div>
) : (
<AnimatePresence mode="wait">
{!selectedAccount ? (
<SlideIn key="account-selection">
<SelectAccount
seasonName={season?.name}
onSelectAccount={setSelectedAccount}
hasPendingClaim={hasPendingClaim || isAwaitingClaim}
isFullyClaimed={isFullyClaimed}
lastClaimAt={lastClaimAt}
/>
</SlideIn>
) : (
<SlideIn key="claim-form">
<RequestClaimForm
seasonName={season?.name}
selectedAccount={selectedAccount}
onClearAccount={() => setSelectedAccount(undefined)}
availableAmount={remainingAmount}
onRequestClaim={handleClaimSeasonReward}
/>
</SlideIn>
)}
</AnimatePresence>
)}

{/* Show locker claim section if there are tokens in locker */}
{/* Locker claim is independent of claiming status — users can always withdraw tokens already in their locker */}
{lockerBalances &&
lockerBalances.length > 0 &&
vesterInfo?.lockerAddress &&
Expand Down Expand Up @@ -268,6 +294,7 @@ export default function SeasonRewardDetailPage() {
Option.getOrElse(() => '0'),
)}
claims={claimsList}
claimingEnabled={claimingEnabled}
/>
<ClaimTransactionList
claims={claimsList}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import BigNumber from 'bignumber.js';
import { CheckCircle, Clock, Coins, TrendingUp, XCircle } from 'lucide-react';
import {
Ban,
CheckCircle,
Clock,
Coins,
TrendingUp,
XCircle,
} from 'lucide-react';
import Link from 'next/link';
import { Badge } from '~/components/ui/badge';
import { Button } from '~/components/ui/button';
Expand All @@ -17,18 +24,20 @@ type VesterInfo = {
maturityValuePerUnit: string;
};

type SeasonRewardCardProps = {
export type SeasonRewardCardProps = {
seasonId: string;
seasonName: string;
amount: string;
claims: Claim[];
onClaim?: () => void;
showRedeem?: boolean;
hasReward?: boolean;
claimingEnabled?: boolean;
vesterInfo?: VesterInfo;
};

type ClaimStatus = 'unclaimed' | 'partial' | 'claimed' | 'pending';
type DisplayStatus = ClaimStatus | 'claim-period-ended';

const getClaimStatus = (
amount: string,
Expand Down Expand Up @@ -56,8 +65,17 @@ const getClaimStatus = (
return { status: 'unclaimed', claimedAmount };
};

const ClaimStatusBadge = ({ status }: { status: ClaimStatus }) => {
const config = {
const ClaimStatusBadge = ({
status,
claimingEnabled = true,
}: {
status: ClaimStatus;
claimingEnabled?: boolean;
}) => {
const config: Record<
DisplayStatus,
{ icon: typeof Ban; label: string; className: string }
> = {
unclaimed: {
icon: XCircle,
label: 'Unclaimed',
Expand All @@ -78,9 +96,16 @@ const ClaimStatusBadge = ({ status }: { status: ClaimStatus }) => {
label: 'Claimed',
className: 'bg-green-500/20 text-green-200 border-green-500/30',
},
'claim-period-ended': {
icon: Ban,
label: 'Closed',
className: 'bg-white/5 text-white/40 border-white/10',
},
};

const { icon: Icon, label, className } = config[status];
const displayKey: DisplayStatus =
!claimingEnabled && status !== 'claimed' ? 'claim-period-ended' : status;
const { icon: Icon, label, className } = config[displayKey];

return (
<Badge
Expand All @@ -101,6 +126,7 @@ export const SeasonRewardCard = ({
onClaim,
showRedeem = false,
hasReward = true,
claimingEnabled = true,
vesterInfo,
}: SeasonRewardCardProps) => {
const { status, claimedAmount } = getClaimStatus(amount, claims);
Expand All @@ -127,7 +153,10 @@ export const SeasonRewardCard = ({
{seasonName}
</span>
{hasReward ? (
<ClaimStatusBadge status={displayStatus} />
<ClaimStatusBadge
status={displayStatus}
claimingEnabled={claimingEnabled}
/>
) : (
<Badge
variant="outline"
Expand Down Expand Up @@ -169,30 +198,37 @@ export const SeasonRewardCard = ({
)}
</div>

<div className="grid grid-cols-2 gap-4 rounded-lg bg-white/5 p-3">
<div>
<div className="text-white/60 text-xs">Claimed</div>
<div
className={cn(
'font-semibold',
hasReward ? 'text-green-400' : 'text-white/40',
)}
>
{hasReward ? formatAmount(claimedAmount.toString()) : '—'}
</div>
{!claimingEnabled && hasReward && status !== 'claimed' ? (
<div className="flex items-center gap-2 rounded-lg bg-white/5 p-3 text-white/60 text-sm">
<Ban className="h-4 w-4 shrink-0" />
<span>The claiming period for this season has ended.</span>
</div>
<div>
<div className="text-white/60 text-xs">Claimable</div>
<div
className={cn(
'font-semibold',
hasReward ? 'text-white' : 'text-white/40',
)}
>
{hasReward ? formatAmount(remainingAmount.toString()) : '—'}
) : (
<div className="grid grid-cols-2 gap-4 rounded-lg bg-white/5 p-3">
<div>
<div className="text-white/60 text-xs">Claimed</div>
<div
className={cn(
'font-semibold',
hasReward ? 'text-green-400' : 'text-white/40',
)}
>
{hasReward ? formatAmount(claimedAmount.toString()) : '—'}
</div>
</div>
<div>
<div className="text-white/60 text-xs">Claimable</div>
<div
className={cn(
'font-semibold',
hasReward ? 'text-white' : 'text-white/40',
)}
>
{hasReward ? formatAmount(remainingAmount.toString()) : '—'}
</div>
</div>
</div>
</div>
)}

<div className="flex gap-2">
{onClaim !== undefined && (
Expand Down
Loading
Loading