10 changed files with 618 additions and 29 deletions
-
290web/app/(app)/contribute/page.tsx
-
33web/app/(app)/dashboard/(components)/community-links.tsx
-
14web/app/(app)/dashboard/layout.tsx
-
4web/app/(landing-page)/(components)/landing-page-header.tsx
-
49web/app/(landing-page)/(components)/support-project-section.tsx
-
16web/components/shared/app-header.tsx
-
141web/components/shared/contribute-modal.tsx
-
96web/components/shared/join-community-modal.tsx
-
1web/config/external-links.ts
-
1web/config/routes.ts
@ -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> |
|||
) |
|||
} |
|||
@ -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> |
|||
) |
|||
} |
|||
@ -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> |
|||
) |
|||
} |
|||
@ -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> |
|||
) |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue