mirror of
https://github.com/vernu/textbee.git
synced 2026-02-20 07:34:00 -05:00
chore(web): add contribute page
This commit is contained in:
290
web/app/(app)/contribute/page.tsx
Normal file
290
web/app/(app)/contribute/page.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Bitcoin,
|
||||
CircleDollarSign,
|
||||
Copy,
|
||||
Github,
|
||||
Heart,
|
||||
MessageSquare,
|
||||
Star,
|
||||
Wallet,
|
||||
Shield,
|
||||
Coins,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ExternalLinks } from '@/config/external-links'
|
||||
import { useToast } from '@/hooks/use-toast'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
const cryptoWallets = [
|
||||
{
|
||||
name: 'Bitcoin (BTC)',
|
||||
address: 'bc1qhffsnhp8ynqy6xvh982cu0x5w7vguuum3nqae9',
|
||||
network: 'Bitcoin',
|
||||
},
|
||||
{
|
||||
name: 'Ethereum (ETH)',
|
||||
address: '0xDB8560a42bdaa42C58462C6b2ee5A7D36F1c1f2a',
|
||||
network: 'Ethereum (ERC20)',
|
||||
},
|
||||
{
|
||||
name: 'Tether (USDT)',
|
||||
address: '0xDB8560a42bdaa42C58462C6b2ee5A7D36F1c1f2a',
|
||||
network: 'Ethereum (ERC20)',
|
||||
},
|
||||
// {
|
||||
// name: 'Tether (USDT)',
|
||||
// address: 'TD6txzY61D6EgnVfMLPsqKhYfyV5iHrbkw',
|
||||
// network: 'Tron (TRC20)',
|
||||
// },
|
||||
{
|
||||
name: 'Monero (XMR)',
|
||||
address:
|
||||
'856J5eHJM7bgBhkc51oCuMYUGKvUvF1zwAWrQsqwuH1shG9qnX4YkoZbMmhCPep1JragY2W1hpzAnDda6BXvCgZxUJhUyTg',
|
||||
network: 'Monero (XMR)',
|
||||
},
|
||||
]
|
||||
|
||||
export default function ContributePage() {
|
||||
const { toast } = useToast()
|
||||
|
||||
const handleCopy = (text: string, type: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
toast({
|
||||
title: `${type} address copied to clipboard`,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='min-h-screen p-4 md:p-8 space-y-8'>
|
||||
<div className='text-center space-y-4'>
|
||||
<h1 className='text-4xl font-bold'>Support TextBee</h1>
|
||||
<p className='text-muted-foreground max-w-2xl mx-auto'>
|
||||
Your contribution, whether financial or through code, helps keep this
|
||||
project alive and growing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='space-y-6 max-w-5xl mx-auto'>
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<CircleDollarSign className='h-5 w-5' />
|
||||
Financial Support
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Help sustain TextBee's development through financial
|
||||
contributions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='grid gap-6 md:grid-cols-2'>
|
||||
<div className='space-y-6'>
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg'>Monthly Support</CardTitle>
|
||||
<CardDescription>
|
||||
Become a patron and support us monthly
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className='w-full' asChild>
|
||||
<Link href={ExternalLinks.patreon} target='_blank'>
|
||||
<Heart className='mr-2 h-4 w-4' />
|
||||
Support on Patreon
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className='space-y-6'>
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg'>One-time Support</CardTitle>
|
||||
<CardDescription>
|
||||
Make a one-time contribution
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button variant='outline' className='w-full' asChild>
|
||||
<Link href={ExternalLinks.polar} target='_blank'>
|
||||
<Heart className='mr-2 h-4 w-4' />
|
||||
Donate on Polar
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className='h-full overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg'>Crypto Donations</CardTitle>
|
||||
<CardDescription>
|
||||
Support us with cryptocurrency
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className='w-full' variant='outline'>
|
||||
<Wallet className='mr-2 h-4 w-4' />
|
||||
View Crypto Addresses
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='max-w-md'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Cryptocurrency Donation Addresses
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='space-y-4'>
|
||||
{cryptoWallets.map((wallet, index) => (
|
||||
<div key={index} className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='flex items-center gap-2'>
|
||||
{wallet.name.includes('Bitcoin') ? (
|
||||
<Bitcoin className='h-4 w-4' />
|
||||
) : wallet.name.includes('Ethereum') ? (
|
||||
<Coins className='h-4 w-4' />
|
||||
) : (
|
||||
<Wallet className='h-4 w-4' />
|
||||
)}{' '}
|
||||
{wallet.name}
|
||||
</span>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() =>
|
||||
handleCopy(wallet.address, wallet.name)
|
||||
}
|
||||
>
|
||||
<Copy className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
<code className='text-xs block bg-muted p-2 rounded break-all whitespace-pre-wrap'>
|
||||
{wallet.address}
|
||||
</code>
|
||||
<p className='text-xs text-muted-foreground'>
|
||||
Network: {wallet.network}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<Github className='h-5 w-5' />
|
||||
Code Contributions
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Help improve TextBee by contributing to the codebase
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='grid gap-6 md:grid-cols-3'>
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg'>Star the Project</CardTitle>
|
||||
<CardDescription>
|
||||
Show your support by starring the repository
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className='w-full' asChild>
|
||||
<Link href={ExternalLinks.github} target='_blank'>
|
||||
<Star className='mr-2 h-4 w-4' />
|
||||
Star on GitHub
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg'>Report Issues</CardTitle>
|
||||
<CardDescription>
|
||||
Help us improve by reporting bugs and suggesting features
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className='w-full' variant='outline' asChild>
|
||||
<Link
|
||||
href={`${ExternalLinks.github}/issues/new`}
|
||||
target='_blank'
|
||||
>
|
||||
<MessageSquare className='mr-2 h-4 w-4' />
|
||||
Create Issue
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg'>Security Reports</CardTitle>
|
||||
<CardDescription>
|
||||
Report security vulnerabilities privately to{' '}
|
||||
<a href='mailto:security@textbee.dev'>
|
||||
security@textbee.dev
|
||||
</a>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className='w-full' variant='outline' asChild>
|
||||
<Link href='mailto:security@textbee.dev'>
|
||||
<Shield className='mr-2 h-4 w-4' />
|
||||
Report Vulnerability
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className='overflow-hidden'>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<MessageSquare className='h-5 w-5' />
|
||||
Join the Community
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Connect with other contributors and users
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className='w-full md:w-auto' variant='outline' asChild>
|
||||
<Link href={ExternalLinks.discord} target='_blank'>
|
||||
<MessageSquare className='mr-2 h-4 w-4' />
|
||||
Join Discord
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,41 @@ import { ExternalLinks } from '@/config/external-links'
|
||||
|
||||
export default function CommunityLinks() {
|
||||
return (
|
||||
<div className='grid gap-4 md:grid-cols-3'>
|
||||
<div className='grid gap-4 md:grid-cols-4'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>One-time Donation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className='text-sm text-muted-foreground mb-4'>
|
||||
Support us with a one-time donation of your desired amount.
|
||||
</p>
|
||||
<Link href={ExternalLinks.polar} prefetch={false} target='_blank'>
|
||||
<Button className='w-full' variant='destructive'>
|
||||
<Heart className='mr-2 h-4 w-4' />
|
||||
Donate Once
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Support on Patreon</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className='text-sm text-muted-foreground mb-4'>
|
||||
Support the development by becoming a patron.
|
||||
</p>
|
||||
<Link href={ExternalLinks.patreon} prefetch={false} target='_blank'>
|
||||
<Button className='w-full' variant='secondary'>
|
||||
<Heart className='mr-2 h-4 w-4' />
|
||||
Become a Patron
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>GitHub</CardTitle>
|
||||
@@ -24,23 +58,6 @@ export default function CommunityLinks() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Support Us</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className='text-sm text-muted-foreground mb-4'>
|
||||
Support the development by becoming a patron.
|
||||
</p>
|
||||
<Link href={ExternalLinks.patreon} prefetch={false} target='_blank'>
|
||||
<Button className='w-full' variant='secondary'>
|
||||
<Heart className='mr-2 h-4 w-4' />
|
||||
Become a Patron
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Discord</CardTitle>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import Dashboard from "./(components)/dashboard-layout";
|
||||
import { JoinCommunityModal } from '@/components/shared/join-community-modal'
|
||||
import { ContributeModal } from '@/components/shared/contribute-modal'
|
||||
import Dashboard from './(components)/dashboard-layout'
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <Dashboard>{children}</Dashboard>;
|
||||
}
|
||||
return (
|
||||
<Dashboard>
|
||||
{children}
|
||||
<JoinCommunityModal />
|
||||
<ContributeModal />
|
||||
</Dashboard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ export default function LandingPageHeader() {
|
||||
</Button> */}
|
||||
<Link
|
||||
className='text-sm font-medium hover:text-blue-500'
|
||||
href={ExternalLinks.github}
|
||||
href={Routes.contribute}
|
||||
>
|
||||
Github
|
||||
Contribute
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { Button } from '../../../components/ui/button'
|
||||
import { Heart, Coins, Check, Copy } from 'lucide-react'
|
||||
import { Heart, Coins, Check, Copy, Star, CreditCard } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
@@ -20,23 +20,29 @@ export default function SupportProjectSection() {
|
||||
const cryptoWallets = [
|
||||
{
|
||||
name: 'Bitcoin (BTC)',
|
||||
address: '1Ag3nQKGDdmcqSZicRRKnGGKwfgSkhhA5M',
|
||||
address: 'bc1qhffsnhp8ynqy6xvh982cu0x5w7vguuum3nqae9',
|
||||
network: 'Bitcoin',
|
||||
},
|
||||
{
|
||||
name: 'Ethereum (ETH)',
|
||||
address: '0x61368be0052ee9245287ee88f7f1fceb5343e207',
|
||||
address: '0xDB8560a42bdaa42C58462C6b2ee5A7D36F1c1f2a',
|
||||
network: 'Ethereum (ERC20)',
|
||||
},
|
||||
{
|
||||
name: 'Tether (USDT)',
|
||||
address: '0x61368be0052ee9245287ee88f7f1fceb5343e207',
|
||||
address: '0xDB8560a42bdaa42C58462C6b2ee5A7D36F1c1f2a',
|
||||
network: 'Ethereum (ERC20)',
|
||||
},
|
||||
// {
|
||||
// name: 'Tether (USDT)',
|
||||
// address: 'TD6txzY61D6EgnVfMLPsqKhYfyV5iHrbkw',
|
||||
// network: 'Tron (TRC20)',
|
||||
// },
|
||||
{
|
||||
name: 'Tether (USDT)',
|
||||
address: 'TD6txzY61D6EgnVfMLPsqKhYfyV5iHrbkw',
|
||||
network: 'Tron (TRC20)',
|
||||
name: 'Monero (XMR)',
|
||||
address:
|
||||
'856J5eHJM7bgBhkc51oCuMYUGKvUvF1zwAWrQsqwuH1shG9qnX4YkoZbMmhCPep1JragY2W1hpzAnDda6BXvCgZxUJhUyTg',
|
||||
network: 'Monero (XMR)',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -56,19 +62,32 @@ export default function SupportProjectSection() {
|
||||
<div className='mx-auto max-w-[58rem] text-center'>
|
||||
<h2 className='text-3xl font-bold mb-4'>Support The Project</h2>
|
||||
<p className='text-gray-500 mb-8'>
|
||||
Maintaining an open-source project requires time and dedication. By
|
||||
becoming a patron or donating cryptocurrency, your contribution will
|
||||
directly support the development, including implementation of new
|
||||
features, enhance performance, and ensure the highest level of
|
||||
security and reliability.
|
||||
Maintaining an open-source project requires time and dedication.
|
||||
Your contribution will directly support the development, including
|
||||
implementation of new features, enhance performance, and ensure the
|
||||
highest level of security and reliability.
|
||||
</p>
|
||||
<div className='flex flex-col sm:flex-row justify-center gap-4'>
|
||||
<div className='flex flex-col sm:flex-row justify-center gap-4 flex-wrap'>
|
||||
<Link href={ExternalLinks.patreon} prefetch={false} target='_blank'>
|
||||
<Button className='bg-blue-500 hover:bg-blue-600 text-white'>
|
||||
<Button className='bg-blue-500 hover:bg-blue-600 text-white sm:w-auto w-full'>
|
||||
<Heart className='mr-2 h-4 w-4' /> Become a Patron
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant='outline' onClick={() => setCryptoOpen(true)}>
|
||||
<Link href={ExternalLinks.github} prefetch={false} target='_blank'>
|
||||
<Button variant='outline' className='sm:w-auto w-full'>
|
||||
<Star className='mr-2 h-4 w-4' /> Star on GitHub
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={ExternalLinks.polar} prefetch={false} target='_blank'>
|
||||
<Button variant='outline' className='sm:w-auto w-full'>
|
||||
<CreditCard className='mr-2 h-4 w-4' /> One-time Donation
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => setCryptoOpen(true)}
|
||||
className='sm:w-auto w-full'
|
||||
>
|
||||
<Coins className='mr-2 h-4 w-4' /> Donate Crypto
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -108,6 +108,13 @@ export default function AppHeader() {
|
||||
<LayoutDashboard className='h-4 w-4' />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href={Routes.contribute}
|
||||
className='flex items-center gap-2 py-2'
|
||||
>
|
||||
<MessageSquarePlus className='h-4 w-4' />
|
||||
Contribute
|
||||
</Link>
|
||||
<Button
|
||||
onClick={handleLogout}
|
||||
variant='ghost'
|
||||
@@ -153,6 +160,15 @@ export default function AppHeader() {
|
||||
<div className='flex flex-1 items-center justify-end space-x-2'>
|
||||
<nav className='flex items-center space-x-6'>
|
||||
<ThemeToggle />
|
||||
<Link
|
||||
href={Routes.contribute}
|
||||
className='items-center gap-2 pr-8 hidden md:block'
|
||||
>
|
||||
<Button variant='outline' className='px-4 py-2 text-sm'>
|
||||
Contribute
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isAuthenticated ? (
|
||||
<AuthenticatedMenu />
|
||||
) : (
|
||||
|
||||
141
web/components/shared/contribute-modal.tsx
Normal file
141
web/components/shared/contribute-modal.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ExternalLinks } from '@/config/external-links'
|
||||
|
||||
// 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.2 // 20% chance to show when eligible
|
||||
|
||||
export function ContributeModal() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkAndShowModal = () => {
|
||||
const hasContributed =
|
||||
localStorage.getItem(STORAGE_KEYS.HAS_CONTRIBUTED) === 'true'
|
||||
if (hasContributed) 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()
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleContributed = () => {
|
||||
localStorage.setItem(STORAGE_KEYS.HAS_CONTRIBUTED, 'true')
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className='max-w-md max-h-[80vh] overflow-y-auto'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Support textbee.dev</DialogTitle>
|
||||
<DialogDescription>
|
||||
Your contribution helps keep this project alive and growing.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='space-y-6'>
|
||||
<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>
|
||||
</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 className='flex justify-end gap-4 pt-4 border-t'>
|
||||
<Button variant='ghost' onClick={handleContributed} asChild>
|
||||
<Link href='#'>I've already donated</Link>
|
||||
</Button>
|
||||
<Button variant='secondary' onClick={() => setIsOpen(false)}>
|
||||
Remind me later
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
96
web/components/shared/join-community-modal.tsx
Normal file
96
web/components/shared/join-community-modal.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ExternalLinks } from '@/config/external-links'
|
||||
|
||||
// Constants for localStorage keys and timing
|
||||
const STORAGE_KEYS = {
|
||||
LAST_SHOWN: 'discord_modal_last_shown',
|
||||
HAS_JOINED: 'discord_modal_has_joined',
|
||||
}
|
||||
|
||||
const SHOW_INTERVAL = 1 * 24 * 60 * 60 * 1000 // 1 days in milliseconds
|
||||
const RANDOM_CHANCE = 0.2 // 20% chance to show when eligible
|
||||
|
||||
export const JoinCommunityModal = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkAndShowModal = () => {
|
||||
const hasJoined = localStorage.getItem(STORAGE_KEYS.HAS_JOINED) === 'true'
|
||||
if (hasJoined) 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check when component mounts
|
||||
checkAndShowModal()
|
||||
|
||||
// Also check when tab becomes visible
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
checkAndShowModal()
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleJoined = () => {
|
||||
localStorage.setItem(STORAGE_KEYS.HAS_JOINED, 'true')
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const handleRemindLater = () => {
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className='sm:max-w-xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Join Our Discord Community!</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='py-4'>
|
||||
<p className='text-muted-foreground'>
|
||||
Join our Discord community to connect with other users, get help,
|
||||
and stay updated with the latest announcements!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:justify-end'>
|
||||
<Button variant='outline' onClick={handleRemindLater}>
|
||||
Remind Me Later
|
||||
</Button>
|
||||
<Button variant='outline' onClick={handleJoined} className='gap-2'>
|
||||
I've Already Joined
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => {
|
||||
window.open(ExternalLinks.discord, '_blank')
|
||||
handleJoined()
|
||||
}}
|
||||
className='gap-2'
|
||||
>
|
||||
Join Discord
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -2,4 +2,5 @@ export const ExternalLinks = {
|
||||
patreon: 'https://patreon.com/vernu',
|
||||
github: 'https://github.com/vernu/textbee',
|
||||
discord: 'https://discord.gg/d7vyfBpWbQ',
|
||||
polar: 'https://donate.textbee.dev',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const Routes = {
|
||||
landingPage: '/',
|
||||
contribute: '/contribute',
|
||||
login: '/login',
|
||||
register: '/register',
|
||||
logout: '/logout',
|
||||
|
||||
Reference in New Issue
Block a user