chore: push site to git

This commit is contained in:
focat 2026-07-04 12:49:09 -07:00
commit 45f2b62d8d
248 changed files with 33283 additions and 0 deletions

27
.gitignore vendored Normal file
View 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
View 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
View 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 &lt;user&gt; 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>
)
}

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

View 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 }

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

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

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

View 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 })
// }
}

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

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

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

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

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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">&larr;</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
View 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
View 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
View 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">&larr;</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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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;

View 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;

View 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
View 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;

View 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;

View 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;

View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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;

View 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;

View 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
View 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;

View 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;

View 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;

View 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;

View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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;

View 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
View 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
View 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;

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