chore(web): add contribute page

This commit is contained in:
isra el
2025-01-06 06:15:13 +03:00
parent 9f3b257588
commit 9409d162ce
10 changed files with 628 additions and 39 deletions

View 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&apos;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>
)
}

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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 />
) : (

View 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&apos;ve already donated</Link>
</Button>
<Button variant='secondary' onClick={() => setIsOpen(false)}>
Remind me later
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View 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&apos;ve Already Joined
</Button>
<Button
variant='default'
onClick={() => {
window.open(ExternalLinks.discord, '_blank')
handleJoined()
}}
className='gap-2'
>
Join Discord
</Button>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -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',
}

View File

@@ -1,5 +1,6 @@
export const Routes = {
landingPage: '/',
contribute: '/contribute',
login: '/login',
register: '/register',
logout: '/logout',