chore: push site to git
This commit is contained in:
commit
45f2b62d8d
248 changed files with 33283 additions and 0 deletions
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
14
READ.txt
Normal file
14
READ.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
------------------------
|
||||
so, rn mining makes NEW tokens into the supply even though it should cap at 1 billion tokens... this causes absurd crashes if a lot of tokens are claimed!
|
||||
idk how to approach mining tbf... do we make users put up/sell their tokens and we make it like a queue for mining? i got no clue...
|
||||
|
||||
|
||||
uhhh recommendations
|
||||
------------------------
|
||||
boost miner feature
|
||||
- you can click the button every 1 hour
|
||||
- it will generate 3k tokens / sec for 10 mins
|
||||
|
||||
and sell your miner for 50% (1.5 sol)
|
||||
|
||||
maybe make miner ratre more rn its uselss
|
||||
196
app/admin/page.tsx
Normal file
196
app/admin/page.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Header } from '@/components/header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { toast } from 'sonner'
|
||||
import { Crown, DollarSign, Search } from 'lucide-react'
|
||||
|
||||
export default function AdminPage() {
|
||||
const { data: session, status } = useSession()
|
||||
const router = useRouter()
|
||||
const [targetUser, setTargetUser] = useState('')
|
||||
const [amount, setAmount] = useState('')
|
||||
const [action, setAction] = useState('add')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [noticeMessage, setNoticeMessage] = useState('')
|
||||
const [noticeReason, setNoticeReason] = useState('')
|
||||
const [noticeLoading, setNoticeLoading] = useState(false)
|
||||
|
||||
if (status === 'loading') return null
|
||||
|
||||
if (!session?.user?.isAdmin) {
|
||||
router.push('/')
|
||||
return null
|
||||
}
|
||||
|
||||
const handleUpdateBalance = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/balance', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
usernameOrId: targetUser,
|
||||
amount: parseFloat(amount),
|
||||
action
|
||||
})
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (res.ok) {
|
||||
toast.success(`Success! ${data.user} balance updated to ${data.newBalance.toFixed(4)} SOL`)
|
||||
setAmount('')
|
||||
} else {
|
||||
toast.error(data.error || 'Failed')
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('Error executing admin command')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetNotice = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setNoticeLoading(true)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/notice', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: noticeMessage,
|
||||
reason: noticeReason || undefined
|
||||
})
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (res.ok) {
|
||||
toast.success('Notice set successfully!')
|
||||
setNoticeMessage('')
|
||||
setNoticeReason('')
|
||||
} else {
|
||||
toast.error(data.error || 'Failed to set notice')
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('Error setting notice')
|
||||
} finally {
|
||||
setNoticeLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="container mx-auto py-20 px-4">
|
||||
<div className="flex items-center gap-2 mb-8">
|
||||
<Crown className="w-8 h-8 text-red-500" />
|
||||
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
Manage User Balance
|
||||
</h2>
|
||||
<form onSubmit={handleUpdateBalance} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Target User</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Username or Discord ID"
|
||||
className="pl-8"
|
||||
value={targetUser}
|
||||
onChange={e => setTargetUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Action</Label>
|
||||
<Select value={action} onValueChange={setAction}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="add">Add (+)</SelectItem>
|
||||
<SelectItem value="subtract">Subtract (-)</SelectItem>
|
||||
<SelectItem value="set">Set (=)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Amount (SOL)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.0001"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={e => setAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading || !targetUser || !amount}>
|
||||
{loading ? 'Executing...' : 'Update Balance'}
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Set Site Notice</h2>
|
||||
<form onSubmit={handleSetNotice} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Notice Message</Label>
|
||||
<Input
|
||||
placeholder="Enter notice message..."
|
||||
value={noticeMessage}
|
||||
onChange={e => setNoticeMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Reason (Optional)</Label>
|
||||
<Input
|
||||
placeholder="Reason for notice..."
|
||||
value={noticeReason}
|
||||
onChange={e => setNoticeReason(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={noticeLoading || !noticeMessage.trim()}>
|
||||
{noticeLoading ? 'Setting...' : 'Set Notice'}
|
||||
</Button>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Quick Links</h2>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use the scripts in /scripts folder for bulk actions:
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sm space-y-1 font-mono bg-muted/30 p-4 rounded-lg">
|
||||
<li>npx ts-node scripts/set-role.ts <user> admin true</li>
|
||||
<li>npx ts-node scripts/give-beta-to-all.ts</li>
|
||||
<li>npx ts-node scripts/database-reset.ts [reason]</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
51
app/api/admin/balance/route.ts
Normal file
51
app/api/admin/balance/route.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || !session.user.isAdmin) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { usernameOrId, amount, action } = await request.json()
|
||||
// action: 'set', 'add', 'subtract'
|
||||
|
||||
if (!usernameOrId || amount === undefined) {
|
||||
return NextResponse.json({ error: 'Missing fields' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
let query: any = { name: usernameOrId }
|
||||
if (/^\d{17,19}$/.test(usernameOrId)) { // Discord ID
|
||||
query = { discordId: usernameOrId }
|
||||
} else if (ObjectId.isValid(usernameOrId)) {
|
||||
query = { _id: new ObjectId(usernameOrId) }
|
||||
}
|
||||
|
||||
const user = await db.collection('users').findOne(query)
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
let newBalance = user.balance
|
||||
const val = parseFloat(amount)
|
||||
|
||||
if (action === 'set') newBalance = val
|
||||
else if (action === 'add') newBalance += val
|
||||
else if (action === 'subtract') newBalance -= val
|
||||
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: user._id },
|
||||
{ $set: { balance: newBalance } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, newBalance, user: user.name })
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Failed to update balance' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
6
app/api/auth/[...nextauth]/route.ts
Normal file
6
app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import NextAuth from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
|
||||
const handler = NextAuth(authOptions)
|
||||
|
||||
export { handler as GET, handler as POST }
|
||||
56
app/api/coins/[id]/boost/route.ts
Normal file
56
app/api/coins/[id]/boost/route.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
if ((user.balance || 0) < 1) {
|
||||
return NextResponse.json({ error: 'Insufficient balance (1 SOL required)' }, { status: 400 })
|
||||
}
|
||||
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(id) })
|
||||
if (!coin) {
|
||||
return NextResponse.json({ error: 'Coin not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
let newBoostedAt = now
|
||||
if (coin.boosted && new Date(coin.boosted) > now) {
|
||||
newBoostedAt = new Date(new Date(coin.boosted).getTime() + 24 * 60 * 60 * 1000)
|
||||
} else {
|
||||
newBoostedAt = new Date(now.getTime() + 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{ $inc: { balance: -1 } }
|
||||
)
|
||||
await db.collection('coins').updateOne(
|
||||
{ _id: new ObjectId(id) },
|
||||
{ $set: { boosted: newBoostedAt } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, boosted: newBoostedAt })
|
||||
} catch (error) {
|
||||
console.error('Error boosting coin:', error)
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
69
app/api/coins/[id]/holders/route.ts
Normal file
69
app/api/coins/[id]/holders/route.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(id) })
|
||||
if (!coin) {
|
||||
return NextResponse.json({ error: 'Coin not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const holders = await db.collection('users').aggregate([
|
||||
{ $unwind: '$portfolio' },
|
||||
{ $match: { 'portfolio.coinId': id } },
|
||||
{ $sort: { 'portfolio.amount': -1 } },
|
||||
{ $limit: 20 },
|
||||
{
|
||||
$project: {
|
||||
name: 1,
|
||||
image: 1,
|
||||
verified: 1,
|
||||
isAdmin: 1,
|
||||
isBetaTester: 1,
|
||||
isBugHunter: 1,
|
||||
amount: '$portfolio.amount'
|
||||
}
|
||||
}
|
||||
]).toArray()
|
||||
|
||||
//? calculate "system" holding (Curve/Raydium)
|
||||
//? get the actual sum of ALL user tokens to derive the system balance accurately
|
||||
const stats = await db.collection('users').aggregate([
|
||||
{ $unwind: '$portfolio' },
|
||||
{ $match: { 'portfolio.coinId': id } },
|
||||
{ $group: { _id: null, totalHeld: { $sum: '$portfolio.amount' } } }
|
||||
]).toArray()
|
||||
|
||||
const totalUserHeld = stats[0]?.totalHeld || 0
|
||||
const curveBalance = coin.supply - totalUserHeld
|
||||
|
||||
// logic for "Raydium" or "Bonding Curve" display
|
||||
const isGraduated = coin.graduated === true;
|
||||
|
||||
if (curveBalance > 100) {
|
||||
const curveHolder = {
|
||||
_id: 'bonding-curve',
|
||||
name: isGraduated ? 'Raydium Liquidity Pool' : 'Bonding Curve',
|
||||
image: isGraduated ? 'https://pummmp.fun/raydium-ray-logo.png' : 'https://pummmp.fun/logo.png',
|
||||
amount: curveBalance,
|
||||
isSystem: true,
|
||||
isLocked: isGraduated // Visual indicator that it's "Forever"
|
||||
}
|
||||
|
||||
holders.push(curveHolder)
|
||||
holders.sort((a, b) => b.amount - a.amount)
|
||||
}
|
||||
|
||||
return NextResponse.json(holders.slice(0, 10))
|
||||
} catch (error) {
|
||||
console.error('Error fetching holders:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch holders' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
104
app/api/coins/[id]/route.ts
Normal file
104
app/api/coins/[id]/route.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
let coin
|
||||
|
||||
// try to find by id first, then by ticker
|
||||
if (ObjectId.isValid(id)) {
|
||||
coin = await db.collection('coins').findOne({ _id: new ObjectId(id) })
|
||||
}
|
||||
|
||||
if (!coin) {
|
||||
coin = await db.collection('coins').findOne({ ticker: id.toUpperCase() })
|
||||
}
|
||||
|
||||
if (!coin) {
|
||||
return NextResponse.json({ error: 'Coin not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
let creatorVerified = false;
|
||||
let creatorIsAdmin = false;
|
||||
let creatorIsBetaTester = false;
|
||||
let creatorIsBugHunter = false;
|
||||
|
||||
if (coin.creatorId) {
|
||||
try {
|
||||
const creator = await db.collection('users').findOne({ _id: new ObjectId(coin.creatorId) }, { projection: { verified: 1, isAdmin: 1, isBetaTester: 1, isBugHunter: 1, name: 1 } });
|
||||
if (creator) {
|
||||
if (creator.verified) creatorVerified = true;
|
||||
if (creator.isAdmin) creatorIsAdmin = true;
|
||||
if (creator.isBetaTester) creatorIsBetaTester = true;
|
||||
if (creator.isBugHunter) creatorIsBugHunter = true;
|
||||
|
||||
// ALWAYS use the up-to-date name from the users collection
|
||||
if (creator.name) {
|
||||
coin.creatorName = creator.name;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// NOPE!
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...coin,
|
||||
creatorVerified,
|
||||
creatorIsAdmin,
|
||||
creatorIsBetaTester,
|
||||
creatorIsBugHunter
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching coin:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch coin' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(id) })
|
||||
|
||||
if (!coin) {
|
||||
return NextResponse.json({ error: 'Coin not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (coin.creatorId !== session.user.id && !session.user.isAdmin) {
|
||||
return NextResponse.json({ error: 'Only the creator or admin can delete this coin' }, { status: 403 })
|
||||
}
|
||||
|
||||
await db.collection('coins').deleteOne({ _id: new ObjectId(id) })
|
||||
|
||||
// Remove from all portfolios to prevent portfolio page crashes
|
||||
await db.collection('users').updateMany(
|
||||
{ 'portfolio.coinId': id },
|
||||
{ $pull: { portfolio: { coinId: id } } as any }
|
||||
)
|
||||
|
||||
await db.collection('trades').deleteMany({ coinId: id }) // ? clean up trade history as well
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Error deleting coin:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete coin' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
47
app/api/coins/[id]/verify/route.ts
Normal file
47
app/api/coins/[id]/verify/route.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase();
|
||||
|
||||
// ! check if the user is actually verified in the DB (never trust client!)
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
|
||||
if (!user || !user.verified) {
|
||||
return NextResponse.json({ error: 'Only verified users can verify coins... nga...' }, { status: 403 })
|
||||
}
|
||||
|
||||
const coinId = id
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(coinId) })
|
||||
|
||||
if (!coin) {
|
||||
return NextResponse.json({ error: 'Coin not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const newStatus = !coin.verified //? toggle
|
||||
await db.collection('coins').updateOne(
|
||||
{ _id: new ObjectId(coinId) },
|
||||
{ $set: { verified: newStatus } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ verified: newStatus })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error verifying coin:', error)
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
186
app/api/coins/route.ts
Normal file
186
app/api/coins/route.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { coinSchema } from '@/lib/validations'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const { db } = await connectToDatabase()
|
||||
const coins = await db
|
||||
.collection('coins')
|
||||
.find({})
|
||||
.sort({ createdAt: -1 })
|
||||
.toArray()
|
||||
|
||||
return NextResponse.json(coins)
|
||||
} catch (error) {
|
||||
console.error('Error fetching coins:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch coins' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const body = await request.json()
|
||||
|
||||
const validationResult = coinSchema.safeParse(body)
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errorMessage = validationResult.error.errors.map(e => e.message).join(', ')
|
||||
return NextResponse.json({ error: errorMessage }, { status: 400 })
|
||||
}
|
||||
|
||||
const { name, ticker, description, website, twitter, telegram, initialBuy, useUserImage, verified, boosted } = validationResult.data
|
||||
const cleanName = name
|
||||
const cleanTicker = ticker
|
||||
const cleanDescription = description
|
||||
|
||||
if (!cleanName || !cleanTicker) {
|
||||
return NextResponse.json({ error: 'Name and ticker are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
//! verify via DB for security cuz client can be manipulated
|
||||
let isVerified = false
|
||||
if (verified && user.verified) {
|
||||
isVerified = true
|
||||
}
|
||||
|
||||
//? costs
|
||||
const creationCost = 0.02;
|
||||
const boostCost = boosted ? 1.0 : 0;
|
||||
const initialBuyAmount = typeof initialBuy === 'number' && initialBuy > 0 ? initialBuy : 0;
|
||||
|
||||
const totalCost = creationCost + boostCost + initialBuyAmount;
|
||||
|
||||
if ((user.balance || 0) < totalCost) {
|
||||
return NextResponse.json({ error: `Insufficient balance (${totalCost.toFixed(3)} SOL required)` }, { status: 400 })
|
||||
}
|
||||
|
||||
const existingCoin = await db.collection('coins').findOne({ ticker: cleanTicker })
|
||||
if (existingCoin) {
|
||||
return NextResponse.json({ error: 'Ticker already exists' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (checkRateLimit(session.user.id, 'coin_creation', 1, 60000 * 5)) {
|
||||
return NextResponse.json({ error: 'Rate limit exceeded. You can only create 1 coin every 5 minutes.' }, { status: 429 })
|
||||
}
|
||||
|
||||
let virtualSolReserves = 30;
|
||||
const INITIAL_SUPPLY = 1000000000;
|
||||
let virtualTokenReserves = 1073000000;
|
||||
let initialLiquidity = 0;
|
||||
let initialPrice = virtualSolReserves / virtualTokenReserves;
|
||||
let initialVolume = 0;
|
||||
|
||||
let boughtTokens = 0;
|
||||
|
||||
// ! process IB (Snipe)
|
||||
if (initialBuyAmount > 0) {
|
||||
// chat wahat does calc stand for
|
||||
// i hate my life
|
||||
const k = virtualSolReserves * virtualTokenReserves;
|
||||
const newVirtualSolReserves = virtualSolReserves + initialBuyAmount;
|
||||
const newVirtualTokenReserves = k / newVirtualSolReserves;
|
||||
|
||||
boughtTokens = virtualTokenReserves - newVirtualTokenReserves;
|
||||
|
||||
// ? update states
|
||||
virtualSolReserves = newVirtualSolReserves;
|
||||
virtualTokenReserves = newVirtualTokenReserves;
|
||||
initialPrice = virtualSolReserves / virtualTokenReserves;
|
||||
initialLiquidity = initialBuyAmount; // ! real liquidity
|
||||
initialVolume = initialBuyAmount;
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const priceHistory = [{
|
||||
timestamp: now,
|
||||
price: initialPrice,
|
||||
volume: initialVolume,
|
||||
}]
|
||||
|
||||
const coinImage = (useUserImage && session.user.image)
|
||||
? session.user.image
|
||||
: `https://api.dicebear.com/7.x/identicon/svg?seed=${cleanTicker}`;
|
||||
|
||||
const coin = {
|
||||
name: cleanName,
|
||||
ticker: cleanTicker,
|
||||
description: cleanDescription,
|
||||
image: coinImage,
|
||||
website: website || '',
|
||||
twitter: twitter || '',
|
||||
telegram: telegram || '',
|
||||
creatorId: session.user.id,
|
||||
creatorName: user.name || 'Anonymous', // Use DB name, not session name
|
||||
createdAt: now,
|
||||
verified: isVerified,
|
||||
boosted: boosted ? new Date(Date.now() + 24 * 60 * 60 * 1000) : null,
|
||||
marketCap: initialPrice * INITIAL_SUPPLY,
|
||||
price: initialPrice,
|
||||
priceHistory,
|
||||
volume24h: initialVolume,
|
||||
holders: boughtTokens > 0 ? 1 : 0,
|
||||
supply: INITIAL_SUPPLY,
|
||||
liquidity: initialLiquidity,
|
||||
virtualSolReserves,
|
||||
virtualTokenReserves,
|
||||
}
|
||||
|
||||
const result = await db.collection('coins').insertOne(coin)
|
||||
|
||||
// deduct total cost
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{ $inc: { balance: -totalCost } }
|
||||
)
|
||||
|
||||
// give tokens if bought
|
||||
if (boughtTokens > 0) {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{
|
||||
$push: {
|
||||
portfolio: {
|
||||
coinId: result.insertedId.toString(),
|
||||
amount: boughtTokens,
|
||||
avgBuyPrice: initialBuyAmount / boughtTokens,
|
||||
},
|
||||
} as any,
|
||||
}
|
||||
)
|
||||
|
||||
//? record trade
|
||||
await db.collection('trades').insertOne({
|
||||
userId: session.user.id,
|
||||
coinId: result.insertedId.toString(),
|
||||
type: 'buy',
|
||||
amount: boughtTokens,
|
||||
price: initialPrice,
|
||||
total: initialBuyAmount,
|
||||
timestamp: now,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ ...coin, _id: result.insertedId })
|
||||
} catch (error) {
|
||||
console.error('Error creating coin:', error)
|
||||
return NextResponse.json({ error: 'Failed to create coin' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
121
app/api/comments/route.ts
Normal file
121
app/api/comments/route.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { commentSchema } from '@/lib/validations'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const coinId = searchParams.get('coinId')
|
||||
|
||||
if (!coinId) {
|
||||
return NextResponse.json({ error: 'Coin ID required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const comments = await db.collection('comments')
|
||||
.find({ coinId })
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(50)
|
||||
.toArray()
|
||||
|
||||
return NextResponse.json(comments)
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Failed to fetch comments' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
//! rate limiting: 5 comments per minute per user
|
||||
if (checkRateLimit(session.user.id, 'comment', 5, 60000)) {
|
||||
return NextResponse.json({ error: 'Rate limit exceeded. Please wait.' }, { status: 429 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validationResult = commentSchema.safeParse(body)
|
||||
|
||||
if (!validationResult.success) {
|
||||
return NextResponse.json({ error: validationResult.error.errors[0].message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { coinId, text } = validationResult.data
|
||||
|
||||
if ((text.match(/\n/g) || []).length > 2) {
|
||||
return NextResponse.json({ error: 'Comment is too tall (max 2 new lines)' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
|
||||
//? owns % of coin? (holder)
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
const isHolder = user?.portfolio?.some((p: any) => p.coinId === coinId && p.amount > 0)
|
||||
|
||||
const comment = {
|
||||
coinId,
|
||||
userId: session.user.id,
|
||||
userName: session.user.name || 'Anonymous',
|
||||
userImage: session.user.image,
|
||||
userVerified: !!user?.verified,
|
||||
userIsAdmin: !!user?.isAdmin,
|
||||
userIsBetaTester: !!user?.isBetaTester,
|
||||
userIsBugHunter: !!user?.isBugHunter,
|
||||
text,
|
||||
isHolder,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
await db.collection('comments').insertOne(comment)
|
||||
|
||||
return NextResponse.json(comment)
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Failed to post comment' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const commentId = searchParams.get('id')
|
||||
|
||||
if (!commentId) {
|
||||
return NextResponse.json({ error: 'Comment ID required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const comment = await db.collection('comments').findOne({ _id: new ObjectId(commentId) })
|
||||
if (!comment) {
|
||||
return NextResponse.json({ error: 'Comment not found' }, { status: 404 })
|
||||
}
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(comment.coinId) })
|
||||
|
||||
const isOwner = comment.userId === session.user.id
|
||||
const isCoinCreator = coin && coin.creatorId === session.user.id
|
||||
const isAdmin = session.user.isAdmin
|
||||
|
||||
if (!isOwner && !isCoinCreator && !isAdmin) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
await db.collection('comments').deleteOne({ _id: new ObjectId(commentId) })
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('Delete comment error:', error)
|
||||
return NextResponse.json({ error: 'Failed to delete comment' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
80
app/api/gamble/route.ts
Normal file
80
app/api/gamble/route.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import crypto from 'crypto'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (checkRateLimit(session.user.id, 'gamble', 20, 60000)) {
|
||||
return NextResponse.json({ error: 'Rate limit exceeded. Please slow down.' }, { status: 429 })
|
||||
}
|
||||
|
||||
const { amount, chance } = await request.json()
|
||||
|
||||
const betAmount = parseFloat(amount)
|
||||
const winChance = parseFloat(chance)
|
||||
|
||||
// validations
|
||||
if (isNaN(betAmount) || betAmount <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid amount' }, { status: 400 })
|
||||
}
|
||||
if (isNaN(winChance) || winChance < 1 || winChance > 95) {
|
||||
return NextResponse.json({ error: 'Chance must be between 1% and 95%' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if ((user.balance || 0) < betAmount) {
|
||||
return NextResponse.json({ error: 'Insufficient balance' }, { status: 400 })
|
||||
}
|
||||
|
||||
// GAMBLE LOGIC
|
||||
// 100-67 = 33% house edge (heh)
|
||||
const multiplier = 67 / winChance
|
||||
|
||||
const buffer = crypto.randomBytes(4);
|
||||
const randomInt = buffer.readUInt32BE(0);
|
||||
const roll = (randomInt / 0xffffffff) * 100;
|
||||
|
||||
const won = roll < winChance
|
||||
|
||||
const payout = won ? (betAmount * multiplier) : 0
|
||||
// Win: + (Payout - Bet) = Bet * (Multiplier - 1)
|
||||
|
||||
const netChange = won ? (payout - betAmount) : -betAmount
|
||||
const finalBalance = (user.balance || 0) + netChange
|
||||
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{ $set: { balance: finalBalance } }
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
won,
|
||||
roll,
|
||||
payout,
|
||||
netChange,
|
||||
newBalance: finalBalance,
|
||||
multiplier
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error gambling:', error)
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
81
app/api/miner/buy/route.ts
Normal file
81
app/api/miner/buy/route.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Miners are temporarily disabled by administrators' }, { status: 503 })
|
||||
|
||||
// try {
|
||||
// const session = await getServerSession(authOptions)
|
||||
// if (!session?.user?.id) {
|
||||
// return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
// }
|
||||
|
||||
// const { coinId } = await request.json()
|
||||
// if (!coinId) {
|
||||
// return NextResponse.json({ error: 'Coin ID required' }, { status: 400 })
|
||||
// }
|
||||
|
||||
// const { db } = await connectToDatabase()
|
||||
|
||||
// // Check if user already has a miner for this coin (allow multiple)
|
||||
// // const existingMiner = await db.collection('miners').findOne({
|
||||
// // userId: session.user.id,
|
||||
// // coinId: coinId,
|
||||
// // isActive: true
|
||||
// // })
|
||||
|
||||
// // if (existingMiner) {
|
||||
// // await client.close()
|
||||
// // return NextResponse.json({ error: 'You already have a miner for this coin' }, { status: 400 })
|
||||
// // }
|
||||
|
||||
// // Check user's balance
|
||||
// const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
// if (!user) {
|
||||
// return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
// }
|
||||
|
||||
// // Check trade ban (prevent buying miners)
|
||||
// if (user.tradeBannedUntil) {
|
||||
// const banExpiry = new Date(user.tradeBannedUntil);
|
||||
// if (banExpiry > new Date()) {
|
||||
// return NextResponse.json({ error: 'You are banned from trading/mining actions' }, { status: 403 })
|
||||
// } else {
|
||||
// try { await db.collection('users').updateOne({ _id: user._id }, { $unset: { tradeBannedUntil: 1 } }) } catch (e) {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (user.balance < 3) {
|
||||
// return NextResponse.json({ error: 'Insufficient balance. Need 3 SOL to buy a miner' }, { status: 400 })
|
||||
// }
|
||||
|
||||
// // Deduct 3 SOL from balance
|
||||
// await db.collection('users').updateOne(
|
||||
// { _id: new ObjectId(session.user.id) },
|
||||
// { $inc: { balance: -3 } }
|
||||
// )
|
||||
|
||||
// // Create miner
|
||||
// const miner = {
|
||||
// userId: session.user.id,
|
||||
// coinId: coinId,
|
||||
// purchasedAt: new Date(),
|
||||
// lastCollectedAt: new Date(),
|
||||
// isActive: true
|
||||
// }
|
||||
|
||||
// await db.collection('miners').insertOne(miner)
|
||||
|
||||
// return NextResponse.json({
|
||||
// success: true,
|
||||
// message: 'Miner purchased successfully! Started mining at 1000 tokens/second.'
|
||||
// })
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('Error buying miner:', error)
|
||||
// return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
// }
|
||||
}
|
||||
158
app/api/miner/collect/route.ts
Normal file
158
app/api/miner/collect/route.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Miners are temporarily disabled by administrators' }, { status: 503 });
|
||||
|
||||
// try {
|
||||
// const session = await getServerSession(authOptions)
|
||||
// if (!session?.user?.id) {
|
||||
// return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
// }
|
||||
|
||||
// const { coinId } = await request.json()
|
||||
// if (!coinId) {
|
||||
// return NextResponse.json({ error: 'Coin ID required' }, { status: 400 })
|
||||
// }
|
||||
|
||||
// const { db } = await connectToDatabase()
|
||||
|
||||
// // Find all active miners for this user and coin
|
||||
// const miners = await db.collection('miners').find({
|
||||
// userId: session.user.id,
|
||||
// coinId: coinId,
|
||||
// isActive: true
|
||||
// }).toArray()
|
||||
|
||||
// // Check trade ban (prevent miner collects/actions)
|
||||
// const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
// if (!user) {
|
||||
// return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
// }
|
||||
|
||||
// if (user.tradeBannedUntil) {
|
||||
// const banExpiry = new Date(user.tradeBannedUntil);
|
||||
// if (banExpiry > new Date()) {
|
||||
// return NextResponse.json({ error: 'You are banned from trading/mining actions' }, { status: 403 })
|
||||
// } else {
|
||||
// try { await db.collection('users').updateOne({ _id: user._id }, { $unset: { tradeBannedUntil: 1 } }) } catch (e) {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (miners.length === 0) {
|
||||
// return NextResponse.json({ error: 'No active miners found for this coin' }, { status: 400 })
|
||||
// }
|
||||
|
||||
// // Calculate total mined tokens from all miners (100 tokens per second per miner)
|
||||
// const now = new Date()
|
||||
// let totalMinedTokens = 0
|
||||
|
||||
// for (const miner of miners) {
|
||||
// const timeDiffMs = now.getTime() - miner.lastCollectedAt.getTime()
|
||||
// const timeDiffSeconds = Math.floor(timeDiffMs / 1000)
|
||||
// totalMinedTokens += timeDiffSeconds * 1000
|
||||
// }
|
||||
|
||||
// const minedTokens = totalMinedTokens
|
||||
|
||||
// if (minedTokens <= 0) {
|
||||
// return NextResponse.json({ error: 'No tokens to collect yet' }, { status: 400 })
|
||||
// }
|
||||
|
||||
// // Get coin info for trade creation
|
||||
// const coin = await db.collection('coins').findOne({ _id: new ObjectId(coinId) })
|
||||
// if (!coin) {
|
||||
// return NextResponse.json({ error: 'Coin not found' }, { status: 404 })
|
||||
// }
|
||||
|
||||
// // Update user's portfolio - add mined tokens
|
||||
// const existingHolding = user.portfolio?.find((p: any) => p.coinId === coinId)
|
||||
// if (existingHolding) {
|
||||
// await db.collection('users').updateOne(
|
||||
// { _id: new ObjectId(session.user.id), 'portfolio.coinId': coinId },
|
||||
// { $inc: { 'portfolio.$.amount': minedTokens } } as any
|
||||
// )
|
||||
// } else {
|
||||
// // If user doesn't have portfolio array, create it with $set
|
||||
// if (!user.portfolio || user.portfolio.length === 0) {
|
||||
// await db.collection('users').updateOne(
|
||||
// { _id: new ObjectId(session.user.id) },
|
||||
// {
|
||||
// $set: {
|
||||
// portfolio: [{
|
||||
// coinId: coinId,
|
||||
// amount: minedTokens,
|
||||
// avgBuyPrice: coin.price
|
||||
// }]
|
||||
// }
|
||||
// } as any
|
||||
// )
|
||||
// } else {
|
||||
// // User has portfolio array, push to it
|
||||
// await db.collection('users').updateOne(
|
||||
// { _id: new ObjectId(session.user.id) },
|
||||
// {
|
||||
// $push: {
|
||||
// portfolio: {
|
||||
// coinId: coinId,
|
||||
// amount: minedTokens,
|
||||
// avgBuyPrice: coin.price
|
||||
// }
|
||||
// }
|
||||
// } as any
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Update all miners' last collected time
|
||||
// await db.collection('miners').updateMany(
|
||||
// { userId: session.user.id, coinId: coinId, isActive: true },
|
||||
// { $set: { lastCollectedAt: now } }
|
||||
// )
|
||||
|
||||
// // Create trade record
|
||||
// const trade = {
|
||||
// userId: session.user.id,
|
||||
// username: user.name,
|
||||
// userImage: user.image,
|
||||
// userVerified: user.verified || false,
|
||||
// userIsAdmin: user.isAdmin || false,
|
||||
// userIsBetaTester: user.isBetaTester || false,
|
||||
// userIsBugHunter: user.isBugHunter || false,
|
||||
// coinId: coinId,
|
||||
// type: 'mined',
|
||||
// amount: minedTokens,
|
||||
// price: coin.price,
|
||||
// total: minedTokens * coin.price,
|
||||
// timestamp: now
|
||||
// }
|
||||
|
||||
// await db.collection('trades').insertOne(trade)
|
||||
|
||||
// // Update coin stats
|
||||
// await db.collection('coins').updateOne(
|
||||
// { _id: new ObjectId(coinId) },
|
||||
// {
|
||||
// $inc: {
|
||||
// holders: existingHolding ? 0 : 1,
|
||||
// volume24h: minedTokens * coin.price,
|
||||
// supply: minedTokens
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
|
||||
// return NextResponse.json({
|
||||
// success: true,
|
||||
// minedTokens: minedTokens,
|
||||
// minerCount: miners.length,
|
||||
// message: `Collected ${minedTokens.toLocaleString()} ${coin.ticker} tokens from ${miners.length} miner${miners.length > 1 ? 's' : ''}!`
|
||||
// })
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('Error collecting miner:', error)
|
||||
// return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
// }
|
||||
}
|
||||
60
app/api/notice/route.ts
Normal file
60
app/api/notice/route.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'bson'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { getServerSession } from 'next-auth'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const { db } = await connectToDatabase()
|
||||
const notice = await db.collection('notices').findOne(
|
||||
{},
|
||||
{ sort: { createdAt: -1 } }
|
||||
)
|
||||
|
||||
if (!notice) {
|
||||
return NextResponse.json({ notice: null })
|
||||
}
|
||||
|
||||
return NextResponse.json({ notice })
|
||||
} catch (error) {
|
||||
console.error('Error fetching notice:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch notice' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
//! this is probablyt vulnerable but whooo cares
|
||||
try {
|
||||
const { db } = await connectToDatabase();
|
||||
const { message, reason } = await request.json();
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
// check if admin
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session?.user.id) })
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
if (!user.isAdmin) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const notice = {
|
||||
message,
|
||||
reason,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
await db.collection('notices').insertOne(notice);
|
||||
|
||||
return NextResponse.json({ success: true, notice });
|
||||
} catch (error) {
|
||||
console.error('Error setting notice:', error);
|
||||
return NextResponse.json({ error: 'Failed to set notice' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
64
app/api/portfolio/route.ts
Normal file
64
app/api/portfolio/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const portfolioWithCoins = await Promise.all(
|
||||
(user.portfolio || []).map(async (item: any) => {
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(item.coinId) })
|
||||
if (!coin) return null
|
||||
|
||||
const currentValue = item.amount * coin.price
|
||||
const costBasis = item.amount * item.avgBuyPrice
|
||||
const pnl = currentValue - costBasis
|
||||
const pnlPercent = costBasis > 0 ? (pnl / costBasis) * 100 : 0
|
||||
|
||||
return {
|
||||
...item,
|
||||
coin: {
|
||||
_id: coin._id.toString(),
|
||||
name: coin.name,
|
||||
ticker: coin.ticker,
|
||||
image: coin.image,
|
||||
price: coin.price,
|
||||
priceHistory: coin.priceHistory?.slice(-24) || [],
|
||||
},
|
||||
currentValue,
|
||||
pnl,
|
||||
pnlPercent,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const validPortfolio = portfolioWithCoins.filter(Boolean)
|
||||
const totalPortfolioValue = validPortfolio.reduce((acc, item) => acc + item.currentValue, 0)
|
||||
const totalPnL = validPortfolio.reduce((acc, item) => acc + item.pnl, 0)
|
||||
|
||||
return NextResponse.json({
|
||||
portfolio: validPortfolio,
|
||||
totalPortfolioValue,
|
||||
totalPnL,
|
||||
cashBalance: user.balance,
|
||||
totalValue: user.balance + totalPortfolioValue,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching portfolio:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch portfolio' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
118
app/api/rain/route.ts
Normal file
118
app/api/rain/route.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (checkRateLimit(session.user.id, 'rain', 1, 60000 * 2)) { // 1 rain every 2 minutes
|
||||
return NextResponse.json({ error: 'Rate limit exceeded. Please wait before starting another rain.' }, { status: 429 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const body = await request.json()
|
||||
const { amount } = body
|
||||
const numAmount = parseFloat(amount)
|
||||
|
||||
if (isNaN(numAmount) || numAmount <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid amount' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check for existing active rain
|
||||
const existingRain = await db.collection('rains').findOne({ active: true })
|
||||
if (existingRain) {
|
||||
return NextResponse.json({ error: 'A rain event is already active!' }, { status: 409 })
|
||||
}
|
||||
|
||||
const userToCheck = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
if (!userToCheck || userToCheck.balance < numAmount) {
|
||||
return NextResponse.json({ error: 'Insufficient balance' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Deduct balance
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: userToCheck._id },
|
||||
{ $inc: { balance: -numAmount } }
|
||||
)
|
||||
|
||||
// Create Rain
|
||||
// ! DEV MODE: 2 minutes instead of 10
|
||||
const rain = {
|
||||
amount: numAmount,
|
||||
hostId: userToCheck._id.toString(),
|
||||
hostName: userToCheck.name,
|
||||
createdAt: new Date(),
|
||||
endsAt: new Date(Date.now() + 2 * 60 * 1000),
|
||||
participants: [],
|
||||
active: true
|
||||
}
|
||||
|
||||
await db.collection('rains').insertOne(rain)
|
||||
|
||||
return NextResponse.json({ success: true, rain })
|
||||
} catch (error: any) {
|
||||
console.error('Rain create error:', error)
|
||||
return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
// Find active rain
|
||||
const activeRain = await db.collection('rains').findOne({ active: true })
|
||||
if (!activeRain) {
|
||||
return NextResponse.json({ error: 'No active rain to join' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if already joined
|
||||
if (activeRain.participants.includes(session.user.id)) {
|
||||
return NextResponse.json({ error: 'Already joined' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Add to participants
|
||||
await db.collection('rains').updateOne(
|
||||
{ _id: activeRain._id },
|
||||
{ $addToSet: { participants: session.user.id } }
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
console.error('Rain join error:', error)
|
||||
return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { db } = await connectToDatabase()
|
||||
const activeRain = await db.collection('rains').findOne({ active: true })
|
||||
|
||||
if (activeRain && activeRain.participants && activeRain.participants.length > 0) {
|
||||
const participantIds = activeRain.participants.map((id: string) => new ObjectId(id))
|
||||
const users = await db.collection('users').find({ _id: { $in: participantIds } }, { projection: { name: 1, image: 1 } }).toArray()
|
||||
activeRain.participants = users.map(user => ({
|
||||
id: user._id.toString(),
|
||||
name: user.name,
|
||||
image: user.image
|
||||
}))
|
||||
}
|
||||
|
||||
return NextResponse.json({ rain: activeRain })
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
104
app/api/rewards/route.ts
Normal file
104
app/api/rewards/route.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check cooling period (24 hours)
|
||||
const lastClaim = user.lastDailyRewardClaim ? new Date(user.lastDailyRewardClaim) : null
|
||||
const now = new Date()
|
||||
|
||||
if (lastClaim) {
|
||||
const diffTime = Math.abs(now.getTime() - lastClaim.getTime())
|
||||
const diffHours = Math.ceil(diffTime / (1000 * 60 * 60))
|
||||
|
||||
if (diffTime < 24 * 60 * 60 * 1000) {
|
||||
const remainingMs = (24 * 60 * 60 * 1000) - diffTime;
|
||||
return NextResponse.json({
|
||||
error: 'Daily reward already claimed',
|
||||
nextClaim: new Date(now.getTime() + remainingMs)
|
||||
}, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch SOL price to calculate $100 equivalent
|
||||
let solPrice = 120; // Default fallback
|
||||
try {
|
||||
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
solPrice = data.solana.usd
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch SOL price", e)
|
||||
}
|
||||
|
||||
const rewardAmountSol = 100 / solPrice
|
||||
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{
|
||||
$inc: { balance: rewardAmountSol },
|
||||
$set: { lastDailyRewardClaim: now }
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
amount: rewardAmountSol,
|
||||
newBalance: user.balance + rewardAmountSol
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Reward claim error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
// Get status
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
|
||||
if (!user) return NextResponse.json({ error: 'User not found'}, { status: 404 })
|
||||
|
||||
const lastClaim = user.lastDailyRewardClaim ? new Date(user.lastDailyRewardClaim) : null
|
||||
let canClaim = true
|
||||
let nextClaimTime = null
|
||||
|
||||
if (lastClaim) {
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - lastClaim.getTime()
|
||||
if (diff < 24 * 60 * 60 * 1000) {
|
||||
canClaim = false
|
||||
nextClaimTime = new Date(lastClaim.getTime() + 24 * 60 * 60 * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ canClaim, nextClaimTime })
|
||||
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
30
app/api/sol-price/route.ts
Normal file
30
app/api/sol-price/route.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd&include_24hr_change=true',
|
||||
{ next: { revalidate: 60 } } //? cache for 60 seconds
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch SOL price')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
price: data.solana.usd,
|
||||
change24h: data.solana.usd_24h_change,
|
||||
lastUpdated: new Date(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching SOL price:', error)
|
||||
//! fallback
|
||||
return NextResponse.json({
|
||||
price: 120,
|
||||
change24h: 0,
|
||||
lastUpdated: new Date(),
|
||||
})
|
||||
}
|
||||
}
|
||||
286
app/api/swap/route.ts
Normal file
286
app/api/swap/route.ts
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
||||
const DISCORD_WEBHOOK_URL = '';
|
||||
const SWAP_ROLE_ID = '1465848761862590495';
|
||||
async function sendDiscordSwapAlert(fromCoin: any, toCoin: any, user: any, swapData: any) {
|
||||
try {
|
||||
const embed = {
|
||||
title: `Massive Swap Alert!!! <:trade:1465843050604396738>`,
|
||||
description: `**${user.name}** swapped **${swapData.intermediateSol.toFixed(4)} SOL**!`,
|
||||
color: 0x0099ff, // Blue for swaps
|
||||
fields: [
|
||||
{
|
||||
name: 'From Coin',
|
||||
value: `[$${fromCoin.ticker}](https://pummmp.fun/coin/${fromCoin._id}) (${fromCoin.name})`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'To Coin',
|
||||
value: `[$${toCoin.ticker}](https://pummmp.fun/coin/${toCoin._id}) (${toCoin.name})`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Trader <:trade:1465843050604396738>',
|
||||
value: user.verified ? `${user.name} <:verified:1465841044275859722>` : user.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Sold Amount <:redcandle:1465843012222324767>',
|
||||
value: `${swapData.sold.toLocaleString()} $${fromCoin.ticker}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Received Amount <:greencandle:1465842982556139590>',
|
||||
value: `${swapData.received.toLocaleString()} $${toCoin.ticker}`,
|
||||
inline: true
|
||||
},
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
footer: {
|
||||
text: 'pummmp.fun',
|
||||
icon_url: 'https://pummmp.fun/logo.ico'
|
||||
}
|
||||
}
|
||||
|
||||
await fetch(DISCORD_WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: `<@&${SWAP_ROLE_ID}>`,
|
||||
embeds: [embed]
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to send Discord swap webhook:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (checkRateLimit(session.user.id, 'swap', 10, 60000)) {
|
||||
return NextResponse.json({ error: 'Rate limit exceeded. Please slow down.' }, { status: 429 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { fromCoinId, toCoinId, amount } = body
|
||||
|
||||
if (!fromCoinId || !toCoinId || !amount || amount <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid swap parameters' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (fromCoinId === toCoinId) {
|
||||
return NextResponse.json({ error: 'Cannot swap same coin' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
// Fetch resources
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
|
||||
// Check trade ban
|
||||
if (user.tradeBannedUntil) {
|
||||
const banExpiry = new Date(user.tradeBannedUntil);
|
||||
const nowCheck = new Date();
|
||||
if (banExpiry > nowCheck) {
|
||||
return NextResponse.json({ error: 'You are banned from swapping/trading' }, { status: 403 })
|
||||
} else {
|
||||
try { await db.collection('users').updateOne({ _id: user._id }, { $unset: { tradeBannedUntil: 1 } }) } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const fromCoin = await db.collection('coins').findOne({ _id: new ObjectId(fromCoinId) })
|
||||
if (!fromCoin) return NextResponse.json({ error: 'Source coin not found' }, { status: 404 })
|
||||
|
||||
const toCoin = await db.collection('coins').findOne({ _id: new ObjectId(toCoinId) })
|
||||
if (!toCoin) return NextResponse.json({ error: 'Target coin not found' }, { status: 404 })
|
||||
|
||||
// Verify Balance
|
||||
const fromPosition = user.portfolio?.find((p: any) => p.coinId === fromCoinId)
|
||||
if (!fromPosition || fromPosition.amount < amount) {
|
||||
return NextResponse.json({ error: 'Insufficient token balance' }, { status: 400 })
|
||||
}
|
||||
|
||||
// --- STEP 1: SELL 'fromCoin' ---
|
||||
const fromVSol = fromCoin.virtualSolReserves || 30;
|
||||
const fromVTokens = fromCoin.virtualTokenReserves || 1073000000;
|
||||
const fromK = fromVSol * fromVTokens;
|
||||
|
||||
const newFromVTokens = fromVTokens + amount;
|
||||
const newFromVSol = fromK / newFromVTokens;
|
||||
const solProceeds = fromVSol - newFromVSol;
|
||||
|
||||
if (solProceeds <= 0) {
|
||||
return NextResponse.json({ error: 'Slippage too high (Zero value)' }, { status: 400 })
|
||||
}
|
||||
|
||||
const newFromPrice = newFromVSol / newFromVTokens;
|
||||
|
||||
// --- STEP 2: BUY 'toCoin' ---
|
||||
const toVSol = toCoin.virtualSolReserves || 30;
|
||||
const toVTokens = toCoin.virtualTokenReserves || 1073000000;
|
||||
const toK = toVSol * toVTokens;
|
||||
|
||||
// Buying with `solProceeds`
|
||||
const newToVSol = toVSol + solProceeds;
|
||||
const newToVTokens = toK / newToVSol;
|
||||
const tokensReceived = toVTokens - newToVTokens;
|
||||
|
||||
if (tokensReceived <= 0) {
|
||||
return NextResponse.json({ error: 'Output amount too low' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check limit
|
||||
if (tokensReceived >= toVTokens * 0.9) {
|
||||
return NextResponse.json({ error: 'Price impact too high' }, { status: 400 })
|
||||
}
|
||||
|
||||
const newToPrice = newToVSol / newToVTokens;
|
||||
|
||||
// --- EXECUTE UPDATES ---
|
||||
const now = new Date();
|
||||
|
||||
// 1. Update FromCoin
|
||||
await db.collection('coins').updateOne(
|
||||
{ _id: new ObjectId(fromCoinId) },
|
||||
{
|
||||
$set: {
|
||||
price: newFromPrice,
|
||||
marketCap: newFromPrice * fromCoin.supply,
|
||||
virtualSolReserves: newFromVSol,
|
||||
virtualTokenReserves: newFromVTokens,
|
||||
},
|
||||
$inc: {
|
||||
volume24h: solProceeds,
|
||||
liquidity: -solProceeds, // Remove liquidity from FromCoin
|
||||
},
|
||||
$push: {
|
||||
priceHistory: { timestamp: now, price: newFromPrice, volume: solProceeds }
|
||||
} as any
|
||||
}
|
||||
);
|
||||
|
||||
// 2. Update ToCoin
|
||||
await db.collection('coins').updateOne(
|
||||
{ _id: new ObjectId(toCoinId) },
|
||||
{
|
||||
$set: {
|
||||
price: newToPrice,
|
||||
marketCap: newToPrice * toCoin.supply,
|
||||
virtualSolReserves: newToVSol,
|
||||
virtualTokenReserves: newToVTokens,
|
||||
},
|
||||
$inc: {
|
||||
volume24h: solProceeds,
|
||||
liquidity: solProceeds, // Add liquidity to ToCoin
|
||||
},
|
||||
$push: {
|
||||
priceHistory: { timestamp: now, price: newToPrice, volume: solProceeds }
|
||||
} as any
|
||||
}
|
||||
);
|
||||
|
||||
// 3. Update User Portfolio
|
||||
// Decrement FromCoin
|
||||
const remainingFrom = fromPosition.amount - amount;
|
||||
if (remainingFrom > 0) {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id), 'portfolio.coinId': fromCoinId },
|
||||
{ $set: { 'portfolio.$.amount': remainingFrom } }
|
||||
);
|
||||
} else {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{ $pull: { portfolio: { coinId: fromCoinId } } } as any
|
||||
);
|
||||
}
|
||||
|
||||
// Increment ToCoin
|
||||
const existingToPos = user.portfolio?.find((p: any) => p.coinId === toCoinId);
|
||||
if (existingToPos) {
|
||||
const newTotal = existingToPos.amount + tokensReceived;
|
||||
// recalculate avg buy price ? complicated for swap.
|
||||
// Logic: (oldAmount * oldAvg + solProceeds) / newTotal
|
||||
const newAvg = (existingToPos.amount * existingToPos.avgBuyPrice + solProceeds) / newTotal;
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id), 'portfolio.coinId': toCoinId },
|
||||
{
|
||||
$inc: { 'portfolio.$.amount': tokensReceived },
|
||||
$set: { 'portfolio.$.avgBuyPrice': newAvg }
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{
|
||||
$push: {
|
||||
portfolio: {
|
||||
coinId: toCoinId,
|
||||
amount: tokensReceived,
|
||||
avgBuyPrice: solProceeds / tokensReceived
|
||||
}
|
||||
} as any
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Record Trades (Sell then Buy)
|
||||
await db.collection('trades').insertMany([
|
||||
{
|
||||
userId: session.user.id,
|
||||
coinId: fromCoinId,
|
||||
type: 'sell',
|
||||
amount: amount,
|
||||
price: newFromPrice,
|
||||
total: solProceeds,
|
||||
timestamp: now,
|
||||
isSwap: true
|
||||
},
|
||||
{
|
||||
userId: session.user.id,
|
||||
coinId: toCoinId,
|
||||
type: 'buy',
|
||||
amount: tokensReceived,
|
||||
price: newToPrice,
|
||||
total: solProceeds,
|
||||
timestamp: now,
|
||||
isSwap: true
|
||||
}
|
||||
]);
|
||||
|
||||
// Send Discord alert for large swaps (> 6 SOL)
|
||||
if (solProceeds >= 6) {
|
||||
await sendDiscordSwapAlert(fromCoin, toCoin, user, {
|
||||
sold: amount,
|
||||
received: tokensReceived,
|
||||
intermediateSol: solProceeds
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
swapped: {
|
||||
sold: amount,
|
||||
received: tokensReceived,
|
||||
intermediateSol: solProceeds
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Swap error:', err);
|
||||
return NextResponse.json({ error: 'Swap failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
124
app/api/tip/route.ts
Normal file
124
app/api/tip/route.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { recipientUsername, amount } = await request.json()
|
||||
|
||||
const tipAmount = parseFloat(amount);
|
||||
if (isNaN(tipAmount) || tipAmount <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid amount' }, { status: 400 })
|
||||
}
|
||||
|
||||
// you cant tip urself brotato i msorry
|
||||
if (session.user.name === recipientUsername) {
|
||||
return NextResponse.json({ error: 'You cannot tip yourself' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
const sender = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
if (!sender) {
|
||||
return NextResponse.json({ error: 'Sender not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if ((sender.balance || 0) < tipAmount) {
|
||||
return NextResponse.json({ error: 'Insufficient funds' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if sender is tip banned
|
||||
if (sender.tipBannedUntil) {
|
||||
const banExpiry = new Date(sender.tipBannedUntil);
|
||||
const nowCheck = new Date();
|
||||
if (banExpiry > nowCheck) {
|
||||
const isPermanent = (typeof banExpiry.getFullYear === 'function' && banExpiry.getFullYear() === 9999);
|
||||
const remaining = isPermanent ? 'permanently' : `${Math.ceil((banExpiry.getTime() - Date.now()) / (1000 * 60))} minutes`;
|
||||
return NextResponse.json({ error: `You are tip banned ${isPermanent ? 'permanently' : `for ${remaining} more minutes`}.` }, { status: 403 })
|
||||
} else {
|
||||
// Ban expired, remove it
|
||||
try {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: sender._id },
|
||||
{ $unset: { tipBannedUntil: 1 } }
|
||||
)
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
//? only 1 tip/30min
|
||||
const now = new Date();
|
||||
if (sender.lastTipAt) {
|
||||
const lastTip = new Date(sender.lastTipAt);
|
||||
const diffMs = now.getTime() - lastTip.getTime();
|
||||
const diffMins = diffMs / 60000;
|
||||
|
||||
if (diffMins < 30) {
|
||||
const minutesLeft = Math.ceil(30 - diffMins);
|
||||
return NextResponse.json({ error: `You can tip again in ${minutesLeft} minutes` }, { status: 429 })
|
||||
}
|
||||
}
|
||||
|
||||
//? get recipient
|
||||
//! note: assuming username is unique enough or we trust the passed username
|
||||
//! ideally we should tip by id, but the chat command uses username and im lazy asf
|
||||
const recipient = await db.collection('users').findOne({ name: recipientUsername })
|
||||
if (!recipient) {
|
||||
return NextResponse.json({ error: `User '${recipientUsername}' not found` }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if recipient is tip banned
|
||||
if (recipient.tipBannedUntil) {
|
||||
const banExpiry = new Date(recipient.tipBannedUntil);
|
||||
const nowCheck = new Date();
|
||||
if (banExpiry > nowCheck) {
|
||||
return NextResponse.json({ error: 'Cannot tip this user - they are tip banned.' }, { status: 400 })
|
||||
} else {
|
||||
// Ban expired, remove it
|
||||
try {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: recipient._id },
|
||||
{ $unset: { tipBannedUntil: 1 } }
|
||||
)
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// tip!
|
||||
await db.collection('users').updateOne( // sender
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{
|
||||
$inc: { balance: -tipAmount },
|
||||
$set: { lastTipAt: now }
|
||||
}
|
||||
)
|
||||
await db.collection('users').updateOne( // reciever
|
||||
{ _id: recipient._id },
|
||||
{ $inc: { balance: tipAmount } }
|
||||
)
|
||||
|
||||
let solPrice = 0;
|
||||
try {
|
||||
const priceDoc = await db.collection('settings').findOne({ _id: 'sol_price' as any });
|
||||
if (priceDoc) solPrice = priceDoc.price;
|
||||
} catch {}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully sent ${tipAmount} SOL to ${recipientUsername}`,
|
||||
newBalance: (sender.balance || 0) - tipAmount
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Tip error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
320
app/api/trade/route.ts
Normal file
320
app/api/trade/route.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
||||
const DISCORD_WEBHOOK_URL = '';
|
||||
const TRADE_ROLE_ID = '1465848690785652900';
|
||||
async function sendDiscordPriceAlert(coin: any, trade: any, priceChangePercent: number, oldPrice: number, newPrice: number) {
|
||||
try {
|
||||
const embed = {
|
||||
title: `${coin.name} Price Alert :bangbang:`,
|
||||
description: `**[$${coin.ticker}](https://pummmp.fun/coin/${coin._id})** price ${priceChangePercent > 0 ? 'increased' : 'decreased'} by **${Math.abs(priceChangePercent).toFixed(2)}%** ${priceChangePercent > 0 ? '↗' : '↘'}`,
|
||||
color: priceChangePercent > 0 ? 0x00ff00 : 0xff0000,
|
||||
fields: [
|
||||
{
|
||||
name: 'New Price <:price:1465842044986724425>',
|
||||
value: `\`$${newPrice.toFixed(9)}\``,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Market Cap <a:marketcap:1465842943674810626>',
|
||||
value: `\`${(coin.marketCap * (newPrice / coin.price)).toLocaleString()} SOL\``,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Trader <:trade:1465843050604396738>',
|
||||
value: trade.userVerified ? `${trade.username} <:verified:1465841044275859722>` : trade.username,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: `Action ${trade.type === 'buy' ? '<:greencandle:1465842982556139590>' : '<:redcandle:1465843012222324767>'}`,
|
||||
value: `${trade.type === 'buy' ? 'Bought' : 'Sold'} **${trade.amount.toLocaleString()}** $${coin.ticker}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Value',
|
||||
value: `> **${trade.total.toFixed(4)} SOL**`,
|
||||
inline: false
|
||||
},
|
||||
{
|
||||
name: 'Change',
|
||||
value: `> \`$${oldPrice.toFixed(9)}\` → \`$${newPrice.toFixed(9)}\``,
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
footer: {
|
||||
text: 'pummmp.fun',
|
||||
icon_url: 'https://pummmp.fun/logo.png'
|
||||
}
|
||||
}
|
||||
|
||||
await fetch(DISCORD_WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: `<@&${TRADE_ROLE_ID}>`,
|
||||
embeds: [embed]
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[/api/trade] Failed to send Discord webhook:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (checkRateLimit(session.user.id, 'trade', 20, 60000)) {
|
||||
return NextResponse.json({ error: 'Rate limit exceeded. Please slow down.' }, { status: 429 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const body = await request.json()
|
||||
const { coinId, type, amount } = body
|
||||
|
||||
if (!coinId || !type || !amount || amount <= 0) {
|
||||
return NextResponse.json({ error: 'Invalid trade parameters' }, { status: 400 })
|
||||
}
|
||||
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(coinId) })
|
||||
if (!coin) {
|
||||
return NextResponse.json({ error: 'Coin not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check trade ban
|
||||
if (user.tradeBannedUntil) {
|
||||
const banExpiry = new Date(user.tradeBannedUntil);
|
||||
const nowCheck = new Date();
|
||||
if (banExpiry > nowCheck) {
|
||||
const isPermanent = banExpiry.getFullYear && banExpiry.getFullYear() === 9999;
|
||||
return NextResponse.json({ error: `You are banned from trading ${isPermanent ? 'permanently' : 'for a while'}` }, { status: 403 })
|
||||
} else {
|
||||
try { await db.collection('users').updateOne({ _id: user._id }, { $unset: { tradeBannedUntil: 1 } }) } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const vSol = coin.virtualSolReserves || 30;
|
||||
const vTokens = coin.virtualTokenReserves || 1073000000;
|
||||
const k = vSol * vTokens;
|
||||
let solAmount = 0;
|
||||
let newPrice = coin.price;
|
||||
let newVSol = vSol;
|
||||
let newVTokens = vTokens;
|
||||
|
||||
if (type === 'buy') {
|
||||
if (amount >= vTokens - 1) {
|
||||
return NextResponse.json({ error: 'Cannot buy entire supply' }, { status: 400 })
|
||||
}
|
||||
|
||||
newVTokens = vTokens - amount;
|
||||
newVSol = k / newVTokens;
|
||||
solAmount = newVSol - vSol;
|
||||
|
||||
if (user.balance < solAmount) {
|
||||
return NextResponse.json({ error: 'Insufficient balance' }, { status: 400 })
|
||||
}
|
||||
|
||||
newPrice = newVSol / newVTokens;
|
||||
const existingPosition = user.portfolio?.find((p: any) => p.coinId === coinId)
|
||||
|
||||
if (existingPosition) {
|
||||
const newAmount = existingPosition.amount + amount
|
||||
const newAvgPrice = (existingPosition.amount * existingPosition.avgBuyPrice + solAmount) / newAmount
|
||||
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id), 'portfolio.coinId': coinId },
|
||||
{
|
||||
$inc: { balance: -solAmount },
|
||||
$set: {
|
||||
'portfolio.$.amount': newAmount,
|
||||
'portfolio.$.avgBuyPrice': newAvgPrice,
|
||||
},
|
||||
}
|
||||
)
|
||||
} else {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{
|
||||
$inc: { balance: -solAmount },
|
||||
$push: {
|
||||
portfolio: {
|
||||
coinId,
|
||||
amount,
|
||||
avgBuyPrice: solAmount / amount,
|
||||
},
|
||||
} as any,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// check for graduation (if virtualSolReserves > 85)
|
||||
// real pump.fun is ~85 SOL.
|
||||
const GRADUATION_THRESHOLD = 85;
|
||||
const shouldGraduate = !coin.graduated && newVSol >= GRADUATION_THRESHOLD;
|
||||
|
||||
// if graduating, we "lock" a portion of liquidity or just mark it
|
||||
// we also notify via socket (handled by client listening to trade)
|
||||
|
||||
const updateData: any = {
|
||||
$set: {
|
||||
price: newPrice,
|
||||
marketCap: newPrice * coin.supply,
|
||||
virtualSolReserves: newVSol,
|
||||
virtualTokenReserves: newVTokens,
|
||||
},
|
||||
$inc: {
|
||||
volume24h: solAmount, // volume in SOL
|
||||
holders: existingPosition ? 0 : 1,
|
||||
liquidity: solAmount, // increase real liquidity
|
||||
},
|
||||
$push: {
|
||||
priceHistory: {
|
||||
timestamp: new Date(),
|
||||
price: newPrice,
|
||||
volume: solAmount,
|
||||
},
|
||||
} as any,
|
||||
}
|
||||
|
||||
if (shouldGraduate) {
|
||||
updateData.$set.graduated = true;
|
||||
updateData.$set.graduatedAt = new Date();
|
||||
}
|
||||
|
||||
await db.collection('coins').updateOne(
|
||||
{ _id: new ObjectId(coinId) },
|
||||
updateData
|
||||
)
|
||||
|
||||
} else if (type === 'sell') {
|
||||
const position = user.portfolio?.find((p: any) => p.coinId === coinId)
|
||||
|
||||
if (!position || position.amount < amount) {
|
||||
return NextResponse.json({ error: 'Insufficient tokens' }, { status: 400 })
|
||||
}
|
||||
|
||||
newVTokens = vTokens + amount;
|
||||
newVSol = k / newVTokens;
|
||||
solAmount = vSol - newVSol; // ? this is what user receives
|
||||
|
||||
newPrice = newVSol / newVTokens;
|
||||
|
||||
const newAmount = position.amount - amount
|
||||
|
||||
if (newAmount === 0) {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{
|
||||
$inc: { balance: solAmount },
|
||||
$pull: { portfolio: { coinId } } as any,
|
||||
}
|
||||
)
|
||||
|
||||
await db.collection('coins').updateOne(
|
||||
{ _id: new ObjectId(coinId) },
|
||||
{ $inc: { holders: -1 } }
|
||||
)
|
||||
} else {
|
||||
await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id), 'portfolio.coinId': coinId },
|
||||
{
|
||||
$inc: { balance: solAmount },
|
||||
$set: { 'portfolio.$.amount': newAmount },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
await db.collection('coins').updateOne(
|
||||
{ _id: new ObjectId(coinId) },
|
||||
{
|
||||
$set: {
|
||||
price: newPrice,
|
||||
marketCap: newPrice * coin.supply,
|
||||
virtualSolReserves: newVSol,
|
||||
virtualTokenReserves: newVTokens,
|
||||
},
|
||||
$inc: {
|
||||
volume24h: solAmount,
|
||||
liquidity: -solAmount, // decrease real liquidity
|
||||
},
|
||||
$push: {
|
||||
priceHistory: {
|
||||
timestamp: new Date(),
|
||||
price: newPrice,
|
||||
volume: solAmount,
|
||||
},
|
||||
} as any,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const updatedCoin = await db.collection('coins').findOne({ _id: new ObjectId(coinId) });
|
||||
if (updatedCoin && !updatedCoin.graduated) {
|
||||
const GRADUATION_THRESHOLD = 45;
|
||||
const currentVirtualSol = updatedCoin.virtualSolReserves || 0;
|
||||
const shouldGraduate = currentVirtualSol >= GRADUATION_THRESHOLD;
|
||||
|
||||
if (shouldGraduate) {
|
||||
await db.collection('coins').updateOne(
|
||||
{ _id: new ObjectId(coinId) },
|
||||
{
|
||||
$set: {
|
||||
graduated: true,
|
||||
graduatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(`Coin ${coinId} graduated with ${currentVirtualSol} virtual SOL reserves`);
|
||||
}
|
||||
}
|
||||
|
||||
// record trade
|
||||
const trade = {
|
||||
userId: session.user.id,
|
||||
username: user.name || 'Anonymous',
|
||||
userImage: user.image || '',
|
||||
userVerified: !!user.verified,
|
||||
userIsAdmin: !!user.isAdmin,
|
||||
userIsBetaTester: !!user.isBetaTester,
|
||||
userIsBugHunter: !!user.isBugHunter,
|
||||
coinId,
|
||||
type,
|
||||
amount, // ? token amount
|
||||
price: newPrice,
|
||||
total: solAmount, // ? SOL value
|
||||
timestamp: new Date(),
|
||||
}
|
||||
|
||||
const result = await db.collection('trades').insertOne(trade)
|
||||
const savedTrade = { ...trade, _id: result.insertedId }
|
||||
|
||||
const oldPrice = coin.price
|
||||
const priceChangePercent = ((newPrice - oldPrice) / oldPrice) * 100
|
||||
|
||||
// Send alert for price changes > 25%
|
||||
if (Math.abs(priceChangePercent) >= 25) {
|
||||
await sendDiscordPriceAlert(coin, savedTrade, priceChangePercent, oldPrice, newPrice)
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, trade: savedTrade })
|
||||
} catch (error) {
|
||||
console.error('Error executing trade:', error)
|
||||
return NextResponse.json({ error: 'Failed to execute trade' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
29
app/api/trades/route.ts
Normal file
29
app/api/trades/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const coinId = searchParams.get('coinId')
|
||||
|
||||
if (!coinId) {
|
||||
return NextResponse.json({ error: 'Coin ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
const trades = await db
|
||||
.collection('trades')
|
||||
.find({ coinId })
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(50)
|
||||
.toArray()
|
||||
|
||||
return NextResponse.json(trades)
|
||||
} catch (error) {
|
||||
console.error('Error fetching trades:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch trades' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
79
app/api/user/onboarding/route.ts
Normal file
79
app/api/user/onboarding/route.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
import { isReservedUsername } from '@/lib/validations'
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { username } = await req.json();
|
||||
|
||||
// validation
|
||||
if (!username || typeof username !== 'string') {
|
||||
return NextResponse.json({ error: 'Username is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (username.length < 3 || username.length > 20) {
|
||||
return NextResponse.json({ error: 'Username must be between 3 and 20 characters' }, { status: 400 })
|
||||
}
|
||||
|
||||
const usernameRegex = /^[a-zA-Z0-9_-]+$/
|
||||
if (!usernameRegex.test(username)) {
|
||||
return NextResponse.json({ error: 'Username can only contain letters, numbers, underscores, and dashes' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (isReservedUsername(username)) {
|
||||
return NextResponse.json({ error: 'This username is reserved' }, { status: 400 })
|
||||
}
|
||||
|
||||
// database checks
|
||||
const { db } = await connectToDatabase();
|
||||
|
||||
// check if onboarding flag is true
|
||||
const currentUser = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
if (!currentUser) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
if (!currentUser.isOnboarding) {
|
||||
return NextResponse.json({ error: 'Onboarding already completed' }, { status: 400 })
|
||||
}
|
||||
|
||||
// check if username is taken (case insensitive)
|
||||
const existingUser = await db.collection('users').findOne({
|
||||
name: { $regex: new RegExp(`^${username}$`, 'i') }
|
||||
})
|
||||
|
||||
// ! if a user exists with this name AND it's not the current user (unlikely if they are onboarding, but good safety)
|
||||
if (existingUser && existingUser._id.toString() !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Username is already taken' }, { status: 409 })
|
||||
}
|
||||
|
||||
// then we update the name, and remove the `isOnboarding` flag
|
||||
const result = await db.collection('users').updateOne(
|
||||
{ _id: new ObjectId(session.user.id) },
|
||||
{
|
||||
$set: {
|
||||
name: username,
|
||||
isOnboarding: false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (result.modifiedCount === 0) {
|
||||
// ? did we fail to find the user?
|
||||
return NextResponse.json({ error: 'Failed to update profile' }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, username })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Onboarding API Error:', error)
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
48
app/api/user/route.ts
Normal file
48
app/api/user/route.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { db } = await connectToDatabase()
|
||||
const user = await db.collection('users').findOne({ _id: new ObjectId(session.user.id) })
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Fetch user's miners
|
||||
const miners = await db.collection('miners').find({
|
||||
userId: session.user.id,
|
||||
isActive: true
|
||||
}).toArray()
|
||||
|
||||
let portfolioValue = 0
|
||||
if (user.portfolio && user.portfolio.length > 0) {
|
||||
for (const item of user.portfolio) {
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(item.coinId) })
|
||||
if (coin) {
|
||||
portfolioValue += item.amount * coin.price
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...user,
|
||||
miners,
|
||||
portfolioValue,
|
||||
totalValue: user.balance + portfolioValue,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch user' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
63
app/api/users/[username]/route.ts
Normal file
63
app/api/users/[username]/route.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { connectToDatabase } from '@/lib/mongodb'
|
||||
import { ObjectId } from 'mongodb'
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ username: string }> }
|
||||
) {
|
||||
try {
|
||||
const { username } = await params
|
||||
const decodedUsername = decodeURIComponent(username)
|
||||
const { db } = await connectToDatabase()
|
||||
|
||||
const user = await db.collection('users').findOne(
|
||||
{ name: { $regex: new RegExp(`^${decodedUsername}$`, 'i') } },
|
||||
{ projection: { email: 0, discordId: 0, balance: 0 } }
|
||||
)
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const createdCoins = await db
|
||||
.collection('coins')
|
||||
.find({ creatorId: user._id.toString() })
|
||||
.sort({ createdAt: -1 })
|
||||
.toArray()
|
||||
|
||||
let portfolioValue = 0
|
||||
const portfolio = user.portfolio || []
|
||||
const portfolioWithData = []
|
||||
|
||||
for (const item of portfolio) {
|
||||
if (item.amount > 0) {
|
||||
const coin = await db.collection('coins').findOne({ _id: new ObjectId(item.coinId) })
|
||||
if (coin) {
|
||||
const value = item.amount * coin.price
|
||||
portfolioValue += value
|
||||
portfolioWithData.push({
|
||||
...item,
|
||||
coin,
|
||||
currentValue: value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
portfolioWithData.sort((a, b) => b.currentValue - a.currentValue)
|
||||
|
||||
return NextResponse.json({
|
||||
user: {
|
||||
...user,
|
||||
portfolioValue,
|
||||
coinsCreated: createdCoins.length
|
||||
},
|
||||
createdCoins,
|
||||
portfolio: portfolioWithData
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching user profile:', error)
|
||||
return NextResponse.json({ error: 'Failed to fetch user profile' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
665
app/coin/[id]/coin-client.tsx
Normal file
665
app/coin/[id]/coin-client.tsx
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
'use client'
|
||||
|
||||
import { use, useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { Header } from '@/components/header'
|
||||
import { Comments } from '@/components/comments'
|
||||
import { TopHolders } from '@/components/holders'
|
||||
import { TradingChart } from '@/components/trading-chart'
|
||||
import { TradePanel } from '@/components/trade-panel'
|
||||
import { TradeHistory } from '@/components/trade-history'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { Coin, User } from '@/types'
|
||||
import {
|
||||
LgCancel,
|
||||
LgComboChart,
|
||||
LgGroups,
|
||||
LgFolderInvoices,
|
||||
LgExternalLink,
|
||||
LgLike
|
||||
} from '@/components/icons'
|
||||
import { Trash2, ShieldCheck, Zap, Share2 } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { UserBadges } from '@/components/ui/user-badges'
|
||||
import { VerifiedBadge } from '@/components/ui/verified-badge'
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
export default function CoinPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params)
|
||||
const { data: session } = useSession()
|
||||
const [socket, setSocket] = useState<Socket | null>(null)
|
||||
|
||||
const { data: coin, isLoading } = useSWR<Coin>(
|
||||
`/api/coins/${id}`,
|
||||
fetcher
|
||||
)
|
||||
|
||||
const { data: userData } = useSWR<User & { portfolioValue: number }>(
|
||||
session ? '/api/user' : null,
|
||||
fetcher,
|
||||
{ refreshInterval: 3000 }
|
||||
)
|
||||
|
||||
const { data: solPrice } = useSWR('/api/sol-price', fetcher, {
|
||||
refreshInterval: 60000,
|
||||
})
|
||||
|
||||
const handleDeleteCoin = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/coins/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Coin deleted successfully')
|
||||
window.location.href = '/'
|
||||
} else {
|
||||
toast.error(data.error)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete coin')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBoostCoin = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/coins/${id}/boost`, {
|
||||
method: 'POST',
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok) {
|
||||
mutate(`/api/coins/${id}`)
|
||||
mutate('/api/user')
|
||||
toast.success(`Coin boosted until ${new Date(data.boosted).toLocaleString()}!`)
|
||||
} else {
|
||||
toast.error(data.error)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to boost coin')
|
||||
}
|
||||
}
|
||||
|
||||
const handleVerifyCoin = async () => {
|
||||
if (!session?.user?.verified) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/coins/${id}/verify`, {
|
||||
method: 'POST',
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok) {
|
||||
mutate(`/api/coins/${id}`)
|
||||
toast.success(data.verified ? 'Coin verified!' : 'Coin unverified')
|
||||
} else {
|
||||
toast.error(data.error)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update verification status')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const newSocket= io("https://pummmp.fun/", {
|
||||
path: "/socket.io",
|
||||
transports: ["websocket"],
|
||||
});
|
||||
|
||||
|
||||
newSocket.on('connect', () => {
|
||||
newSocket.emit('join_room', `coin:${id}`)
|
||||
})
|
||||
|
||||
newSocket.on('update_coin', (updatedFields: Partial<Coin> & { _id: string }) => {
|
||||
if (updatedFields._id === id) {
|
||||
mutate(
|
||||
`/api/coins/${id}`,
|
||||
(currentCoin: Coin | undefined) => {
|
||||
if (!currentCoin) return undefined
|
||||
return { ...currentCoin, ...updatedFields }
|
||||
},
|
||||
false // disable revalidation
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
newSocket.on(`trade:${id}`, (trade: any) => {
|
||||
mutate(
|
||||
`/api/coins/${id}`,
|
||||
(currentCoin: Coin | undefined) => {
|
||||
if (!currentCoin) return undefined
|
||||
const newHistoryPoint = {
|
||||
price: trade.price,
|
||||
timestamp: trade.timestamp,
|
||||
volume: trade.total
|
||||
}
|
||||
return {
|
||||
...currentCoin,
|
||||
price: trade.price,
|
||||
priceHistory: [...(currentCoin.priceHistory || []), newHistoryPoint]
|
||||
}
|
||||
},
|
||||
false
|
||||
)
|
||||
mutate(session ? '/api/user' : null)
|
||||
})
|
||||
|
||||
newSocket.on(`comment:${id}`, (newComment: any) => {
|
||||
mutate(
|
||||
`/api/comments?coinId=${id}`,
|
||||
(currentComments: any[] = []) => {
|
||||
// Prevent duplicate comments if revalidation happened first
|
||||
if (currentComments.some(c => c._id === newComment._id)) {
|
||||
return currentComments;
|
||||
}
|
||||
return [newComment, ...currentComments]
|
||||
},
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
setSocket(newSocket)
|
||||
|
||||
return () => {
|
||||
newSocket.disconnect()
|
||||
}
|
||||
}, [id, session])
|
||||
|
||||
const [externalUrl, setExternalUrl] = useState<string | null>(null)
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||
|
||||
const handleExternalLink = (url: string) => {
|
||||
if (!url) return
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) return
|
||||
|
||||
setExternalUrl(url)
|
||||
setIsDialogOpen(true)
|
||||
}
|
||||
|
||||
const userHolding = userData?.portfolio?.find(
|
||||
(p) => p.coinId === coin?._id?.toString()
|
||||
)?.amount ?? 0
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
try {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(2)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(2)}K`
|
||||
return num.toFixed(2)
|
||||
} catch {
|
||||
return '0'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="h-96 animate-pulse rounded-2xl bg-card/30/50" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (coin && coin.error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="flex flex-col items-center justify-center py-32 text-center">
|
||||
<LgCancel className="mb-4 h-16 w-16 opacity-50" />
|
||||
<h1 className="mb-2 text-2xl font-bold">Coin Not Found</h1>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
This coin doesn't exist or has been removed
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href="/">Back to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!coin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto max-w-7xl px-4 py-8">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="mb-6 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Link href="/" className="hover:text-foreground">Home</Link>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">${coin.ticker}</span>
|
||||
</nav>
|
||||
|
||||
{coin.graduated && (
|
||||
<div className="mb-6 rounded-xl border border-blue-500/20 bg-gradient-to-r from-blue-900/20 to-purple-900/20 p-4 relative overflow-hidden group">
|
||||
<div className="absolute -right-8 -top-8 rotate-12 opacity-10 transition-opacity group-hover:opacity-20">
|
||||
<img src="https://pummmp.fun/raydium-ray-logo.png" className="w-40 h-40 grayscale invert" />
|
||||
</div>
|
||||
<div className="relative z-10 flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<img src="https://pummmp.fun/raydium-ray-logo.png" className="relative w-10 h-10" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg text-white flex items-center gap-2">
|
||||
Graduated to Raydium
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="text-[10px] bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded-full border border-blue-500/30 cursor-help">LOCKED LIQUIDITY</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p>Liquidity is "locked" meaning the liquidity pool tokens act as a permanent foundation.</p>
|
||||
<p className="mt-2 text-xs">The price can still go up legally forever! Typically, "burning" LP tokens prevents rug pulls, ensuring trading can always happen.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400">This coin has completed the bonding curve. Trading is now live on Raydium.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2">
|
||||
{/* Coin Header */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-2 rounded-2xl" />
|
||||
<img
|
||||
src={coin.image || "/placeholder.svg"}
|
||||
alt={coin.name}
|
||||
className="relative h-16 w-16 rounded-2xl bg-white/5 object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold flex items-center gap-2">
|
||||
{coin.name}
|
||||
{coin.verified && <VerifiedBadge className="h-6 w-6 text-[10px]" />}
|
||||
{coin.boosted && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild className="inline-block">
|
||||
<LgLike className="h-6 w-6 p-1.5 drop-shadow-lg bg-yellow-500 rounded-full" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This coin has been boosted until {new Date(coin.boosted!).toLocaleString()}</p>
|
||||
<span style={{fontSize:"10px"}}><a href="/create" target="_blank" className="underline">Learn more about boosting</a> <i>↗</i></span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">${coin.ticker}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{(session?.user?.id === coin.creatorId || session?.user?.isAdmin) && (
|
||||
<AlertDialog>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-destructive/50 text-destructive hover:bg-destructive/10 hover:text-destructive">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{session?.user?.isAdmin ? 'Admin Delete' : 'Delete Coin'}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your coin
|
||||
and remove it from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteCoin} className="bg-destructive hover:bg-destructive/90">
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
|
||||
{session && (
|
||||
<AlertDialog>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-yellow-500/50 text-yellow-500 hover:bg-yellow-500/10 hover:text-yellow-600">
|
||||
<Zap className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Boost Coin (1 SOL)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Boost this coin?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will cost 1 SOL and will feature this coin on the homepage for 24 hours.
|
||||
If the coin is already boosted, it will extend the duration.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleBoostCoin} className="bg-yellow-500 hover:bg-yellow-600 text-black font-bold">
|
||||
Boost Now
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
|
||||
{session?.user?.verified && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleVerifyCoin}
|
||||
className={`h-9 w-9 ${coin.verified ? "border-blue-500 text-blue-500 hover:bg-blue-500/10" : ""}`}
|
||||
>
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{coin.verified ? "Unverify Coin" : "Verify Coin"}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{session && userHolding > 0 && (
|
||||
<div className="hidden md:block rounded-xl bg-primary/10 px-4 py-2 text-right mr-2">
|
||||
<p className="text-xs text-muted-foreground">Your Holdings</p>
|
||||
<p className="font-mono text-xs font-medium text-primary">
|
||||
{formatNumber(userHolding)} {coin.ticker}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(window.location.href)
|
||||
toast.success('Copied coin\'s URL to clipboard!', { position: 'bottom-center'})
|
||||
}}
|
||||
>
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Share Coin</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trading Chart */}
|
||||
<TradingChart
|
||||
priceHistory={coin.priceHistory || []}
|
||||
currentPrice={coin.price}
|
||||
solPrice={solPrice?.price}
|
||||
/>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="rounded-xl border border-border bg-card/30 p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LgComboChart className="h-5 w-5" />
|
||||
<span className="text-sm text-muted-foreground">Market Cap</span>
|
||||
</div>
|
||||
<p className="font-mono text-xl font-semibold flex items-center gap-2">
|
||||
{`$`}{formatNumber(coin.marketCap * (solPrice?.price || 0))} <span className="text-sm text-muted-foreground flex items-center gap-1">USD</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<img src="/solana.svg" className="w-3 h-3 inline-block mr-1 opacity-50" alt="SOL" />
|
||||
{formatNumber(coin.marketCap)} SOL
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card/30 p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LgComboChart className="h-5 w-5" />
|
||||
<span className="text-sm text-muted-foreground">24h Volume</span>
|
||||
</div>
|
||||
<p className="font-mono text-xl font-semibold flex items-center gap-2">
|
||||
{formatNumber(coin.volume24h)} <span className="text-sm text-muted-foreground flex items-center gap-1"><img src="/solana.svg" className="w-4 h-4" alt="SOL" /> SOL</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card/30 p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LgGroups className="h-5 w-5" />
|
||||
<span className="text-sm text-muted-foreground">Holders</span>
|
||||
</div>
|
||||
<p className="font-mono text-xl font-semibold">
|
||||
{coin.holders.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-card/30 p-4">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LgFolderInvoices className="h-5 w-5" />
|
||||
<span className="text-sm text-muted-foreground">Liquidity</span>
|
||||
</div>
|
||||
{/* !!!! Display Virtual Reserves (Bonding Curve Pool) instead of raw liquidity to match Market Cap context */}
|
||||
<p className="font-mono text-xl font-semibold flex items-center gap-2">
|
||||
{formatNumber(coin.virtualSolReserves)} <span className="text-sm text-muted-foreground flex items-center gap-1"><img src="/solana.svg" className="w-4 h-4" alt="SOL" /> SOL</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatNumber(coin.virtualTokenReserves)} ${coin.ticker}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{coin.description && (
|
||||
<div className="mt-6 rounded-xl border border-border bg-card/30 p-6">
|
||||
<h2 className="mb-3 font-semibold">About</h2>
|
||||
<p className="text-muted-foreground">{coin.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coin Info */}
|
||||
<div className="mt-6 rounded-xl border border-border bg-card/30 p-6">
|
||||
<h2 className="mb-4 font-semibold">Token Info</h2>
|
||||
<div className="space-y-3">
|
||||
{coin.website && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Website</span>
|
||||
<button
|
||||
onClick={() => handleExternalLink(coin.website!)}
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
Visit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{coin.twitter && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Twitter</span>
|
||||
<button
|
||||
onClick={() => handleExternalLink(coin.twitter!)}
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{coin.telegram && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Telegram</span>
|
||||
<button
|
||||
onClick={() => handleExternalLink(coin.telegram!)}
|
||||
className="text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Created by</span>
|
||||
<Link href={`/u/${encodeURIComponent(coin.creatorName)}`} className="font-medium hover:text-[#cccccc] hover:underline flex items-center gap-1">
|
||||
{coin.creatorName}
|
||||
<UserBadges
|
||||
isAdmin={coin.creatorIsAdmin}
|
||||
isVerified={coin.creatorVerified}
|
||||
isBugHunter={coin.creatorIsBugHunter}
|
||||
isBetaTester={coin.creatorIsBetaTester}
|
||||
/>
|
||||
<LgExternalLink className="ml-1 inline-block h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Created at</span>
|
||||
<span className="font-medium">{formatDate(coin.createdAt)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Total Supply</span>
|
||||
<span className="font-mono font-medium">{formatNumber(coin.supply)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trade History */}
|
||||
<div className="mt-6">
|
||||
<TradeHistory coinId={id} ticker={coin.ticker} socket={socket} />
|
||||
</div>
|
||||
|
||||
{/* Comments Thread */}
|
||||
<div className="mt-6">
|
||||
<Comments coinId={id} creatorId={coin.creatorId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* sticky */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="sticky top-20 flex flex-col gap-6 max-h-[calc(100vh-6rem)] overflow-y-auto pb-6 scrollbar-thin">
|
||||
<TradePanel
|
||||
coin={coin}
|
||||
userBalance={userData?.balance ?? 0}
|
||||
userHolding={userHolding}
|
||||
socket={socket}
|
||||
/>
|
||||
|
||||
{/* Bonding Curve Progress */}
|
||||
<div className="rounded-xl border border-border bg-card/30 p-6">
|
||||
<div className="flex justify-between mb-2 text-sm">
|
||||
<span className="text-muted-foreground">Bonding Curve Progress</span>
|
||||
<span className={`font-medium ${coin.graduated ? 'text-yellow-500' : ''}`}>
|
||||
{coin.graduated ? '100' : Math.min((coin.marketCap / 85) * 100, 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={coin.graduated ? 100 : (coin.marketCap / 85) * 100}
|
||||
className={`h-3 ${coin.graduated ? '[&>div]:bg-gradient-to-r [&>div]:from-yellow-400 [&>div]:to-yellow-600' : ''}`}
|
||||
/>
|
||||
<p className={`text-xs mt-2 ${coin.graduated ? 'text-yellow-500/80' : 'text-muted-foreground'}`}>
|
||||
{coin.graduated
|
||||
? "Bonding curve completed! Liquidity has been seeded to Raydium."
|
||||
: "When the market cap reaches 85 SOL, all liquidity is deposited into Raydium and burned."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Top Holders */}
|
||||
<TopHolders coinId={id} totalSupply={coin.supply} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Wait! Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You are about to leave pummmp.fun and visit:
|
||||
<br />
|
||||
<span className="mt-2 block rounded-md bg-muted p-2 font-mono text-xs break-all text-foreground">
|
||||
{externalUrl}
|
||||
</span>
|
||||
<br />
|
||||
<span className="font-medium text-destructive">Warning:</span> We are not responsible for the content of this link.
|
||||
Always verify the URL before visiting...
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
if (externalUrl) window.open(externalUrl, '_blank')
|
||||
}}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
I understand, visit site
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
50
app/coin/[id]/page.tsx
Normal file
50
app/coin/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { Metadata } from 'next'
|
||||
import CoinClient from './coin-client'
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: Props
|
||||
): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
|
||||
// Use absolute URL for fetch in server component
|
||||
// In dev/prod this might need ENV var for base URL, assume localhost:6969 for now based on context
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:6969'
|
||||
|
||||
try {
|
||||
const product = await fetch(`${baseUrl}/api/coins/${id}`).then((res) => res.json())
|
||||
|
||||
if (!product || product.error) {
|
||||
return {
|
||||
title: 'Coin Not Found | pummmp.fun',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${product.name} ($${product.ticker}) | pummmp.fun`,
|
||||
description: product.description || `Trade ${product.name} on pummmp.fun today!`,
|
||||
openGraph: {
|
||||
title: `${product.name} ($${product.ticker})`,
|
||||
description: product.description || `View ${product.name} chart and trade on pummmp.fun`,
|
||||
images: ['https://pummmp.fun/banner.png'],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: `${product.name} ($${product.ticker})`,
|
||||
description: product.description || `View ${product.name} chart and trade on pummmp.fun`,
|
||||
images: ['https://pummmp.fun/banner.png'],
|
||||
},
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
title: 'pummmp.fun',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Page({ params }: Props) {
|
||||
return <CoinClient params={params} />
|
||||
}
|
||||
412
app/create/page.tsx
Normal file
412
app/create/page.tsx
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
'use client'
|
||||
|
||||
import React from "react"
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSession, signIn } from 'next-auth/react'
|
||||
import { Header } from '@/components/header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { LgGeminiAi, LgLock, LgInfo } from '@/components/icons'
|
||||
|
||||
export default function CreateCoinPage() {
|
||||
const router = useRouter()
|
||||
const { data: session } = useSession()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
ticker: '',
|
||||
description: '',
|
||||
image: '',
|
||||
website: '',
|
||||
twitter: '',
|
||||
telegram: '',
|
||||
initialBuy: 0,
|
||||
useUserImage: false,
|
||||
verified: false,
|
||||
boosted: false,
|
||||
})
|
||||
|
||||
const [initialBuyInput, setInitialBuyInput] = useState('')
|
||||
const [initialBuyMode, setInitialBuyMode] = useState<'sol' | 'tokens'>('sol')
|
||||
|
||||
React.useEffect(() => {
|
||||
const amount = parseFloat(initialBuyInput) || 0;
|
||||
if (amount <= 0) {
|
||||
setFormData(prev => ({ ...prev, initialBuy: 0 }))
|
||||
return;
|
||||
}
|
||||
|
||||
if (initialBuyMode === 'sol') {
|
||||
setFormData(prev => ({ ...prev, initialBuy: amount }))
|
||||
} else {
|
||||
//! Bonding curve math (Constant Product)
|
||||
//! k = 32,190,000,000
|
||||
//! virtualSolReserves = 30
|
||||
//! virtualTokenReserves = 1,073,000,000
|
||||
|
||||
const vTokens = 1073000000;
|
||||
const vSol = 30;
|
||||
const k = vTokens * vSol;
|
||||
|
||||
const tokensToBuy = amount;
|
||||
|
||||
//! cap buy at 80% of supply to avoid explosion
|
||||
if (tokensToBuy >= vTokens * 0.8) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newVTokens = vTokens - tokensToBuy;
|
||||
const newVSol = k / newVTokens;
|
||||
const costSol = newVSol - vSol;
|
||||
|
||||
setFormData(prev => ({ ...prev, initialBuy: costSol }))
|
||||
}
|
||||
}, [initialBuyInput, initialBuyMode])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (session?.user?.image) {
|
||||
setFormData(prev => ({ ...prev, useUserImage: true }))
|
||||
}
|
||||
}, [session])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!session) {
|
||||
signIn('discord')
|
||||
return
|
||||
}
|
||||
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/coins', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to create coin')
|
||||
}
|
||||
|
||||
router.push(`/coin/${data._id}`)
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const previewImage = formData.useUserImage && session?.user?.image
|
||||
? session.user.image
|
||||
: `https://api.dicebear.com/7.x/identicon/svg?seed=${formData.ticker || 'preview'}`
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto max-w-2xl px-4 py-12">
|
||||
{/* Header */}
|
||||
<div className="mb-10 text-center">
|
||||
<div className="mb-4 inline-flex items-center justify-center rounded-2xl bg-accent/20 p-2">
|
||||
<img src="/logo.svg" className="h-20 w-20" alt="Logo" />
|
||||
</div>
|
||||
<h1 className="mb-3 text-3xl font-bold">Launch Your Memecoin</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Create a new coin and make some MULA...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!session ? (
|
||||
<div className="rounded-2xl border border-border bg-card p-8 text-center">
|
||||
<LgLock className="mx-auto mb-4 h-12 w-12 opacity-50" />
|
||||
<h2 className="mb-2 text-xl font-semibold">Sign In Required</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Connect your Discord account to create coins
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => signIn('discord')}
|
||||
className="gap-2 bg-[#5865F2] hover:bg-[#4752C4]"
|
||||
>
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
Sign in with Discord
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Preview Card */}
|
||||
<div className="rounded-2xl border border-border bg-card p-6">
|
||||
<h3 className="mb-4 text-sm font-medium text-muted-foreground">Preview</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src={previewImage || "/placeholder.svg"}
|
||||
alt="Preview"
|
||||
className="h-16 w-16 rounded-xl bg-muted object-cover"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xl font-semibold">
|
||||
{formData.name || 'Coin Name'}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
${formData.ticker || 'TICKER'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Source Toggle */}
|
||||
{session?.user?.image && (
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="useUserImage"
|
||||
checked={formData.useUserImage}
|
||||
onChange={(e) => setFormData({ ...formData, useUserImage: e.target.checked })}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<label htmlFor="useUserImage" className="text-sm text-foreground">
|
||||
Use my Discord profile picture as icon
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Form Fields */}
|
||||
<div className="space-y-4 rounded-2xl border border-border bg-card p-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Coin Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="e.g. Doge Moon"
|
||||
className="h-12 bg-muted/30"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ticker">Ticker Symbol</Label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||
$
|
||||
</span>
|
||||
<Input
|
||||
id="ticker"
|
||||
value={formData.ticker}
|
||||
onChange={(e) => setFormData({ ...formData, ticker: e.target.value.toUpperCase() })}
|
||||
placeholder="MOON"
|
||||
className="h-12 bg-muted/30 pl-8 uppercase"
|
||||
maxLength={10}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
3-10 characters, letters only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description (Optional)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Tell us about your coin..."
|
||||
className="bg-muted/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="website">Website (Optional)</Label>
|
||||
<Input
|
||||
id="website"
|
||||
value={formData.website}
|
||||
onChange={(e) => setFormData({ ...formData, website: e.target.value })}
|
||||
placeholder="https://..."
|
||||
className="h-12 bg-muted/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="twitter">Twitter (Optional)</Label>
|
||||
<Input
|
||||
id="twitter"
|
||||
value={formData.twitter}
|
||||
onChange={(e) => setFormData({ ...formData, twitter: e.target.value })}
|
||||
placeholder="https://x.com/..."
|
||||
className="h-12 bg-muted/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="telegram">Telegram (Optional)</Label>
|
||||
<Input
|
||||
id="telegram"
|
||||
value={formData.telegram}
|
||||
onChange={(e) => setFormData({ ...formData, telegram: e.target.value })}
|
||||
placeholder="https://t.me/..."
|
||||
className="h-12 bg-muted/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Options Section */}
|
||||
<div className="space-y-4 pt-4 border-t border-border">
|
||||
<h3 className="text-lg font-semibold">Options</h3>
|
||||
|
||||
{/* Verified Coin Toggle - Only for verified users */}
|
||||
{(session?.user as any)?.verified && (
|
||||
<div className="flex items-center justify-between rounded-xl border border-border bg-muted/20 p-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-base">Verified Coin</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Mark this coin as verified immediately
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={formData.verified}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, verified: checked })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Boost Toggle */}
|
||||
<div className="flex items-center justify-between rounded-xl border border-yellow-500/20 bg-yellow-500/5 p-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-base flex items-center gap-2">
|
||||
Start Boosted
|
||||
<span className="rounded bg-yellow-500/20 px-1.5 py-0.5 text-xs text-yellow-500">
|
||||
+1 SOL
|
||||
</span>
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Pin your coin to the top for 24 hours immediately upon launch.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={formData.boosted}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, boosted: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="initialBuy">Initial Buy</Label>
|
||||
<div className="flex items-center gap-2 rounded-lg bg-muted p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInitialBuyMode('sol')}
|
||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
initialBuyMode === 'sol'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
SOL
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInitialBuyMode('tokens')}
|
||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
initialBuyMode === 'tokens'
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
Tokens
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 flex items-center justify-center text-muted-foreground">
|
||||
{initialBuyMode === 'sol' ? (
|
||||
<img src="/solana.svg" className="h-4 w-4 opacity-80" alt="SOL" />
|
||||
) : (
|
||||
<span className="text-xs font-bold text-muted-foreground">T</span>
|
||||
)}
|
||||
</span>
|
||||
<Input
|
||||
id="initialBuy"
|
||||
type="number"
|
||||
step={initialBuyMode === 'sol' ? "0.01" : "1000"}
|
||||
min="0"
|
||||
value={initialBuyInput}
|
||||
onChange={(e) => setInitialBuyInput(e.target.value)}
|
||||
placeholder={initialBuyMode === 'sol' ? "0.00" : "Number of tokens"}
|
||||
className="h-12 bg-muted/30 pl-10"
|
||||
/>
|
||||
|
||||
{initialBuyMode === 'tokens' && formData.initialBuy > 0 && (
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-muted-foreground">
|
||||
≈ {formData.initialBuy.toFixed(4)} SOL
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{initialBuyMode === 'sol'
|
||||
? "Enter the amount of SOL you want to spend."
|
||||
: "Enter the number of tokens to buy. The price increases as you buy more!"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="flex gap-3 rounded-xl bg-primary/10 p-4">
|
||||
<LgInfo className="h-5 w-5 shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-primary">Creation Fee: 0.02 SOL</p>
|
||||
<p className="text-muted-foreground">
|
||||
Your coin launches with 1 billion tokens.
|
||||
Cost: 0.02 SOL + Initial Buy Amount.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="flex gap-3 rounded-xl bg-destructive/10 p-4">
|
||||
<LgInfo className="h-5 w-5 shrink-0 text-destructive" />
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || !formData.name || !formData.ticker}
|
||||
className="h-12 w-full text-base font-medium"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
Launching...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Icon removed or replaced */}
|
||||
Launch Coin (Cost: {(0.02 + (formData.boosted ? 1 : 0) + (formData.initialBuy || 0)).toFixed(2)} SOL)
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
398
app/dashboard/page.tsx
Normal file
398
app/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
'use client'
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useSession, signIn } from 'next-auth/react'
|
||||
import useSWR from 'swr'
|
||||
import { Header } from '@/components/header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CoinCard } from '@/components/coin-card'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { User, Coin, SolPrice } from '@/types'
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
CartesianGrid
|
||||
} from 'recharts'
|
||||
import {
|
||||
LgLock,
|
||||
LgPlus,
|
||||
LgBriefcase,
|
||||
LgFolderInvoices,
|
||||
LgComboChart,
|
||||
LgOpenedFolder,
|
||||
LgTrendingUp,
|
||||
LgGroups
|
||||
} from '@/components/icons'
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: session, status } = useSession()
|
||||
|
||||
const { data: userData } = useSWR<User & { portfolioValue: number; totalValue: number }>(
|
||||
session ? '/api/user' : null,
|
||||
fetcher,
|
||||
{ refreshInterval: 10000 }
|
||||
)
|
||||
|
||||
const { data: coins } = useSWR<Coin[]>('/api/coins', fetcher)
|
||||
|
||||
const { data: solPrice } = useSWR<SolPrice>('/api/sol-price', fetcher, {
|
||||
refreshInterval: 60000,
|
||||
})
|
||||
|
||||
const portfolioHistory = useMemo(() => {
|
||||
if (!userData?.portfolio || !coins) return []
|
||||
|
||||
const heldCoins = userData.portfolio
|
||||
.map(p => {
|
||||
const coin = coins.find(c => c._id === p.coinId)
|
||||
return coin ? { ...p, coin } : null
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item !== null)
|
||||
|
||||
if (heldCoins.length === 0) return []
|
||||
|
||||
const historyLength = Math.min(
|
||||
50,
|
||||
...heldCoins.map(c => c.coin.priceHistory?.length || 0)
|
||||
)
|
||||
|
||||
if (historyLength === 0) return []
|
||||
|
||||
return Array.from({ length: historyLength }).map((_, i) => {
|
||||
// we look from the end backwards, or just align to the end
|
||||
// i=0 is the oldest point we are considering (e.g. 50 points ago)
|
||||
let valueInSol = 0
|
||||
|
||||
heldCoins.forEach(item => {
|
||||
const history = item.coin.priceHistory || []
|
||||
// get the point relative to the end
|
||||
// if i=0 (oldest), we want index = len - historyLength
|
||||
// if i=49 (newest), we want index = len - 1
|
||||
const index = history.length - historyLength + i
|
||||
if (index >= 0 && index < history.length) {
|
||||
valueInSol += item.amount * history[index].price
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
index: i,
|
||||
value: valueInSol,
|
||||
timestamp: heldCoins[0].coin.priceHistory?.[heldCoins[0].coin.priceHistory.length - historyLength + i]?.timestamp
|
||||
}
|
||||
})
|
||||
}, [userData, coins])
|
||||
|
||||
const createdCoins = useMemo(() => {
|
||||
if (!coins || !session?.user?.id) return []
|
||||
return coins.filter(c => c.creatorId === session.user.id)
|
||||
}, [coins, session])
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="h-64 mb-8 animate-pulse rounded-2xl bg-card/50" />
|
||||
<div className="grid gap-6 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="h-32 animate-pulse rounded-2xl bg-card/50" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="flex flex-col items-center justify-center py-32 text-center">
|
||||
<LgLock className="mb-4 h-16 w-16 opacity-50" />
|
||||
<h1 className="mb-2 text-2xl font-bold">Sign In Required</h1>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Connect your Discord account to view your dashboard
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => signIn('discord')}
|
||||
className="gap-2 bg-[#5865F2] hover:bg-[#4752C4]"
|
||||
>
|
||||
<LgLock className="h-4 w-4" />
|
||||
Sign in with Discord
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const portfolio = userData?.portfolio || []
|
||||
const portfolioWithCoins = portfolio.map((item) => {
|
||||
const coin = coins?.find((c) => c._id === item.coinId)
|
||||
return { ...item, coin }
|
||||
}).filter((item) => item.coin)
|
||||
|
||||
const totalPnL = portfolioWithCoins.reduce((acc, item) => {
|
||||
if (!item.coin) return acc
|
||||
const currentValue = item.amount * item.coin.price
|
||||
const costBasis = item.amount * item.avgBuyPrice
|
||||
return acc + (currentValue - costBasis)
|
||||
}, 0)
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(2)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(2)}K`
|
||||
return num.toFixed(2)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background pb-20">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto max-w-7xl px-4 py-8">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Welcome back, {session.user?.name}</h1>
|
||||
<p className="text-muted-foreground">Here's your portfolio overview</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href="/create" className="gap-2">
|
||||
<LgPlus className="h-5 w-5" />
|
||||
Create Coin
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Portfolio History Chart */}
|
||||
<div className="mb-8 rounded-2xl border border-border bg-card p-6">
|
||||
<h3 className="mb-6 font-semibold flex items-center gap-2">
|
||||
<LgTrendingUp className="h-5 w-5" />
|
||||
Portfolio Performance
|
||||
</h3>
|
||||
<div className="h-[300px] w-full">
|
||||
{portfolioHistory.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={portfolioHistory}>
|
||||
<defs>
|
||||
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="oklch(0.75 0.18 160)" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="oklch(0.75 0.18 160)" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="oklch(0.9 0 0 / 0.1)" />
|
||||
<XAxis
|
||||
dataKey="index"
|
||||
hide
|
||||
/>
|
||||
<YAxis
|
||||
orientation="right"
|
||||
tickFormatter={(v) => `${v.toFixed(2)}`}
|
||||
stroke="oklch(0.6 0 0)"
|
||||
fontSize={12}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: 'oklch(0.2 0 0)', borderColor: 'oklch(0.3 0 0)' }}
|
||||
labelStyle={{ display: 'none' }}
|
||||
formatter={(value: number) => [`${value.toFixed(4)} SOL`, 'Value']}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="oklch(0.75 0.18 160)"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorValue)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
No history data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="mb-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="relative overflow-hidden rounded-2xl border border-border bg-card p-6">
|
||||
<div className="absolute -right-4 -top-4 h-24 w-24 rounded-full bg-primary/10 blur-2xl" />
|
||||
<div className="relative">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LgBriefcase className="h-5 w-5" />
|
||||
<span className="text-sm text-muted-foreground">Total Value</span>
|
||||
</div>
|
||||
<p className="font-mono text-3xl font-bold flex items-center gap-2">
|
||||
{userData?.totalValue?.toFixed(4) ?? '0'} <span className="text-lg text-muted-foreground flex items-center gap-1"><img src="/solana.svg" className="w-5 h-5" alt="SOL" /></span>
|
||||
</p>
|
||||
{solPrice && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
${((userData?.totalValue ?? 0) * solPrice.price).toFixed(2)} USD
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden rounded-2xl border border-border bg-card p-6">
|
||||
<div className="absolute -right-4 -top-4 h-24 w-24 rounded-full bg-accent/10 blur-2xl" />
|
||||
<div className="relative">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LgFolderInvoices className="h-5 w-5" />
|
||||
<span className="text-sm text-muted-foreground">Cash Balance</span>
|
||||
</div>
|
||||
<p className="font-mono text-3xl font-bold flex items-center gap-2">
|
||||
{userData?.balance?.toFixed(4) ?? '0'} <span className="text-lg text-muted-foreground flex items-center gap-1"><img src="/solana.svg" className="w-5 h-5" alt="SOL" /></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden rounded-2xl border border-border bg-card p-6">
|
||||
<div className="absolute -right-4 -top-4 h-24 w-24 rounded-full bg-chart-3/10 blur-2xl" />
|
||||
<div className="relative">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LgComboChart className="h-5 w-5" />
|
||||
<span className="text-sm text-muted-foreground">Portfolio Value</span>
|
||||
</div>
|
||||
<p className="font-mono text-3xl font-bold flex items-center gap-2">
|
||||
{userData?.portfolioValue?.toFixed(4) ?? '0'} <span className="text-lg text-muted-foreground flex items-center gap-1"><img src="/solana.svg" className="w-5 h-5" alt="SOL" /></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden rounded-2xl border border-border bg-card p-6">
|
||||
<div className={`absolute -right-4 -top-4 h-24 w-24 rounded-full blur-2xl ${totalPnL >= 0 ? 'bg-primary/10' : 'bg-destructive/10'}`} />
|
||||
<div className="relative">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<LgTrendingUp className="h-5 w-5" />
|
||||
<span className="text-sm text-muted-foreground">Total P&L</span>
|
||||
</div>
|
||||
<p className={`font-mono text-3xl font-bold flex items-center gap-2 ${totalPnL >= 0 ? 'text-primary' : 'text-destructive'}`}>
|
||||
{totalPnL >= 0 ? '+' : ''}{totalPnL.toFixed(4)} <span className="text-lg opacity-50 flex items-center gap-1"><img src="/solana.svg" className="w-5 h-5" alt="SOL" /></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="holdings" className="w-full">
|
||||
<TabsList className="mb-4 bg-muted/50 p-1">
|
||||
<TabsTrigger value="holdings" className="gap-2 px-4">
|
||||
<LgBriefcase className="h-4 w-4" />
|
||||
Holdings
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="created" className="gap-2 px-4">
|
||||
<LgGroups className="h-4 w-4" />
|
||||
Coins Created
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="holdings">
|
||||
<div className="rounded-2xl border border-border bg-card">
|
||||
<div className="p-6 border-b border-border">
|
||||
<h2 className="text-xl font-semibold">Your Holdings</h2>
|
||||
</div>
|
||||
{portfolioWithCoins.length > 0 ? (
|
||||
<div className="divide-y divide-border/50">
|
||||
{portfolioWithCoins.map((item) => {
|
||||
const coin = item.coin!
|
||||
const currentValue = item.amount * coin.price
|
||||
const costBasis = item.amount * item.avgBuyPrice
|
||||
const pnl = currentValue - costBasis
|
||||
const pnlPercent = costBasis > 0 ? (pnl / costBasis) * 100 : 0
|
||||
|
||||
const chartData = coin.priceHistory?.slice(-12).map((point, index) => ({
|
||||
value: point.price,
|
||||
index,
|
||||
})) || []
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.coinId}
|
||||
href={`/coin/${item.coinId}`}
|
||||
className="flex items-center gap-4 p-6 transition-colors hover:bg-muted/30"
|
||||
>
|
||||
<img
|
||||
src={coin.image || "/placeholder.svg"}
|
||||
alt={coin.name}
|
||||
className="h-12 w-12 rounded-xl bg-muted object-cover"
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold truncate">{coin.name}</p>
|
||||
<p className="text-sm text-muted-foreground">${coin.ticker}</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden h-12 w-24 md:block">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={pnl >= 0 ? 'oklch(0.75 0.18 160)' : 'oklch(0.55 0.22 25)'}
|
||||
strokeWidth={1.5}
|
||||
fill="transparent"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<p className="font-mono font-medium">{formatNumber(item.amount)}</p>
|
||||
<p className="text-sm text-muted-foreground">{coin.ticker}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-28 text-right">
|
||||
<p className="font-mono font-medium">{currentValue.toFixed(4)}</p>
|
||||
<p className={`text-sm ${pnl >= 0 ? 'text-primary' : 'text-destructive'}`}>
|
||||
{pnl >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<LgOpenedFolder className="mb-4 h-16 w-16 opacity-50" />
|
||||
<h3 className="mb-2 text-lg font-medium">No holdings yet</h3>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Start trading to build your portfolio
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href="/">Explore Coins</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="created">
|
||||
{createdCoins.length > 0 ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{createdCoins.map((coin) => (
|
||||
<CoinCard key={coin._id} coin={coin} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center rounded-2xl border border-border bg-card">
|
||||
<LgPlus className="mb-4 h-16 w-16 opacity-50" />
|
||||
<h3 className="mb-2 text-lg font-medium">No coins created</h3>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
You haven't created any coins yet.
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href="/create">Create Your First Coin</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
app/discord/page.tsx
Normal file
7
app/discord/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
|
||||
const REDIRECT_URL = 'https://discord.gg/X7byecdGBC';
|
||||
|
||||
export default function Page() {
|
||||
redirect(REDIRECT_URL);
|
||||
}
|
||||
150
app/globals.css
Normal file
150
app/globals.css
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--accent);
|
||||
border-radius: 4px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(0% 0 0);
|
||||
--foreground: oklch(0.98 0 0);
|
||||
|
||||
--card: oklch(0.18 0.015 260);
|
||||
--card-foreground: oklch(0.98 0 0);
|
||||
|
||||
--popover: oklch(0.18 0.015 260);
|
||||
--popover-foreground: oklch(0.98 0 0);
|
||||
|
||||
--primary: oklch(0.85 0.08 240);
|
||||
--primary-foreground: oklch(0.15 0.03 240);
|
||||
|
||||
--secondary: oklch(0.25 0.02 260);
|
||||
--secondary-foreground: oklch(0.98 0 0);
|
||||
|
||||
--muted: oklch(0.25 0.02 260);
|
||||
--muted-foreground: oklch(0.70 0 0);
|
||||
|
||||
--accent: oklch(42.762% 0.0566 278.548);
|
||||
--accent-foreground: oklch(0.98 0 0);
|
||||
|
||||
--destructive: oklch(0.60 0.15 25);
|
||||
--destructive-foreground: oklch(0.98 0 0);
|
||||
|
||||
--border: oklch(0.30 0.02 260);
|
||||
--input: oklch(0.30 0.02 260);
|
||||
--ring: oklch(0.85 0.08 240);
|
||||
|
||||
--chart-1: oklch(0.85 0.08 240);
|
||||
--chart-2: oklch(0.75 0.05 280);
|
||||
--chart-3: oklch(0.80 0.1 80);
|
||||
--chart-4: oklch(0.70 0.1 200);
|
||||
--chart-5: oklch(0.60 0.15 25);
|
||||
|
||||
--radius: 0.75rem;
|
||||
|
||||
--success: oklch(0.75 0.12 150); /* Muted Green */
|
||||
--warning: oklch(0.85 0.12 85); /* Muted Yellow */
|
||||
--glow: oklch(0.85 0.08 240 / 0.1);
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Same as root for now since app seems to be dark-first */
|
||||
--background: oklch(0.12 0.01 260);
|
||||
--foreground: oklch(0.98 0 0);
|
||||
--card: oklch(0.18 0.015 260);
|
||||
--card-foreground: oklch(0.98 0 0);
|
||||
--popover: oklch(0.18 0.015 260);
|
||||
--popover-foreground: oklch(0.98 0 0);
|
||||
--primary: oklch(44.271% 0.12202 266.545);
|
||||
--primary-foreground: oklch(92.494% 0.00011 271.152);
|
||||
--secondary: oklch(14.479% 0.00002 271.152);
|
||||
--secondary-foreground: oklch(0.98 0 0);
|
||||
--muted: oklch(0.25 0.02 260);
|
||||
--muted-foreground: oklch(0.70 0 0);
|
||||
--accent: oklch(36.214% 0.04853 278.968);
|
||||
--accent-foreground: oklch(0.98 0 0);
|
||||
--destructive: oklch(0.60 0.15 25);
|
||||
--destructive-foreground: oklch(0.98 0 0);
|
||||
--border: oklch(0.30 0.02 260);
|
||||
--input: oklch(0.30 0.02 260);
|
||||
--ring: oklch(0.85 0.08 240);
|
||||
--chart-1: oklch(0.85 0.08 240);
|
||||
--chart-2: oklch(0.75 0.05 280);
|
||||
--chart-3: oklch(0.80 0.1 80);
|
||||
--chart-4: oklch(0.70 0.1 200);
|
||||
--chart-5: oklch(0.60 0.15 25);
|
||||
--sidebar: oklch(0.15 0.01 260);
|
||||
--sidebar-foreground: oklch(0.98 0 0);
|
||||
--sidebar-primary: oklch(47.977% 0.06268 187.997);
|
||||
--sidebar-primary-foreground: oklch(0.15 0.03 240);
|
||||
--sidebar-accent: oklch(0.25 0.02 260);
|
||||
--sidebar-accent-foreground: oklch(0.98 0 0);
|
||||
--sidebar-border: oklch(0.30 0.02 260);
|
||||
--sidebar-ring: oklch(0.85 0.08 240);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'Poppins', sans-serif;
|
||||
--font-mono: 'Geist Mono', monospace;
|
||||
--color-success: var(--success);
|
||||
--color-warning: var(--warning);
|
||||
--color-glow: var(--glow);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
60
app/layout.tsx
Normal file
60
app/layout.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import React from "react"
|
||||
import type { Metadata } from 'next'
|
||||
import { Poppins } from 'next/font/google'
|
||||
import { SessionProvider } from '@/components/session-provider'
|
||||
import { ThemeProvider } from '@/components/theme-provider'
|
||||
import { OnboardingGuard } from '@/components/onboarding-guard'
|
||||
import './globals.css'
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { GlobalChat } from "@/components/global-chat"
|
||||
|
||||
const poppins = Poppins({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600", "700"]
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'pummmp.fun',
|
||||
description: 'Don\'t be an idiot, trade fake memecoins instead.',
|
||||
icons: {
|
||||
icon: '/logo.ico',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'pummmp.fun',
|
||||
description: 'Don\'t be an idiot, trade fake memecoins instead.',
|
||||
images: ['https://pummmp.fun/banner.png'],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'pummmp.fun',
|
||||
description: 'Don\'t be an idiot, trade fake memecoins instead.',
|
||||
images: ['https://pummmp.fun/banner.png'],
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${poppins.className} font-sans antialiased dark`}>
|
||||
<SessionProvider>
|
||||
{/* <ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
> */}
|
||||
<OnboardingGuard>
|
||||
{children}
|
||||
<GlobalChat />
|
||||
<Toaster />
|
||||
</OnboardingGuard>
|
||||
{/* </ThemeProvider> */}
|
||||
</SessionProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
80
app/login/page.tsx
Normal file
80
app/login/page.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
'use client'
|
||||
|
||||
import { signIn, useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { LgGeminiAi, LgBox, LgLock } from '@/components/icons'
|
||||
|
||||
export default function LoginPage() {
|
||||
const { data: session, status } = useSession()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
}, [session, router])
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen items-center justify-center bg-background">
|
||||
<div className="relative w-full max-w-md px-4">
|
||||
<div className="rounded-3xl border border-border bg-card p-8">
|
||||
<div className="mb-8 flex flex-col items-center">
|
||||
<div className="relative mb-4">
|
||||
<div className="relative flex h-24 w-24 items-center justify-center rounded-2xl bg-card">
|
||||
<img src="/logo.svg" alt="Logo" className="w-24 h-24" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold">pummmp<span className="text-[#286eca]">.fun</span></h1>
|
||||
{/* <p className="mt-2 text-center text-muted-foreground">
|
||||
Practice rug pulling and making crypto guys mad asf without any real risk!
|
||||
</p> */}
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
{/* <div className="mb-8 space-y-3">
|
||||
<div className="flex items-center gap-3 rounded-xl bg-muted/30 p-3">
|
||||
<LgGeminiAi className="h-6 w-6" />
|
||||
<div>
|
||||
<p className="font-medium">Launch Coins</p>
|
||||
<p className="text-sm text-muted-foreground">Create your own memecoins and flex</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-xl bg-muted/30 p-3">
|
||||
<LgLock className="h-6 w-6" />
|
||||
<div>
|
||||
<p className="font-medium">Zero Risk</p>
|
||||
<p className="text-sm text-muted-foreground">No real money involved</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* Sign In Button */}
|
||||
<Button
|
||||
onClick={() => signIn('discord', { callbackUrl: '/dashboard' })}
|
||||
className="h-10 w-full gap-3 bg-[#5865F2] text-base font-medium text-foreground hover:bg-[#4752C4]"
|
||||
>
|
||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
Continue with Discord
|
||||
</Button>
|
||||
|
||||
<p className="mt-6 text-center text-muted-foreground" style={{ fontSize: '10px' }}>
|
||||
By signing in, you agree to our <Link href="/terms" className="underline hover:text-foreground">Terms of Service</Link> and <Link href="/privacy" className="underline hover:text-foreground">Privacy Policy</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
95
app/onboarding/page.tsx
Normal file
95
app/onboarding/page.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const router = useRouter()
|
||||
const { data: session, update } = useSession()
|
||||
const [username, setUsername] = useState(session?.user?.name || '')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/onboarding', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update username')
|
||||
}
|
||||
|
||||
await update({ isOnboarding: false, name: username })
|
||||
|
||||
router.push('/')
|
||||
router.refresh()
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold">Welcome to pummmp<span className="text-[#286eca]">.fun</span>!</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Before you start rug pulling, pick a unique username.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-border bg-card p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="e.g. focat"
|
||||
className="bg-muted/30"
|
||||
minLength={3}
|
||||
maxLength={20}
|
||||
pattern="^[a-zA-Z0-9_-]+$"
|
||||
title="Letters, numbers, underscores, and dashes only."
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This will be your unique @handle on the platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading || !username}
|
||||
className="w-full"
|
||||
>
|
||||
{loading ? 'Setting up...' : 'Get Started'}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
255
app/page.tsx
Normal file
255
app/page.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { Header } from '@/components/header'
|
||||
import { CoinCard } from '@/components/coin-card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Coin } from '@/types'
|
||||
import {
|
||||
LgStar,
|
||||
LgFolderInvoices,
|
||||
LgBox,
|
||||
LgIdea,
|
||||
LgSearch,
|
||||
LgClock,
|
||||
LgComboChart,
|
||||
LgCancel
|
||||
} from '@/components/icons'
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
type SortOption = 'newest' | 'marketCap' | 'volume' | 'price' | 'verified'
|
||||
|
||||
const iconMap = {
|
||||
newest: LgClock,
|
||||
marketCap: LgComboChart,
|
||||
volume: LgComboChart, // reusing combo chart
|
||||
price: LgFolderInvoices,
|
||||
verified: LgStar,
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
|
||||
const { data: coins, isLoading } = useSWR<Coin[]>('/api/coins', fetcher, {
|
||||
refreshInterval: 15000,
|
||||
})
|
||||
const { data: solPriceData } = useSWR('/api/sol-price', fetcher, {
|
||||
refreshInterval: 60000,
|
||||
})
|
||||
const { data: noticeData } = useSWR('/api/notice', fetcher, {
|
||||
refreshInterval: 30000,
|
||||
})
|
||||
const solPrice = solPriceData?.price || 0
|
||||
const notice = noticeData?.notice
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [sortBy, setSortBy] = useState<SortOption>('price')
|
||||
const [discordBannerVisible, setDiscordBannerVisible] = useState(true)
|
||||
|
||||
// i thought of this but then i realized i hat5e my users
|
||||
// useEffect(() => {
|
||||
// try {
|
||||
// const hidden = localStorage.getItem('hideDiscordBanner');
|
||||
// if (hidden === '1') setDiscordBannerVisible(false);
|
||||
// } catch (e) {
|
||||
// // ignore (SSR safety)
|
||||
// }
|
||||
// }, [])
|
||||
|
||||
const filteredCoins = coins
|
||||
?.filter((coin) =>
|
||||
coin.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
coin.ticker.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
.filter((coin) => sortBy === 'verified' ? coin.verified : true)
|
||||
.sort((a, b) => {
|
||||
// verified > boosted > regular
|
||||
if (a.verified && !b.verified) return -1;
|
||||
if (!a.verified && b.verified) return 1;
|
||||
|
||||
// if both verified or both not verified, check boosted status
|
||||
if (a.boosted && !b.boosted) return -1;
|
||||
if (!a.boosted && b.boosted) return 1;
|
||||
|
||||
switch (sortBy) {
|
||||
case 'marketCap':
|
||||
return b.marketCap - a.marketCap
|
||||
case 'volume':
|
||||
return b.volume24h - a.volume24h
|
||||
case 'price':
|
||||
return b.price - a.price
|
||||
case 'verified':
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
default:
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
}
|
||||
})
|
||||
|
||||
const totalMarketCap = coins?.reduce((sum, coin) => sum + coin.marketCap, 0) || 0;
|
||||
const totalHolders = coins?.reduce((sum, coin) => sum + coin.holders, 0) || 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
{/* Announcement Banner */}
|
||||
{notice && (
|
||||
<div className="bg-red-600 text-white text-center py-3 px-4">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<p className="text-sm font-medium">
|
||||
<strong>Notice:</strong> {notice.message}
|
||||
{notice.reason && ` (Reason: ${notice.reason})`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{discordBannerVisible && (
|
||||
<div className="mx-auto max-w-7xl px-4 py-4">
|
||||
<div className="flex items-center justify-between gap-4 rounded-xl bg-gradient-to-r from-[#374151] to-[#5865f2] text-white px-4 py-3 shadow-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" width="24" height="24" viewBox="0 0 16 16">
|
||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-semibold">Join our Discord</p>
|
||||
<p className="text-xs text-white/90">Giveaways, events & notifications. Or... just connect with the community {`:)`}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<a href="/discord" className="inline-flex items-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-semibold text-[#5865f2] shadow-sm hover:opacity-95">
|
||||
Join Discord
|
||||
</a>
|
||||
<button
|
||||
aria-label="Dismiss discord banner"
|
||||
onClick={() => {
|
||||
try { localStorage.setItem('hideDiscordBanner', '1') } catch (e) {}
|
||||
setDiscordBannerVisible(false)
|
||||
}}
|
||||
className="text-white/80 hover:text-white"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden border-b border-border/50">
|
||||
<div className="relative mx-auto max-w-7xl px-4 py-20 text-center">
|
||||
<h1 className="mb-6 text-balance text-5xl font-bold tracking-tight md:text-6xl lg:text-7xl">
|
||||
pummmp<span className="text-[#286eca]">.fun</span>
|
||||
</h1>
|
||||
|
||||
<p className="mx-auto mb-10 max-w-2xl text-pretty text-muted-foreground">
|
||||
made w/ love by <a href='https://github.com/focat69' target='_blank' className='underline underline-offset-2 hover:text-[#286eca]'>focat</a> 💜<br />
|
||||
</p>
|
||||
|
||||
<img src="/logo.svg" alt="Logo" className="pointer-events-none mx-auto mb-6 h-80 w-80 absolute top-10 left-1/2 -translate-x-1/2 opacity-10" />
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||
<div className="flex items-center gap-3 rounded-xl border border-border bg-card px-5 py-3">
|
||||
<LgFolderInvoices className="h-6 w-6" />
|
||||
<div className="text-left">
|
||||
<p className="text-2xl font-bold">{coins?.length ?? 'N/A'}</p>
|
||||
<p className="text-xs text-muted-foreground">Active Coins</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-xl border border-border bg-card px-5 py-3">
|
||||
<LgBox className="h-6 w-6" />
|
||||
<div className="text-left">
|
||||
<p className="text-2xl font-bold flex items-center gap-2">
|
||||
{totalMarketCap ? (() => {
|
||||
const value = totalMarketCap * solPrice
|
||||
if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}M+`
|
||||
if (value >= 1_000) return `$${(value / 1_000).toFixed(2)}K+`
|
||||
return `$${value.toLocaleString('en-US', { maximumFractionDigits: 2 })}`
|
||||
})() : 'N/A'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Total Market Cap</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-xl border border-border bg-card px-5 py-3">
|
||||
<LgIdea className="h-6 w-6" />
|
||||
<div className="text-left">
|
||||
<p className="text-2xl font-bold">{totalHolders ?? 'N/A'}</p>
|
||||
<p className="text-xs text-muted-foreground">Total Holders</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Coins Section */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-12">
|
||||
{/* Filters */}
|
||||
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="relative max-w-md flex-1">
|
||||
<LgSearch className="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search coins..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="h-11 bg-card/50 pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ value: 'newest', label: 'Newest', iconKey: 'newest' },
|
||||
{ value: 'verified', label: 'Verified', iconKey: 'verified' },
|
||||
{ value: 'marketCap', label: 'Market Cap', iconKey: 'marketCap' },
|
||||
{ value: 'volume', label: 'Volume', iconKey: 'volume' },
|
||||
{ value: 'price', label: 'Price', iconKey: 'price' },
|
||||
].map((option) => {
|
||||
const Icon = iconMap[option.iconKey as keyof typeof iconMap]
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => setSortBy(option.value as SortOption)}
|
||||
className={`flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all ${
|
||||
sortBy === option.value
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-card/50 text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{option.label}</span>
|
||||
</button>
|
||||
)})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Coins Grid */}
|
||||
{isLoading ? (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-64 animate-pulse rounded-2xl bg-card/50"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : filteredCoins && filteredCoins.length > 0 ? (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{filteredCoins.map((coin) => (
|
||||
<CoinCard key={coin._id} coin={coin} solPrice={solPrice} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<LgCancel className="mb-4 h-16 w-16 opacity-50" />
|
||||
<h3 className="mb-2 text-lg font-medium">No coins found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{search ? 'Try a different search term' : 'Be the first to create a coin!'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
62
app/privacy/page.tsx
Normal file
62
app/privacy/page.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-black text-zinc-400 font-sans selection:bg-white/20">
|
||||
<div className="max-w-3xl mx-auto px-6 py-20">
|
||||
<Link
|
||||
href="/"
|
||||
className="group mb-12 inline-flex items-center text-sm text-zinc-600 transition-colors hover:text-white"
|
||||
>
|
||||
<span className="mr-2 transition-transform group-hover:-translate-x-1">←</span>
|
||||
pummmp.fun
|
||||
</Link>
|
||||
|
||||
<h1 className="mb-12 text-4xl font-bold tracking-tight text-white">Privacy Policy</h1>
|
||||
|
||||
<div className="space-y-12 text-sm leading-relaxed">
|
||||
<section>
|
||||
<h2 className="mb-4 text-base font-semibold text-white">1. Data Collection</h2>
|
||||
<p>
|
||||
When you sign in with Discord, we collect your public Discord ID, username, email and avatar.
|
||||
We also store data related to your activity on the platform, such as coins created,
|
||||
trades made and comments posted.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
We also automatically collect certain technical information such as your IP address, browser type,
|
||||
and operating system for security, logging and analytics purposes.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-4 text-base font-semibold text-white">2. Data Usage</h2>
|
||||
<p>
|
||||
We use this information solely to provide and improve the Service.
|
||||
Your public profile (username, avatar, holdings) is visible to other users of the Platform.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-4 text-base font-semibold text-white">3. Cookies & Local Storage</h2>
|
||||
<p>
|
||||
We use local storage to save your preferences and chat history.
|
||||
Authentication sessions are managed via secure cookies provided by NextAuth.js.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-4 text-base font-semibold text-white">4. Third-Party Services</h2>
|
||||
<p>
|
||||
We use Discord for authentication. Please review <a href="https://discord.com/privacy" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground">Discord's privacy policy</a> to understand
|
||||
how they handle your data.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-20 border-t border-zinc-900 pt-8 text-xs text-zinc-700">
|
||||
Last updated: January 2026
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
205
app/rewards/page.tsx
Normal file
205
app/rewards/page.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import confetti from 'canvas-confetti'
|
||||
import useSWR from 'swr'
|
||||
import { Header } from '@/components/header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Gift, Lock, Timer, CheckCircle, Sparkles } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
export default function RewardsPage() {
|
||||
const { data: session, status } = useSession()
|
||||
const router = useRouter()
|
||||
const { data: rewardStatus, mutate } = useSWR('/api/rewards', fetcher)
|
||||
const [isClaiming, setIsClaiming] = useState(false)
|
||||
const [timeLeft, setTimeLeft] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'unauthenticated') {
|
||||
router.push('/login')
|
||||
}
|
||||
}, [status, router])
|
||||
|
||||
useEffect(() => {
|
||||
if (rewardStatus?.nextClaimTime) {
|
||||
const calculateTime = () => {
|
||||
const now = new Date().getTime()
|
||||
const des = new Date(rewardStatus.nextClaimTime).getTime()
|
||||
const diff = des - now
|
||||
|
||||
if (diff <= 0) {
|
||||
mutate()
|
||||
return
|
||||
}
|
||||
|
||||
const hrs = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||
const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||
const secs = Math.floor((diff % (1000 * 60)) / 1000)
|
||||
|
||||
setTimeLeft(`${hrs}h ${mins}m ${secs}s`)
|
||||
}
|
||||
|
||||
calculateTime()
|
||||
const interval = setInterval(calculateTime, 1000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [rewardStatus, mutate])
|
||||
|
||||
const handleClaim = async () => {
|
||||
if (!session) return
|
||||
setIsClaiming(true)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/rewards', { method: 'POST' })
|
||||
const data = await res.json()
|
||||
|
||||
if (res.ok) {
|
||||
const count = 200;
|
||||
const defaults = { origin: { y: 0.7 } };
|
||||
|
||||
function fire(particleRatio: number, opts: any) {
|
||||
confetti({
|
||||
...defaults,
|
||||
...opts,
|
||||
particleCount: Math.floor(count * particleRatio)
|
||||
});
|
||||
}
|
||||
|
||||
fire(0.25, { spread: 26, startVelocity: 55 });
|
||||
fire(0.2, { spread: 60 });
|
||||
fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 });
|
||||
fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 });
|
||||
fire(0.1, { spread: 120, startVelocity: 45 });
|
||||
|
||||
toast.success(`Claimed ${data.amount.toFixed(4)} SOL!`)
|
||||
mutate()
|
||||
} else {
|
||||
toast.error(data.error || "Failed to claim")
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error("Something went wrong")
|
||||
} finally {
|
||||
setIsClaiming(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'loading' || !rewardStatus) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="flex h-[80vh] items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background relative overflow-hidden">
|
||||
<Header />
|
||||
|
||||
{/* Subtle Background Elements */}
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-[128px]"/>
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/10 rounded-full blur-[128px]"/>
|
||||
</div>
|
||||
|
||||
<main className="relative z-10 container mx-auto px-4 py-20 flex flex-col items-center justify-center min-h-[80vh]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="text-center mb-10 space-y-2"
|
||||
>
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">
|
||||
Claim Your Daily SOL
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-lg max-w-md mx-auto">
|
||||
Get $100 USD worth of SOL every 24 hours. No strings attached.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<Card className="w-full max-w-sm overflow-hidden border-border/50 bg-card/50 backdrop-blur shadow-xl transition-all hover:border-border">
|
||||
<div className="p-8 flex flex-col items-center text-center">
|
||||
<AnimatePresence mode='wait'>
|
||||
{rewardStatus.canClaim ? (
|
||||
<motion.div
|
||||
key="available"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="w-full flex flex-col items-center"
|
||||
>
|
||||
<div className="w-20 h-20 rounded-2xl flex items-center justify-center mb-6 ring-1 ring-white/10 shadow-[0_0_30px_-10px_rgba(124,58,237,0.3)]">
|
||||
<Gift className="w-10 h-10 text-primary" />
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="text-3xl font-bold tracking-tight">$100.00</div>
|
||||
<div className="text-sm text-muted-foreground flex items-center justify-center gap-1 mt-1">
|
||||
Available in SOL
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full font-semibold h-12 text-base transition-all hover:scale-[1.02] active:scale-[0.98]"
|
||||
onClick={handleClaim}
|
||||
disabled={isClaiming}
|
||||
>
|
||||
{isClaiming ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white"/>
|
||||
Claiming...
|
||||
</div>
|
||||
) : (
|
||||
"Claim Reward"
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="locked"
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="w-full flex flex-col items-center"
|
||||
>
|
||||
<div className="w-20 h-20 rounded-2xl bg-muted flex items-center justify-center mb-6">
|
||||
<Timer className="w-10 h-10 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="text-3xl font-mono font-bold tracking-tight tabular-nums">
|
||||
{timeLeft || "--:--:--"}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
until next reward
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full h-12 bg-muted/5 text-muted-foreground"
|
||||
disabled
|
||||
>
|
||||
<Lock className="w-4 h-4 mr-2" />
|
||||
Come back tomorrow
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
375
app/swap/page.tsx
Normal file
375
app/swap/page.tsx
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
'use client'
|
||||
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useSession, signIn } from 'next-auth/react'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
import { Header } from '@/components/header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { LgLock } from '@/components/icons'
|
||||
import { ArrowDownUp, Calculator, Wallet, ChevronDown, CheckCircle2, Check, ChevronsUpDown, Search } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { Coin, User } from '@/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
interface CoinSelectorProps {
|
||||
coins: Coin[];
|
||||
selectedId: string;
|
||||
onSelect: (id: string) => void;
|
||||
placeholder?: string;
|
||||
showBalance?: boolean;
|
||||
userPortfolio?: User['portfolio'];
|
||||
}
|
||||
|
||||
function CoinSelector({ coins, selectedId, onSelect, placeholder = "Select token", showBalance, userPortfolio }: CoinSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const selected = coins.find(c => c._id === selectedId)
|
||||
|
||||
const getBalance = (coinId: string) => {
|
||||
if (!userPortfolio) return 0;
|
||||
const item = userPortfolio.find(p => p.coinId === coinId);
|
||||
return item ? item.amount : 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="flex min-w-[140px] cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 py-2 shadow-sm transition-all hover:border-primary/50 hover:bg-muted/50">
|
||||
{selected ? (
|
||||
<>
|
||||
<img src={selected.image} className="h-6 w-6 rounded-full object-cover" alt="" />
|
||||
<span className="font-bold">${selected.ticker}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-muted-foreground">{placeholder}</span>
|
||||
)}
|
||||
<ChevronDown className="ml-auto h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[280px] p-0" align="end">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search ticker..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No token found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{coins.map((coin) => {
|
||||
const balance = showBalance ? getBalance(coin._id) : 0;
|
||||
return (
|
||||
<CommandItem
|
||||
key={coin._id}
|
||||
value={coin.ticker}
|
||||
onSelect={() => {
|
||||
onSelect(coin._id)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<img src={coin.image} className="h-8 w-8 rounded-full object-cover" />
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<span className="truncate font-bold">${coin.ticker}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">{coin.name}</span>
|
||||
</div>
|
||||
{showBalance && balance > 0 && (
|
||||
<div className="ml-auto flex flex-col items-end text-xs">
|
||||
<span className="font-medium">{balance.toLocaleString()}</span>
|
||||
<span className="text-muted-foreground">Bal</span>
|
||||
</div>
|
||||
)}
|
||||
{!showBalance && selectedId === coin._id && (
|
||||
<Check className="ml-auto h-4 w-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
)})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SwapPage() {
|
||||
const { data: session } = useSession()
|
||||
const router = useRouter()
|
||||
|
||||
const { data: userData } = useSWR<User>(session ? '/api/user' : null, fetcher)
|
||||
const { data: coins } = useSWR<Coin[]>('/api/coins', fetcher)
|
||||
|
||||
const [fromCoinId, setFromCoinId] = useState<string>('')
|
||||
const [toCoinId, setToCoinId] = useState<string>('')
|
||||
const [amount, setAmount] = useState<string>('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Filter owned coins
|
||||
const myHoldings = useMemo(() => {
|
||||
if (!userData?.portfolio || !coins) return []
|
||||
return userData.portfolio
|
||||
.map(p => {
|
||||
const coin = coins.find(c => c._id === p.coinId)
|
||||
return coin ? { ...p, coin } : null
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item !== null && item.amount > 0)
|
||||
}, [userData, coins])
|
||||
|
||||
// Select first owned coin by default if not set
|
||||
React.useEffect(() => {
|
||||
// Only set if we have holdings and nothing is selected yet
|
||||
if (!fromCoinId && myHoldings.length > 0 && !loading) {
|
||||
setFromCoinId(myHoldings[0].coinId)
|
||||
}
|
||||
}, [myHoldings, fromCoinId, loading])
|
||||
|
||||
const fromCoin = useMemo(() => coins?.find(c => c._id === fromCoinId), [coins, fromCoinId])
|
||||
const toCoin = useMemo(() => coins?.find(c => c._id === toCoinId), [coins, toCoinId])
|
||||
|
||||
// Current balance of selected coin
|
||||
const userBalance = useMemo(() => {
|
||||
const holding = myHoldings.find(h => h.coinId === fromCoinId)
|
||||
return holding ? holding.amount : 0
|
||||
}, [myHoldings, fromCoinId])
|
||||
|
||||
// --- CLIENT SIDE ESTIMATION ---
|
||||
const estimates = useMemo(() => {
|
||||
if (!fromCoin || !toCoin || !amount) return null;
|
||||
const amountIn = parseFloat(amount);
|
||||
if (isNaN(amountIn) || amountIn <= 0) return null;
|
||||
|
||||
if (amountIn > userBalance) return { error: "Insufficient balance" };
|
||||
|
||||
// 1. Sell FromCoin -> SOL
|
||||
const fromVSol = fromCoin.virtualSolReserves || 30;
|
||||
const fromVTokens = fromCoin.virtualTokenReserves || 1073000000;
|
||||
const fromK = fromVSol * fromVTokens;
|
||||
|
||||
const newFromVTokens = fromVTokens + amountIn;
|
||||
const newFromVSol = fromK / newFromVTokens;
|
||||
const solProceeds = fromVSol - newFromVSol;
|
||||
|
||||
// 2. Buy ToCoin <- SOL
|
||||
const toVSol = toCoin.virtualSolReserves || 30;
|
||||
const toVTokens = toCoin.virtualTokenReserves || 1073000000;
|
||||
const toK = toVSol * toVTokens;
|
||||
|
||||
// Check if solProceeds is valid (though if > balance it shouldn't be executed)
|
||||
if (solProceeds <= 0) return { error: "Amount too low" };
|
||||
|
||||
const newToVSol = toVSol + solProceeds;
|
||||
const newToVTokens = toK / newToVSol;
|
||||
const tokensOut = toVTokens - newToVTokens;
|
||||
|
||||
return {
|
||||
solProceeds,
|
||||
tokensOut,
|
||||
rate: tokensOut / amountIn
|
||||
}
|
||||
}, [fromCoin, toCoin, amount, userBalance])
|
||||
|
||||
const handleSwap = async () => {
|
||||
if (!session || !fromCoinId || !toCoinId || !amount) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/swap', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
fromCoinId,
|
||||
toCoinId,
|
||||
amount: parseFloat(amount)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Swap failed');
|
||||
|
||||
toast.success(`Swapped ${amount} ${fromCoin?.ticker} to ${data.swapped.received.toFixed(2)} ${toCoin?.ticker}`);
|
||||
setAmount('');
|
||||
// Refresh data
|
||||
await mutate('/api/user');
|
||||
await mutate('/api/coins');
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const setMax = () => {
|
||||
if (userBalance > 0) setAmount(userBalance.toString());
|
||||
}
|
||||
|
||||
// Cap input visually (prevent typing more than balance?)
|
||||
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = parseFloat(e.target.value);
|
||||
if (!Number.isNaN(val) && val > userBalance) {
|
||||
setAmount(userBalance.toString());
|
||||
toast.info("Max balance reached");
|
||||
} else {
|
||||
setAmount(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="flex flex-col items-center justify-center py-32 text-center animate-in fade-in duration-500">
|
||||
<LgLock className="mb-4 h-16 w-16 opacity-50" />
|
||||
<h1 className="mb-2 text-2xl font-bold">Sign In Required</h1>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Connect your Discord account to swap coins
|
||||
</p>
|
||||
<Button onClick={() => signIn('discord')}>Sign in with Discord</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-primary/10 via-background to-background">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto max-w-lg px-4 py-16">
|
||||
<div className="mb-8 text-center space-y-2">
|
||||
<h1 className="text-4xl font-black tracking-tight">Swap Tokens</h1>
|
||||
<p className="text-muted-foreground">Swap between different tokens on pummmp.fun</p>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden rounded-[2rem] border border-border bg-card/50 p-4 shadow-2xl backdrop-blur-xl">
|
||||
|
||||
{/* FROM SECTION */}
|
||||
<div className="group relative rounded-[1.5rem] bg-muted/40 p-5 transition-all hover:bg-muted/60">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-muted-foreground">You Pay</span>
|
||||
{fromCoin && (
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1.5 rounded-full bg-background/50 px-2 py-0.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-background hover:text-primary"
|
||||
onClick={setMax}
|
||||
>
|
||||
<Wallet className="h-3 w-3" />
|
||||
<span>{userBalance.toLocaleString()} Max</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={handleAmountChange}
|
||||
placeholder="0.00"
|
||||
className="w-full bg-transparent text-4xl font-bold tracking-tight outline-none placeholder:text-muted-foreground/20"
|
||||
/>
|
||||
|
||||
<div className="shrink-0">
|
||||
{/* Replace Native Select with CoinSelector */}
|
||||
<CoinSelector
|
||||
coins={myHoldings.map(h => h.coin)}
|
||||
selectedId={fromCoinId}
|
||||
onSelect={setFromCoinId}
|
||||
placeholder="Select"
|
||||
showBalance={true}
|
||||
userPortfolio={userData?.portfolio}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 h-6">
|
||||
{fromCoin && estimates?.solProceeds && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground/80">
|
||||
<img src="/solana.svg" className="h-3 w-3 opacity-70" alt="SOL" />
|
||||
≈ {estimates.solProceeds.toFixed(4)} SOL Value
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SWAP INDICATOR */}
|
||||
<div className="relative -my-5 z-10 flex justify-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl border-4 border-card bg-muted text-muted-foreground shadow-sm transition-transform hover:scale-110 hover:bg-primary hover:text-primary-foreground">
|
||||
<ArrowDownUp className="h-5 w-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TO SECTION */}
|
||||
<div className="rounded-[1.5rem] bg-muted/40 p-5 pt-8 transition-all hover:bg-muted/60">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-muted-foreground">You Receive</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={cn(
|
||||
"w-full text-4xl font-bold tracking-tight bg-transparent outline-none truncate",
|
||||
estimates?.tokensOut ? "text-primary" : "text-muted-foreground/30"
|
||||
)}>
|
||||
{estimates?.tokensOut ? estimates.tokensOut.toFixed(4) : "0.00"}
|
||||
</div>
|
||||
|
||||
<div className="shrink-0">
|
||||
<CoinSelector
|
||||
coins={coins?.filter(c => c._id !== fromCoinId) || []}
|
||||
selectedId={toCoinId}
|
||||
onSelect={setToCoinId}
|
||||
placeholder="Select"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 h-6" />
|
||||
</div>
|
||||
|
||||
{/* ACTION */}
|
||||
{estimates?.error ? (
|
||||
<div className="mt-4 rounded-xl bg-destructive/10 p-3 text-center text-sm font-medium text-destructive animate-in fade-in slide-in-from-top-2">
|
||||
{estimates.error}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4" />
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleSwap}
|
||||
disabled={loading || !estimates || !!estimates.error}
|
||||
className="mt-2 h-16 w-full rounded-2xl text-xl font-bold shadow-xl shadow-primary/20 transition-all hover:scale-[1.02] hover:shadow-primary/30 active:scale-[0.98]"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||
Swapping...
|
||||
</div>
|
||||
) : (
|
||||
"Swap Tokens"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* PRICE INFO */}
|
||||
{estimates && !estimates.error && (
|
||||
<div className="mt-4 mb-1 flex items-center justify-center gap-2 text-xs font-medium text-muted-foreground">
|
||||
<CheckCircle2 className="h-3 w-3 text-green-500" />
|
||||
<span>1 {fromCoin?.ticker} ≈ {estimates.rate?.toFixed(4)} {toCoin?.ticker}</span>
|
||||
<span className="text-muted-foreground/50">•</span>
|
||||
<span>This is the best price via bonding curve.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
59
app/terms/page.tsx
Normal file
59
app/terms/page.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-black text-zinc-400 font-sans selection:bg-white/20">
|
||||
<div className="max-w-3xl mx-auto px-6 py-20">
|
||||
<Link
|
||||
href="/"
|
||||
className="group mb-12 inline-flex items-center text-sm text-zinc-600 transition-colors hover:text-white"
|
||||
>
|
||||
<span className="mr-2 transition-transform group-hover:-translate-x-1">←</span>
|
||||
pummmp.fun
|
||||
</Link>
|
||||
|
||||
<h1 className="mb-12 text-4xl font-bold tracking-tight text-white">Terms of Service</h1>
|
||||
|
||||
<div className="space-y-12 text-sm leading-relaxed">
|
||||
<section>
|
||||
<h2 className="mb-4 text-base font-semibold text-white">1. Acceptance of Terms</h2>
|
||||
<p>
|
||||
By accessing or using pummmp.fun ("the Platform"), you agree to be bound by these Terms of Service.
|
||||
The Platform is a simulated trading environment for entertainment purposes only.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-4 text-base font-semibold text-white">2. No Financial Advice</h2>
|
||||
<p>
|
||||
Nothing on this Platform constitutes financial, investment, or trading advice.
|
||||
All "coins," "tokens," and "assets" on this platform are virtual items with no real-world monetary value.
|
||||
We are not responsible for any perceived losses or damages.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-4 text-base font-semibold text-white">3. User Conduct</h2>
|
||||
<p>
|
||||
You agree not to use the Platform for any illegal purpose. You are responsible for all content
|
||||
(names, images, comments) you create. We reserve the right to remove any content or terminate
|
||||
any account at our sole discretion, without notice.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-4 text-base font-semibold text-white">4. Disclaimer</h2>
|
||||
<p>
|
||||
THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND.
|
||||
WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-20 border-t border-zinc-900 pt-8 text-xs text-zinc-700">
|
||||
Last updated: January 2026
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
357
app/u/[username]/page.tsx
Normal file
357
app/u/[username]/page.tsx
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
'use client'
|
||||
|
||||
import { use } from 'react'
|
||||
import Link from 'next/link'
|
||||
import useSWR from 'swr'
|
||||
import { Header } from '@/components/header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CoinCard } from '@/components/coin-card'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
LgBriefcase,
|
||||
LgGroups,
|
||||
LgCancel,
|
||||
LgUser,
|
||||
LgCalendar,
|
||||
LgComboChart,
|
||||
LgLike
|
||||
} from '@/components/icons'
|
||||
import { Area, AreaChart, ResponsiveContainer } from 'recharts'
|
||||
import { Coin } from '@/types'
|
||||
import { UserBadges } from '@/components/ui/user-badges'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useState } from 'react'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
interface PublicUser {
|
||||
_id: string
|
||||
name: string
|
||||
image: string
|
||||
createdAt: string
|
||||
portfolioValue: number
|
||||
coinsCreated: number
|
||||
verified?: boolean
|
||||
isAdmin?: boolean
|
||||
isBetaTester?: boolean
|
||||
isBugHunter?: boolean
|
||||
}
|
||||
|
||||
interface PortfolioItem {
|
||||
coinId: string
|
||||
amount: number
|
||||
avgBuyPrice: number
|
||||
coin: Coin
|
||||
currentValue: number
|
||||
}
|
||||
|
||||
interface UserProfileData {
|
||||
user: PublicUser
|
||||
createdCoins: Coin[]
|
||||
portfolio: PortfolioItem[]
|
||||
}
|
||||
|
||||
export default function UserProfilePage({ params }: { params: Promise<{ username: string }> }) {
|
||||
const { username } = use(params)
|
||||
const decodedUsername = decodeURIComponent(username)
|
||||
const { data: session } = useSession()
|
||||
|
||||
const { data: solPriceData } = useSWR('/api/sol-price', fetcher, {
|
||||
refreshInterval: 60000,
|
||||
})
|
||||
const solPrice = solPriceData?.price || 0
|
||||
|
||||
const [isTipOpen, setIsTipOpen] = useState(false)
|
||||
const [tipAmount, setTipAmount] = useState('')
|
||||
const [tipLoading, setTipLoading] = useState(false)
|
||||
|
||||
const handleTip = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!session || !tipAmount) return
|
||||
|
||||
setTipLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/tip', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
recipientUsername: decodedUsername,
|
||||
amount: tipAmount
|
||||
})
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (res.ok) {
|
||||
toast.success(data.message)
|
||||
setIsTipOpen(false)
|
||||
setTipAmount('')
|
||||
} else {
|
||||
toast.error(data.error)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to send tip')
|
||||
} finally {
|
||||
setTipLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const { data, isLoading, error } = useSWR<UserProfileData>(
|
||||
`/api/users/${username}`,
|
||||
fetcher,
|
||||
{
|
||||
refreshInterval: 15000,
|
||||
}
|
||||
)
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(2)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(2)}K`
|
||||
return num.toFixed(2)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="mb-8 flex items-center gap-6 animate-pulse">
|
||||
<div className="h-24 w-24 rounded-full bg-muted" />
|
||||
<div className="space-y-2">
|
||||
<div className="h-8 w-48 rounded bg-muted" />
|
||||
<div className="h-4 w-32 rounded bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-96 rounded-2xl bg-muted/30" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !data || (data as any).error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<div className="flex flex-col items-center justify-center py-32 text-center">
|
||||
<LgUser className="mb-4 h-16 w-16 opacity-50" />
|
||||
<h1 className="mb-2 text-2xl font-bold">User Not Found</h1>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
The user "{decodedUsername}" could not be found.
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href="/">Back to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { user, createdCoins, portfolio } = data
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
<main className="mx-auto max-w-7xl px-4 py-8">
|
||||
{/* User Header */}
|
||||
<div className="mb-10 flex flex-col gap-6 md:flex-row md:items-center md:gap-8">
|
||||
<Avatar className="h-24 w-24 border-2 border-border">
|
||||
<AvatarImage src={user.image} alt={user.name} />
|
||||
<AvatarFallback className="text-2xl">{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h1 className="text-3xl font-bold">
|
||||
{user.name}
|
||||
</h1>
|
||||
<UserBadges
|
||||
isAdmin={user.isAdmin}
|
||||
isVerified={user.verified}
|
||||
isBugHunter={user.isBugHunter}
|
||||
isBetaTester={user.isBetaTester}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<LgCalendar className="h-4 w-4" />
|
||||
Joined {formatDate(user.createdAt)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LgGroups className="h-4 w-4" />
|
||||
{user.coinsCreated} coins created
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{session && session.user?.name !== user.name && (
|
||||
<div className="mt-4">
|
||||
<Dialog open={isTipOpen} onOpenChange={setIsTipOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<LgLike /> Tip {user.name}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Send a Tip to {user.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Send SOL directly to this user. You may tip once every 30 minutes.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleTip} className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="amount">Amount (SOL)</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
value={tipAmount}
|
||||
onChange={(e) => setTipAmount(e.target.value)}
|
||||
placeholder="0.00"
|
||||
className="pl-9"
|
||||
required
|
||||
/>
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
||||
<img src="/solana.svg" className="h-4 w-4" alt="SOL" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={tipLoading || !tipAmount}
|
||||
className="w-full"
|
||||
>
|
||||
{tipLoading ? 'Sending...' : 'Send Tip'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start gap-1 rounded-xl border border-border bg-card/50 px-6 py-4 md:items-end">
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<LgComboChart className="h-4 w-4" />
|
||||
Portfolio Value
|
||||
</span>
|
||||
<p className="font-mono text-2xl font-bold flex items-center gap-2">
|
||||
{formatNumber(user.portfolioValue)} <span className="text-sm font-normal text-muted-foreground flex items-center gap-1"><img src="/solana.svg" className="w-4 h-4" alt="SOL" /> SOL</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="created" className="w-full">
|
||||
<TabsList className="mb-6 bg-muted/50 p-1 w-full justify-start md:w-auto">
|
||||
<TabsTrigger value="created" className="flex-1 gap-2 px-6 md:flex-none">
|
||||
<LgGroups className="h-4 w-4" />
|
||||
Coins Created ({createdCoins.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="holdings" className="flex-1 gap-2 px-6 md:flex-none">
|
||||
<LgBriefcase className="h-4 w-4" />
|
||||
Holdings ({portfolio.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="created" className="space-y-6">
|
||||
{createdCoins.length > 0 ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{createdCoins.map((coin) => (
|
||||
<CoinCard key={coin._id} coin={coin} solPrice={solPrice} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center rounded-2xl border border-border bg-card/30">
|
||||
<LgGroups className="mb-4 h-16 w-16 opacity-20" />
|
||||
<p className="text-lg font-medium text-muted-foreground">No coins created yet</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="holdings">
|
||||
<div className="rounded-2xl border border-border bg-card overflow-hidden">
|
||||
{portfolio.length > 0 ? (
|
||||
<div className="divide-y divide-border/50">
|
||||
{portfolio.map((item) => {
|
||||
const coin = item.coin
|
||||
const pnl = item.currentValue - (item.amount * item.avgBuyPrice)
|
||||
const pnlPercent = (item.amount * item.avgBuyPrice) > 0 ? (pnl / (item.amount * item.avgBuyPrice)) * 100 : 0
|
||||
|
||||
const chartData = coin.priceHistory?.slice(-12).map((point, index) => ({
|
||||
value: point.price,
|
||||
index,
|
||||
})) || []
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={coin._id}
|
||||
href={`/coin/${coin._id}`}
|
||||
className="flex items-center gap-4 p-6 transition-colors hover:bg-muted/30"
|
||||
>
|
||||
<img
|
||||
src={coin.image || "/placeholder.svg"}
|
||||
alt={coin.name}
|
||||
className="h-10 w-10 rounded-lg bg-muted object-cover"
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold truncate">{coin.name}</p>
|
||||
<span className="text-xs text-muted-foreground">${coin.ticker}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden h-10 w-24 sm:block">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={pnl >= 0 ? 'oklch(0.75 0.18 160)' : 'oklch(0.55 0.22 25)'}
|
||||
strokeWidth={1.5}
|
||||
fill="transparent"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<p className="font-mono text-sm font-medium">{formatNumber(item.amount)}</p>
|
||||
<p className="font-mono text-sm font-medium flex items-center justify-end gap-1">
|
||||
{item.currentValue.toFixed(2)} <span className="text-[10px] text-muted-foreground">SOL</span>
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<LgBriefcase className="mb-4 h-16 w-16 opacity-20" />
|
||||
<p className="text-lg font-medium text-muted-foreground">No holdings found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
components.json
Normal file
21
components.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
186
components/coin-card.tsx
Normal file
186
components/coin-card.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Coin } from '@/types'
|
||||
import { Area, AreaChart, ResponsiveContainer } from 'recharts'
|
||||
import {
|
||||
LgUpload2,
|
||||
LgDownload,
|
||||
LgGroups,
|
||||
LgComboChart,
|
||||
LgDomain,
|
||||
LgLike
|
||||
} from '@/components/icons'
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { VerifiedBadge } from '@/components/ui/verified-badge'
|
||||
|
||||
interface CoinCardProps {
|
||||
coin: Coin
|
||||
solPrice?: number
|
||||
}
|
||||
|
||||
export function CoinCard({ coin, solPrice = 0 }: CoinCardProps) {
|
||||
const isBoosted = coin.boosted && new Date(coin.boosted).getTime() > new Date().getTime()
|
||||
|
||||
const priceChange = coin.priceHistory?.length > 1
|
||||
? ((coin.price - coin.priceHistory[0].price) / coin.priceHistory[0].price) * 100
|
||||
: 0
|
||||
|
||||
const isPositive = priceChange >= 0
|
||||
|
||||
const chartData = coin.priceHistory?.slice(-24).map((point, index) => ({
|
||||
value: point.price,
|
||||
index,
|
||||
})) || []
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(2)}M`
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(2)}K`
|
||||
return num.toFixed(2)
|
||||
}
|
||||
|
||||
// Calculate values in USD if solPrice is available
|
||||
const marketCapUsd = coin.marketCap * solPrice
|
||||
const priceUsd = coin.price * solPrice
|
||||
|
||||
const formatPrice = (price: number) => {
|
||||
if (price === 0) return '0.00'
|
||||
if (price < 0.000001) return price.toFixed(9)
|
||||
if (price < 0.001) return price.toFixed(6)
|
||||
return price.toFixed(4)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={`/coin/${coin._id}`}>
|
||||
<div className="group relative overflow-hidden rounded-2xl border border-border bg-card p-4 transition-all duration-300 hover:border-primary/50 hover:shadow-lg hover:shadow-primary/5">
|
||||
{/* Glow effect - Removed for monotone
|
||||
<div className="pointer-events-none absolute -inset-px rounded-2xl bg-gradient-to-r from-primary/10 via-transparent to-accent/10 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
||||
*/}
|
||||
|
||||
<div className="relative">
|
||||
{/* Boosted Pin */}
|
||||
{/* {isBoosted && (
|
||||
<div className="absolute -top-3 -right-3 z-20">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<LgLike className="h-8 w-8 p-1.5 drop-shadow-lg bg-yellow-500 rounded-full" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This coin has been boosted until {new Date(coin.boosted!).toLocaleString()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-3 overflow-hidden pr-2">
|
||||
<div className="relative shrink-0">
|
||||
<div className="absolute -inset-1 rounded-xl bg-gradient-to-br from-primary/30 to-accent/30 opacity-0 blur transition-opacity group-hover:opacity-100" />
|
||||
<img
|
||||
src={coin.image || "/placeholder.svg"}
|
||||
alt={coin.name}
|
||||
className="relative h-12 w-12 rounded-xl bg-muted object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-foreground truncate" title={coin.name}>
|
||||
{coin.name}
|
||||
{coin.verified ? (
|
||||
<VerifiedBadge className="ml-2 h-4 w-4" />
|
||||
) : isBoosted ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild className="inline-block ml-2">
|
||||
<LgLike className="h-6 w-6 p-1.5 drop-shadow-lg bg-yellow-500 rounded-full" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This coin has been boosted until {new Date(coin.boosted!).toLocaleString()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : null}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground truncate">${coin.ticker}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 rounded-lg px-2 py-1 text-sm font-medium ${
|
||||
isPositive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'bg-destructive/10 text-destructive'
|
||||
}`}>
|
||||
{isPositive ? (
|
||||
'↑'
|
||||
) : (
|
||||
'↓'
|
||||
)}
|
||||
{Math.abs(priceChange).toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mini Chart */}
|
||||
<div className="my-4 h-16">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id={`gradient-${coin._id}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={isPositive ? 'oklch(0.75 0.18 160)' : 'oklch(0.55 0.22 25)'}
|
||||
stopOpacity={0.3}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={isPositive ? 'oklch(0.75 0.18 160)' : 'oklch(0.55 0.22 25)'}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={isPositive ? 'oklch(0.75 0.18 160)' : 'oklch(0.55 0.22 25)'}
|
||||
strokeWidth={2}
|
||||
fill={`url(#gradient-${coin._id})`}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Price</p>
|
||||
<p className="font-mono text-sm font-medium flex items-center gap-1">
|
||||
${formatPrice(priceUsd)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Market Cap</p>
|
||||
<p className="font-mono text-sm font-medium flex items-center gap-1">
|
||||
${formatNumber(marketCapUsd)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-4 flex items-center justify-between border-t border-border/50 pt-4">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<LgGroups className="h-3.5 w-3.5" />
|
||||
{coin.holders} holders
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<LgComboChart className="h-3.5 w-3.5" />
|
||||
{formatNumber(coin.volume24h)} vol
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
152
components/comments.tsx
Normal file
152
components/comments.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
import Link from 'next/link'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { UserBadges } from './ui/user-badges'
|
||||
import { toast } from 'sonner'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
|
||||
interface Comment {
|
||||
_id: string
|
||||
userId: string
|
||||
userName: string
|
||||
userImage: string
|
||||
userVerified?: boolean
|
||||
userIsAdmin?: boolean
|
||||
userIsBetaTester?: boolean
|
||||
userIsBugHunter?: boolean
|
||||
text: string
|
||||
isHolder: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
export function Comments({ coinId, creatorId }: { coinId: string, creatorId?: string }) {
|
||||
const { data: session } = useSession()
|
||||
const { data: comments } = useSWR<Comment[]>(`/api/comments?coinId=${coinId}`, fetcher)
|
||||
const [text, setText] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!text.trim() || !session) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
await fetch('/api/comments', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ coinId, text }),
|
||||
})
|
||||
setText('')
|
||||
mutate(`/api/comments?coinId=${coinId}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to post comment', error)
|
||||
toast.error('Failed to post comment')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (commentId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/comments?id=${commentId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
mutate(`/api/comments?coinId=${coinId}`)
|
||||
toast.success('Comment deleted')
|
||||
} else {
|
||||
const data = await res.json()
|
||||
toast.error(data.error || 'Failed to delete')
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('Failed to delete comment')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="rounded-xl border border-border bg-card/30 p-4">
|
||||
<h3 className="mb-4 font-semibold">Discussion</h3>
|
||||
|
||||
{session ? (
|
||||
<form onSubmit={handleSubmit} className="mb-6 space-y-3">
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="What do you think about this coin?"
|
||||
className="min-h-[80px] bg-muted/30"
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" size="sm" disabled={loading || !text.trim()}>
|
||||
{loading ? 'Posting...' : 'Post Comment'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="mb-6 rounded-lg bg-muted/20 p-4 text-center text-sm text-muted-foreground">
|
||||
Please sign in to join the discussion
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{comments?.map((comment, i) => (
|
||||
<div key={`${comment._id}-${i}`} className="flex gap-3 group">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={comment.userImage} />
|
||||
<AvatarFallback>{comment.userName?.substring(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/u/${comment.userName}`} className="text-sm font-semibold hover:underline">
|
||||
{comment.userName}
|
||||
</Link>
|
||||
<UserBadges
|
||||
isAdmin={comment.userIsAdmin}
|
||||
isVerified={comment.userVerified}
|
||||
isBugHunter={comment.userIsBugHunter}
|
||||
isBetaTester={comment.userIsBetaTester}
|
||||
onlyShowHighest
|
||||
/>
|
||||
{creatorId === comment.userId && (
|
||||
<span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[10px] font-bold text-blue-500 border border-blue-500/30">
|
||||
CREATOR
|
||||
</span>
|
||||
)}
|
||||
{comment.isHolder && creatorId !== comment.userId && (
|
||||
<span className="rounded bg-primary/20 px-1.5 py-0.5 text-[10px] font-medium text-primary">
|
||||
HOLDER
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDistanceToNow(new Date(comment.createdAt), { addSuffix: true })}
|
||||
</span>
|
||||
|
||||
{session && (session.user.id === comment.userId || session.user.id === creatorId || (session.user as any).isAdmin) && (
|
||||
<button
|
||||
onClick={() => handleDelete(comment._id)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity ml-auto text-muted-foreground hover:text-destructive p-1"
|
||||
title="Delete comment"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-foreground/90 whitespace-pre-wrap">{comment.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{comments?.length === 0 && (
|
||||
<p className="text-center text-sm text-muted-foreground">No comments yet. Be the first!</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
614
components/global-chat.tsx
Normal file
614
components/global-chat.tsx
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
import { useSession, signIn } from 'next-auth/react'
|
||||
import useSWR from 'swr'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { MessageCircle, Send, X, Minimize2, Maximize2, Trash2, CloudRain } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
|
||||
import { ExternalLink } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { toast } from 'sonner';
|
||||
import { VerifiedBadge } from '@/components/ui/verified-badge'
|
||||
import { UserBadges } from '@/components/ui/user-badges'
|
||||
import {
|
||||
NET_EVENTS,
|
||||
verifyMessage
|
||||
} from "@/lib/vendor-metrics";
|
||||
|
||||
interface Notification {
|
||||
_id: string;
|
||||
coinId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userImage: string;
|
||||
text: string;
|
||||
createdAt: string;
|
||||
userVerified?: boolean;
|
||||
userIsAdmin?: boolean;
|
||||
userIsBetaTester?: boolean;
|
||||
userIsBugHunter?: boolean;
|
||||
}
|
||||
|
||||
interface RainEvent {
|
||||
_id: string;
|
||||
amount: number;
|
||||
hostId: string;
|
||||
hostName: string;
|
||||
endsAt: string;
|
||||
participants: { id: string, name: string, image: string }[];
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface SessionConfig {
|
||||
market: string;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
export function GlobalChat() {
|
||||
const { data: session } = useSession()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [socket, setSocket] = useState<Socket | null>(null)
|
||||
const [messages, setMessages] = useState<Notification[]>([])
|
||||
const [newMessage, setNewMessage] = useState('')
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const [activeRain, setActiveRain] = useState<RainEvent | null>(null)
|
||||
|
||||
const [sessionConfig, setSessionConfig] = useState<SessionConfig | null>(null)
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const renderTextWithLinks = (text: string) => {
|
||||
const parts = text.split(/(@\w+|\$\w+)/);
|
||||
return parts.map((part, i) => {
|
||||
if (part.startsWith('@')) {
|
||||
const username = part.slice(1);
|
||||
return <Link key={i} href={`/u/${encodeURIComponent(username)}`} className="text-blue-500 hover:underline">{part}</Link>;
|
||||
} else if (part.startsWith('$')) {
|
||||
const ticker = part.slice(1);
|
||||
return <Link key={i} href={`/?search=${encodeURIComponent(ticker)}`} className="text-green-500 hover:underline">{part}</Link>;
|
||||
} else {
|
||||
return <span key={i}>{part}</span>;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/rain').then(res => res.json()).then(data => {
|
||||
if(data.rain && data.rain.active) setActiveRain(data.rain);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('pummmp_global_chat')
|
||||
if (saved) {
|
||||
try {
|
||||
setMessages(JSON.parse(saved))
|
||||
} catch (e) {
|
||||
console.error('Failed to parse chat history', e)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
const toSave = messages.slice(-100);
|
||||
localStorage.setItem('pummmp_global_chat', JSON.stringify(toSave))
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
const SOCKET_URL = window.location.hostname === 'localhost'
|
||||
? "http://localhost:6767"
|
||||
: "https://pummmp.fun";
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SOCKET_URL || SOCKET_URL;
|
||||
|
||||
const newSocket = io(url, {
|
||||
path: "/socket.io",
|
||||
transports: ["websocket"],
|
||||
withCredentials: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 10,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
timeout: 20000,
|
||||
forceNew: false,
|
||||
});
|
||||
|
||||
newSocket.on('connect', () => {
|
||||
console.log('Chat connected');
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
newSocket.on(NET_EVENTS.CONNECT, (data: any) => {
|
||||
setSessionConfig({
|
||||
market: data._h_mid,
|
||||
nonce: data._h_nonce
|
||||
});
|
||||
});
|
||||
|
||||
newSocket.on('nonce_update', (data: any) => {
|
||||
setSessionConfig(prev => prev ? { ...prev, nonce: data.nonce } : null);
|
||||
});
|
||||
|
||||
newSocket.on('rain_update', (rain: any) => {
|
||||
if(rain) setActiveRain(rain);
|
||||
});
|
||||
|
||||
newSocket.on('rain_ended', (data: any) => {
|
||||
setActiveRain(null);
|
||||
if (data && data.amount) {
|
||||
toast.success(`Rain Ended! ${data.totalParticipants} users got ${(data.payoutPerUser || 0).toFixed(4)} SOL!`);
|
||||
}
|
||||
});
|
||||
|
||||
newSocket.on(NET_EVENTS.INCOMING, (signedData: string) => {
|
||||
if (!signedData || typeof signedData !== 'string') return;
|
||||
});
|
||||
|
||||
newSocket.on('disconnect', (reason) => {
|
||||
console.log('Chat disconnected:', reason);
|
||||
setIsConnected(false);
|
||||
setSessionConfig(null);
|
||||
});
|
||||
|
||||
newSocket.on('connect_error', (error) => {
|
||||
console.error('Chat connection error:', error);
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
newSocket.on('reconnect', (attemptNumber) => {
|
||||
console.log('Chat reconnected after', attemptNumber, 'attempts');
|
||||
setIsConnected(true);
|
||||
});
|
||||
|
||||
newSocket.on('reconnect_error', (error) => {
|
||||
console.error('Chat reconnection failed:', error);
|
||||
});
|
||||
|
||||
newSocket.on('reconnect_failed', () => {
|
||||
console.error('Chat reconnection failed completely');
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
newSocket.on('error_message', (msg) => toast.error(msg));
|
||||
|
||||
setSocket(newSocket)
|
||||
|
||||
return () => {
|
||||
newSocket.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket || !sessionConfig) return;
|
||||
|
||||
const handler = async (signedData: string) => {
|
||||
const raw = await verifyMessage(signedData, sessionConfig.market);
|
||||
if (!raw) {
|
||||
console.log('[Chat] Failed to verify message');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const clean = raw.replace(/\0/g, '').trim();
|
||||
if (!clean.startsWith('{') && !clean.startsWith('[')) {
|
||||
console.log('[Chat] Message not JSON:', clean.substring(0, 50));
|
||||
return;
|
||||
}
|
||||
|
||||
const msg: Notification = JSON.parse(clean);
|
||||
console.log('[Chat] Received message:', msg.userName, msg.text.substring(0, 50));
|
||||
setMessages((prev) => {
|
||||
if (prev.find(m => m._id === msg._id)) return prev;
|
||||
return [...prev, msg].slice(-100);
|
||||
});
|
||||
} catch(e) {
|
||||
console.error('[Chat] Error parsing message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
socket.on(NET_EVENTS.INCOMING, handler);
|
||||
|
||||
return () => {
|
||||
socket.off(NET_EVENTS.INCOMING, handler);
|
||||
}
|
||||
}, [socket, sessionConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}, [messages, isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeRain && !isOpen) {
|
||||
setIsOpen(true);
|
||||
toast.success(`🌧️ Rain started by ${activeRain.hostName}! You could win ${activeRain.amount} SOL!`, {
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
}, [activeRain, isOpen])
|
||||
|
||||
const handleJoinRain = async () => {
|
||||
if (!activeRain || !session) return;
|
||||
try {
|
||||
const res = await fetch('/api/rain', { method: 'PUT' });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
toast.success('You entered the rain!');
|
||||
setActiveRain(prev => {
|
||||
if(!prev) return null;
|
||||
const uid = (session.user as any).id || (session as any).id;
|
||||
if (!uid) return prev;
|
||||
return {
|
||||
...prev,
|
||||
participants: prev.participants.some(p => p.id === uid) ? prev.participants : [...prev.participants, {
|
||||
id: uid,
|
||||
name: session.user.name || 'Unknown',
|
||||
image: session.user.image || '/logo.ico'
|
||||
}]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast.error(data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('Failed to join rain');
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendMessage = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!newMessage.trim() || !session || !socket || !sessionConfig || !sessionConfig.nonce) return;
|
||||
|
||||
if (newMessage.startsWith('.help')) {
|
||||
const isAdmin = session?.user?.isAdmin;
|
||||
const helpMsg = {
|
||||
_id: Math.random().toString(),
|
||||
coinId: 'system',
|
||||
userId: 'system',
|
||||
userName: 'System',
|
||||
userImage: '/logo.ico',
|
||||
text: isAdmin
|
||||
? `Commands: .tip <username> <amount> - Tip someone SOL ~ .gamble <amount> <chance%> - Roll the dice (1-95%) ~ .ban <username> <duration> - Ban user (30m/2h/1d/perm) ~ .unban <username> - Unban user`
|
||||
: `Commands: .tip <username> <amount> - Tip someone SOL ~ .gamble <amount> <chance%> - Roll the dice (1-95%)`,
|
||||
createdAt: new Date().toISOString(),
|
||||
userVerified: true
|
||||
}
|
||||
setMessages(prev => [...prev, helpMsg].slice(-100))
|
||||
setNewMessage('')
|
||||
return
|
||||
}
|
||||
|
||||
if (newMessage.startsWith('.gamble ')) {
|
||||
const parts = newMessage.split(' ');
|
||||
if (parts.length >= 3) {
|
||||
const amount = parts[1];
|
||||
const chance = parts[2];
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/gamble', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ amount, chance })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
setNewMessage('');
|
||||
|
||||
const payloadObj = {
|
||||
type: 'gamble',
|
||||
amount: parseFloat(amount),
|
||||
won: data.won,
|
||||
payout: data.payout,
|
||||
chance: parseFloat(chance),
|
||||
_nonce: sessionConfig.nonce
|
||||
};
|
||||
|
||||
socket.emit(NET_EVENTS.OUTGOING, payloadObj);
|
||||
|
||||
const resultMsg = {
|
||||
_id: Math.random().toString(),
|
||||
coinId: 'system',
|
||||
userId: 'system',
|
||||
userName: 'pummmp.casino',
|
||||
userImage: '/logo.ico',
|
||||
text: data.won
|
||||
? `You WON ${data.payout.toFixed(4)} SOL! (Rolled ${data.roll.toFixed(2)})`
|
||||
: `You lost ${amount} SOL (Rolled ${data.roll.toFixed(2)})`,
|
||||
createdAt: new Date().toISOString(),
|
||||
userVerified: true
|
||||
}
|
||||
setMessages(prev => [...prev, resultMsg].slice(-100))
|
||||
|
||||
} else {
|
||||
toast.error(data.error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('Gamble failed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (newMessage.startsWith('.rain ')) {
|
||||
const parts = newMessage.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
const amount = parts[1];
|
||||
try {
|
||||
const res = await fetch('/api/rain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ amount })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
setNewMessage('');
|
||||
if (data.rain) setActiveRain(data.rain); // Immediate update
|
||||
toast.success('Rain started! 🌧️');
|
||||
} else {
|
||||
toast.error(data.error);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Failed to start rain');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (newMessage.startsWith('.tip ')) {
|
||||
const parts = newMessage.split(' ');
|
||||
if (parts.length >= 3) {
|
||||
const recipient = parts[1];
|
||||
const amount = parts[2];
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/tip', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
recipientUsername: recipient,
|
||||
amount: amount
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
setNewMessage('');
|
||||
|
||||
const payloadObj = {
|
||||
type: 'tip',
|
||||
recipientName: recipient,
|
||||
amount: amount,
|
||||
_nonce: sessionConfig.nonce
|
||||
};
|
||||
|
||||
socket.emit(NET_EVENTS.OUTGOING, payloadObj);
|
||||
|
||||
const confirmMsg = {
|
||||
_id: Math.random().toString(),
|
||||
coinId: 'system',
|
||||
userId: 'system',
|
||||
userName: 'pummmp.bot',
|
||||
userImage: '/logo.ico',
|
||||
text: `✅ Tipped ${recipient} ${amount} SOL!`,
|
||||
createdAt: new Date().toISOString(),
|
||||
userVerified: true
|
||||
}
|
||||
setMessages(prev => [...prev, confirmMsg].slice(-100))
|
||||
} else {
|
||||
toast.error(data.error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error('Failed to send tip');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
socket.emit(NET_EVENTS.OUTGOING, {
|
||||
text: newMessage,
|
||||
_nonce: sessionConfig.nonce
|
||||
});
|
||||
|
||||
setNewMessage('')
|
||||
}
|
||||
|
||||
const handleClearChat = () => {
|
||||
localStorage.removeItem('pummmp_global_chat')
|
||||
setMessages([])
|
||||
toast.success('Chat history cleared', {position: 'bottom-center'})
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<div
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed right-0 top-20 z-40 flex h-12 w-10 cursor-pointer items-center justify-center rounded-l-xl border-y border-l border-border bg-background shadow-md transition-all hover:w-12 hover:bg-accent/50 group"
|
||||
>
|
||||
<div className="relative">
|
||||
<MessageCircle className="h-5 w-5 text-primary group-hover:text-primary/80" />
|
||||
{isConnected && (
|
||||
<span className="absolute -right-1 -top-1 flex h-2.5 w-2.5">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed right-0 top-16 bottom-0 z-40 flex w-80 flex-col border-l border-border bg-background/95 backdrop-blur-sm shadow-2xl transition-all animate-in slide-in-from-right duration-300">
|
||||
<div className="flex h-12 shrink-0 items-center justify-between border-b border-border px-4 bg-muted/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold">pummmp.chat</h3>
|
||||
<div className={`h-1.5 w-1.5 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
<span className="text-[10px] text-muted-foreground">{isConnected ? 'Connected' : 'Connecting...'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleClearChat}
|
||||
className="h-7 w-7 rounded-full hover:bg-background hover:text-destructive"
|
||||
title="Clear Local History"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => setIsOpen(false)} className="h-7 w-7 rounded-full hover:bg-background">
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeRain && (
|
||||
<div className="relative overflow-hidden border-b border-primary/20 bg-gradient-to-r from-primary/5 via-primary/10 to-primary/5 p-4 animate-in slide-in-from-top-2 shadow-inner">
|
||||
|
||||
<div className="relative z-10 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded text-primary shadow-sm">
|
||||
<CloudRain className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-black drop-shadow-sm">RAIN EVENT ACTIVE!</span>
|
||||
<span className="text-[10px] font-medium text-muted-foreground">Hosted by {activeRain.hostName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold font-mono leading-none text-foreground">{activeRain.amount} SOL</div>
|
||||
<div className="text-[10px] text-muted-foreground font-medium">Total distributed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 mt-1">
|
||||
<div className="flex -space-x-2 overflow-hidden">
|
||||
{activeRain.participants.slice(0, 3).map((participant, i) => (
|
||||
<Avatar key={participant.id} className="h-6 w-6 shrink-0 border border-border">
|
||||
<AvatarImage src={participant.image} />
|
||||
<AvatarFallback>{participant.name?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
{(activeRain.participants?.length || 0) > 3 && (
|
||||
<div className="inline-block h-6 w-6 rounded-full ring-2 ring-background bg-muted flex items-center justify-center text-[12px] font-bold text-muted-foreground">
|
||||
+{(activeRain.participants?.length || 0) - 3}
|
||||
</div>
|
||||
)}
|
||||
{(activeRain.participants?.length || 0) === 0 && <span className="text-xs text-muted-foreground pl-2">0 participants | Be the first!</span>}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className={`h-8 px-4 font-bold transition-all shadow-md ${
|
||||
activeRain.participants?.some(p => p.id === (session?.user?.id || ''))
|
||||
? "bg-green-500/20 text-green-600 hover:bg-green-500/30 border border-green-500/50"
|
||||
: "hover:scale-105 active:scale-95"
|
||||
}`}
|
||||
onClick={() => handleJoinRain()}
|
||||
disabled={!session || activeRain.participants?.some(p => p.id === session.user?.id) || activeRain.hostId === session?.user?.id}
|
||||
variant={activeRain.participants?.some(p => p.id === (session?.user?.id || '')) || activeRain.hostId === session?.user?.id ? "secondary" : "default"}
|
||||
>
|
||||
{activeRain.hostId === session?.user?.id ? 'Hosting' : (activeRain.participants?.some(p => p.id === session?.user?.id) ? 'Entered ✓' : 'Join Rain')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ScrollArea className="flex-1 p-3 min-h-0 overflow-hidden">
|
||||
<div className="flex flex-col gap-3 max-w-[80%]">
|
||||
{messages.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center h-40 text-muted-foreground text-sm">
|
||||
<p>No messages yet.</p>
|
||||
<p className="text-xs opacity-50">Say hello! 👋</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
<div key={`${msg._id || i}-${i}`} className={cn(
|
||||
"flex gap-2 duration-300",
|
||||
i === messages.length - 1 ? "animate-in fade-in slide-in-from-bottom-2" : ""
|
||||
)}>
|
||||
<Avatar className="h-6 w-6 shrink-0 mt-0.5 border border-border">
|
||||
<AvatarImage src={msg.userImage} />
|
||||
<AvatarFallback>{msg.userName?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className={cn("text-xs font-semibold truncate hover:underline cursor-pointer", msg.coinId === 'system' && "text-primary")}>
|
||||
<a href={msg.coinId !== 'system' ? `/u/${encodeURIComponent(msg.userName)}` : undefined}>
|
||||
{msg.userName}
|
||||
</a>
|
||||
</span>
|
||||
<UserBadges
|
||||
isAdmin={msg.userIsAdmin}
|
||||
isVerified={msg.userVerified}
|
||||
isBugHunter={msg.userIsBugHunter}
|
||||
isBetaTester={msg.userIsBetaTester}
|
||||
className="h-3 w-3"
|
||||
onlyShowHighest
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground">{new Date(msg.createdAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
</div>
|
||||
<p className="text-xs leading-relaxed break-words text-foreground/90 font-medium">
|
||||
{renderTextWithLinks(msg.text)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={scrollRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<form onSubmit={handleSendMessage} className="p-3 border-t border-border bg-background">
|
||||
<div className="flex gap-2">
|
||||
{!session ? (
|
||||
<Button type="button" variant="outline" className="w-full h-8 text-xs" onClick={() => signIn()}>
|
||||
Sign in to chat
|
||||
</Button>
|
||||
) : (session?.user?.chatBannedUntil && new Date(session.user.chatBannedUntil) > new Date()) ? (
|
||||
<Button type="button" variant="outline" className="w-full h-8 text-xs" disabled>
|
||||
You are chat banned until {new Date(session.user.chatBannedUntil).toLocaleString()}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
placeholder="Type a message... | .help for commands"
|
||||
className="h-8 text-xs"
|
||||
disabled={!isConnected || !sessionConfig || !sessionConfig.nonce}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
disabled={!isConnected || !sessionConfig || !sessionConfig.nonce || !newMessage.trim()}
|
||||
>
|
||||
<Send className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
200
components/header.tsx
Normal file
200
components/header.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useSession, signIn, signOut } from 'next-auth/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import useSWR from 'swr'
|
||||
import {
|
||||
LgGeminiAi,
|
||||
LgFolderInvoices,
|
||||
LgComboChart,
|
||||
LgPlus,
|
||||
LgExit,
|
||||
LgIdea // using LgIdea for rewards icon temporarily or can import another
|
||||
} from '@/components/icons'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { UserBadges } from './ui/user-badges'
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
export function Header() {
|
||||
const { data: session, status } = useSession()
|
||||
const { data: userData } = useSWR(session ? '/api/user' : null, fetcher, {
|
||||
refreshInterval: 10000,
|
||||
})
|
||||
const { data: solPrice } = useSWR('/api/sol-price', fetcher, {
|
||||
refreshInterval: 60000,
|
||||
})
|
||||
|
||||
// userouter -> login
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-border bg-background">
|
||||
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4">
|
||||
<div className="flex items-center gap-8">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="relative flex h-10 w-10 items-center justify-center">
|
||||
{/* <LgGeminiAi className="h-6 w-6" /> */}
|
||||
<img src="/logo.svg" alt="Logo" className="h-10 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xl font-semibold tracking-tight">
|
||||
{/* pummmp - make this a gradient */}
|
||||
{/* <span style={{ background: 'linear-gradient(90deg, #286eca, #ffffff)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>pummmp</span> */}
|
||||
pummmp<span className="text-[#286eca]">.fun</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-6 md:flex">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Explore
|
||||
</Link>
|
||||
<Link
|
||||
href="/create"
|
||||
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Create
|
||||
</Link>
|
||||
<Link
|
||||
href="/swap"
|
||||
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Swap
|
||||
</Link>
|
||||
<Link
|
||||
href="/rewards"
|
||||
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Daily Rewards
|
||||
</Link>
|
||||
{session && (
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{solPrice && (
|
||||
<div className="hidden items-center gap-2 rounded-xl bg-card px-3 py-1.5 md:flex">
|
||||
{/* <LgFolderInvoices className="h-4 w-4" /> */}
|
||||
<img src="/solana.svg" alt="SOL" className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">${solPrice.price?.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <span className='text-muted-foreground' style={{fontSize: '8px'}}>USD</span></span>
|
||||
<span className={`text-xs ${solPrice.change24h >= 0 ? 'text-primary' : 'text-destructive'}`}>
|
||||
{solPrice.change24h >= 0 ? '+' : ''}{solPrice.change24h?.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'loading' ? (
|
||||
<div className="h-9 w-24 animate-pulse rounded-lg bg-muted" />
|
||||
) : session ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden flex-col items-end md:flex">
|
||||
<span className="text-sm font-medium">
|
||||
{userData?.balance?.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 4 }) ?? '...'} SOL
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
${((userData?.balance ?? 0) * (solPrice?.price ?? 0)).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USD
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-9 w-9 rounded-full">
|
||||
<Avatar className="h-9 w-9 ring-2 ring-primary/20">
|
||||
<AvatarImage src={session.user?.image ?? ''} alt={session.user?.name ?? ''} />
|
||||
<AvatarFallback className="bg-primary/10 text-primary">
|
||||
{session.user?.name?.[0]?.toUpperCase() ?? '?'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={session.user?.image ?? ''} />
|
||||
<AvatarFallback>{session.user?.name?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium">{session.user?.name}</span>
|
||||
<UserBadges
|
||||
isAdmin={session.user?.isAdmin}
|
||||
isVerified={session.user?.verified}
|
||||
isBugHunter={session.user?.isBugHunter}
|
||||
isBetaTester={session.user?.isBetaTester}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{session.user?.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild className="cursor-pointer hover:bg-primary/10 focus:bg-primary/10">
|
||||
<Link href="/dashboard" className="cursor-pointer">
|
||||
<LgComboChart className="mr-2 h-4 w-4" />
|
||||
Dashboard
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer hover:bg-primary/10 focus:bg-primary/10">
|
||||
<Link href="/create" className="cursor-pointer">
|
||||
<LgPlus className="mr-2 h-4 w-4" />
|
||||
Create Coin
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer hover:bg-primary/10 focus:bg-primary/10">
|
||||
<Link href={`/u/${encodeURIComponent(session.user?.name ?? '')}`} className="cursor-pointer">
|
||||
<LgFolderInvoices className="mr-2 h-4 w-4" />
|
||||
My Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => signOut()}
|
||||
className="cursor-pointer hover:bg-primary/10 focus:bg-primary/10"
|
||||
>
|
||||
<LgExit className="mr-2 h-4 w-4" />
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
) : (
|
||||
// <Button
|
||||
// onClick={() => signIn('discord')}
|
||||
// className="gap-2 bg-[#5865F2] text-foreground hover:bg-[#4752C4]"
|
||||
// >
|
||||
// <svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
// <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
// </svg>
|
||||
// Sign in with Discord
|
||||
// </Button>
|
||||
<Button
|
||||
onClick={() => router.push('/login')}
|
||||
className="gap-2 bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
113
components/holders.tsx
Normal file
113
components/holders.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { io } from 'socket.io-client'
|
||||
import Link from 'next/link'
|
||||
import useSWR from 'swr'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { UserBadges } from '@/components/ui/user-badges'
|
||||
import { VerifiedBadge } from './ui/verified-badge'
|
||||
|
||||
interface Holder {
|
||||
_id: string
|
||||
name: string
|
||||
image: string
|
||||
amount: number
|
||||
percentage?: number
|
||||
isSystem?: boolean
|
||||
verified?: boolean
|
||||
isAdmin?: boolean
|
||||
isBetaTester?: boolean
|
||||
isBugHunter?: boolean
|
||||
}
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json())
|
||||
|
||||
export function TopHolders({ coinId, totalSupply }: { coinId: string, totalSupply: number }) {
|
||||
const { data: holders, error, mutate } = useSWR<Holder[]>(
|
||||
`/api/coins/${coinId}/holders`,
|
||||
fetcher
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const socket = io("https://pummmp.fun/", {
|
||||
path: "/socket.io",
|
||||
transports: ["websocket"],
|
||||
});
|
||||
|
||||
|
||||
socket.on(`trade:${coinId}`, () => {
|
||||
mutate()
|
||||
})
|
||||
|
||||
return () => {
|
||||
socket.disconnect()
|
||||
}
|
||||
}, [coinId, mutate])
|
||||
|
||||
if (!Array.isArray(holders)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card/30 p-4">
|
||||
<h3 className="mb-4 font-semibold">Top Holders</h3>
|
||||
<div className="space-y-3">
|
||||
{holders.map((holder, i) => (
|
||||
<div key={holder._id} className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-4 text-xs text-muted-foreground">{i + 1}.</span>
|
||||
<Link href={holder.isSystem ? '#' : `/u/${encodeURIComponent(holder.name)}`} className={`flex items-center gap-2 group ${holder.isSystem ? 'pointer-events-none' : ''}`}>
|
||||
<Avatar className="h-6 w-6 border border-border">
|
||||
{holder.name === 'Raydium Pool' ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-violet-500/20 text-[12px] font-bold text-violet-500">
|
||||
R<span className="text-white">P</span>
|
||||
</div>
|
||||
) : holder.name === 'Bonding Curve' ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-primary/20 text-[12px] font-bold text-primary">
|
||||
B<span className="text-white">C</span>
|
||||
</div>
|
||||
) : (<>
|
||||
<AvatarImage src={holder.image} />
|
||||
<AvatarFallback>{holder.name.substring(0, 2)}</AvatarFallback></>
|
||||
)}
|
||||
</Avatar>
|
||||
<span className={`font-medium truncate max-w-[100px] ${!holder.isSystem && 'group-hover:underline'}`}>
|
||||
{holder.name}
|
||||
</span>
|
||||
{!holder.isSystem && (
|
||||
<UserBadges
|
||||
isAdmin={holder.isAdmin}
|
||||
isVerified={holder.verified}
|
||||
isBugHunter={holder.isBugHunter}
|
||||
isBetaTester={holder.isBetaTester}
|
||||
onlyShowHighest
|
||||
/>
|
||||
)}
|
||||
{holder.isSystem && (
|
||||
// <span className="ml-[-4px] flex h-3 w-3 items-center justify-center rounded-full bg-violet-500 text-[6px] text-white">✓</span>
|
||||
<VerifiedBadge className="h-4 w-4 text-white fill-purple-500 [&>path:first-child]:stroke-purple-500" />
|
||||
)}
|
||||
{holder.name === 'Bonding Curve' && (
|
||||
// <span className="ml-[-4px] flex h-3 w-3 items-center justify-center rounded-full bg-primary text-[6px] text-white">✓</span>
|
||||
<VerifiedBadge className="h-4 w-4" />
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-muted-foreground">
|
||||
{((holder.amount / totalSupply) * 100).toFixed(2)}%
|
||||
</span>
|
||||
<span className="font-mono">
|
||||
{(holder.amount / 1_000_000).toFixed(1)}M
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!holders?.length && (
|
||||
<p className="text-sm text-muted-foreground mt-[-12px]">No holders yet {`;-;`}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
44
components/icons/OK.tsx
Normal file
44
components/icons/OK.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgOK = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={12} r={10} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m0-.5C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9.793} x2={14.207} y1={8.793} y2={13.207} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M11 16a1 1 0 0 1-.707-.293l-3-3a.999.999 0 1 1 1.414-1.414L11 13.586l4.293-4.293a.999.999 0 1 1 1.414 1.414l-5 5A1 1 0 0 1 11 16" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgOK);
|
||||
export default ForwardRef;
|
||||
55
components/icons/about.tsx
Normal file
55
components/icons/about.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgAbout = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.648} x2={17.79} y1={6.21} y2={20.352} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2C6.477 2 2 6.477 2 12a9.95 9.95 0 0 0 1.043 4.427l-1.005 4.019a1.25 1.25 0 0 0 1.516 1.516l4.019-1.005A9.95 9.95 0 0 0 12 22c5.523 0 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.648} x2={17.79} y1={6.21} y2={20.352} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5a9.4 9.4 0 0 1-4.205-.991l-.165-.082-.178.045-4.019 1.005a.743.743 0 0 1-.775-.269.74.74 0 0 1-.135-.641l1.005-4.019.045-.178-.082-.165A9.4 9.4 0 0 1 2.5 12c0-5.238 4.262-9.5 9.5-9.5m0-.5C6.477 2 2 6.477 2 12a9.95 9.95 0 0 0 1.043 4.427l-1.005 4.019a1.252 1.252 0 0 0 1.517 1.516l4.019-1.005A9.94 9.94 0 0 0 12 22c5.523 0 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={10.043} x2={13.957} y1={12.543} y2={16.457} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M11 17v-5a1 1 0 0 1 2 0v5a1 1 0 0 1-2 0" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={10.939} x2={13.061} y1={6.439} y2={8.561} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><circle cx={12} cy={7.5} r={1.5} style={{
|
||||
fill: "url(#d)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgAbout);
|
||||
export default ForwardRef;
|
||||
79
components/icons/add-user-male.tsx
Normal file
79
components/icons/add-user-male.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgAddUserMale = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={8.464} x2={15.536} y1={3.465} y2={10.536} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={7} r={5} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={7} x2={17} y1={7} y2={7} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c2.481 0 4.5 2.019 4.5 4.5s-2.019 4.5-4.5 4.5S7.5 9.481 7.5 7 9.519 2.5 12 2.5m0-.5a5 5 0 1 0 .001 10.001A5 5 0 0 0 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={6.299} x2={14.402} y1={13.458} y2={21.56} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M14.54 19c0-1.643.742-3.022 2.386-4H6a3 3 0 1 0 0 6h8.962a5 5 0 0 1-.422-2" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={3} x2={16.926} y1={18} y2={18} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M15.454 15.5c-.94.936-1.415 2.108-1.415 3.5 0 .509.071 1.012.211 1.5H6c-1.379 0-2.5-1.121-2.5-2.5s1.121-2.5 2.5-2.5zm1.472-.5H6a3 3 0 1 0 0 6h8.962a5 5 0 0 1-.422-2c0-1.643.742-3.022 2.386-4" style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={15.464} x2={22.535} y1={15.464} y2={22.535} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={19} cy={19} r={5} style={{
|
||||
fill: "url(#e)"
|
||||
}} /><linearGradient id="f" x1={15.464} x2={22.535} y1={15.464} y2={22.535} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 14.5c2.481 0 4.5 2.019 4.5 4.5s-2.019 4.5-4.5 4.5-4.5-2.019-4.5-4.5 2.019-4.5 4.5-4.5m0-.5a5 5 0 1 0 .001 10.001A5 5 0 0 0 19 14" style={{
|
||||
fill: "url(#f)"
|
||||
}} /><linearGradient id="g" x1={15.5} x2={22.5} y1={19} y2={19} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.4
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.2
|
||||
}} /></linearGradient><path d="M21.5 18H20v-1.5a1 1 0 0 0-2 0V18h-1.5a1 1 0 0 0 0 2H18v1.5a1 1 0 0 0 2 0V20h1.5a1 1 0 0 0 0-2" style={{
|
||||
fill: "url(#g)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgAddUserMale);
|
||||
export default ForwardRef;
|
||||
44
components/icons/appointment-reminders.tsx
Normal file
44
components/icons/appointment-reminders.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgAppointmentReminders = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.58} x2={19.37} y1={6.521} y2={21.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="m21.55 16.4-1.15-1.533a7 7 0 0 1-1.4-4.2V9.294c0-3.833-2.953-7.175-6.785-7.29A7 7 0 0 0 5 9v1.667a7 7 0 0 1-1.4 4.2L2.45 16.4a2.25 2.25 0 0 0 1.8 3.6H9a3 3 0 1 0 6 0h4.75a2.25 2.25 0 0 0 1.8-3.6" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.58} x2={19.37} y1={6.521} y2={21.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5q.1 0 .2.003c3.474.104 6.3 3.15 6.3 6.79v1.373a7.55 7.55 0 0 0 1.5 4.5l1.15 1.533a1.752 1.752 0 0 1-1.4 2.8H14.5V20c0 1.379-1.122 2.5-2.5 2.5S9.5 21.379 9.5 20v-.5H4.25a1.752 1.752 0 0 1-1.4-2.8L4 15.167a7.55 7.55 0 0 0 1.5-4.5V9c0-3.584 2.916-6.5 6.5-6.5m0-.5a7 7 0 0 0-7 7v1.667a7 7 0 0 1-1.4 4.2L2.45 16.4a2.25 2.25 0 0 0 1.8 3.6H9a3 3 0 1 0 6 0h4.75a2.25 2.25 0 0 0 1.8-3.6l-1.15-1.533a7 7 0 0 1-1.4-4.2V9.294c0-3.833-2.953-7.175-6.785-7.29A5 5 0 0 0 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={10.189} x2={13.811} y1={18.811} y2={22.432} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M9 20a3 3 0 1 0 6 0z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgAppointmentReminders);
|
||||
export default ForwardRef;
|
||||
33
components/icons/back.tsx
Normal file
33
components/icons/back.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgBack = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={9.379} x2={20.621} y1={6.379} y2={17.621} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="m12.243 12 4.879-4.879a3 3 0 1 0-4.243-4.243l-7 7a3.003 3.003 0 0 0 0 4.243l7 7a3 3 0 1 0 4.243-4.243c-.38-.378-2.951-2.949-4.879-4.878" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={9.379} x2={20.621} y1={6.379} y2={17.621} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M15 2.5c.668 0 1.296.26 1.768.732S17.5 4.332 17.5 5s-.26 1.296-.732 1.768l-4.879 4.879-.353.353.354.354 4.879 4.879c.471.471.731 1.099.731 1.767s-.26 1.296-.732 1.768-1.1.732-1.768.732-1.296-.26-1.768-.732l-6.06-6.06-.94-.94a2.49 2.49 0 0 1 0-3.536l7-7A2.48 2.48 0 0 1 15 2.5m0-.5c-.768 0-1.536.293-2.121.879l-7 7a3.003 3.003 0 0 0 0 4.243l7 7c.585.585 1.353.878 2.121.878s1.536-.293 2.121-.879a3 3 0 0 0 0-4.243l-4.879-4.879 4.879-4.879A3 3 0 0 0 15 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgBack);
|
||||
export default ForwardRef;
|
||||
88
components/icons/binoculars.tsx
Normal file
88
components/icons/binoculars.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgBinoculars = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.336} x2={20.664} y1={4.836} y2={22.164} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M22.984 18.752 22.89 18 21.375 5.876A1 1 0 0 0 20.383 5H20a2 2 0 0 0-2-2h-1a2 2 0 0 0-2 2 1 1 0 0 0-1 1v2h-4V6a1 1 0 0 0-1-1 2 2 0 0 0-2-2H6a2 2 0 0 0-2 2h-.383a1 1 0 0 0-.992.876L1.11 18l-.094.752A2 2 0 0 0 3 21h5a2 2 0 0 0 2-2v-4a2 2 0 1 1 4 0v4a2 2 0 0 0 2 2h5a2 2 0 0 0 1.984-2.248" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.336} x2={20.664} y1={4.836} y2={22.164} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c.827 0 1.5.673 1.5 1.5v.5h.883a.5.5 0 0 1 .496.438l1.516 12.124.094.752A1.5 1.5 0 0 1 21 20.5h-5c-.827 0-1.5-.673-1.5-1.5v-4c0-1.378-1.121-2.5-2.5-2.5S9.5 13.622 9.5 15v4c0 .827-.673 1.5-1.5 1.5H3a1.5 1.5 0 0 1-1.489-1.686l.094-.752L3.121 5.938a.5.5 0 0 1 .496-.438H4.5V5c0-.827.673-1.5 1.5-1.5h1c.827 0 1.5.673 1.5 1.5v.5H9a.5.5 0 0 1 .5.5v2.5h5V6a.5.5 0 0 1 .5-.5h.5V5c0-.827.673-1.5 1.5-1.5zm0-.5h-1a2 2 0 0 0-2 2 1 1 0 0 0-1 1v2h-4V6a1 1 0 0 0-1-1 2 2 0 0 0-2-2H6a2 2 0 0 0-2 2h-.383a1 1 0 0 0-.992.876L1.11 18l-.094.752A2 2 0 0 0 3 21h5a2 2 0 0 0 2-2v-4a2 2 0 1 1 4 0v4a2 2 0 0 0 2 2h5a2 2 0 0 0 1.985-2.248L22.89 18 21.375 5.876A1 1 0 0 0 20.383 5H20a2 2 0 0 0-2-2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9.25} x2={14.75} y1={8.75} y2={14.25} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M10 8v7a2 2 0 1 1 4 0V8z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={16.043} x2={18.957} y1={3.129} y2={6.043} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M20 5h-5a2 2 0 0 1 2-2h1a2 2 0 0 1 2 2" style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={5.043} x2={7.957} y1={3.129} y2={6.043} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M4 5h5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2" style={{
|
||||
fill: "url(#e)"
|
||||
}} /><linearGradient id="f" x1={15.765} x2={21.18} y1={16.235} y2={21.649} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M22.984 18.752 22.89 18H14v1a2 2 0 0 0 2 2h5a2 2 0 0 0 1.984-2.248" style={{
|
||||
fill: "url(#f)"
|
||||
}} /><linearGradient id="g" x1={2.848} x2={8.207} y1={16.262} y2={21.621} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M1.016 18.752 1.11 18H10v1a2 2 0 0 1-2 2H3a2 2 0 0 1-1.984-2.248" style={{
|
||||
fill: "url(#g)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgBinoculars);
|
||||
export default ForwardRef;
|
||||
44
components/icons/bookmark-ribbon.tsx
Normal file
44
components/icons/bookmark-ribbon.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgBookmarkRibbon = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.473} x2={20.527} y1={3.406} y2={20.459} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M21 4.5A1.5 1.5 0 0 0 19.5 3h-15A1.495 1.495 0 0 0 4 5.908V20.27a1.728 1.728 0 0 0 2.501 1.546L12 19.066l5.499 2.75A1.73 1.73 0 0 0 20 20.27V5.908c.581-.206 1-.756 1-1.408" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.473} x2={20.527} y1={3.406} y2={20.46} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19.5 3.5c.551 0 1 .449 1 1a1 1 0 0 1-.667.937l-.333.118V20.27c0 .76-.638 1.23-1.229 1.23q-.284 0-.549-.132l-5.499-2.75-.223-.111-.224.112-5.499 2.75a1.2 1.2 0 0 1-.548.131c-.591 0-1.229-.47-1.229-1.23V5.555l-.333-.118A1 1 0 0 1 3.5 4.5c0-.551.449-1 1-1zm0-.5h-15A1.495 1.495 0 0 0 4 5.908V20.27A1.73 1.73 0 0 0 5.729 22c.256 0 .519-.058.772-.185L12 19.066l5.499 2.75A1.725 1.725 0 0 0 20 20.27V5.908A1.496 1.496 0 0 0 19.5 3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={7.189} x2={16.811} y1={-0.311} y2={9.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M19.5 3h-15a1.5 1.5 0 0 0 0 3h15a1.5 1.5 0 0 0 0-3" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgBookmarkRibbon);
|
||||
export default ForwardRef;
|
||||
44
components/icons/bookmark.tsx
Normal file
44
components/icons/bookmark.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgBookmark = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3} x2={19.828} y1={3.586} y2={20.414} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M4 21V3a1 1 0 0 1 1-1h12a3 3 0 0 1 3 3v14a3 3 0 0 1-3 3H5a1 1 0 0 1-1-1" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3} x2={19.828} y1={3.586} y2={20.414} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M17 2.5c1.379 0 2.5 1.122 2.5 2.5v14c0 1.378-1.121 2.5-2.5 2.5H5a.5.5 0 0 1-.5-.5V3a.5.5 0 0 1 .5-.5zm0-.5H5a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h12a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={6.615} x2={14.385} y1={3.385} y2={11.156} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M13 2H8v10.216a.782.782 0 0 0 1.271.611l1.229-.983 1.229.983A.782.782 0 0 0 13 12.216z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgBookmark);
|
||||
export default ForwardRef;
|
||||
44
components/icons/box.tsx
Normal file
44
components/icons/box.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgBox = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.586} x2={20.414} y1={3} y2={19.828} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v14a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.586} x2={20.414} y1={3} y2={19.828} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M20 3.5a.5.5 0 0 1 .5.5v14c0 1.378-1.122 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V4a.5.5 0 0 1 .5-.5zm0-.5H4a1 1 0 0 0-1 1v14a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V4a1 1 0 0 0-1-1" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={10.293} x2={13.707} y1={5.293} y2={8.707} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M14 8h-4a1 1 0 0 1 0-2h4a1 1 0 0 1 0 2" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgBox);
|
||||
export default ForwardRef;
|
||||
66
components/icons/briefcase.tsx
Normal file
66
components/icons/briefcase.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgBriefcase = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.629} x2={20.371} y1={4.129} y2={20.871} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 4H5a3 3 0 0 0-3 3v11a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.629} x2={20.371} y1={4.129} y2={20.871} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 4.5c1.379 0 2.5 1.122 2.5 2.5v11c0 1.378-1.121 2.5-2.5 2.5H5A2.503 2.503 0 0 1 2.5 18V7c0-1.378 1.121-2.5 2.5-2.5zm0-.5H5a3 3 0 0 0-3 3v11a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={10.939} x2={13.061} y1={9.939} y2={12.061} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><circle cx={12} cy={11} r={1.5} style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={10.293} x2={13.707} y1={1.879} y2={5.293} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M13 2h-2a2 2 0 0 0-2 2h6a2 2 0 0 0-2-2" style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={5.189} x2={18.811} y1={8.811} y2={22.432} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M19 15H5a3 3 0 0 1-3-3v6a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-6a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#e)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgBriefcase);
|
||||
export default ForwardRef;
|
||||
55
components/icons/calendar.tsx
Normal file
55
components/icons/calendar.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgCalendar = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c1.378 0 2.5 1.121 2.5 2.5v12c0 1.379-1.122 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V6c0-1.379 1.122-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={3} x2={21} y1={5} y2={5} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M18 3H6a3 3 0 0 0-3 3v1h18V6a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={8.293} x2={15.707} y1={10.293} y2={17.707} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M15 18a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-5-1a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-4 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2m0-4a1 1 0 1 0 0 2 1 1 0 0 0 0-2m4 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2m4 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2m4 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-4-2a1 1 0 1 0 0 2 1 1 0 0 0 0-2m-4 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2" style={{
|
||||
fill: "url(#d)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgCalendar);
|
||||
export default ForwardRef;
|
||||
33
components/icons/cancel-2.tsx
Normal file
33
components/icons/cancel-2.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgCancel2 = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2m0 16a6 6 0 0 1-6-6c0-.977.238-1.896.652-2.711l8.058 8.058A5.95 5.95 0 0 1 12 18m5.348-3.289L9.289 6.652A6 6 0 0 1 12 6a6 6 0 0 1 6 6c0 .977-.238 1.896-.652 2.711" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m5.482 13.051.312-.614A6.44 6.44 0 0 0 18.5 12c0-3.584-2.916-6.5-6.5-6.5a6.44 6.44 0 0 0-2.937.706l-.614.312.487.487 8.058 8.058zM12 18.5a6.44 6.44 0 0 0 2.937-.706l.614-.312-.487-.487-8.058-8.059-.488-.487-.312.614A6.44 6.44 0 0 0 5.5 12c0 3.584 2.916 6.5 6.5 6.5M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2m5.348 12.711L9.289 6.652A6 6 0 0 1 12 6a6 6 0 0 1 6 6c0 .977-.238 1.896-.652 2.711M12 18a6 6 0 0 1-6-6c0-.977.238-1.896.652-2.711l8.058 8.058A5.95 5.95 0 0 1 12 18" style={{
|
||||
fill: "url(#b)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgCancel2);
|
||||
export default ForwardRef;
|
||||
44
components/icons/cancel.tsx
Normal file
44
components/icons/cancel.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgCancel = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={12} r={10} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m0-.5C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={7.791} x2={16.209} y1={7.79} y2={16.209} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m13.403 12 2.812-2.812a.985.985 0 0 0 0-1.392l-.011-.011a.985.985 0 0 0-1.392 0L12 10.597 9.188 7.785a.985.985 0 0 0-1.392 0l-.011.011a.985.985 0 0 0 0 1.392L10.597 12l-2.812 2.812a.985.985 0 0 0 0 1.392l.011.011a.985.985 0 0 0 1.392 0L12 13.403l2.812 2.812a.985.985 0 0 0 1.392 0l.011-.011a.985.985 0 0 0 0-1.392z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgCancel);
|
||||
export default ForwardRef;
|
||||
55
components/icons/check-all.tsx
Normal file
55
components/icons/check-all.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgCheckAll = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18.86 5.14A2.99 2.99 0 0 0 16 3H6a3 3 0 0 0-3 3v10a2.99 2.99 0 0 0 2.14 2.86A2.99 2.99 0 0 0 8 21h10a3 3 0 0 0 3-3V8a2.99 2.99 0 0 0-2.14-2.86" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M16 3.5c1.103 0 2.06.717 2.381 1.784l.077.257.257.077A2.48 2.48 0 0 1 20.5 8v10c0 1.379-1.121 2.5-2.5 2.5H8a2.48 2.48 0 0 1-2.381-1.784l-.077-.257-.257-.077A2.48 2.48 0 0 1 3.5 16V6c0-1.378 1.121-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v10a2.99 2.99 0 0 0 2.14 2.86A2.99 2.99 0 0 0 8 21h10a3 3 0 0 0 3-3V8a2.99 2.99 0 0 0-2.14-2.86A2.99 2.99 0 0 0 16 3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={12} x2={20.121} y1={12} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M18.86 5.14c.082.274.14.559.14.86v10a3 3 0 0 1-3 3H6c-.301 0-.586-.058-.86-.14A2.99 2.99 0 0 0 8 21h10a3 3 0 0 0 3-3V8a2.99 2.99 0 0 0-2.14-2.86" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={8.793} x2={13.207} y1={7.793} y2={12.207} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M10 15a1 1 0 0 1-.707-.293l-3-3a.999.999 0 1 1 1.414-1.414L10 12.586l4.293-4.293a.999.999 0 1 1 1.414 1.414l-5 5A1 1 0 0 1 10 15" style={{
|
||||
fill: "url(#d)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgCheckAll);
|
||||
export default ForwardRef;
|
||||
44
components/icons/checked-2.tsx
Normal file
44
components/icons/checked-2.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgChecked2 = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c1.379 0 2.5 1.122 2.5 2.5v12c0 1.378-1.121 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V6c0-1.378 1.121-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={12.293} x2={16.707} y1={6.293} y2={10.707} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M11 16a1 1 0 0 1-.707-.293l-3-3a.999.999 0 1 1 1.414-1.414L11 13.586l9.293-9.293a.999.999 0 1 1 1.414 1.414l-10 10A1 1 0 0 1 11 16" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgChecked2);
|
||||
export default ForwardRef;
|
||||
44
components/icons/checked.tsx
Normal file
44
components/icons/checked.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgChecked = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={12} r={10} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m0-.5C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={12.293} x2={16.707} y1={6.293} y2={10.707} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M11 16a1 1 0 0 1-.707-.293l-3-3a.999.999 0 1 1 1.414-1.414L11 13.586l9.293-9.293a.999.999 0 1 1 1.414 1.414l-10 10A1 1 0 0 1 11 16" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgChecked);
|
||||
export default ForwardRef;
|
||||
33
components/icons/checkmark.tsx
Normal file
33
components/icons/checkmark.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgCheckmark = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={7.482} x2={16.518} y1={4.982} y2={14.018} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M22.268 4.732a2.5 2.5 0 0 0-3.536 0L9 14.464l-3.732-3.732a2.501 2.501 0 0 0-3.536 3.536l4.645 4.645a3.71 3.71 0 0 0 5.246 0L22.268 8.268a2.5 2.5 0 0 0 0-3.536" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={7.482} x2={16.518} y1={4.982} y2={14.018} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M20.5 4.5c.534 0 1.036.208 1.414.586s.586.88.586 1.414-.208 1.036-.586 1.414L11.269 18.559c-.606.606-1.412.94-2.269.94s-1.663-.334-2.269-.94l-4.645-4.645A1.99 1.99 0 0 1 1.5 12.5c0-.534.208-1.036.586-1.414s.88-.586 1.414-.586 1.036.208 1.414.586l3.732 3.732.354.354.354-.354 9.732-9.732c.378-.378.88-.586 1.414-.586m0-.5c-.64 0-1.28.244-1.768.732L9 14.464l-3.732-3.732C4.78 10.244 4.14 10 3.5 10s-1.28.244-1.768.732a2.5 2.5 0 0 0 0 3.536l4.645 4.645c.724.724 1.674 1.086 2.623 1.086s1.899-.362 2.623-1.086L22.268 8.268A2.501 2.501 0 0 0 20.5 4" style={{
|
||||
fill: "url(#b)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgCheckmark);
|
||||
export default ForwardRef;
|
||||
44
components/icons/clock.tsx
Normal file
44
components/icons/clock.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgClock = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={12} r={10} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m0-.5C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9.709} x2={16.619} y1={6.877} y2={13.787} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m15.203 13.789-1.167-1.509a3 3 0 0 1-.615-1.567L13 6a1 1 0 0 0-2 0l-.381 4.885a3 3 0 0 0 1.051 2.521l2.119 1.797a.999.999 0 1 0 1.414-1.414" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgClock);
|
||||
export default ForwardRef;
|
||||
52
components/icons/close-window.tsx
Normal file
52
components/icons/close-window.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgCloseWindow = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c1.378 0 2.5 1.121 2.5 2.5v12c0 1.379-1.122 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V6c0-1.379 1.122-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3m-1.439 13.561a1.5 1.5 0 0 1-2.121 0l-2.439-2.439-2.439 2.439a1.5 1.5 0 1 1-2.121-2.121c.071-.073 1.15-1.152 2.438-2.44L7.44 9.561A1.5 1.5 0 1 1 9.561 7.44L12 9.879l2.439-2.439a1.5 1.5 0 1 1 2.121 2.121L14.121 12l2.439 2.439c.586.586.586 1.536.001 2.122" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c1.378 0 2.5 1.121 2.5 2.5v12c0 1.379-1.122 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V6c0-1.379 1.122-2.5 2.5-2.5zM9.172 12l-2.086 2.086A2.001 2.001 0 0 0 8.5 17.5c.534 0 1.036-.208 1.414-.586l.134-.134L12 14.828l1.77 1.77.316.316c.378.378.88.586 1.414.586s1.036-.208 1.414-.586c.78-.78.78-2.049 0-2.828L14.828 12l1.938-1.938.148-.147A2.001 2.001 0 0 0 15.5 6.5c-.534 0-1.036.208-1.414.586L12 9.172l-1.938-1.938-.148-.148A1.99 1.99 0 0 0 8.5 6.5a2.001 2.001 0 0 0-1.414 3.414zM18 3H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3M8.5 17a1.5 1.5 0 0 1-1.061-2.56L9.879 12 7.44 9.561a1.5 1.5 0 1 1 2.121-2.122L12 9.879l2.439-2.439c.293-.294.677-.44 1.061-.44s.768.146 1.061.439a1.5 1.5 0 0 1 0 2.121l-2.439 2.439 2.439 2.439A1.5 1.5 0 0 1 15.5 17c-.384 0-.768-.146-1.061-.439L12 14.122l-2.439 2.439A1.5 1.5 0 0 1 8.5 17" style={{
|
||||
fill: "url(#d)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgCloseWindow);
|
||||
export default ForwardRef;
|
||||
123
components/icons/combo-chart.tsx
Normal file
123
components/icons/combo-chart.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgComboChart = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={2.336} x2={7.664} y1={14.836} y2={20.164} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M6 21H4a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={2.336} x2={7.664} y1={14.836} y2={20.164} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M6 14.5c.827 0 1.5.673 1.5 1.5v3c0 .827-.673 1.5-1.5 1.5H4c-.827 0-1.5-.673-1.5-1.5v-3c0-.827.673-1.5 1.5-1.5zm0-.5H4a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={15.586} x2={22.414} y1={12.586} y2={19.414} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M20 21h-2a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={15.586} x2={22.414} y1={12.586} y2={19.414} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M20 11.5c.827 0 1.5.673 1.5 1.5v6c0 .827-.673 1.5-1.5 1.5h-2c-.827 0-1.5-.673-1.5-1.5v-6c0-.827.673-1.5 1.5-1.5zm0-.5h-2a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2" style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={7.836} x2={16.164} y1={10.336} y2={18.664} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M13 21h-2a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2" style={{
|
||||
fill: "url(#e)"
|
||||
}} /><linearGradient id="f" x1={7.836} x2={16.164} y1={10.336} y2={18.664} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M13 8.5c.827 0 1.5.673 1.5 1.5v9c0 .827-.673 1.5-1.5 1.5h-2c-.827 0-1.5-.673-1.5-1.5v-9c0-.827.673-1.5 1.5-1.5zm0-.5h-2a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2" style={{
|
||||
fill: "url(#f)"
|
||||
}} /><linearGradient id="g" x1={7.586} x2={16.414} y1={2.586} y2={11.414} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 4c-.514 0-.978.2-1.333.519l-3.75-1.057A1.994 1.994 0 0 0 12 2a2 2 0 0 0-1.996 1.961L5.999 6.277A2 2 0 0 0 5 6a2 2 0 1 0 1.999 2.009l3.976-2.3a1.98 1.98 0 0 0 2.45-.307l3.622 1.021A1.999 1.999 0 1 0 19 4" style={{
|
||||
fill: "url(#g)"
|
||||
}} /><linearGradient id="h" x1={3} x2={21} y1={6} y2={6} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5a1.5 1.5 0 0 1 1.436 1.097l.076.27.27.076L17.532 5l.265.075.205-.184c.279-.252.634-.391.998-.391.827 0 1.5.673 1.5 1.5s-.673 1.5-1.5 1.5a1.51 1.51 0 0 1-1.465-1.183l-.063-.294-.29-.082L13.56 4.92l-.285-.08-.207.211c-.285.29-.664.449-1.068.449q-.402 0-.767-.22l-.253-.151-.255.148-3.976 2.3-.249.143-.001.287A1.503 1.503 0 0 1 5 9.5c-.827 0-1.5-.673-1.5-1.5S4.173 6.5 5 6.5q.388 0 .748.209l.251.146.251-.145 4.005-2.317.244-.141.006-.282c.015-.81.686-1.47 1.495-1.47m0-.5a2 2 0 0 0-1.996 1.961L5.999 6.277A2 2 0 0 0 5 6a2 2 0 1 0 1.999 2.009l3.976-2.3a1.98 1.98 0 0 0 2.45-.307l3.622 1.021A1.999 1.999 0 1 0 19 4c-.514 0-.978.2-1.333.519l-3.75-1.057A1.994 1.994 0 0 0 12 2" style={{
|
||||
fill: "url(#h)"
|
||||
}} /><linearGradient id="i" x1={17.586} x2={20.414} y1={4.586} y2={7.414} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><circle cx={19} cy={6} r={2} style={{
|
||||
fill: "url(#i)"
|
||||
}} /><linearGradient id="j" x1={10.586} x2={13.414} y1={2.586} y2={5.414} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><circle cx={12} cy={4} r={2} style={{
|
||||
fill: "url(#j)"
|
||||
}} /><linearGradient id="k" x1={3.586} x2={6.414} y1={6.586} y2={9.414} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><circle cx={5} cy={8} r={2} style={{
|
||||
fill: "url(#k)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgComboChart);
|
||||
export default ForwardRef;
|
||||
98
components/icons/conference-call.tsx
Normal file
98
components/icons/conference-call.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgConferenceCall = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={5.982} x2={18.018} y1={11.482} y2={23.518} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M20.5 15h-1.85a3.49 3.49 0 0 0-3.15-2h-7a3.49 3.49 0 0 0-3.15 2H3.5a2.5 2.5 0 1 0 0 5h17a2.5 2.5 0 1 0 0-5" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={5.982} x2={18.018} y1={11.482} y2={23.518} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M15.5 13.5c1.16 0 2.194.657 2.699 1.715l.136.285H20.5c1.103 0 2 .897 2 2s-.897 2-2 2h-17c-1.103 0-2-.897-2-2s.897-2 2-2h2.165l.136-.285A2.97 2.97 0 0 1 8.5 13.5zm0-.5h-7a3.49 3.49 0 0 0-3.15 2H3.5a2.5 2.5 0 1 0 0 5h17a2.5 2.5 0 1 0 0-5h-1.85a3.49 3.49 0 0 0-3.15-2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={5.982} x2={18.018} y1={11.482} y2={23.518} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.4
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.2
|
||||
}} /></linearGradient><path d="M20.5 15h-1.85a3.49 3.49 0 0 0-3.15-2h-7a3.49 3.49 0 0 0-3.15 2H3.5a2.5 2.5 0 1 0 0 5h17a2.5 2.5 0 1 0 0-5" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={9.172} x2={14.828} y1={4.172} y2={9.828} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={7} r={4} style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={9.172} x2={14.828} y1={4.172} y2={9.828} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 3.5c1.93 0 3.5 1.57 3.5 3.5s-1.57 3.5-3.5 3.5S8.5 8.93 8.5 7s1.57-3.5 3.5-3.5m0-.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8" style={{
|
||||
fill: "url(#e)"
|
||||
}} /><linearGradient id="f" x1={17.879} x2={22.121} y1={6.879} y2={11.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={20} cy={9} r={3} style={{
|
||||
fill: "url(#f)"
|
||||
}} /><linearGradient id="g" x1={17.879} x2={22.121} y1={6.879} y2={11.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M20 6.5c1.379 0 2.5 1.121 2.5 2.5s-1.121 2.5-2.5 2.5-2.5-1.121-2.5-2.5 1.121-2.5 2.5-2.5m0-.5a3 3 0 1 0 0 6 3 3 0 0 0 0-6" style={{
|
||||
fill: "url(#g)"
|
||||
}} /><linearGradient id="h" x1={1.879} x2={6.121} y1={6.879} y2={11.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={4} cy={9} r={3} style={{
|
||||
fill: "url(#h)"
|
||||
}} /><linearGradient id="i" x1={1.879} x2={6.121} y1={6.879} y2={11.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M4 6.5c1.379 0 2.5 1.121 2.5 2.5S5.379 11.5 4 11.5 1.5 10.379 1.5 9 2.621 6.5 4 6.5M4 6a3 3 0 1 0 0 6 3 3 0 0 0 0-6" style={{
|
||||
fill: "url(#i)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgConferenceCall);
|
||||
export default ForwardRef;
|
||||
44
components/icons/contacts.tsx
Normal file
44
components/icons/contacts.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgContacts = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M4 19V5a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v14a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M17 2.5c1.379 0 2.5 1.121 2.5 2.5v14c0 1.379-1.121 2.5-2.5 2.5H7A2.5 2.5 0 0 1 4.5 19V5c0-1.379 1.121-2.5 2.5-2.5zm0-.5H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={7.732} x2={16.268} y1={9.025} y2={17.561} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M9 9a3 3 0 1 1 6 0 3 3 0 0 1-6 0m6 5H9a2 2 0 1 0 0 4h6a2 2 0 0 0 0-4" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgContacts);
|
||||
export default ForwardRef;
|
||||
44
components/icons/cursor.tsx
Normal file
44
components/icons/cursor.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgCursor = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={2.592} x2={16.864} y1={6.124} y2={20.395} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18.584 12.854 8.091 2.361C7.319 1.59 6 2.136 6 3.227v15.044a1.256 1.256 0 0 0 1.939 1.054l3.1-2.008 1.911 3.72a1.77 1.77 0 1 0 3.151-1.618l-1.878-3.651 3.735-.797a1.256 1.256 0 0 0 .626-2.117" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={2.592} x2={16.864} y1={6.124} y2={20.395} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M7.234 2.5a.7.7 0 0 1 .504.215l10.493 10.493c.199.199.27.479.19.749a.75.75 0 0 1-.567.525l-3.735.797-.639.136.299.581 1.878 3.651a1.272 1.272 0 0 1-2.261 1.161l-1.911-3.72-.253-.492-.464.301-3.1 2.008a.74.74 0 0 1-.408.125.76.76 0 0 1-.76-.758V3.227c0-.455.373-.727.734-.727m0-.5C6.604 2 6 2.489 6 3.227v15.044a1.258 1.258 0 0 0 1.94 1.054l3.1-2.008 1.911 3.72a1.77 1.77 0 0 0 2.384.767 1.77 1.77 0 0 0 .766-2.384l-1.878-3.651 3.735-.797a1.257 1.257 0 0 0 .626-2.117L8.091 2.361A1.2 1.2 0 0 0 7.234 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={11.74} x2={16.191} y1={16.617} y2={21.068} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m11.039 17.318 1.911 3.72a1.77 1.77 0 1 0 3.151-1.618l-1.878-3.651" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgCursor);
|
||||
export default ForwardRef;
|
||||
44
components/icons/delete-sign.tsx
Normal file
44
components/icons/delete-sign.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgDeleteSign = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c1.378 0 2.5 1.122 2.5 2.5v12c0 1.378-1.122 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V6c0-1.378 1.122-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={7.791} x2={16.209} y1={7.79} y2={16.209} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m13.403 12 2.812-2.812a.985.985 0 0 0 0-1.392l-.011-.011a.985.985 0 0 0-1.392 0L12 10.597 9.188 7.785a.985.985 0 0 0-1.392 0l-.011.011a.985.985 0 0 0 0 1.392L10.597 12l-2.812 2.812a.985.985 0 0 0 0 1.392l.011.011a.985.985 0 0 0 1.392 0L12 13.403l2.812 2.812a.985.985 0 0 0 1.392 0l.011-.011a.985.985 0 0 0 0-1.392z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgDeleteSign);
|
||||
export default ForwardRef;
|
||||
44
components/icons/delete.tsx
Normal file
44
components/icons/delete.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgDelete = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.144} x2={19.856} y1={3.027} y2={18.739} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M21 5a2 2 0 0 0-2-2h-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1H5a1.993 1.993 0 0 0-.903 3.775l1.575 12.597A3 3 0 0 0 8.648 22h6.703a3 3 0 0 0 2.977-2.628l1.575-12.597A1.99 1.99 0 0 0 21 5" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.144} x2={19.856} y1={3.027} y2={18.739} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M15 2.5a.5.5 0 0 1 .5.5v.5H19c.827 0 1.5.673 1.5 1.5 0 .561-.316 1.07-.824 1.33l-.236.12-.033.263-1.575 12.597a2.505 2.505 0 0 1-2.481 2.19H8.648a2.504 2.504 0 0 1-2.481-2.19L4.593 6.713 4.56 6.45l-.236-.12A1.49 1.49 0 0 1 3.5 5c0-.827.673-1.5 1.5-1.5h3.5V3a.5.5 0 0 1 .5-.5zm0-.5H9a1 1 0 0 0-1 1H5a1.993 1.993 0 0 0-.903 3.775l1.575 12.597A3 3 0 0 0 8.648 22h6.703a3 3 0 0 0 2.977-2.628l1.575-12.597A1.993 1.993 0 0 0 19 3h-3a1 1 0 0 0-1-1" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={7.086} x2={16.914} y1={0.086} y2={9.914} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M19 3h-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1H5a2 2 0 1 0 0 4h14a2 2 0 1 0 0-4" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgDelete);
|
||||
export default ForwardRef;
|
||||
66
components/icons/document.tsx
Normal file
66
components/icons/document.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgDocument = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={2.818} x2={19.061} y1={4.939} y2={21.182} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M4 19V5a3 3 0 0 1 3-3h7l6 6v11a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={2.818} x2={19.061} y1={4.939} y2={21.182} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M13.793 2.5 19.5 8.207V19c0 1.379-1.122 2.5-2.5 2.5H7A2.503 2.503 0 0 1 4.5 19V5c0-1.379 1.122-2.5 2.5-2.5zM14 2H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8z" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={12.793} x2={18.793} y1={3.207} y2={9.207} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M14 6V2l6 6h-4a2 2 0 0 1-2-2" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={9.793} x2={14.207} y1={9.793} y2={14.207} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M15 13H9a1 1 0 1 1 0-2h6a1 1 0 1 1 0 2" style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={9.293} x2={12.707} y1={14.293} y2={17.707} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M13 17H9a1 1 0 1 1 0-2h4a1 1 0 1 1 0 2" style={{
|
||||
fill: "url(#e)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgDocument);
|
||||
export default ForwardRef;
|
||||
66
components/icons/domain.tsx
Normal file
66
components/icons/domain.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgDomain = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={12} r={10} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m0-.5C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9.341} x2={14.656} y1={8.848} y2={14.164} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M13.462 15h-.004a.864.864 0 0 1-.851-.709L12 10.975h-.017l-.601 3.315a.865.865 0 0 1-1.7.005l-.854-4.543A.635.635 0 0 1 9.452 9h.069c.314 0 .581.23.628.541l.539 3.636h.034l.551-3.527a.768.768 0 0 1 1.518-.004l.574 3.532h.025l.531-3.635a.634.634 0 1 1 1.25.209l-.86 4.543a.86.86 0 0 1-.849.705" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={16.34} x2={21.656} y1={8.848} y2={14.164} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M20.462 15h-.004a.864.864 0 0 1-.851-.709L19 10.975h-.017l-.601 3.315a.865.865 0 0 1-1.7.005l-.853-4.543A.634.634 0 0 1 16.452 9h.069c.314 0 .581.23.628.541l.539 3.636h.034l.551-3.527a.768.768 0 0 1 1.518-.004l.574 3.532h.025l.531-3.635a.634.634 0 1 1 1.25.209l-.86 4.543a.86.86 0 0 1-.849.705" style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={2.341} x2={7.656} y1={8.848} y2={14.164} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M6.462 15h-.004a.864.864 0 0 1-.851-.709L5 10.975h-.016l-.601 3.315a.865.865 0 0 1-1.701.005l-.854-4.543A.635.635 0 0 1 2.452 9h.069c.314 0 .581.23.628.541l.539 3.636h.034l.55-3.527a.769.769 0 0 1 1.519-.004l.574 3.532h.025l.531-3.635a.634.634 0 1 1 1.25.209l-.86 4.543a.86.86 0 0 1-.849.705" style={{
|
||||
fill: "url(#e)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgDomain);
|
||||
export default ForwardRef;
|
||||
44
components/icons/download.tsx
Normal file
44
components/icons/download.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgDownload = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c1.379 0 2.5 1.121 2.5 2.5v12c0 1.379-1.121 2.5-2.5 2.5H6A2.5 2.5 0 0 1 3.5 18V6c0-1.379 1.121-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={7.583} x2={16.417} y1={5.01} y2={13.845} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M16.131 12H14V3.421a2 2 0 1 0-4 0V12H7.869c-.771 0-1.159.93-.616 1.478l3.856 3.893a1.255 1.255 0 0 0 1.782 0l3.856-3.893c.543-.548.155-1.478-.616-1.478" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgDownload);
|
||||
export default ForwardRef;
|
||||
44
components/icons/downloads-2.tsx
Normal file
44
components/icons/downloads-2.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgDownloads2 = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.697} x2={19.095} y1={6.993} y2={21.391} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19.483 8.192C18.345 5.161 15.429 3 12 3a8 8 0 0 0-7.945 7.095A4.997 4.997 0 0 0 5 20h13a5.998 5.998 0 0 0 1.483-11.808" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={0} x2={24} y1={11.5} y2={11.5} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 3.5c3.103 0 5.922 1.956 7.015 4.868l.092.244.253.064a5.49 5.49 0 0 1 4.14 5.323c0 3.033-2.467 5.5-5.5 5.5H5a4.505 4.505 0 0 1-4.5-4.5 4.5 4.5 0 0 1 3.649-4.414l.361-.069.041-.366A7.49 7.49 0 0 1 12 3.5m0-.5a8 8 0 0 0-7.945 7.095A4.997 4.997 0 0 0 5 20h13a5.998 5.998 0 0 0 1.483-11.808C18.345 5.161 15.429 3 12 3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9.398} x2={14.602} y1={10.188} y2={15.391} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M15.294 13H13V9a1 1 0 0 0-2 0v4H8.706a.691.691 0 0 0-.491 1.178l3.075 3.104a1 1 0 0 0 1.421 0l3.075-3.104A.692.692 0 0 0 15.294 13" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgDownloads2);
|
||||
export default ForwardRef;
|
||||
44
components/icons/edit.tsx
Normal file
44
components/icons/edit.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgEdit = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={9.275} x2={14.058} y1={9.931} y2={14.715} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M20.011 3.989a3.375 3.375 0 0 0-4.773 0L4.208 14.998a2.07 2.07 0 0 0-.576 1.11l-.615 3.567a1.132 1.132 0 0 0 1.31 1.308l3.525-.613a2.06 2.06 0 0 0 1.104-.573L20.011 8.761a3.37 3.37 0 0 0 0-4.772" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3} x2={21} y1={12} y2={12} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M17.625 3.5c.768 0 1.49.299 2.033.842s.842 1.265.842 2.033-.299 1.49-.842 2.033L8.603 19.443a1.56 1.56 0 0 1-.837.434l-3.525.613a.633.633 0 0 1-.732-.73l.615-3.567c.055-.321.206-.611.436-.841l11.031-11.01a2.86 2.86 0 0 1 2.034-.842m0-.5c-.864 0-1.727.33-2.387.989L4.208 14.998a2.07 2.07 0 0 0-.576 1.11l-.615 3.567a1.132 1.132 0 0 0 1.31 1.308l3.525-.613a2.06 2.06 0 0 0 1.104-.573L20.011 8.761A3.375 3.375 0 0 0 17.625 3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={7.999} x2={12.775} y1={11.221} y2={15.997} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m19.832 8.94-1.984 1.978-4.773-4.773 1.984-1.978zM3.017 19.675a1.132 1.132 0 0 0 1.31 1.308l2.171-.378L3.392 17.5z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgEdit);
|
||||
export default ForwardRef;
|
||||
44
components/icons/email.tsx
Normal file
44
components/icons/email.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgEmail = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={12} r={10} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m0-.5C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={6.343} x2={17.207} y1={6.343} y2={17.207} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M12.579 4.021a8 8 0 0 0-6.236 2.323 8 8 0 0 0-2.322 6.234C4.314 16.74 7.934 20 12.26 20H14a1 1 0 1 0 0-2h-1.74c-3.281 0-6.024-2.443-6.244-5.562a6.02 6.02 0 0 1 1.742-4.68 6.04 6.04 0 0 1 4.681-1.743C15.557 6.235 18 8.979 18 12.26V13a1.001 1.001 0 0 1-2 0v-1c0-2.206-1.794-4-4-4s-4 1.794-4 4 1.794 4 4 4c1.05 0 2-.415 2.714-1.08A2.98 2.98 0 0 0 17 16c1.654 0 3-1.346 3-3v-.74c0-4.326-3.26-7.947-7.421-8.239M12 14c-1.103 0-2-.897-2-2s.897-2 2-2 2 .897 2 2-.897 2-2 2" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgEmail);
|
||||
export default ForwardRef;
|
||||
33
components/icons/empty-trush.tsx
Normal file
33
components/icons/empty-trush.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgEmptyTrush = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.478} x2={19.522} y1={3.108} y2={18.152} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 3H5a1 1 0 0 0-.994 1.11L5.55 18l.037.331A3 3 0 0 0 8.568 21h6.864a3 3 0 0 0 2.982-2.669L18.45 18l1.543-13.89A1 1 0 0 0 19 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.478} x2={19.522} y1={3.108} y2={18.152} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 3.5c.144 0 .276.059.373.167.096.107.14.245.124.389l-1.543 13.89-.037.331a2.497 2.497 0 0 1-2.485 2.224H8.568a2.497 2.497 0 0 1-2.485-2.224l-.037-.331L4.503 4.055a.5.5 0 0 1 .125-.388A.5.5 0 0 1 5 3.5zm0-.5H5a1 1 0 0 0-.994 1.11L5.55 18l.037.331A3 3 0 0 0 8.568 21h6.864a3 3 0 0 0 2.982-2.669L18.45 18l1.543-13.89A1 1 0 0 0 19 3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgEmptyTrush);
|
||||
export default ForwardRef;
|
||||
44
components/icons/exit.tsx
Normal file
44
components/icons/exit.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgExit = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M20 5v14a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M17 2.5c1.379 0 2.5 1.121 2.5 2.5v14c0 1.379-1.121 2.5-2.5 2.5H7A2.503 2.503 0 0 1 4.5 19V5c0-1.379 1.122-2.5 2.5-2.5zm0-.5H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9} x2={22.322} y1={12} y2={12} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m21.95 11.109-3.893-3.856c-.548-.542-1.478-.154-1.478.616V10H11a2 2 0 1 0 0 4h5.579v2.131c0 .771.93 1.159 1.478.616l3.893-3.856a1.255 1.255 0 0 0 0-1.782" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgExit);
|
||||
export default ForwardRef;
|
||||
33
components/icons/expand-arrow.tsx
Normal file
33
components/icons/expand-arrow.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgExpandArrow = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={6.379} x2={17.621} y1={3.379} y2={14.621} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M21.121 6.879a3 3 0 0 0-4.243 0L12 11.757 7.121 6.878a3 3 0 1 0-4.243 4.243l7 7a2.99 2.99 0 0 0 4.031.173c.07-.058.146-.107.212-.173l7-7a3 3 0 0 0 0-4.242" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={6.379} x2={17.621} y1={3.379} y2={14.621} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M5 6.5c.668 0 1.296.26 1.768.732l4.879 4.879.353.354.354-.354 4.879-4.879C17.704 6.76 18.332 6.5 19 6.5s1.296.26 1.768.732a2.503 2.503 0 0 1 0 3.536l-5.587 5.587-1.413 1.413c-.026.026-.055.046-.083.068l-.095.075a2.52 2.52 0 0 1-1.608.583 2.46 2.46 0 0 1-1.75-.725l-7-7C2.76 10.296 2.5 9.668 2.5 9s.26-1.296.732-1.768A2.48 2.48 0 0 1 5 6.5M5 6a3 3 0 0 0-2.121 5.122l7 7a2.96 2.96 0 0 0 2.103.871 3 3 0 0 0 1.928-.699c.07-.058.146-.107.212-.173l7-7a3 3 0 1 0-4.243-4.242L12 11.757 7.121 6.878A3 3 0 0 0 5 6" style={{
|
||||
fill: "url(#b)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgExpandArrow);
|
||||
export default ForwardRef;
|
||||
44
components/icons/external-link.tsx
Normal file
44
components/icons/external-link.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgExternalLink = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.025} x2={20.268} y1={3.732} y2={19.975} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h13a2 2 0 0 1 2 2v13a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.025} x2={20.268} y1={3.732} y2={19.975} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 3.5c.827 0 1.5.673 1.5 1.5v13c0 1.378-1.122 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V6c0-1.378 1.122-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V5a2 2 0 0 0-2-2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={11.816} x2={19.135} y1={4.865} y2={12.184} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M19 3h-4.834c-1.037 0-1.556 1.253-.823 1.986l1.54 1.54-4.347 4.347a1.833 1.833 0 0 0 2.591 2.591l4.347-4.347 1.54 1.54c.733.733 1.986.214 1.986-.823V5a2 2 0 0 0-2-2" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgExternalLink);
|
||||
export default ForwardRef;
|
||||
44
components/icons/facebook-new.tsx
Normal file
44
components/icons/facebook-new.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgFacebookNew = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={12} r={10} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m0-.5C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9} x2={17.319} y1={9.613} y2={17.932} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M10.505 10.272v1.749H8.031v2.629h2.474v7.226c.489.074.986.124 1.495.124.46 0 .91-.042 1.354-.102V14.65h2.588l.406-2.629h-2.995v-1.437c0-1.092.357-2.061 1.379-2.061h1.642V6.229c-.289-.039-.898-.124-2.051-.124-2.407 0-3.818 1.271-3.818 4.167" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgFacebookNew);
|
||||
export default ForwardRef;
|
||||
44
components/icons/facebook.tsx
Normal file
44
components/icons/facebook.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgFacebook = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c1.379 0 2.5 1.122 2.5 2.5v12c0 1.378-1.121 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V6c0-1.378 1.121-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={11.118} x2={19.209} y1={9.115} y2={17.206} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M12.621 21v-6.961h-2.343v-2.725h2.343V9.309c0-2.324 1.421-3.591 3.495-3.591q1.049-.002 2.092.105v2.43H16.78c-1.13 0-1.35.534-1.35 1.322v1.735h2.7l-.351 2.725h-2.365V21z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgFacebook);
|
||||
export default ForwardRef;
|
||||
44
components/icons/file.tsx
Normal file
44
components/icons/file.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgFile = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={2.818} x2={19.061} y1={4.939} y2={21.182} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M4 19V5a3 3 0 0 1 3-3h7l6 6v11a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={2.818} x2={19.061} y1={4.939} y2={21.182} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M13.793 2.5 19.5 8.207V19c0 1.378-1.122 2.5-2.5 2.5H7A2.503 2.503 0 0 1 4.5 19V5c0-1.379 1.122-2.5 2.5-2.5zM14 2H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8z" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={12.793} x2={18.793} y1={3.207} y2={9.207} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M14 6V2l6 6h-4a2 2 0 0 1-2-2" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgFile);
|
||||
export default ForwardRef;
|
||||
55
components/icons/filled-trash.tsx
Normal file
55
components/icons/filled-trash.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgFilledTrash = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.478} x2={19.522} y1={3.108} y2={18.152} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 3H5a1 1 0 0 0-.994 1.11L5.55 18l.037.331A3 3 0 0 0 8.568 21h6.864a3 3 0 0 0 2.982-2.669L18.45 18l1.543-13.89A1 1 0 0 0 19 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.478} x2={19.522} y1={3.108} y2={18.152} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 3.5c.144 0 .276.059.373.167s.14.245.124.389l-1.543 13.89-.037.331a2.497 2.497 0 0 1-2.485 2.224H8.568a2.497 2.497 0 0 1-2.485-2.224l-.037-.331L4.503 4.055c-.016-.143.028-.281.125-.388S4.856 3.5 5 3.5zm0-.5H5a1 1 0 0 0-.994 1.11L5.55 18l.037.331A3 3 0 0 0 8.568 21h6.864a3 3 0 0 0 2.982-2.669L18.45 18l1.543-13.89A1 1 0 0 0 19 3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9.079} x2={14.874} y1={9.921} y2={15.716} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m8.547 15.095-1.019-2.039a1.64 1.64 0 0 1 .307-1.891l2.562-2.562a.7.7 0 0 1 1.077.107l1.059 1.589c.292.438.783.701 1.309.701h1.85a.7.7 0 0 1 .582 1.088l-1.094 1.641c-.117.177-.18.384-.18.597v.974a.7.7 0 0 1-.7.7h-4.288a1.64 1.64 0 0 1-1.465-.905" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={13.457} x2={16.398} y1={5.781} y2={8.721} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m12.883 7.325.697 1.046c.263.393.704.629 1.176.629h1.036a.79.79 0 0 0 .776-.646l.207-1.125A1.04 1.04 0 0 0 15.751 6h-2.159a.853.853 0 0 0-.709 1.325" style={{
|
||||
fill: "url(#d)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgFilledTrash);
|
||||
export default ForwardRef;
|
||||
44
components/icons/folder-invoices-19.tsx
Normal file
44
components/icons/folder-invoices-19.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgFolderInvoices19 = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={2.336} x2={19.871} y1={2.836} y2={20.371} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.4
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.2
|
||||
}} /></linearGradient><path d="M19 5h-8l-.544-1.632A2 2 0 0 0 8.558 2H4a2 2 0 0 0-2 2v13a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={2.336} x2={19.871} y1={2.836} y2={20.371} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.4
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.2
|
||||
}} /></linearGradient><path d="M8.558 2.5c.647 0 1.219.412 1.423 1.026l.544 1.632.115.342H19c1.379 0 2.5 1.121 2.5 2.5v9c0 1.379-1.121 2.5-2.5 2.5H5A2.5 2.5 0 0 1 2.5 17V4c0-.827.673-1.5 1.5-1.5zm0-.5H4a2 2 0 0 0-2 2v13a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3h-8l-.544-1.632A2 2 0 0 0 8.558 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={3.683} x2={9.097} y1={1.489} y2={6.903} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.524} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.4
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.5
|
||||
}} /></linearGradient><path d="M11 5H2V4a2 2 0 0 1 2-2h4.558a2 2 0 0 1 1.897 1.368z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgFolderInvoices19);
|
||||
export default ForwardRef;
|
||||
44
components/icons/folder-invoices.tsx
Normal file
44
components/icons/folder-invoices.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgFolderInvoices = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={2.336} x2={19.871} y1={2.836} y2={20.371} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 5h-8l-.544-1.632A2 2 0 0 0 8.558 2H4a2 2 0 0 0-2 2v13a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={2.336} x2={19.871} y1={2.836} y2={20.371} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M8.558 2.5c.647 0 1.219.412 1.423 1.026l.544 1.632.115.342H19c1.379 0 2.5 1.121 2.5 2.5v9c0 1.379-1.121 2.5-2.5 2.5H5A2.5 2.5 0 0 1 2.5 17V4c0-.827.673-1.5 1.5-1.5zm0-.5H4a2 2 0 0 0-2 2v13a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3h-8l-.544-1.632A2 2 0 0 0 8.558 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={3.683} x2={9.097} y1={1.489} y2={6.903} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M11 5H2V4a2 2 0 0 1 2-2h4.558a2 2 0 0 1 1.897 1.368z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgFolderInvoices);
|
||||
export default ForwardRef;
|
||||
46
components/icons/for-you.tsx
Normal file
46
components/icons/for-you.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgForYou = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 3.5c1.379 0 2.5 1.122 2.5 2.5v12c0 1.379-1.121 2.5-2.5 2.5H6A2.5 2.5 0 0 1 3.5 18V6c0-1.378 1.121-2.5 2.5-2.5zm0-.5H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={8.567} x2={15.433} y1={7.866} y2={14.733} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M17.5 11.3A3.3 3.3 0 0 0 14.2 8a3.28 3.28 0 0 0-2.2.856A3.28 3.28 0 0 0 9.8 8a3.3 3.3 0 0 0-3.3 3.3c0 .932.389 1.77 1.01 2.37l3.299 3.306a1.685 1.685 0 0 0 2.381 0l3.299-3.306A3.28 3.28 0 0 0 17.5 11.3" style={{
|
||||
fillRule: "evenodd",
|
||||
clipRule: "evenodd",
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgForYou);
|
||||
export default ForwardRef;
|
||||
33
components/icons/forward.tsx
Normal file
33
components/icons/forward.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgForward = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.379} x2={14.621} y1={6.379} y2={17.621} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18.506 10.349a3 3 0 0 0-.384-.471l-7-7a3 3 0 1 0-4.243 4.243L11.757 12l-4.879 4.879a3 3 0 1 0 4.243 4.243l7-7q.22-.22.384-.471c.66-.998.66-2.304.001-3.302" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.379} x2={14.621} y1={6.379} y2={17.621} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M9 2.5c.668 0 1.296.26 1.768.732l7 7q.18.18.321.393a2.49 2.49 0 0 1-.321 3.143l-.94.94-6.06 6.06c-.472.472-1.1.732-1.768.732s-1.296-.26-1.768-.732S6.5 19.668 6.5 19s.26-1.296.732-1.768l4.879-4.879.353-.353-.354-.354-4.878-4.878A2.48 2.48 0 0 1 6.5 5c0-.668.26-1.296.732-1.768A2.48 2.48 0 0 1 9 2.5M9 2a3 3 0 0 0-2.121 5.122L11.757 12l-4.879 4.879a3 3 0 1 0 4.243 4.242l7-7q.22-.22.384-.471a3 3 0 0 0-.384-3.772l-7-7A3 3 0 0 0 9 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgForward);
|
||||
export default ForwardRef;
|
||||
63
components/icons/gemini-ai.tsx
Normal file
63
components/icons/gemini-ai.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgGeminiAi = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={5.786} x2={14.214} y1={5.857} y2={14.285} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="m11.612 15.61-.809 1.852a.872.872 0 0 1-1.607 0l-.808-1.852a7.14 7.14 0 0 0-3.631-3.678l-2.226-.988c-.708-.314-.708-1.344 0-1.658l2.156-.957A7.14 7.14 0 0 0 8.37 4.528l.82-1.974a.872.872 0 0 1 1.62 0l.819 1.974a7.14 7.14 0 0 0 3.683 3.801l2.156.957c.708.314.708 1.344 0 1.658l-2.226.988a7.13 7.13 0 0 0-3.63 3.678" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={5.786} x2={14.214} y1={5.857} y2={14.285} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M10 2.504c.075 0 .258.023.348.241l.819 1.974a7.6 7.6 0 0 0 3.942 4.066l2.157.957a.4.4 0 0 1 .234.372.4.4 0 0 1-.234.372l-2.226.988a7.6 7.6 0 0 0-3.886 3.935l-.809 1.852a.37.37 0 0 1-.345.234.37.37 0 0 1-.345-.234l-.809-1.852a7.62 7.62 0 0 0-3.886-3.935l-2.226-.988a.4.4 0 0 1-.234-.371.4.4 0 0 1 .234-.372l2.156-.957a7.6 7.6 0 0 0 3.943-4.067l.819-1.974A.37.37 0 0 1 10 2.504m0-.5a.87.87 0 0 0-.81.549l-.819 1.975a7.14 7.14 0 0 1-3.684 3.801l-2.156.957c-.708.314-.708 1.344 0 1.658l2.226.988a7.13 7.13 0 0 1 3.631 3.678l.809 1.852a.87.87 0 0 0 .803.534.87.87 0 0 0 .803-.534l.809-1.852a7.14 7.14 0 0 1 3.631-3.678l2.226-.988c.708-.314.708-1.344 0-1.658l-2.156-.957a7.14 7.14 0 0 1-3.683-3.801l-.82-1.974a.87.87 0 0 0-.81-.55" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={15.878} x2={20.122} y1={15.916} y2={20.159} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="m18.713 21.125-.247.565a.506.506 0 0 1-.934 0l-.247-.565a4.36 4.36 0 0 0-2.219-2.249l-.76-.337a.53.53 0 0 1 0-.962l.717-.319a4.36 4.36 0 0 0 2.251-2.324l.253-.611a.506.506 0 0 1 .941 0l.253.611a4.36 4.36 0 0 0 2.251 2.324l.717.319a.53.53 0 0 1 0 .962l-.76.337a4.37 4.37 0 0 0-2.216 2.249" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={15.878} x2={20.122} y1={15.916} y2={20.159} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="m18 14.504.009.011.253.611a4.84 4.84 0 0 0 2.51 2.589l.717.367-.76.337a4.85 4.85 0 0 0-2.474 2.506l-.264.565-.247-.565a4.85 4.85 0 0 0-2.474-2.506l-.76-.386.717-.319a4.84 4.84 0 0 0 2.51-2.589l.258-.621zm0-.5a.505.505 0 0 0-.471.319l-.253.611a4.36 4.36 0 0 1-2.251 2.324l-.717.319a.53.53 0 0 0 0 .962l.76.337a4.36 4.36 0 0 1 2.219 2.249l.247.565c.089.207.278.31.466.31a.5.5 0 0 0 .467-.31l.247-.565a4.36 4.36 0 0 1 2.219-2.249l.76-.337a.53.53 0 0 0 0-.962l-.717-.319a4.36 4.36 0 0 1-2.251-2.324l-.253-.611a.51.51 0 0 0-.472-.319" style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={15.878} x2={20.122} y1={15.916} y2={20.159} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="m18.713 21.125-.247.565a.506.506 0 0 1-.934 0l-.247-.565a4.36 4.36 0 0 0-2.219-2.249l-.76-.337a.53.53 0 0 1 0-.962l.717-.319a4.36 4.36 0 0 0 2.251-2.324l.253-.611a.506.506 0 0 1 .941 0l.253.611a4.36 4.36 0 0 0 2.251 2.324l.717.319a.53.53 0 0 1 0 .962l-.76.337a4.37 4.37 0 0 0-2.216 2.249" style={{
|
||||
fill: "url(#e)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgGeminiAi);
|
||||
export default ForwardRef;
|
||||
52
components/icons/gmail.tsx
Normal file
52
components/icons/gmail.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgGmail = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 4H5a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19 4.5c1.378 0 2.5 1.121 2.5 2.5v10c0 1.379-1.122 2.5-2.5 2.5H5A2.503 2.503 0 0 1 2.5 17V7c0-1.379 1.122-2.5 2.5-2.5zm0-.5H5a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={3.879} x2={20.121} y1={3.879} y2={20.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="m19 4-7 4.99L5 4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3V7.707l7 4.959 7-4.959V20a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={2} x2={22} y1={12} y2={12} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19.153 4.505A2.5 2.5 0 0 1 21.5 7v10c0 1.207-.86 2.217-2 2.45V6.741l-.789.559L12 12.053 5.289 7.299 4.5 6.741V19.45a2.504 2.504 0 0 1-2-2.45V7a2.5 2.5 0 0 1 2.347-2.495l6.863 4.893.29.206.29-.207zM19 4l-7 4.99L5 4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3V7.707l7 4.959 7-4.959V20a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3" style={{
|
||||
fill: "url(#d)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgGmail);
|
||||
export default ForwardRef;
|
||||
55
components/icons/google-logo.tsx
Normal file
55
components/icons/google-logo.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgGoogleLogo = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.808} x2={18.939} y1={5.056} y2={19.187} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="m21.805 10.042-.01-.042H14a2 2 0 1 0 0 4h3.651a6 6 0 0 1-2.067 2.799A5.95 5.95 0 0 1 12 18a6 6 0 0 1-5.647-3.989A5.95 5.95 0 0 1 6.458 9.7 6 6 0 0 1 12 6c1 0 1.941.247 2.769.681.79.414 1.75.301 2.381-.33.945-.945.72-2.545-.459-3.176A9.9 9.9 0 0 0 12 2a10 10 0 0 0-8.843 5.337 9.95 9.95 0 0 0-.053 9.217C4.762 19.784 8.119 22 12 22a9.95 9.95 0 0 0 6.695-2.587 9.96 9.96 0 0 0 3.11-9.371" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.808} x2={18.939} y1={5.056} y2={19.187} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c1.551 0 3.092.386 4.454 1.115.417.223.696.622.766 1.093.071.478-.084.948-.425 1.288a1.47 1.47 0 0 1-1.045.428 1.6 1.6 0 0 1-.75-.187 6.5 6.5 0 0 0-3-.737 6.48 6.48 0 0 0-6.003 4.009 6.45 6.45 0 0 0-.115 4.67A6.51 6.51 0 0 0 12 18.5c1.407 0 2.75-.45 3.884-1.301a6.5 6.5 0 0 0 2.238-3.032l.236-.667H14c-.827 0-1.5-.673-1.5-1.5s.673-1.5 1.5-1.5h7.38c.08.501.12 1.004.12 1.5a9.52 9.52 0 0 1-3.14 7.042 9.475 9.475 0 0 1-14.811-2.717A9.4 9.4 0 0 1 2.5 12c0-1.559.37-3.049 1.099-4.43A9.49 9.49 0 0 1 12 2.5m0-.5a10 10 0 0 0-8.843 5.337 9.95 9.95 0 0 0-.053 9.217C4.762 19.784 8.119 22 12 22a9.95 9.95 0 0 0 6.695-2.587 9.96 9.96 0 0 0 3.11-9.371l-.01-.042H14a2 2 0 1 0 0 4h3.651a6 6 0 0 1-2.067 2.799A5.95 5.95 0 0 1 12 18a6 6 0 0 1-5.647-3.989A5.95 5.95 0 0 1 6.458 9.7 6 6 0 0 1 12 6c1 0 1.941.247 2.769.681.311.163.649.244.982.244.513 0 1.016-.192 1.399-.575.945-.945.72-2.545-.459-3.176A9.9 9.9 0 0 0 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={1.074} x2={6.009} y1={9.42} y2={14.355} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M6 12c0-.815.164-1.591.458-2.3L3.157 7.337a9.95 9.95 0 0 0-.053 9.217l3.248-2.542A6 6 0 0 1 6 12" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={14.231} x2={21.708} y1={8.941} y2={16.418} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M22 12c0-.671-.069-1.325-.195-1.958l-.01-.042H14a2 2 0 1 0 0 4h3.651a6 6 0 0 1-2.067 2.799l3.111 2.614A9.96 9.96 0 0 0 22 12" style={{
|
||||
fill: "url(#d)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgGoogleLogo);
|
||||
export default ForwardRef;
|
||||
169
components/icons/groups.tsx
Normal file
169
components/icons/groups.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgGroups = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.189} x2={8.811} y1={16.689} y2={22.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M9.5 18h-7a1.5 1.5 0 0 0 0 3h7a1.5 1.5 0 0 0 0-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={3.189} x2={8.811} y1={16.689} y2={22.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M9.5 18.5c.551 0 1 .449 1 1s-.449 1-1 1h-7c-.551 0-1-.449-1-1s.449-1 1-1zm0-.5h-7a1.5 1.5 0 0 0 0 3h7a1.5 1.5 0 0 0 0-3" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={15.189} x2={20.811} y1={16.689} y2={22.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M21.5 18h-7a1.5 1.5 0 0 0 0 3h7a1.5 1.5 0 0 0 0-3" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={15.189} x2={20.811} y1={16.689} y2={22.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M21.5 18.5c.551 0 1 .449 1 1s-.449 1-1 1h-7c-.551 0-1-.449-1-1s.449-1 1-1zm0-.5h-7a1.5 1.5 0 0 0 0 3h7a1.5 1.5 0 0 0 0-3" style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={3.879} x2={8.121} y1={10.879} y2={15.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={6} cy={13} r={3} style={{
|
||||
fill: "url(#e)"
|
||||
}} /><linearGradient id="f" x1={3.879} x2={8.121} y1={10.879} y2={15.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M6 10.5c1.378 0 2.5 1.121 2.5 2.5S7.378 15.5 6 15.5 3.5 14.379 3.5 13s1.122-2.5 2.5-2.5m0-.5a3 3 0 1 0 0 6 3 3 0 0 0 0-6" style={{
|
||||
fill: "url(#f)"
|
||||
}} /><linearGradient id="g" x1={15.879} x2={20.121} y1={10.879} y2={15.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={18} cy={13} r={3} style={{
|
||||
fill: "url(#g)"
|
||||
}} /><linearGradient id="h" x1={15.879} x2={20.121} y1={10.879} y2={15.121} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 10.5c1.378 0 2.5 1.121 2.5 2.5s-1.122 2.5-2.5 2.5-2.5-1.121-2.5-2.5 1.122-2.5 2.5-2.5m0-.5a3 3 0 1 0 0 6 3 3 0 0 0 0-6" style={{
|
||||
fill: "url(#h)"
|
||||
}} /><linearGradient id="i" x1={10.232} x2={13.768} y1={5.732} y2={9.268} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={7.5} r={2.5} style={{
|
||||
fill: "url(#i)"
|
||||
}} /><linearGradient id="j" x1={10.232} x2={13.768} y1={5.732} y2={9.268} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 5.5c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2m0-.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5" style={{
|
||||
fill: "url(#j)"
|
||||
}} /><linearGradient id="k" x1={3.732} x2={7.268} y1={3.732} y2={7.268} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={5.5} cy={5.5} r={2.5} style={{
|
||||
fill: "url(#k)"
|
||||
}} /><linearGradient id="l" x1={3.732} x2={7.268} y1={3.732} y2={7.268} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M5.5 3.5c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2m0-.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5" style={{
|
||||
fill: "url(#l)"
|
||||
}} /><linearGradient id="m" x1={16.732} x2={20.268} y1={3.732} y2={7.268} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={18.5} cy={5.5} r={2.5} style={{
|
||||
fill: "url(#m)"
|
||||
}} /><linearGradient id="n" x1={16.732} x2={20.268} y1={3.732} y2={7.268} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18.5 3.5c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2m0-.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5" style={{
|
||||
fill: "url(#n)"
|
||||
}} /><linearGradient id="o" x1={3.189} x2={8.811} y1={16.689} y2={22.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M9.5 18h-7a1.5 1.5 0 0 0 0 3h7a1.5 1.5 0 0 0 0-3" style={{
|
||||
fill: "url(#o)"
|
||||
}} /><linearGradient id="p" x1={15.189} x2={20.811} y1={16.689} y2={22.311} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M21.5 18h-7a1.5 1.5 0 0 0 0 3h7a1.5 1.5 0 0 0 0-3" style={{
|
||||
fill: "url(#p)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgGroups);
|
||||
export default ForwardRef;
|
||||
44
components/icons/hand-cursor.tsx
Normal file
44
components/icons/hand-cursor.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgHandCursor = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={3.106} x2={19.581} y1={6.066} y2={22.541} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M19.965 19.081c.018-.115.035-.231.035-.351v-6.689a3 3 0 0 0-2.507-2.959L11 8V3a2 2 0 0 0-4 0v10.064l-.186-.186a3 3 0 0 0-4.243 0l-.279.279a1 1 0 0 0 0 1.414l4.573 4.573A1.5 1.5 0 0 0 6 20.5 1.5 1.5 0 0 0 7.5 22h12a1.495 1.495 0 0 0 .465-2.919" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={2} x2={21} y1={11.5} y2={11.5} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M9 1.5c.827 0 1.5.673 1.5 1.5v5.424l.418.07 6.493 1.082a2.49 2.49 0 0 1 2.089 2.466v6.689c0 .094-.015.184-.029.274l-.066.42.404.132a.995.995 0 0 1-.309 1.944h-12c-.551 0-1-.449-1-1a.99.99 0 0 1 .578-.902l.645-.303-.503-.505-4.573-4.573a.501.501 0 0 1-.001-.707l.279-.279a2.48 2.48 0 0 1 1.768-.732c.668 0 1.296.26 1.768.732l.186.186.853.853V3c0-.827.673-1.5 1.5-1.5M9 1a2 2 0 0 0-2 2v10.064l-.186-.186C6.229 12.293 5.461 12 4.693 12s-1.536.293-2.121.879l-.279.279a1 1 0 0 0 0 1.414l4.573 4.573A1.497 1.497 0 0 0 7.5 22h12a1.495 1.495 0 0 0 .465-2.919c.018-.115.035-.231.035-.351v-6.689a3 3 0 0 0-2.507-2.959L11 8V3a2 2 0 0 0-2-2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9.439} x2={17.561} y1={16.439} y2={24.561} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M19.5 19h-12a1.5 1.5 0 0 0 0 3h12a1.5 1.5 0 0 0 0-3" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgHandCursor);
|
||||
export default ForwardRef;
|
||||
44
components/icons/home.tsx
Normal file
44
components/icons/home.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgHome = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.57} x2={19.43} y1={5.952} y2={20.813} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M18 21H6a3 3 0 0 1-3-3V8.765a3 3 0 0 1 1.543-2.622l6-3.333a3 3 0 0 1 2.914 0l6 3.333A3 3 0 0 1 21 8.765V18a3 3 0 0 1-3 3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.57} x2={19.43} y1={5.952} y2={20.813} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.932c.424 0 .844.109 1.214.315l6 3.333A2.5 2.5 0 0 1 20.5 8.765V18c0 1.378-1.122 2.5-2.5 2.5H6A2.503 2.503 0 0 1 3.5 18V8.765A2.5 2.5 0 0 1 4.786 6.58l6-3.333c.37-.206.79-.315 1.214-.315m0-.5c-.502 0-1.004.126-1.457.378l-6 3.333A3 3 0 0 0 3 8.765V18a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V8.765a3 3 0 0 0-1.543-2.622l-6-3.333A3 3 0 0 0 12 2.432" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={8.793} x2={15.207} y1={14.379} y2={20.793} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M15 21H9v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2z" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgHome);
|
||||
export default ForwardRef;
|
||||
46
components/icons/idea.tsx
Normal file
46
components/icons/idea.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgIdea = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<path d="M11.329 22" style={{
|
||||
opacity: 0.35
|
||||
}} /><linearGradient id="a" x1={5.443} x2={18.557} y1={4.243} y2={17.357} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 1a8 8 0 0 0-8 8 7.98 7.98 0 0 0 3.333 6.489L7.976 18l.446 1.744A3 3 0 0 0 11.328 22h1.343a3 3 0 0 0 2.906-2.256L16.024 18l.643-2.511A7.98 7.98 0 0 0 20 9a8 8 0 0 0-8-8" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={5.443} x2={18.557} y1={4.243} y2={17.357} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 1.5c4.136 0 7.5 3.364 7.5 7.5a7.52 7.52 0 0 1-3.126 6.083l-.147.106-.045.176-.643 2.511-.446 1.744a2.5 2.5 0 0 1-2.422 1.88h-1.343a2.5 2.5 0 0 1-2.421-1.88l-.446-1.744-.643-2.511-.045-.176-.147-.106A7.52 7.52 0 0 1 4.5 9c0-4.136 3.364-7.5 7.5-7.5m0-.5a8 8 0 0 0-8 8 7.98 7.98 0 0 0 3.333 6.489L7.976 18l.446 1.744A3 3 0 0 0 11.328 22h1.343a3 3 0 0 0 2.906-2.256L16.024 18l.643-2.511A7.98 7.98 0 0 0 20 9a8 8 0 0 0-8-8" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={9.516} x2={14.484} y1={16.461} y2={21.43} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M15.577 19.744 16.024 18H7.976l.446 1.744A3 3 0 0 0 11.328 22h1.343a3 3 0 0 0 2.906-2.256" style={{
|
||||
fill: "url(#c)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgIdea);
|
||||
export default ForwardRef;
|
||||
66
components/icons/image-file.tsx
Normal file
66
components/icons/image-file.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgImageFile = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={2.818} x2={19.061} y1={4.939} y2={21.182} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M4 19V5a3 3 0 0 1 3-3h7l6 6v11a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3" style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={2.818} x2={19.061} y1={4.939} y2={21.182} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M13.793 2.5 19.5 8.207V19c0 1.378-1.121 2.5-2.5 2.5H7A2.503 2.503 0 0 1 4.5 19V5c0-1.378 1.122-2.5 2.5-2.5zM14 2H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8z" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={12.793} x2={18.793} y1={3.207} y2={9.207} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M14 6V2l6 6h-4a2 2 0 0 1-2-2" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={13.439} x2={15.561} y1={10.439} y2={12.561} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><circle cx={14.5} cy={11.5} r={1.5} style={{
|
||||
fill: "url(#d)"
|
||||
}} /><linearGradient id="e" x1={8.367} x2={15.105} y1={13.793} y2={20.53} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M7 18.375c0 .342.28.625.625.625h8.75a.624.624 0 0 0 .625-.625c0-.852-.472-3.375-1.5-3.375-.507 0-1.295 1-2 1-1.4 0-2.75-3-4-3S7 16.726 7 18.375" style={{
|
||||
fill: "url(#e)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgImageFile);
|
||||
export default ForwardRef;
|
||||
101
components/icons/index.ts
Normal file
101
components/icons/index.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
export { default as LgOK } from './OK';
|
||||
export { default as LgAbout } from './about';
|
||||
export { default as LgAddUserMale } from './add-user-male';
|
||||
export { default as LgAppointmentReminders } from './appointment-reminders';
|
||||
export { default as LgBack } from './back';
|
||||
export { default as LgBinoculars } from './binoculars';
|
||||
export { default as LgBookmarkRibbon } from './bookmark-ribbon';
|
||||
export { default as LgBookmark } from './bookmark';
|
||||
export { default as LgBox } from './box';
|
||||
export { default as LgBriefcase } from './briefcase';
|
||||
export { default as LgCalendar } from './calendar';
|
||||
export { default as LgCancel2 } from './cancel-2';
|
||||
export { default as LgCancel } from './cancel';
|
||||
export { default as LgCheckAll } from './check-all';
|
||||
export { default as LgChecked2 } from './checked-2';
|
||||
export { default as LgChecked } from './checked';
|
||||
export { default as LgCheckmark } from './checkmark';
|
||||
export { default as LgClock } from './clock';
|
||||
export { default as LgCloseWindow } from './close-window';
|
||||
export { default as LgComboChart } from './combo-chart';
|
||||
export { default as LgConferenceCall } from './conference-call';
|
||||
export { default as LgContacts } from './contacts';
|
||||
export { default as LgCursor } from './cursor';
|
||||
export { default as LgDeleteSign } from './delete-sign';
|
||||
export { default as LgDelete } from './delete';
|
||||
export { default as LgDocument } from './document';
|
||||
export { default as LgDomain } from './domain';
|
||||
export { default as LgDownload } from './download';
|
||||
export { default as LgDownloads2 } from './downloads-2';
|
||||
export { default as LgEdit } from './edit';
|
||||
export { default as LgEmail } from './email';
|
||||
export { default as LgEmptyTrush } from './empty-trush';
|
||||
export { default as LgExit } from './exit';
|
||||
export { default as LgExpandArrow } from './expand-arrow';
|
||||
export { default as LgExternalLink } from './external-link';
|
||||
export { default as LgFacebookNew } from './facebook-new';
|
||||
export { default as LgFacebook } from './facebook';
|
||||
export { default as LgFile } from './file';
|
||||
export { default as LgFilledTrash } from './filled-trash';
|
||||
export { default as LgFolderInvoices19 } from './folder-invoices-19';
|
||||
export { default as LgFolderInvoices } from './folder-invoices';
|
||||
export { default as LgForYou } from './for-you';
|
||||
export { default as LgForward } from './forward';
|
||||
export { default as LgGeminiAi } from './gemini-ai';
|
||||
export { default as LgGmail } from './gmail';
|
||||
export { default as LgGoogleLogo } from './google-logo';
|
||||
export { default as LgGroups } from './groups';
|
||||
export { default as LgHandCursor } from './hand-cursor';
|
||||
export { default as LgHome } from './home';
|
||||
export { default as LgIdea } from './idea';
|
||||
export { default as LgImageFile } from './image-file';
|
||||
export { default as LgInfo } from './info';
|
||||
export { default as LgInstagramNew } from './instagram-new';
|
||||
export { default as LgKey } from './key';
|
||||
export { default as LgLike } from './like';
|
||||
export { default as LgLinkedin } from './linkedin';
|
||||
export { default as LgLock } from './lock';
|
||||
export { default as LgMailboxClosedFlagDown } from './mailbox-closed-flag-down';
|
||||
export { default as LgMaintenance } from './maintenance';
|
||||
export { default as LgMarker } from './marker';
|
||||
export { default as LgMenu } from './menu';
|
||||
export { default as LgMusic } from './music';
|
||||
export { default as LgNews } from './news';
|
||||
export { default as LgNoSynchronize } from './no-synchronize';
|
||||
export { default as LgOpenedFolder } from './opened-folder';
|
||||
export { default as LgPhone } from './phone';
|
||||
export { default as LgPicture } from './picture';
|
||||
export { default as LgPinterest } from './pinterest';
|
||||
export { default as LgPlusMath } from './plus-math';
|
||||
export { default as LgPlus } from './plus';
|
||||
export { default as LgPuzzle } from './puzzle';
|
||||
export { default as LgRefresh } from './refresh';
|
||||
export { default as LgRestart } from './restart';
|
||||
export { default as LgSave } from './save';
|
||||
export { default as LgScroll } from './scroll';
|
||||
export { default as LgSearch } from './search';
|
||||
export { default as LgSecuredLetter } from './secured-letter';
|
||||
export { default as LgService } from './service';
|
||||
export { default as LgSettings } from './settings';
|
||||
export { default as LgShare2 } from './share-2';
|
||||
export { default as LgShare3 } from './share-3';
|
||||
export { default as LgShare } from './share';
|
||||
export { default as LgShutdown } from './shutdown';
|
||||
export { default as LgSpeechBubble } from './speech-bubble';
|
||||
export { default as LgStar } from './star';
|
||||
export { default as LgSun } from './sun';
|
||||
export { default as LgSupport } from './support';
|
||||
export { default as LgSynchronize } from './synchronize';
|
||||
export { default as LgToolbox } from './toolbox';
|
||||
export { default as LgTrash } from './trash';
|
||||
export { default as LgTwitter } from './twitter';
|
||||
export { default as LgUncheckAll } from './uncheck-all';
|
||||
export { default as LgUnlock } from './unlock';
|
||||
export { default as LgUpload2 } from './upload-2';
|
||||
export { default as LgUserFemale } from './user-female';
|
||||
export { default as LgUserMale } from './user-male';
|
||||
export { default as LgUser } from './user';
|
||||
export { default as LgVisible } from './visible';
|
||||
export { default as LgWhatsapp } from './whatsapp';
|
||||
export { default as LgYoutubePlay } from './youtube-play';
|
||||
export { default as LgTrendingUp } from './trending-up';
|
||||
55
components/icons/info.tsx
Normal file
55
components/icons/info.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import type { SVGProps } from "react";
|
||||
import { Ref, forwardRef } from "react";
|
||||
interface SVGRProps {
|
||||
title?: string;
|
||||
titleId?: string;
|
||||
}
|
||||
const LgInfo = ({
|
||||
title,
|
||||
titleId,
|
||||
...props
|
||||
}: SVGProps<SVGSVGElement> & SVGRProps, ref: Ref<SVGSVGElement>) => <svg xmlns="http://www.w3.org/2000/svg" xmlSpace="preserve" baseProfile="basic" viewBox="0 0 24 24" ref={ref} aria-labelledby={titleId} {...props}>{title ? <title id={titleId}>{title}</title> : null}<linearGradient id="a" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><circle cx={12} cy={12} r={10} style={{
|
||||
fill: "url(#a)"
|
||||
}} /><linearGradient id="b" x1={4.929} x2={19.071} y1={4.929} y2={19.071} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.6
|
||||
}} /><stop offset={0.493} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0
|
||||
}} /><stop offset={0.997} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.3
|
||||
}} /></linearGradient><path d="M12 2.5c5.238 0 9.5 4.262 9.5 9.5s-4.262 9.5-9.5 9.5-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5m0-.5C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2" style={{
|
||||
fill: "url(#b)"
|
||||
}} /><linearGradient id="c" x1={10.043} x2={13.957} y1={12.543} y2={16.457} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><path d="M13 12v5a1 1 0 0 1-2 0v-5a1 1 0 0 1 2 0" style={{
|
||||
fill: "url(#c)"
|
||||
}} /><linearGradient id="d" x1={10.939} x2={13.061} y1={6.439} y2={8.561} gradientUnits="userSpaceOnUse"><stop offset={0} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.7
|
||||
}} /><stop offset={0.519} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.45
|
||||
}} /><stop offset={1} style={{
|
||||
stopColor: "#fff",
|
||||
stopOpacity: 0.55
|
||||
}} /></linearGradient><circle cx={12} cy={7.5} r={1.5} style={{
|
||||
fill: "url(#d)"
|
||||
}} /></svg>;
|
||||
const ForwardRef = forwardRef(LgInfo);
|
||||
export default ForwardRef;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue