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
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ export const GeneralWinnerCard = ({
winner,
submission,
}: GeneralWinnerCardProps) => {
const projectUrl = `/projects/${winner.submissionId}?type=submission`;

return (
<div className='flex items-center justify-between gap-4 rounded-xl border border-white/5 bg-[#0A0A0A] p-4 transition-all hover:border-white/10'>
<a
href={projectUrl}
target='_blank'
rel='noopener noreferrer'
className='flex items-center justify-between gap-4 rounded-xl border border-white/5 bg-[#0A0A0A] p-4 transition-all hover:border-white/10 hover:bg-white/2'
>
<div className='flex items-center gap-4'>
<div className='flex h-8 w-8 items-center justify-center rounded-lg bg-white/5 text-[10px] font-bold text-white/40'>
#{winner.rank}
Expand All @@ -28,6 +35,6 @@ export const GeneralWinnerCard = ({
</div>

<div className='text-primary text-xs font-bold'>{winner.prize}</div>
</div>
</a>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { HackathonWinner } from '@/lib/api/hackathons';
import { Trophy } from 'lucide-react';
import { SubmissionCardProps } from '@/types/hackathon';
import { BoundlessButton } from '@/components/buttons/BoundlessButton';
import Image from 'next/image';

interface PodiumWinnerCardProps {
Expand All @@ -13,6 +14,8 @@ export const PodiumWinnerCard = ({
winner,
submission,
}: PodiumWinnerCardProps) => {
const projectUrl = `/projects/${winner.submissionId}?type=submission`;

return (
<div className='relative w-full overflow-hidden rounded-2xl border border-white/5 bg-[#0A0A0A] p-6 transition-all hover:border-white/10'>
<div className='mb-4 flex items-center justify-between gap-4'>
Expand All @@ -35,18 +38,30 @@ export const PodiumWinnerCard = ({
{submission?.description || 'No description provided for this project.'}
</p>

<div className='flex items-center gap-3'>
<Avatar className='h-8 w-8 border border-white/10'>
<AvatarImage src={winner.participants[0]?.avatar} />
<AvatarFallback className='bg-white/5 text-[10px] font-bold text-white/60'>
{winner.participants[0]?.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className='flex flex-col'>
<span className='text-[11px] font-medium text-white/80'>
{winner.teamName || winner.participants[0]?.username}
</span>
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center gap-3'>
<Avatar className='h-8 w-8 border border-white/10'>
<AvatarImage src={winner.participants[0]?.avatar} />
<AvatarFallback className='bg-white/5 text-[10px] font-bold text-white/60'>
{winner.participants[0]?.username.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className='flex flex-col'>
<span className='text-[11px] font-medium text-white/80'>
{winner.teamName || winner.participants[0]?.username}
</span>
</div>
</div>

<a href={projectUrl} target='_blank' rel='noopener noreferrer'>
<BoundlessButton
variant='outline'
size='sm'
className='hover:bg-primary h-8 rounded-lg border-white/5 bg-white/5 px-3 text-[10px] font-bold transition-all hover:text-black'
>
View Project
</BoundlessButton>
</a>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Trophy } from 'lucide-react';
import Image from 'next/image';
import { SubmissionCardProps } from '@/types/hackathon';
import BasicAvatar from '@/components/avatars/BasicAvatar';
import { BoundlessButton } from '@/components/buttons/BoundlessButton';

interface TopWinnerCardProps {
winner: HackathonWinner;
Expand All @@ -12,6 +13,7 @@ interface TopWinnerCardProps {

export const TopWinnerCard = ({ winner, submission }: TopWinnerCardProps) => {
const bannerUrl = winner?.logo || '/images/default-project-banner.png'; // Fallback to logo or default
const projectUrl = `/projects/${winner.submissionId}?type=submission`;

return (
<div className='relative w-full overflow-hidden rounded-2xl border border-white/5 bg-[#0A0A0A] p-6 transition-all hover:border-white/10'>
Expand Down Expand Up @@ -63,36 +65,29 @@ export const TopWinnerCard = ({ winner, submission }: TopWinnerCardProps) => {
'No description provided for this project.'}
</p>

<div className='mt-auto flex items-center gap-4'>
<div className='flex -space-x-3'>
{winner.participants.map((participant, idx) => (
<BasicAvatar
key={idx}
image={participant.avatar}
name={'Participant'}
username={participant.username}
/>
// <Avatar
// key={idx}
// className='h-10 w-10 border-2 border-[#0A0A0A]'
// >
// <AvatarImage src={participant.avatar} />
// <AvatarFallback className='bg-white/5 text-xs font-bold text-white/60'>
// {participant.username.slice(0, 2).toUpperCase()}
// </AvatarFallback>
// </Avatar>
))}
<div className='mt-auto flex items-center justify-between gap-4'>
<div className='flex items-center gap-4'>
<div className='flex -space-x-3'>
{winner.participants.map((participant, idx) => (
<BasicAvatar
key={idx}
image={participant.avatar}
name={'Participant'}
username={participant.username}
/>
))}
</div>
</div>
{/* <div className='flex flex-col'>
<span className='text-[10px] font-bold tracking-wider text-white/40 uppercase'>
{winner.teamName ? 'Team members' : 'Participant'}
</span>
<span className='text-xs font-medium text-white/80'>
{winner.teamName
? winner.teamName
: winner.participants[0]?.username}
</span>
</div> */}

<a href={projectUrl} target='_blank' rel='noopener noreferrer'>
<BoundlessButton
variant='outline'
size='sm'
className='hover:bg-primary h-9 rounded-xl border-white/5 bg-white/5 px-4 text-xs font-bold transition-all hover:text-black'
>
View Project
</BoundlessButton>
</a>
</div>
</div>
</div>
Expand Down
164 changes: 98 additions & 66 deletions components/wallet/FamilyWalletDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ import {
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import type { ApiError } from '@/lib/api/api';
import { AssetIcon } from './AssetIcon';
import { cn } from '@/lib/utils';
Expand Down Expand Up @@ -88,8 +99,6 @@ export function FamilyWalletDrawer({
const [sendDestination, setSendDestination] = useState('');
const [sendCurrency, setSendCurrency] = useState('');
const [sendAmount, setSendAmount] = useState('');
const [sendMemo, setSendMemo] = useState('');
const [sendMemoRequired, setSendMemoRequired] = useState(false);
const [validateLoading, setValidateLoading] = useState(false);
const [validateResult, setValidateResult] = useState<
'idle' | 'valid' | 'invalid'
Expand Down Expand Up @@ -127,8 +136,6 @@ export function FamilyWalletDrawer({
const resetSendForm = useCallback(() => {
setSendDestination('');
setSendAmount('');
setSendMemo('');
setSendMemoRequired(false);
setValidateResult('idle');
setValidateError('');
setValidateErrorDetails([]);
Expand Down Expand Up @@ -297,24 +304,13 @@ export function FamilyWalletDrawer({
);
return;
}
if (sendMemoRequired && !sendMemo.trim()) {
setSendError('Memo is required by the recipient');
return;
}
const memoBytes = new TextEncoder().encode(sendMemo).length;
if (sendMemo && memoBytes > 28) {
setSendError('Memo must be 28 bytes or less (UTF-8)');
return;
}
setSendLoading(true);
setSendError('');
try {
await sendFunds({
destinationPublicKey: dest,
amount,
currency,
memo: sendMemo.trim() || undefined,
memoRequired: sendMemoRequired || undefined,
idempotencyKey: crypto.randomUUID(),
});
toast.success('Send submitted successfully');
Expand All @@ -332,8 +328,6 @@ export function FamilyWalletDrawer({
sendDestination,
sendCurrency,
sendAmount,
sendMemo,
sendMemoRequired,
validateResult,
balances,
refreshWallet,
Expand Down Expand Up @@ -863,6 +857,30 @@ export function FamilyWalletDrawer({
</div>

<div className='space-y-4'>
<Alert className='mx-1 border-orange-500/20 bg-orange-500/10 text-orange-600'>
<AlertCircle className='h-4 w-4 text-orange-600' />
<AlertTitle>Important Withdrawal Notice</AlertTitle>
<AlertDescription className='text-xs leading-relaxed'>
<p className='mt-1 text-orange-700 dark:text-orange-400'>
Do not withdraw directly to a Centralized Exchange
(e.g., Binance, Coinbase) wallet. This wallet does
not support memos, and your funds will be lost if
you use one. You must use a self-custodial wallet
that does not require a memo.
</p>
<p className='mt-2'>
<a
href='https://docs.boundlessfi.xyz/how-to-guides/withdraw-funds'
target='_blank'
rel='noopener noreferrer'
className='font-semibold underline underline-offset-2 hover:text-orange-800 dark:hover:text-orange-300'
>
Read our withdrawal guide here.
</a>
</p>
</AlertDescription>
</Alert>

<div className='space-y-2'>
<Label htmlFor='send-destination'>
Destination (Stellar G...)
Expand Down Expand Up @@ -983,33 +1001,6 @@ export function FamilyWalletDrawer({
})()}
</div>

<div className='space-y-2'>
<Label htmlFor='send-memo'>
Memo (optional, max 28 bytes)
</Label>
<Input
id='send-memo'
placeholder='Memo for exchange/deposit'
value={sendMemo}
onChange={e => setSendMemo(e.target.value)}
/>
<div className='flex items-center gap-2'>
<Checkbox
id='send-memo-required'
checked={sendMemoRequired}
onCheckedChange={c =>
setSendMemoRequired(c === true)
}
/>
<Label
htmlFor='send-memo-required'
className='text-muted-foreground cursor-pointer text-sm font-normal'
>
Memo required by recipient (e.g. exchange)
</Label>
</div>
</div>

{sendError && (
<Alert variant='destructive' className='mt-2'>
<AlertCircle className='h-4 w-4' />
Expand All @@ -1029,28 +1020,69 @@ export function FamilyWalletDrawer({
</Alert>
)}

<Button
className='w-full'
onClick={handleSendSubmit}
disabled={
sendLoading ||
validateResult !== 'valid' ||
!sendAmount ||
parseFloat(sendAmount) <= 0
}
>
{sendLoading ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Sending…
</>
) : (
<>
<ArrowUpRight className='mr-2 h-4 w-4' />
Send
</>
)}
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className='w-full'
disabled={
sendLoading ||
validateResult !== 'valid' ||
!sendAmount ||
parseFloat(sendAmount) <= 0
}
>
{sendLoading ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Sending…
</>
) : (
<>
<ArrowUpRight className='mr-2 h-4 w-4' />
Send
</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent className='z-100 max-w-[90vw] sm:max-w-md'>
<AlertDialogHeader>
<AlertDialogTitle>
Confirm Withdrawal
</AlertDialogTitle>
<AlertDialogDescription className='space-y-3 text-left'>
<span className='block text-sm'>
You are about to send{' '}
<strong className='text-foreground'>
{sendAmount} {sendCurrency}
</strong>{' '}
to{' '}
<strong className='text-foreground font-mono break-all'>
{sendDestination}
</strong>
.
</span>
<span className='text-destructive mt-4 block text-sm font-semibold'>
⚠️ WARNING: Do not withdraw to Centralized
Exchanges (e.g. Binance, Coinbase).
</span>
<span className='block text-sm'>
Exchanges require a memo, which this wallet does
NOT support. Doing so will result in permanent
loss of your funds.
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className='mt-4 gap-2 sm:gap-0'>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleSendSubmit}
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
>
Yes, I confirm this is a self-custodial wallet
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</motion.div>
)}
Expand Down
Loading
Loading