feat: promo code
This commit is contained in:
parent
0ddb431536
commit
bcac2584ed
12 changed files with 1908 additions and 54 deletions
|
|
@ -13,16 +13,17 @@
|
|||
BriefcaseBusiness,
|
||||
Coins,
|
||||
ChevronsUpDownIcon,
|
||||
SparklesIcon,
|
||||
BadgeCheckIcon,
|
||||
CreditCardIcon,
|
||||
BellIcon,
|
||||
LogOutIcon,
|
||||
Wallet,
|
||||
Trophy,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
TrendingDown
|
||||
TrendingDown,
|
||||
User,
|
||||
Settings,
|
||||
Gift,
|
||||
Shield,
|
||||
Ticket
|
||||
} from 'lucide-svelte';
|
||||
import { mode, setMode } from 'mode-watcher';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
|
||||
import SignInConfirmDialog from './SignInConfirmDialog.svelte';
|
||||
import DailyRewards from './DailyRewards.svelte';
|
||||
import PromoCodeDialog from './PromoCodeDialog.svelte';
|
||||
import { signOut } from '$lib/auth-client';
|
||||
import { formatValue, getPublicUrl } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -43,13 +45,13 @@
|
|||
{ title: 'Portfolio', url: '/portfolio', icon: BriefcaseBusiness },
|
||||
{ title: 'Leaderboard', url: '/leaderboard', icon: Trophy },
|
||||
{ title: 'Create coin', url: '/coin/create', icon: Coins }
|
||||
],
|
||||
navAdmin: [{ title: 'Admin', url: '/admin', icon: ShieldAlert }]
|
||||
]
|
||||
};
|
||||
type MenuButtonProps = HTMLAttributes<HTMLAnchorElement | HTMLButtonElement>;
|
||||
|
||||
const { setOpenMobile, isMobile } = useSidebar();
|
||||
let shouldSignIn = $state(false);
|
||||
let showPromoCode = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if ($USER_DATA) {
|
||||
|
|
@ -84,9 +86,32 @@
|
|||
goto(`/coin/${coinSymbol.toLowerCase()}`);
|
||||
setOpenMobile(false);
|
||||
}
|
||||
|
||||
function handleAccountClick() {
|
||||
if ($USER_DATA) {
|
||||
goto(`/user/${$USER_DATA.id}`);
|
||||
setOpenMobile(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSettingsClick() {
|
||||
goto('/settings');
|
||||
setOpenMobile(false);
|
||||
}
|
||||
|
||||
function handleAdminClick() {
|
||||
goto('/admin');
|
||||
setOpenMobile(false);
|
||||
}
|
||||
|
||||
function handlePromoCodesClick() {
|
||||
goto('/admin/promo');
|
||||
setOpenMobile(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||
<PromoCodeDialog bind:open={showPromoCode} />
|
||||
<Sidebar.Root collapsible="offcanvas">
|
||||
<Sidebar.Header>
|
||||
<div class="flex items-center gap-1 px-2 py-2">
|
||||
|
|
@ -121,25 +146,6 @@
|
|||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
|
||||
{#if $USER_DATA?.isAdmin}
|
||||
{#each data.navAdmin as item}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton>
|
||||
{#snippet child({ props }: { props: MenuButtonProps })}
|
||||
<a
|
||||
href={item.url}
|
||||
onclick={() => handleNavClick(item.title)}
|
||||
class={`${props.class}`}
|
||||
>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton>
|
||||
{#snippet child({ props }: { props: MenuButtonProps })}
|
||||
|
|
@ -348,26 +354,38 @@
|
|||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item disabled={true}>
|
||||
<SparklesIcon />
|
||||
Upgrade to Pro
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item onclick={() => goto('/settings')}>
|
||||
<BadgeCheckIcon />
|
||||
<DropdownMenu.Item onclick={handleAccountClick}>
|
||||
<User />
|
||||
Account
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item disabled={true}>
|
||||
<CreditCardIcon />
|
||||
Billing
|
||||
<DropdownMenu.Item onclick={handleSettingsClick}>
|
||||
<Settings />
|
||||
Settings
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item disabled={true}>
|
||||
<BellIcon />
|
||||
Notifications
|
||||
<DropdownMenu.Item onclick={() => (showPromoCode = true)}>
|
||||
<Gift />
|
||||
Promo code
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{#if $USER_DATA?.isAdmin}
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item
|
||||
onclick={handleAdminClick}
|
||||
class="text-primary hover:text-primary!"
|
||||
>
|
||||
<Shield class="text-primary" />
|
||||
Admin Panel
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={handlePromoCodesClick}
|
||||
class="text-primary hover:text-primary!"
|
||||
>
|
||||
<Ticket class="text-primary" />
|
||||
Manage codes
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
{/if}
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Gift, Clock, CheckCircle, Flame } from 'lucide-svelte';
|
||||
import { Gift, Clock, CheckCircle, Flame, Loader2 } from 'lucide-svelte';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { formatTimeRemaining } from '$lib/utils';
|
||||
|
||||
interface RewardStatus {
|
||||
canClaim: boolean;
|
||||
|
|
@ -123,16 +124,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function formatTimeRemaining(timeMs: number): string {
|
||||
const hours = Math.floor(timeMs / (60 * 60 * 1000));
|
||||
const minutes = Math.floor((timeMs % (60 * 60 * 1000)) / (60 * 1000));
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
function formatCurrency(value: number): string {
|
||||
return value.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 0,
|
||||
|
|
@ -149,7 +140,7 @@
|
|||
variant={claimState === 'success' ? 'secondary' : rewardStatus?.canClaim ? 'default' : 'outline'}
|
||||
>
|
||||
{#if !rewardStatus || claimState === 'loading'}
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-b-2 border-current"></div>
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
<span>{!rewardStatus ? 'Loading...' : 'Claiming...'}</span>
|
||||
{:else if claimState === 'success'}
|
||||
<CheckCircle class="h-4 w-4" />
|
||||
|
|
|
|||
132
website/src/lib/components/self/PromoCodeDialog.svelte
Normal file
132
website/src/lib/components/self/PromoCodeDialog.svelte
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Alert, AlertDescription } from '$lib/components/ui/alert';
|
||||
import { Gift, XCircle, Loader2, CheckIcon } from 'lucide-svelte';
|
||||
|
||||
let { open = $bindable() } = $props();
|
||||
|
||||
let promoCode = $state('');
|
||||
let isVerifying = $state(false);
|
||||
let isSuccess = $state(false);
|
||||
let message = $state('');
|
||||
let hasResult = $state(false);
|
||||
|
||||
async function verifyPromoCode() {
|
||||
if (!promoCode.trim()) return;
|
||||
|
||||
isVerifying = true;
|
||||
hasResult = false;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/promo/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ code: promoCode.trim() })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
isSuccess = response.ok;
|
||||
message = response.ok ? result.message : result.error;
|
||||
hasResult = true;
|
||||
} catch (error) {
|
||||
isSuccess = false;
|
||||
message = 'Failed to verify promo code. Please try again.';
|
||||
hasResult = true;
|
||||
} finally {
|
||||
isVerifying = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
verifyPromoCode();
|
||||
}
|
||||
|
||||
function resetDialog() {
|
||||
promoCode = '';
|
||||
isSuccess = false;
|
||||
message = '';
|
||||
hasResult = false;
|
||||
isVerifying = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
resetDialog();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root
|
||||
bind:open
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) resetDialog();
|
||||
}}
|
||||
>
|
||||
<Dialog.Content class="sm:max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2">
|
||||
<Gift class="h-5 w-5" />
|
||||
Promo Code
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Enter your promo code below to redeem rewards and bonuses.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="promo-code">Promo Code</Label>
|
||||
<Input
|
||||
id="promo-code"
|
||||
bind:value={promoCode}
|
||||
placeholder="CODE..."
|
||||
disabled={isVerifying}
|
||||
class="uppercase"
|
||||
style="text-transform: uppercase;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if hasResult}
|
||||
<Alert
|
||||
variant={isSuccess ? 'default' : 'destructive'}
|
||||
class={isSuccess ? 'text-success' : ''}
|
||||
>
|
||||
{#if isSuccess}
|
||||
<CheckIcon class="h-4 w-4" />
|
||||
{:else}
|
||||
<XCircle class="h-4 w-4" />
|
||||
{/if}
|
||||
<AlertDescription>
|
||||
{message}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onclick={() => (open = false)}
|
||||
disabled={isVerifying}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!promoCode.trim() || isVerifying}>
|
||||
{#if isVerifying}
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
Verifying...
|
||||
{:else}
|
||||
Redeem Code
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
Reference in a new issue