10 changed files with 618 additions and 29 deletions
-
290web/app/(app)/contribute/page.tsx
-
33web/app/(app)/dashboard/(components)/community-links.tsx
-
16web/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({ |
export default function DashboardLayout({ |
||||
children, |
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