You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

242 lines
7.7 KiB

'use client'
import { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import {
CircleDollarSign,
Github,
Heart,
MessageSquare,
Star,
Coins,
Check,
Copy,
} from 'lucide-react'
import Link from 'next/link'
import { ExternalLinks } from '@/config/external-links'
import { CRYPTO_ADDRESSES } from '@/lib/constants'
import Image from 'next/image'
import { ApiEndpoints } from '@/config/api'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { useQuery } from '@tanstack/react-query'
// Add constants for localStorage and timing
const STORAGE_KEYS = {
LAST_SHOWN: 'contribute_modal_last_shown',
HAS_CONTRIBUTED: 'contribute_modal_has_contributed',
}
const SHOW_INTERVAL = 1 * 24 * 60 * 60 * 1000 // 1 days in milliseconds
const RANDOM_CHANCE = 0.3 // 30% chance to show when eligible
export function ContributeModal() {
const [isOpen, setIsOpen] = useState(false)
const [cryptoOpen, setCryptoOpen] = useState(false)
const [copiedAddress, setCopiedAddress] = useState('')
const copyToClipboard = (address: string) => {
navigator.clipboard.writeText(address)
setCopiedAddress(address)
setTimeout(() => setCopiedAddress(''), 3000)
}
const {
data: currentPlan,
isLoading: isLoadingPlan,
error: planError,
} = useQuery({
queryKey: ['currentPlan'],
queryFn: () =>
httpBrowserClient
.get(ApiEndpoints.billing.currentPlan())
.then((res) => res.data),
})
useEffect(() => {
const checkAndShowModal = () => {
if (isLoadingPlan) return
if (planError) return
if (currentPlan?.name?.toLowerCase() !== 'free') {
return
}
const hasContributed =
localStorage.getItem(STORAGE_KEYS.HAS_CONTRIBUTED) === 'true'
if (hasContributed) return
setIsOpen(true)
return;
const lastShown = localStorage.getItem(STORAGE_KEYS.LAST_SHOWN)
const now = Date.now()
if (!lastShown || now - parseInt(lastShown) >= SHOW_INTERVAL) {
if (Math.random() < RANDOM_CHANCE) {
setIsOpen(true)
localStorage.setItem(STORAGE_KEYS.LAST_SHOWN, now.toString())
}
}
}
checkAndShowModal()
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
checkAndShowModal()
}
})
}, [currentPlan?.name, isLoadingPlan, planError])
const handleContributed = () => {
localStorage.setItem(STORAGE_KEYS.HAS_CONTRIBUTED, 'true')
setIsOpen(false)
}
return (
<>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className='max-w-md max-h-[90vh] overflow-y-auto flex flex-col'>
<DialogHeader>
<DialogTitle>Support textbee.dev</DialogTitle>
<DialogDescription>
Your contribution helps keep this project alive and growing.
</DialogDescription>
</DialogHeader>
<div className='space-y-6 flex-1'>
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2 text-lg'>
<CircleDollarSign className='h-5 w-5' />
Financial Support
</CardTitle>
</CardHeader>
<CardContent>
<div className='space-y-4'>
<Button className='w-full' asChild>
<Link href={ExternalLinks.patreon} target='_blank'>
<Heart className='mr-2 h-4 w-4' />
Monthly Support on Patreon
</Link>
</Button>
<Button variant='outline' className='w-full' asChild>
<Link href={ExternalLinks.polar} target='_blank'>
<Star className='mr-2 h-4 w-4' />
One-time Donation via Polar.sh
</Link>
</Button>
<Button
variant='outline'
className='w-full'
onClick={() => setCryptoOpen(true)}
>
<Coins className='mr-2 h-4 w-4' />
Donate Cryptocurrency
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2 text-lg'>
<Github className='h-5 w-5' />
Code Contributions
</CardTitle>
</CardHeader>
<CardContent>
<div className='flex flex-wrap gap-4'>
<Button asChild>
<Link href={ExternalLinks.github} target='_blank'>
<Star className='mr-2 h-4 w-4' />
Star on GitHub
</Link>
</Button>
<Button variant='outline' asChild>
<Link
href={`${ExternalLinks.github}/issues/new`}
target='_blank'
>
<MessageSquare className='mr-2 h-4 w-4' />
Report Issue
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
<DialogFooter className='sticky bottom-0 bg-background pt-4 border-t mt-auto'>
<Button variant='ghost' onClick={handleContributed} asChild>
<Link href='#'>I&apos;ve already donated</Link>
</Button>
<Button variant='secondary' onClick={() => setIsOpen(false)}>
Remind me later
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={cryptoOpen} onOpenChange={setCryptoOpen}>
<DialogContent className='sm:max-w-[425px]'>
<DialogHeader>
<DialogTitle>Donate Cryptocurrency</DialogTitle>
</DialogHeader>
<div className='grid gap-2'>
{CRYPTO_ADDRESSES.map((wallet, index) => (
<div
key={index}
className='flex gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors'
>
<Image
src={wallet.icon}
alt={wallet.name}
width={32}
height={32}
className='shrink-0 mt-1'
/>
<div className='min-w-0 flex-1'>
<div className='flex items-center justify-between'>
<div>
<p className='font-medium text-sm'>{wallet.name}</p>
<p className='text-xs text-muted-foreground'>{wallet.network}</p>
</div>
<Button
variant='ghost'
size='sm'
className='h-8 px-2 shrink-0'
onClick={() => copyToClipboard(wallet.address)}
>
{copiedAddress === wallet.address ? (
<Check className='h-3.5 w-3.5' />
) : (
<Copy className='h-3.5 w-3.5' />
)}
</Button>
</div>
<p
className='text-xs text-muted-foreground break-all'
title={wallet.address}
>
{wallet.address}
</p>
</div>
</div>
))}
</div>
</DialogContent>
</Dialog>
</>
)
}