From 45f2b62d8de5460220d87195439402246b265fb6 Mon Sep 17 00:00:00 2001
From: focat <78060856+focat69@users.noreply.github.com>
Date: Sat, 4 Jul 2026 12:49:09 -0700
Subject: [PATCH] chore: push site to git
---
.gitignore | 27 +
READ.txt | 14 +
app/admin/page.tsx | 196 +
app/api/admin/balance/route.ts | 51 +
app/api/auth/[...nextauth]/route.ts | 6 +
app/api/coins/[id]/boost/route.ts | 56 +
app/api/coins/[id]/holders/route.ts | 69 +
app/api/coins/[id]/route.ts | 104 +
app/api/coins/[id]/verify/route.ts | 47 +
app/api/coins/route.ts | 186 +
app/api/comments/route.ts | 121 +
app/api/gamble/route.ts | 80 +
app/api/miner/buy/route.ts | 81 +
app/api/miner/collect/route.ts | 158 +
app/api/notice/route.ts | 60 +
app/api/portfolio/route.ts | 64 +
app/api/rain/route.ts | 118 +
app/api/rewards/route.ts | 104 +
app/api/sol-price/route.ts | 30 +
app/api/swap/route.ts | 286 +
app/api/tip/route.ts | 124 +
app/api/trade/route.ts | 320 +
app/api/trades/route.ts | 29 +
app/api/user/onboarding/route.ts | 79 +
app/api/user/route.ts | 48 +
app/api/users/[username]/route.ts | 63 +
app/coin/[id]/coin-client.tsx | 665 +
app/coin/[id]/page.tsx | 50 +
app/create/page.tsx | 412 +
app/dashboard/page.tsx | 398 +
app/discord/page.tsx | 7 +
app/globals.css | 150 +
app/layout.tsx | 60 +
app/login/page.tsx | 80 +
app/onboarding/page.tsx | 95 +
app/page.tsx | 255 +
app/privacy/page.tsx | 62 +
app/rewards/page.tsx | 205 +
app/swap/page.tsx | 375 +
app/terms/page.tsx | 59 +
app/u/[username]/page.tsx | 357 +
components.json | 21 +
components/coin-card.tsx | 186 +
components/comments.tsx | 152 +
components/global-chat.tsx | 614 +
components/header.tsx | 200 +
components/holders.tsx | 113 +
components/icons/OK.tsx | 44 +
components/icons/about.tsx | 55 +
components/icons/add-user-male.tsx | 79 +
components/icons/appointment-reminders.tsx | 44 +
components/icons/back.tsx | 33 +
components/icons/binoculars.tsx | 88 +
components/icons/bookmark-ribbon.tsx | 44 +
components/icons/bookmark.tsx | 44 +
components/icons/box.tsx | 44 +
components/icons/briefcase.tsx | 66 +
components/icons/calendar.tsx | 55 +
components/icons/cancel-2.tsx | 33 +
components/icons/cancel.tsx | 44 +
components/icons/check-all.tsx | 55 +
components/icons/checked-2.tsx | 44 +
components/icons/checked.tsx | 44 +
components/icons/checkmark.tsx | 33 +
components/icons/clock.tsx | 44 +
components/icons/close-window.tsx | 52 +
components/icons/combo-chart.tsx | 123 +
components/icons/conference-call.tsx | 98 +
components/icons/contacts.tsx | 44 +
components/icons/cursor.tsx | 44 +
components/icons/delete-sign.tsx | 44 +
components/icons/delete.tsx | 44 +
components/icons/document.tsx | 66 +
components/icons/domain.tsx | 66 +
components/icons/download.tsx | 44 +
components/icons/downloads-2.tsx | 44 +
components/icons/edit.tsx | 44 +
components/icons/email.tsx | 44 +
components/icons/empty-trush.tsx | 33 +
components/icons/exit.tsx | 44 +
components/icons/expand-arrow.tsx | 33 +
components/icons/external-link.tsx | 44 +
components/icons/facebook-new.tsx | 44 +
components/icons/facebook.tsx | 44 +
components/icons/file.tsx | 44 +
components/icons/filled-trash.tsx | 55 +
components/icons/folder-invoices-19.tsx | 44 +
components/icons/folder-invoices.tsx | 44 +
components/icons/for-you.tsx | 46 +
components/icons/forward.tsx | 33 +
components/icons/gemini-ai.tsx | 63 +
components/icons/gmail.tsx | 52 +
components/icons/google-logo.tsx | 55 +
components/icons/groups.tsx | 169 +
components/icons/hand-cursor.tsx | 44 +
components/icons/home.tsx | 44 +
components/icons/idea.tsx | 46 +
components/icons/image-file.tsx | 66 +
components/icons/index.ts | 101 +
components/icons/info.tsx | 55 +
components/icons/instagram-new.tsx | 55 +
components/icons/key.tsx | 33 +
components/icons/like.tsx | 33 +
components/icons/linkedin.tsx | 44 +
components/icons/lock.tsx | 55 +
components/icons/mailbox-closed-flag-down.tsx | 55 +
components/icons/maintenance.tsx | 63 +
components/icons/marker.tsx | 44 +
components/icons/menu.tsx | 33 +
components/icons/music.tsx | 33 +
components/icons/news.tsx | 77 +
components/icons/no-synchronize.tsx | 63 +
components/icons/opened-folder.tsx | 44 +
components/icons/phone.tsx | 33 +
components/icons/picture.tsx | 55 +
components/icons/pinterest.tsx | 44 +
components/icons/plus-math.tsx | 33 +
components/icons/plus.tsx | 44 +
components/icons/puzzle.tsx | 44 +
components/icons/refresh.tsx | 33 +
components/icons/restart.tsx | 33 +
components/icons/save.tsx | 63 +
components/icons/scroll.tsx | 55 +
components/icons/search.tsx | 33 +
components/icons/secured-letter.tsx | 44 +
components/icons/service.tsx | 74 +
components/icons/settings.tsx | 44 +
components/icons/share-2.tsx | 66 +
components/icons/share-3.tsx | 44 +
components/icons/share.tsx | 66 +
components/icons/shutdown.tsx | 55 +
components/icons/speech-bubble.tsx | 33 +
components/icons/star.tsx | 35 +
components/icons/sun.tsx | 46 +
components/icons/support.tsx | 44 +
components/icons/synchronize.tsx | 52 +
components/icons/toolbox.tsx | 44 +
components/icons/trash.tsx | 44 +
components/icons/trending-up.tsx | 123 +
components/icons/twitter.tsx | 44 +
components/icons/uncheck-all.tsx | 44 +
components/icons/unlock.tsx | 55 +
components/icons/upload-2.tsx | 44 +
components/icons/user-female.tsx | 44 +
components/icons/user-male.tsx | 44 +
components/icons/user.tsx | 52 +
components/icons/visible.tsx | 44 +
components/icons/whatsapp.tsx | 44 +
components/icons/youtube-play.tsx | 44 +
components/onboarding-guard.tsx | 36 +
components/session-provider.tsx | 9 +
components/theme-provider.tsx | 11 +
components/trade-history.tsx | 245 +
components/trade-panel.tsx | 399 +
components/trading-chart.tsx | 247 +
components/ui/accordion.tsx | 66 +
components/ui/alert-dialog.tsx | 157 +
components/ui/alert.tsx | 66 +
components/ui/aspect-ratio.tsx | 11 +
components/ui/avatar.tsx | 53 +
components/ui/badge.tsx | 46 +
components/ui/breadcrumb.tsx | 109 +
components/ui/button-group.tsx | 83 +
components/ui/button.tsx | 60 +
components/ui/calendar.tsx | 213 +
components/ui/card.tsx | 92 +
components/ui/carousel.tsx | 241 +
components/ui/chart.tsx | 353 +
components/ui/checkbox.tsx | 32 +
components/ui/collapsible.tsx | 33 +
components/ui/command.tsx | 184 +
components/ui/context-menu.tsx | 252 +
components/ui/dialog.tsx | 143 +
components/ui/drawer.tsx | 135 +
components/ui/dropdown-menu.tsx | 257 +
components/ui/empty.tsx | 104 +
components/ui/field.tsx | 244 +
components/ui/form.tsx | 167 +
components/ui/hover-card.tsx | 44 +
components/ui/input-group.tsx | 169 +
components/ui/input-otp.tsx | 77 +
components/ui/input.tsx | 21 +
components/ui/item.tsx | 193 +
components/ui/kbd.tsx | 28 +
components/ui/label.tsx | 24 +
components/ui/menubar.tsx | 276 +
components/ui/navigation-menu.tsx | 166 +
components/ui/pagination.tsx | 127 +
components/ui/popover.tsx | 48 +
components/ui/progress.tsx | 31 +
components/ui/radio-group.tsx | 45 +
components/ui/resizable.tsx | 56 +
components/ui/scroll-area.tsx | 58 +
components/ui/select.tsx | 185 +
components/ui/separator.tsx | 28 +
components/ui/sheet.tsx | 139 +
components/ui/sidebar.tsx | 726 ++
components/ui/skeleton.tsx | 13 +
components/ui/slider.tsx | 63 +
components/ui/sonner.tsx | 25 +
components/ui/spinner.tsx | 16 +
components/ui/switch.tsx | 31 +
components/ui/table.tsx | 116 +
components/ui/tabs.tsx | 66 +
components/ui/textarea.tsx | 18 +
components/ui/toast.tsx | 129 +
components/ui/toaster.tsx | 35 +
components/ui/toggle-group.tsx | 73 +
components/ui/toggle.tsx | 47 +
components/ui/tooltip.tsx | 61 +
components/ui/use-mobile.tsx | 19 +
components/ui/use-toast.ts | 191 +
components/ui/user-badges.tsx | 135 +
components/ui/verified-badge.tsx | 22 +
hooks/use-mobile.ts | 19 +
hooks/use-toast.ts | 191 +
lib/auth.ts | 100 +
lib/mongodb.ts | 24 +
lib/rate-limit.ts | 19 +
lib/socket-protocol.ts.unobf.old | 177 +
lib/utils.ts | 6 +
lib/validations.ts | 56 +
lib/vendor-metrics.ts | 84 +
next.config.mjs | 14 +
package-lock.json | 10726 ++++++++++++++++
package.json | 95 +
pnpm-lock.yaml | 5 +
postcss.config.mjs | 8 +
public/banner.png | Bin 0 -> 328656 bytes
public/logo.ico | Bin 0 -> 8148 bytes
public/logo.png | Bin 0 -> 25422 bytes
public/logo.svg | 4 +
public/raydium-ray-logo.png | Bin 0 -> 226317 bytes
public/rules.png | Bin 0 -> 222022 bytes
public/solana.svg | 28 +
public/update.png | Bin 0 -> 202651 bytes
scripts/ban-alts.ts | 110 +
scripts/database-reset.ts | 77 +
scripts/delete-miners-for-coin.ts | 79 +
scripts/give-all-sol.ts | 64 +
scripts/give-beta-to-all.ts | 36 +
scripts/set-role.ts | 80 +
scripts/verify.ts | 59 +
server/socket-server.ts | 1105 ++
server/tsconfig.json | 13 +
styles/globals.css | 125 +
tsconfig.json | 41 +
types/index.ts | 124 +
248 files changed, 33283 insertions(+)
create mode 100644 .gitignore
create mode 100644 READ.txt
create mode 100644 app/admin/page.tsx
create mode 100644 app/api/admin/balance/route.ts
create mode 100644 app/api/auth/[...nextauth]/route.ts
create mode 100644 app/api/coins/[id]/boost/route.ts
create mode 100644 app/api/coins/[id]/holders/route.ts
create mode 100644 app/api/coins/[id]/route.ts
create mode 100644 app/api/coins/[id]/verify/route.ts
create mode 100644 app/api/coins/route.ts
create mode 100644 app/api/comments/route.ts
create mode 100644 app/api/gamble/route.ts
create mode 100644 app/api/miner/buy/route.ts
create mode 100644 app/api/miner/collect/route.ts
create mode 100644 app/api/notice/route.ts
create mode 100644 app/api/portfolio/route.ts
create mode 100644 app/api/rain/route.ts
create mode 100644 app/api/rewards/route.ts
create mode 100644 app/api/sol-price/route.ts
create mode 100644 app/api/swap/route.ts
create mode 100644 app/api/tip/route.ts
create mode 100644 app/api/trade/route.ts
create mode 100644 app/api/trades/route.ts
create mode 100644 app/api/user/onboarding/route.ts
create mode 100644 app/api/user/route.ts
create mode 100644 app/api/users/[username]/route.ts
create mode 100644 app/coin/[id]/coin-client.tsx
create mode 100644 app/coin/[id]/page.tsx
create mode 100644 app/create/page.tsx
create mode 100644 app/dashboard/page.tsx
create mode 100644 app/discord/page.tsx
create mode 100644 app/globals.css
create mode 100644 app/layout.tsx
create mode 100644 app/login/page.tsx
create mode 100644 app/onboarding/page.tsx
create mode 100644 app/page.tsx
create mode 100644 app/privacy/page.tsx
create mode 100644 app/rewards/page.tsx
create mode 100644 app/swap/page.tsx
create mode 100644 app/terms/page.tsx
create mode 100644 app/u/[username]/page.tsx
create mode 100644 components.json
create mode 100644 components/coin-card.tsx
create mode 100644 components/comments.tsx
create mode 100644 components/global-chat.tsx
create mode 100644 components/header.tsx
create mode 100644 components/holders.tsx
create mode 100644 components/icons/OK.tsx
create mode 100644 components/icons/about.tsx
create mode 100644 components/icons/add-user-male.tsx
create mode 100644 components/icons/appointment-reminders.tsx
create mode 100644 components/icons/back.tsx
create mode 100644 components/icons/binoculars.tsx
create mode 100644 components/icons/bookmark-ribbon.tsx
create mode 100644 components/icons/bookmark.tsx
create mode 100644 components/icons/box.tsx
create mode 100644 components/icons/briefcase.tsx
create mode 100644 components/icons/calendar.tsx
create mode 100644 components/icons/cancel-2.tsx
create mode 100644 components/icons/cancel.tsx
create mode 100644 components/icons/check-all.tsx
create mode 100644 components/icons/checked-2.tsx
create mode 100644 components/icons/checked.tsx
create mode 100644 components/icons/checkmark.tsx
create mode 100644 components/icons/clock.tsx
create mode 100644 components/icons/close-window.tsx
create mode 100644 components/icons/combo-chart.tsx
create mode 100644 components/icons/conference-call.tsx
create mode 100644 components/icons/contacts.tsx
create mode 100644 components/icons/cursor.tsx
create mode 100644 components/icons/delete-sign.tsx
create mode 100644 components/icons/delete.tsx
create mode 100644 components/icons/document.tsx
create mode 100644 components/icons/domain.tsx
create mode 100644 components/icons/download.tsx
create mode 100644 components/icons/downloads-2.tsx
create mode 100644 components/icons/edit.tsx
create mode 100644 components/icons/email.tsx
create mode 100644 components/icons/empty-trush.tsx
create mode 100644 components/icons/exit.tsx
create mode 100644 components/icons/expand-arrow.tsx
create mode 100644 components/icons/external-link.tsx
create mode 100644 components/icons/facebook-new.tsx
create mode 100644 components/icons/facebook.tsx
create mode 100644 components/icons/file.tsx
create mode 100644 components/icons/filled-trash.tsx
create mode 100644 components/icons/folder-invoices-19.tsx
create mode 100644 components/icons/folder-invoices.tsx
create mode 100644 components/icons/for-you.tsx
create mode 100644 components/icons/forward.tsx
create mode 100644 components/icons/gemini-ai.tsx
create mode 100644 components/icons/gmail.tsx
create mode 100644 components/icons/google-logo.tsx
create mode 100644 components/icons/groups.tsx
create mode 100644 components/icons/hand-cursor.tsx
create mode 100644 components/icons/home.tsx
create mode 100644 components/icons/idea.tsx
create mode 100644 components/icons/image-file.tsx
create mode 100644 components/icons/index.ts
create mode 100644 components/icons/info.tsx
create mode 100644 components/icons/instagram-new.tsx
create mode 100644 components/icons/key.tsx
create mode 100644 components/icons/like.tsx
create mode 100644 components/icons/linkedin.tsx
create mode 100644 components/icons/lock.tsx
create mode 100644 components/icons/mailbox-closed-flag-down.tsx
create mode 100644 components/icons/maintenance.tsx
create mode 100644 components/icons/marker.tsx
create mode 100644 components/icons/menu.tsx
create mode 100644 components/icons/music.tsx
create mode 100644 components/icons/news.tsx
create mode 100644 components/icons/no-synchronize.tsx
create mode 100644 components/icons/opened-folder.tsx
create mode 100644 components/icons/phone.tsx
create mode 100644 components/icons/picture.tsx
create mode 100644 components/icons/pinterest.tsx
create mode 100644 components/icons/plus-math.tsx
create mode 100644 components/icons/plus.tsx
create mode 100644 components/icons/puzzle.tsx
create mode 100644 components/icons/refresh.tsx
create mode 100644 components/icons/restart.tsx
create mode 100644 components/icons/save.tsx
create mode 100644 components/icons/scroll.tsx
create mode 100644 components/icons/search.tsx
create mode 100644 components/icons/secured-letter.tsx
create mode 100644 components/icons/service.tsx
create mode 100644 components/icons/settings.tsx
create mode 100644 components/icons/share-2.tsx
create mode 100644 components/icons/share-3.tsx
create mode 100644 components/icons/share.tsx
create mode 100644 components/icons/shutdown.tsx
create mode 100644 components/icons/speech-bubble.tsx
create mode 100644 components/icons/star.tsx
create mode 100644 components/icons/sun.tsx
create mode 100644 components/icons/support.tsx
create mode 100644 components/icons/synchronize.tsx
create mode 100644 components/icons/toolbox.tsx
create mode 100644 components/icons/trash.tsx
create mode 100644 components/icons/trending-up.tsx
create mode 100644 components/icons/twitter.tsx
create mode 100644 components/icons/uncheck-all.tsx
create mode 100644 components/icons/unlock.tsx
create mode 100644 components/icons/upload-2.tsx
create mode 100644 components/icons/user-female.tsx
create mode 100644 components/icons/user-male.tsx
create mode 100644 components/icons/user.tsx
create mode 100644 components/icons/visible.tsx
create mode 100644 components/icons/whatsapp.tsx
create mode 100644 components/icons/youtube-play.tsx
create mode 100644 components/onboarding-guard.tsx
create mode 100644 components/session-provider.tsx
create mode 100644 components/theme-provider.tsx
create mode 100644 components/trade-history.tsx
create mode 100644 components/trade-panel.tsx
create mode 100644 components/trading-chart.tsx
create mode 100644 components/ui/accordion.tsx
create mode 100644 components/ui/alert-dialog.tsx
create mode 100644 components/ui/alert.tsx
create mode 100644 components/ui/aspect-ratio.tsx
create mode 100644 components/ui/avatar.tsx
create mode 100644 components/ui/badge.tsx
create mode 100644 components/ui/breadcrumb.tsx
create mode 100644 components/ui/button-group.tsx
create mode 100644 components/ui/button.tsx
create mode 100644 components/ui/calendar.tsx
create mode 100644 components/ui/card.tsx
create mode 100644 components/ui/carousel.tsx
create mode 100644 components/ui/chart.tsx
create mode 100644 components/ui/checkbox.tsx
create mode 100644 components/ui/collapsible.tsx
create mode 100644 components/ui/command.tsx
create mode 100644 components/ui/context-menu.tsx
create mode 100644 components/ui/dialog.tsx
create mode 100644 components/ui/drawer.tsx
create mode 100644 components/ui/dropdown-menu.tsx
create mode 100644 components/ui/empty.tsx
create mode 100644 components/ui/field.tsx
create mode 100644 components/ui/form.tsx
create mode 100644 components/ui/hover-card.tsx
create mode 100644 components/ui/input-group.tsx
create mode 100644 components/ui/input-otp.tsx
create mode 100644 components/ui/input.tsx
create mode 100644 components/ui/item.tsx
create mode 100644 components/ui/kbd.tsx
create mode 100644 components/ui/label.tsx
create mode 100644 components/ui/menubar.tsx
create mode 100644 components/ui/navigation-menu.tsx
create mode 100644 components/ui/pagination.tsx
create mode 100644 components/ui/popover.tsx
create mode 100644 components/ui/progress.tsx
create mode 100644 components/ui/radio-group.tsx
create mode 100644 components/ui/resizable.tsx
create mode 100644 components/ui/scroll-area.tsx
create mode 100644 components/ui/select.tsx
create mode 100644 components/ui/separator.tsx
create mode 100644 components/ui/sheet.tsx
create mode 100644 components/ui/sidebar.tsx
create mode 100644 components/ui/skeleton.tsx
create mode 100644 components/ui/slider.tsx
create mode 100644 components/ui/sonner.tsx
create mode 100644 components/ui/spinner.tsx
create mode 100644 components/ui/switch.tsx
create mode 100644 components/ui/table.tsx
create mode 100644 components/ui/tabs.tsx
create mode 100644 components/ui/textarea.tsx
create mode 100644 components/ui/toast.tsx
create mode 100644 components/ui/toaster.tsx
create mode 100644 components/ui/toggle-group.tsx
create mode 100644 components/ui/toggle.tsx
create mode 100644 components/ui/tooltip.tsx
create mode 100644 components/ui/use-mobile.tsx
create mode 100644 components/ui/use-toast.ts
create mode 100644 components/ui/user-badges.tsx
create mode 100644 components/ui/verified-badge.tsx
create mode 100644 hooks/use-mobile.ts
create mode 100644 hooks/use-toast.ts
create mode 100644 lib/auth.ts
create mode 100644 lib/mongodb.ts
create mode 100644 lib/rate-limit.ts
create mode 100644 lib/socket-protocol.ts.unobf.old
create mode 100644 lib/utils.ts
create mode 100644 lib/validations.ts
create mode 100644 lib/vendor-metrics.ts
create mode 100644 next.config.mjs
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 pnpm-lock.yaml
create mode 100644 postcss.config.mjs
create mode 100644 public/banner.png
create mode 100644 public/logo.ico
create mode 100644 public/logo.png
create mode 100644 public/logo.svg
create mode 100644 public/raydium-ray-logo.png
create mode 100644 public/rules.png
create mode 100644 public/solana.svg
create mode 100644 public/update.png
create mode 100644 scripts/ban-alts.ts
create mode 100644 scripts/database-reset.ts
create mode 100644 scripts/delete-miners-for-coin.ts
create mode 100644 scripts/give-all-sol.ts
create mode 100644 scripts/give-beta-to-all.ts
create mode 100644 scripts/set-role.ts
create mode 100644 scripts/verify.ts
create mode 100644 server/socket-server.ts
create mode 100644 server/tsconfig.json
create mode 100644 styles/globals.css
create mode 100644 tsconfig.json
create mode 100644 types/index.ts
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f650315
--- /dev/null
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/READ.txt b/READ.txt
new file mode 100644
index 0000000..91fb7d7
--- /dev/null
+++ b/READ.txt
@@ -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
\ No newline at end of file
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
new file mode 100644
index 0000000..8c9b26b
--- /dev/null
+++ b/app/admin/page.tsx
@@ -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 (
+
+
+
+
+
+
Admin Dashboard
+
+
+
+
+
+
+ Manage User Balance
+
+
+
+
+
+ Set Site Notice
+
+
+
+
+ Quick Links
+
+
+ Use the scripts in /scripts folder for bulk actions:
+
+
+ npx ts-node scripts/set-role.ts <user> admin true
+ npx ts-node scripts/give-beta-to-all.ts
+ npx ts-node scripts/database-reset.ts [reason]
+
+
+
+
+
+
+ )
+}
diff --git a/app/api/admin/balance/route.ts b/app/api/admin/balance/route.ts
new file mode 100644
index 0000000..397d572
--- /dev/null
+++ b/app/api/admin/balance/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..e10628f
--- /dev/null
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -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 }
diff --git a/app/api/coins/[id]/boost/route.ts b/app/api/coins/[id]/boost/route.ts
new file mode 100644
index 0000000..2ce1487
--- /dev/null
+++ b/app/api/coins/[id]/boost/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/coins/[id]/holders/route.ts b/app/api/coins/[id]/holders/route.ts
new file mode 100644
index 0000000..ec3bb1f
--- /dev/null
+++ b/app/api/coins/[id]/holders/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/coins/[id]/route.ts b/app/api/coins/[id]/route.ts
new file mode 100644
index 0000000..3f3b763
--- /dev/null
+++ b/app/api/coins/[id]/route.ts
@@ -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 })
+ }
+}
\ No newline at end of file
diff --git a/app/api/coins/[id]/verify/route.ts b/app/api/coins/[id]/verify/route.ts
new file mode 100644
index 0000000..20cee31
--- /dev/null
+++ b/app/api/coins/[id]/verify/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/coins/route.ts b/app/api/coins/route.ts
new file mode 100644
index 0000000..a897039
--- /dev/null
+++ b/app/api/coins/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/comments/route.ts b/app/api/comments/route.ts
new file mode 100644
index 0000000..b6bdf61
--- /dev/null
+++ b/app/api/comments/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/gamble/route.ts b/app/api/gamble/route.ts
new file mode 100644
index 0000000..45f3347
--- /dev/null
+++ b/app/api/gamble/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/miner/buy/route.ts b/app/api/miner/buy/route.ts
new file mode 100644
index 0000000..bab6af1
--- /dev/null
+++ b/app/api/miner/buy/route.ts
@@ -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 })
+ // }
+}
\ No newline at end of file
diff --git a/app/api/miner/collect/route.ts b/app/api/miner/collect/route.ts
new file mode 100644
index 0000000..1b76b38
--- /dev/null
+++ b/app/api/miner/collect/route.ts
@@ -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 })
+ // }
+}
\ No newline at end of file
diff --git a/app/api/notice/route.ts b/app/api/notice/route.ts
new file mode 100644
index 0000000..693a66d
--- /dev/null
+++ b/app/api/notice/route.ts
@@ -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 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/portfolio/route.ts b/app/api/portfolio/route.ts
new file mode 100644
index 0000000..e99279e
--- /dev/null
+++ b/app/api/portfolio/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/rain/route.ts b/app/api/rain/route.ts
new file mode 100644
index 0000000..fbbe0eb
--- /dev/null
+++ b/app/api/rain/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/rewards/route.ts b/app/api/rewards/route.ts
new file mode 100644
index 0000000..8dafcf4
--- /dev/null
+++ b/app/api/rewards/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/sol-price/route.ts b/app/api/sol-price/route.ts
new file mode 100644
index 0000000..a4f610d
--- /dev/null
+++ b/app/api/sol-price/route.ts
@@ -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(),
+ })
+ }
+}
diff --git a/app/api/swap/route.ts b/app/api/swap/route.ts
new file mode 100644
index 0000000..6aa0f0d
--- /dev/null
+++ b/app/api/swap/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/tip/route.ts b/app/api/tip/route.ts
new file mode 100644
index 0000000..0300ccf
--- /dev/null
+++ b/app/api/tip/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/trade/route.ts b/app/api/trade/route.ts
new file mode 100644
index 0000000..5ca414c
--- /dev/null
+++ b/app/api/trade/route.ts
@@ -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 ',
+ 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 })
+ }
+}
diff --git a/app/api/trades/route.ts b/app/api/trades/route.ts
new file mode 100644
index 0000000..71dc980
--- /dev/null
+++ b/app/api/trades/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/user/onboarding/route.ts b/app/api/user/onboarding/route.ts
new file mode 100644
index 0000000..9c26b01
--- /dev/null
+++ b/app/api/user/onboarding/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/user/route.ts b/app/api/user/route.ts
new file mode 100644
index 0000000..9372ec9
--- /dev/null
+++ b/app/api/user/route.ts
@@ -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 })
+ }
+}
diff --git a/app/api/users/[username]/route.ts b/app/api/users/[username]/route.ts
new file mode 100644
index 0000000..bea9f17
--- /dev/null
+++ b/app/api/users/[username]/route.ts
@@ -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 })
+ }
+}
diff --git a/app/coin/[id]/coin-client.tsx b/app/coin/[id]/coin-client.tsx
new file mode 100644
index 0000000..0d1d5d7
--- /dev/null
+++ b/app/coin/[id]/coin-client.tsx
@@ -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(null)
+
+ const { data: coin, isLoading } = useSWR(
+ `/api/coins/${id}`,
+ fetcher
+ )
+
+ const { data: userData } = useSWR(
+ 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 & { _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(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 (
+
+ )
+ }
+
+ if (coin && coin.error) {
+ return (
+
+
+
+
+
Coin Not Found
+
+ This coin doesn't exist or has been removed
+
+
+ Back to Home
+
+
+
+ )
+ }
+
+ if (!coin) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {/* Breadcrumb */}
+
+ Home
+ /
+ ${coin.ticker}
+
+
+ {coin.graduated && (
+
+
+
+
+
+
+
+
+
+
+ Graduated to Raydium
+
+
+
+ LOCKED LIQUIDITY
+
+
+ Liquidity is "locked" meaning the liquidity pool tokens act as a permanent foundation.
+ The price can still go up legally forever! Typically, "burning" LP tokens prevents rug pulls, ensuring trading can always happen.
+
+
+
+
+
This coin has completed the bonding curve. Trading is now live on Raydium.
+
+
+
+ )}
+
+
+ {/* Main Content */}
+
+ {/* Coin Header */}
+
+
+
+
+
+
+
+
+ {coin.name}
+ {coin.verified && }
+ {coin.boosted && (
+
+
+
+
+
+
+ This coin has been boosted until {new Date(coin.boosted!).toLocaleString()}
+ Learn more about boosting β
+
+
+
+ )}
+
+
${coin.ticker}
+
+
+
+
+ {(session?.user?.id === coin.creatorId || session?.user?.isAdmin) && (
+
+
+
+
+
+
+
+
+
+
+
+ {session?.user?.isAdmin ? 'Admin Delete' : 'Delete Coin'}
+
+
+
+
+
+
+ Are you absolutely sure?
+
+ This action cannot be undone. This will permanently delete your coin
+ and remove it from our servers.
+
+
+
+ Cancel
+
+ Delete
+
+
+
+
+ )}
+
+ {session && (
+
+
+
+
+
+
+
+
+
+
+
+ Boost Coin (1 SOL)
+
+
+
+
+
+ Boost this coin?
+
+ 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.
+
+
+
+ Cancel
+
+ Boost Now
+
+
+
+
+ )}
+
+ {session?.user?.verified && (
+
+
+
+
+
+
+
+
+ {coin.verified ? "Unverify Coin" : "Verify Coin"}
+
+
+
+ )}
+
+ {session && userHolding > 0 && (
+
+
Your Holdings
+
+ {formatNumber(userHolding)} {coin.ticker}
+
+
+ )}
+
+
+
+
+ {
+ navigator.clipboard.writeText(window.location.href)
+ toast.success('Copied coin\'s URL to clipboard!', { position: 'bottom-center'})
+ }}
+ >
+
+
+
+
+ Share Coin
+
+
+
+
+
+
+ {/* Trading Chart */}
+
+
+ {/* Stats Grid */}
+
+
+
+
+ Market Cap
+
+
+ {`$`}{formatNumber(coin.marketCap * (solPrice?.price || 0))} USD
+
+
+
+ {formatNumber(coin.marketCap)} SOL
+
+
+
+
+
+
+ 24h Volume
+
+
+ {formatNumber(coin.volume24h)} SOL
+
+
+
+
+
+
+ Holders
+
+
+ {coin.holders.toLocaleString()}
+
+
+
+
+
+
+ Liquidity
+
+ {/* !!!! Display Virtual Reserves (Bonding Curve Pool) instead of raw liquidity to match Market Cap context */}
+
+ {formatNumber(coin.virtualSolReserves)} SOL
+
+
+ {formatNumber(coin.virtualTokenReserves)} ${coin.ticker}
+
+
+
+
+ {/* Description */}
+ {coin.description && (
+
+
About
+
{coin.description}
+
+ )}
+
+ {/* Coin Info */}
+
+
Token Info
+
+ {coin.website && (
+
+ Website
+ handleExternalLink(coin.website!)}
+ className="text-primary hover:underline cursor-pointer"
+ >
+ Visit
+
+
+ )}
+ {coin.twitter && (
+
+ Twitter
+ handleExternalLink(coin.twitter!)}
+ className="text-primary hover:underline cursor-pointer"
+ >
+ View
+
+
+ )}
+ {coin.telegram && (
+
+ Telegram
+ handleExternalLink(coin.telegram!)}
+ className="text-primary hover:underline cursor-pointer"
+ >
+ Join
+
+
+ )}
+
+ Created by
+
+ {coin.creatorName}
+
+
+
+
+
+ Created at
+ {formatDate(coin.createdAt)}
+
+
+ Total Supply
+ {formatNumber(coin.supply)}
+
+
+
+
+ {/* Trade History */}
+
+
+
+
+ {/* Comments Thread */}
+
+
+
+
+
+ {/* sticky */}
+
+
+
+
+ {/* Bonding Curve Progress */}
+
+
+ Bonding Curve Progress
+
+ {coin.graduated ? '100' : Math.min((coin.marketCap / 85) * 100, 100).toFixed(0)}%
+
+
+
div]:bg-gradient-to-r [&>div]:from-yellow-400 [&>div]:to-yellow-600' : ''}`}
+ />
+
+ {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."}
+
+
+
+ {/* Top Holders */}
+
+
+
+
+
+
+
+
+
+ Wait! Are you sure?
+
+ You are about to leave pummmp.fun and visit:
+
+
+ {externalUrl}
+
+
+ Warning: We are not responsible for the content of this link.
+ Always verify the URL before visiting...
+
+
+
+ Cancel
+ {
+ if (externalUrl) window.open(externalUrl, '_blank')
+ }}
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+ >
+ I understand, visit site
+
+
+
+
+
+ )
+}
diff --git a/app/coin/[id]/page.tsx b/app/coin/[id]/page.tsx
new file mode 100644
index 0000000..397d4f5
--- /dev/null
+++ b/app/coin/[id]/page.tsx
@@ -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 {
+ 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
+}
diff --git a/app/create/page.tsx b/app/create/page.tsx
new file mode 100644
index 0000000..369a0ba
--- /dev/null
+++ b/app/create/page.tsx
@@ -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 (
+
+
+
+
+ {/* Header */}
+
+
+
+
+
Launch Your Memecoin
+
+ Create a new coin and make some MULA...
+
+
+
+ {!session ? (
+
+
+
Sign In Required
+
+ Connect your Discord account to create coins
+
+
signIn('discord')}
+ className="gap-2 bg-[#5865F2] hover:bg-[#4752C4]"
+ >
+
+
+
+ Sign in with Discord
+
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
new file mode 100644
index 0000000..0a31399
--- /dev/null
+++ b/app/dashboard/page.tsx
@@ -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(
+ session ? '/api/user' : null,
+ fetcher,
+ { refreshInterval: 10000 }
+ )
+
+ const { data: coins } = useSWR('/api/coins', fetcher)
+
+ const { data: solPrice } = useSWR('/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 => 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 (
+
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+
+ )
+ }
+
+ if (!session) {
+ return (
+
+
+
+
+
Sign In Required
+
+ Connect your Discord account to view your dashboard
+
+
signIn('discord')}
+ className="gap-2 bg-[#5865F2] hover:bg-[#4752C4]"
+ >
+
+ Sign in with Discord
+
+
+
+ )
+ }
+
+ 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 (
+
+
+
+
+ {/* Welcome Section */}
+
+
+
Welcome back, {session.user?.name}
+
Here's your portfolio overview
+
+
+
+
+ Create Coin
+
+
+
+
+ {/* Portfolio History Chart */}
+
+
+
+ Portfolio Performance
+
+
+ {portfolioHistory.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+ `${v.toFixed(2)}`}
+ stroke="oklch(0.6 0 0)"
+ fontSize={12}
+ />
+ [`${value.toFixed(4)} SOL`, 'Value']}
+ />
+
+
+
+ ) : (
+
+ No history data available
+
+ )}
+
+
+
+ {/* Stats Grid */}
+
+
+
+
+
+
+ Total Value
+
+
+ {userData?.totalValue?.toFixed(4) ?? '0'}
+
+ {solPrice && (
+
+ ${((userData?.totalValue ?? 0) * solPrice.price).toFixed(2)} USD
+
+ )}
+
+
+
+
+
+
+
+
+ Cash Balance
+
+
+ {userData?.balance?.toFixed(4) ?? '0'}
+
+
+
+
+
+
+
+
+
+ Portfolio Value
+
+
+ {userData?.portfolioValue?.toFixed(4) ?? '0'}
+
+
+
+
+
+
= 0 ? 'bg-primary/10' : 'bg-destructive/10'}`} />
+
+
+
+ Total P&L
+
+
= 0 ? 'text-primary' : 'text-destructive'}`}>
+ {totalPnL >= 0 ? '+' : ''}{totalPnL.toFixed(4)}
+
+
+
+
+
+
+
+
+
+ Holdings
+
+
+
+ Coins Created
+
+
+
+
+
+
+
Your Holdings
+
+ {portfolioWithCoins.length > 0 ? (
+
+ {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 (
+
+
+
+
+
{coin.name}
+
${coin.ticker}
+
+
+
+
+
+ = 0 ? 'oklch(0.75 0.18 160)' : 'oklch(0.55 0.22 25)'}
+ strokeWidth={1.5}
+ fill="transparent"
+ />
+
+
+
+
+
+
{formatNumber(item.amount)}
+
{coin.ticker}
+
+
+
+
{currentValue.toFixed(4)}
+
= 0 ? 'text-primary' : 'text-destructive'}`}>
+ {pnl >= 0 ? '+' : ''}{pnlPercent.toFixed(2)}%
+
+
+
+ )
+ })}
+
+ ) : (
+
+
+
No holdings yet
+
+ Start trading to build your portfolio
+
+
+ Explore Coins
+
+
+ )}
+
+
+
+
+ {createdCoins.length > 0 ? (
+
+ {createdCoins.map((coin) => (
+
+ ))}
+
+ ) : (
+
+
+
No coins created
+
+ You haven't created any coins yet.
+
+
+ Create Your First Coin
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/app/discord/page.tsx b/app/discord/page.tsx
new file mode 100644
index 0000000..10f0d11
--- /dev/null
+++ b/app/discord/page.tsx
@@ -0,0 +1,7 @@
+import { redirect } from 'next/navigation';
+
+const REDIRECT_URL = 'https://discord.gg/X7byecdGBC';
+
+export default function Page() {
+ redirect(REDIRECT_URL);
+}
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..651a7b7
--- /dev/null
+++ b/app/globals.css
@@ -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;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..0e1602b
--- /dev/null
+++ b/app/layout.tsx
@@ -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 (
+
+
+
+ {/* */}
+
+ {children}
+
+
+
+ {/* */}
+
+
+
+ )
+}
diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 0000000..0f448d9
--- /dev/null
+++ b/app/login/page.tsx
@@ -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 (
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
pummmp.fun
+ {/*
+ Practice rug pulling and making crypto guys mad asf without any real risk!
+
*/}
+
+
+ {/* Features */}
+ {/*
+
+
+
+
Launch Coins
+
Create your own memecoins and flex
+
+
+
+
+
+
Zero Risk
+
No real money involved
+
+
+
*/}
+
+ {/* Sign In Button */}
+
signIn('discord', { callbackUrl: '/dashboard' })}
+ className="h-10 w-full gap-3 bg-[#5865F2] text-base font-medium text-foreground hover:bg-[#4752C4]"
+ >
+
+
+
+ Continue with Discord
+
+
+
+ By signing in, you agree to our Terms of Service and Privacy Policy
+
+
+
+
+ )
+}
diff --git a/app/onboarding/page.tsx b/app/onboarding/page.tsx
new file mode 100644
index 0000000..a9f8278
--- /dev/null
+++ b/app/onboarding/page.tsx
@@ -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 (
+
+
+
+
Welcome to pummmp.fun !
+
+ Before you start rug pulling, pick a unique username.
+
+
+
+
+
+
+
Username
+
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
+ />
+
+ This will be your unique @handle on the platform.
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {loading ? 'Setting up...' : 'Get Started'}
+
+
+
+
+
+ )
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..0d76c3c
--- /dev/null
+++ b/app/page.tsx
@@ -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('/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('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 (
+
+
+
+ {/* Announcement Banner */}
+ {notice && (
+
+
+
+ Notice: {notice.message}
+ {notice.reason && ` (Reason: ${notice.reason})`}
+
+
+
+ )}
+
+ {discordBannerVisible && (
+
+
+
+
+
+
+
+
Join our Discord
+
Giveaways, events & notifications. Or... just connect with the community {`:)`}
+
+
+
+
+
+ Join Discord
+
+
{
+ try { localStorage.setItem('hideDiscordBanner', '1') } catch (e) {}
+ setDiscordBannerVisible(false)
+ }}
+ className="text-white/80 hover:text-white"
+ >
+ β
+
+
+
+
+ )}
+
+ {/* Hero Section */}
+
+
+
+ pummmp.fun
+
+
+
+ made w/ love by focat π
+
+
+
+
+
+
+
+
+
{coins?.length ?? 'N/A'}
+
Active Coins
+
+
+
+
+
+
+ {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'}
+
+
Total Market Cap
+
+
+
+
+
+
{totalHolders ?? 'N/A'}
+
Total Holders
+
+
+
+
+
+
+ {/* Coins Section */}
+
+ {/* Filters */}
+
+
+
+ setSearch(e.target.value)}
+ className="h-11 bg-card/50 pl-10"
+ />
+
+
+
+ {[
+ { 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 (
+ 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'
+ }`}
+ >
+
+ {option.label}
+
+ )})}
+
+
+
+ {/* Coins Grid */}
+ {isLoading ? (
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+ ) : filteredCoins && filteredCoins.length > 0 ? (
+
+ {filteredCoins.map((coin) => (
+
+ ))}
+
+ ) : (
+
+
+
No coins found
+
+ {search ? 'Try a different search term' : 'Be the first to create a coin!'}
+
+
+ )}
+
+
+ )
+}
diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx
new file mode 100644
index 0000000..5c7b6d4
--- /dev/null
+++ b/app/privacy/page.tsx
@@ -0,0 +1,62 @@
+import Link from 'next/link'
+
+export default function PrivacyPage() {
+ return (
+
+
+
+
←
+ pummmp.fun
+
+
+
Privacy Policy
+
+
+
+ 1. Data Collection
+
+ 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.
+
+
+ We also automatically collect certain technical information such as your IP address, browser type,
+ and operating system for security, logging and analytics purposes.
+
+
+
+
+ 2. Data Usage
+
+ 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.
+
+
+
+
+ 3. Cookies & Local Storage
+
+ We use local storage to save your preferences and chat history.
+ Authentication sessions are managed via secure cookies provided by NextAuth.js.
+
+
+
+
+ 4. Third-Party Services
+
+ We use Discord for authentication. Please review Discord's privacy policy to understand
+ how they handle your data.
+
+
+
+
+
+ Last updated: January 2026
+
+
+
+ )
+}
diff --git a/app/rewards/page.tsx b/app/rewards/page.tsx
new file mode 100644
index 0000000..e204b06
--- /dev/null
+++ b/app/rewards/page.tsx
@@ -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 (
+
+ )
+ }
+
+ return (
+
+
+
+ {/* Subtle Background Elements */}
+
+
+
+
+
+ Claim Your Daily SOL
+
+
+ Get $100 USD worth of SOL every 24 hours. No strings attached.
+
+
+
+
+
+
+ {rewardStatus.canClaim ? (
+
+
+
+
+
+
+
$100.00
+
+ Available in SOL
+
+
+
+
+ {isClaiming ? (
+
+ ) : (
+ "Claim Reward"
+ )}
+
+
+ ) : (
+
+
+
+
+
+
+
+ {timeLeft || "--:--:--"}
+
+
+ until next reward
+
+
+
+
+
+ Come back tomorrow
+
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/app/swap/page.tsx b/app/swap/page.tsx
new file mode 100644
index 0000000..8abaeea
--- /dev/null
+++ b/app/swap/page.tsx
@@ -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 (
+
+
+
+ {selected ? (
+ <>
+
+
${selected.ticker}
+ >
+ ) : (
+
{placeholder}
+ )}
+
+
+
+
+
+
+
+ No token found.
+
+ {coins.map((coin) => {
+ const balance = showBalance ? getBalance(coin._id) : 0;
+ return (
+ {
+ onSelect(coin._id)
+ setOpen(false)
+ }}
+ className="cursor-pointer"
+ >
+
+
+
+ ${coin.ticker}
+ {coin.name}
+
+ {showBalance && balance > 0 && (
+
+ {balance.toLocaleString()}
+ Bal
+
+ )}
+ {!showBalance && selectedId === coin._id && (
+
+ )}
+
+
+ )})}
+
+
+
+
+
+ )
+}
+
+export default function SwapPage() {
+ const { data: session } = useSession()
+ const router = useRouter()
+
+ const { data: userData } = useSWR(session ? '/api/user' : null, fetcher)
+ const { data: coins } = useSWR('/api/coins', fetcher)
+
+ const [fromCoinId, setFromCoinId] = useState('')
+ const [toCoinId, setToCoinId] = useState('')
+ const [amount, setAmount] = useState('')
+ 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 => 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) => {
+ 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 (
+
+
+
+
+
Sign In Required
+
+ Connect your Discord account to swap coins
+
+
signIn('discord')}>Sign in with Discord
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
Swap Tokens
+
Swap between different tokens on pummmp.fun
+
+
+
+
+ {/* FROM SECTION */}
+
+
+
You Pay
+ {fromCoin && (
+
+
+ {userBalance.toLocaleString()} Max
+
+ )}
+
+
+
+
+
+
+ {/* Replace Native Select with CoinSelector */}
+ h.coin)}
+ selectedId={fromCoinId}
+ onSelect={setFromCoinId}
+ placeholder="Select"
+ showBalance={true}
+ userPortfolio={userData?.portfolio}
+ />
+
+
+
+
+ {fromCoin && estimates?.solProceeds && (
+
+
+ β {estimates.solProceeds.toFixed(4)} SOL Value
+
+ )}
+
+
+
+ {/* SWAP INDICATOR */}
+
+
+ {/* TO SECTION */}
+
+
+ You Receive
+
+
+
+
+ {estimates?.tokensOut ? estimates.tokensOut.toFixed(4) : "0.00"}
+
+
+
+ c._id !== fromCoinId) || []}
+ selectedId={toCoinId}
+ onSelect={setToCoinId}
+ placeholder="Select"
+ />
+
+
+
+
+
+ {/* ACTION */}
+ {estimates?.error ? (
+
+ {estimates.error}
+
+ ) : (
+
+ )}
+
+
+ {loading ? (
+
+ ) : (
+ "Swap Tokens"
+ )}
+
+
+ {/* PRICE INFO */}
+ {estimates && !estimates.error && (
+
+
+ 1 {fromCoin?.ticker} β {estimates.rate?.toFixed(4)} {toCoin?.ticker}
+ β’
+ This is the best price via bonding curve.
+
+ )}
+
+
+
+ )
+}
+
diff --git a/app/terms/page.tsx b/app/terms/page.tsx
new file mode 100644
index 0000000..ace97c1
--- /dev/null
+++ b/app/terms/page.tsx
@@ -0,0 +1,59 @@
+import Link from 'next/link'
+
+export default function TermsPage() {
+ return (
+
+
+
+
←
+ pummmp.fun
+
+
+
Terms of Service
+
+
+
+ 1. Acceptance of Terms
+
+ 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.
+
+
+
+
+ 2. No Financial Advice
+
+ 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.
+
+
+
+
+ 3. User Conduct
+
+ 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.
+
+
+
+
+ 4. Disclaimer
+
+ THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND.
+ WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED.
+
+
+
+
+
+ Last updated: January 2026
+
+
+
+ )
+}
diff --git a/app/u/[username]/page.tsx b/app/u/[username]/page.tsx
new file mode 100644
index 0000000..15eb108
--- /dev/null
+++ b/app/u/[username]/page.tsx
@@ -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(
+ `/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 (
+
+ )
+ }
+
+ if (error || !data || (data as any).error) {
+ return (
+
+
+
+
+
User Not Found
+
+ The user "{decodedUsername}" could not be found.
+
+
+ Back to Home
+
+
+
+ )
+ }
+
+ const { user, createdCoins, portfolio } = data
+
+ return (
+
+
+
+
+ {/* User Header */}
+
+
+
+ {user.name.slice(0, 2).toUpperCase()}
+
+
+
+
+
+ {user.name}
+
+
+
+
+
+
+ Joined {formatDate(user.createdAt)}
+
+
+
+ {user.coinsCreated} coins created
+
+
+
+ {session && session.user?.name !== user.name && (
+
+
+
+
+ Tip {user.name}
+
+
+
+
+ Send a Tip to {user.name}
+
+ Send SOL directly to this user. You may tip once every 30 minutes.
+
+
+
+
+
Amount (SOL)
+
+
setTipAmount(e.target.value)}
+ placeholder="0.00"
+ className="pl-9"
+ required
+ />
+
+
+
+
+
+
+
+ {tipLoading ? 'Sending...' : 'Send Tip'}
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ Portfolio Value
+
+
+ {formatNumber(user.portfolioValue)} SOL
+
+
+
+
+
+
+
+
+ Coins Created ({createdCoins.length})
+
+
+
+ Holdings ({portfolio.length})
+
+
+
+
+ {createdCoins.length > 0 ? (
+
+ {createdCoins.map((coin) => (
+
+ ))}
+
+ ) : (
+
+
+
No coins created yet
+
+ )}
+
+
+
+
+ {portfolio.length > 0 ? (
+
+ {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 (
+
+
+
+
+
+
{coin.name}
+
${coin.ticker}
+
+
+
+
+
+
+ = 0 ? 'oklch(0.75 0.18 160)' : 'oklch(0.55 0.22 25)'}
+ strokeWidth={1.5}
+ fill="transparent"
+ />
+
+
+
+
+
+
{formatNumber(item.amount)}
+
+ {item.currentValue.toFixed(2)} SOL
+
+
+
+ )
+ })}
+
+ ) : (
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..4ee62ee
--- /dev/null
+++ b/components.json
@@ -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"
+}
diff --git a/components/coin-card.tsx b/components/coin-card.tsx
new file mode 100644
index 0000000..8460075
--- /dev/null
+++ b/components/coin-card.tsx
@@ -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 (
+
+
+ {/* Glow effect - Removed for monotone
+
+ */}
+
+
+ {/* Boosted Pin */}
+ {/* {isBoosted && (
+
+
+
+
+
+
+
+ This coin has been boosted until {new Date(coin.boosted!).toLocaleString()}
+
+
+
+
+ )} */}
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+
+ {coin.name}
+ {coin.verified ? (
+
+ ) : isBoosted ? (
+
+
+
+
+
+
+ This coin has been boosted until {new Date(coin.boosted!).toLocaleString()}
+
+
+
+ ) : null}
+
+
+
${coin.ticker}
+
+
+
+ {isPositive ? (
+ 'β'
+ ) : (
+ 'β'
+ )}
+ {Math.abs(priceChange).toFixed(2)}%
+
+
+
+ {/* Mini Chart */}
+
+
+ {/* Stats */}
+
+
+
Price
+
+ ${formatPrice(priceUsd)}
+
+
+
+
Market Cap
+
+ ${formatNumber(marketCapUsd)}
+
+
+
+
+ {/* Footer */}
+
+
+
+ {coin.holders} holders
+
+
+
+ {formatNumber(coin.volume24h)} vol
+
+
+
+
+
+ )
+}
diff --git a/components/comments.tsx b/components/comments.tsx
new file mode 100644
index 0000000..6eae13c
--- /dev/null
+++ b/components/comments.tsx
@@ -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(`/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 (
+
+
+
Discussion
+
+ {session ? (
+
+ setText(e.target.value)}
+ placeholder="What do you think about this coin?"
+ className="min-h-[80px] bg-muted/30"
+ />
+
+
+ {loading ? 'Posting...' : 'Post Comment'}
+
+
+
+ ) : (
+
+ Please sign in to join the discussion
+
+ )}
+
+
+ {comments?.map((comment, i) => (
+
+
+
+ {comment.userName?.substring(0, 2)}
+
+
+
+
+ {comment.userName}
+
+
+ {creatorId === comment.userId && (
+
+ CREATOR
+
+ )}
+ {comment.isHolder && creatorId !== comment.userId && (
+
+ HOLDER
+
+ )}
+
+ {formatDistanceToNow(new Date(comment.createdAt), { addSuffix: true })}
+
+
+ {session && (session.user.id === comment.userId || session.user.id === creatorId || (session.user as any).isAdmin) && (
+ 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"
+ >
+
+
+ )}
+
+
{comment.text}
+
+
+ ))}
+ {comments?.length === 0 && (
+
No comments yet. Be the first!
+ )}
+
+
+
+ )
+}
diff --git a/components/global-chat.tsx b/components/global-chat.tsx
new file mode 100644
index 0000000..fd5ba34
--- /dev/null
+++ b/components/global-chat.tsx
@@ -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(null)
+ const [messages, setMessages] = useState([])
+ const [newMessage, setNewMessage] = useState('')
+ const [isConnected, setIsConnected] = useState(false)
+ const [activeRain, setActiveRain] = useState(null)
+
+ const [sessionConfig, setSessionConfig] = useState(null)
+
+ const scrollRef = useRef(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 {part};
+ } else if (part.startsWith('$')) {
+ const ticker = part.slice(1);
+ return {part};
+ } else {
+ return {part} ;
+ }
+ });
+ };
+
+ 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 - Tip someone SOL ~ .gamble - Roll the dice (1-95%) ~ .ban - Ban user (30m/2h/1d/perm) ~ .unban - Unban user`
+ : `Commands: .tip - Tip someone SOL ~ .gamble - 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 (
+ 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"
+ >
+
+
+ {isConnected && (
+
+
+
+
+ )}
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
pummmp.chat
+
+
{isConnected ? 'Connected' : 'Connecting...'}
+
+
+
+
+
+ setIsOpen(false)} className="h-7 w-7 rounded-full hover:bg-background">
+
+
+
+
+
+ {activeRain && (
+
+
+
+
+
+
+
+
+
+ RAIN EVENT ACTIVE!
+ Hosted by {activeRain.hostName}
+
+
+
+
{activeRain.amount} SOL
+
Total distributed
+
+
+
+
+
+ {activeRain.participants.slice(0, 3).map((participant, i) => (
+
+
+ {participant.name?.[0]}
+
+ ))}
+ {(activeRain.participants?.length || 0) > 3 && (
+
+ +{(activeRain.participants?.length || 0) - 3}
+
+ )}
+ {(activeRain.participants?.length || 0) === 0 &&
0 participants | Be the first! }
+
+
+
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')}
+
+
+
+
+ )}
+
+
+
+ {messages.length === 0 && (
+
+
No messages yet.
+
Say hello! π
+
+ )}
+ {messages.map((msg, i) => (
+
+
+
+ {msg.userName?.[0]}
+
+
+
+
+
+ {msg.userName}
+
+
+
+
{new Date(msg.createdAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
+
+
+ {renderTextWithLinks(msg.text)}
+
+
+
+ ))}
+
+
+
+
+
+
+ {!session ? (
+ signIn()}>
+ Sign in to chat
+
+ ) : (session?.user?.chatBannedUntil && new Date(session.user.chatBannedUntil) > new Date()) ? (
+
+ You are chat banned until {new Date(session.user.chatBannedUntil).toLocaleString()}
+
+ ) : (
+ <>
+ setNewMessage(e.target.value)}
+ placeholder="Type a message... | .help for commands"
+ className="h-8 text-xs"
+ disabled={!isConnected || !sessionConfig || !sessionConfig.nonce}
+ />
+
+
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/components/header.tsx b/components/header.tsx
new file mode 100644
index 0000000..29c371e
--- /dev/null
+++ b/components/header.tsx
@@ -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 (
+
+
+
+
+
+
+ {/*
*/}
+
+
+
+
+ {/* pummmp - make this a gradient */}
+ {/* pummmp */}
+ pummmp.fun
+
+
+
+
+
+ Explore
+
+
+ Create
+
+
+ Swap
+
+
+ Daily Rewards
+
+ {session && (
+
+ Dashboard
+
+ )}
+
+
+
+
+ {solPrice && (
+
+ {/*
*/}
+
+
${solPrice.price?.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USD
+
= 0 ? 'text-primary' : 'text-destructive'}`}>
+ {solPrice.change24h >= 0 ? '+' : ''}{solPrice.change24h?.toFixed(2)}%
+
+
+ )}
+
+ {status === 'loading' ? (
+
+ ) : session ? (
+
+
+
+ {userData?.balance?.toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 4 }) ?? '...'} SOL
+
+
+ ${((userData?.balance ?? 0) * (solPrice?.price ?? 0)).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} USD
+
+
+
+
+
+
+
+
+
+ {session.user?.name?.[0]?.toUpperCase() ?? '?'}
+
+
+
+
+
+
+
+
+ {session.user?.name?.[0]}
+
+
+
+ {session.user?.name}
+
+
+
{session.user?.email}
+
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+ Create Coin
+
+
+
+
+
+ My Profile
+
+
+
+ signOut()}
+ className="cursor-pointer hover:bg-primary/10 focus:bg-primary/10"
+ >
+
+ Sign Out
+
+
+
+
+ ) : (
+ //
signIn('discord')}
+ // className="gap-2 bg-[#5865F2] text-foreground hover:bg-[#4752C4]"
+ // >
+ //
+ //
+ //
+ // Sign in with Discord
+ //
+
router.push('/login')}
+ className="gap-2 bg-primary text-primary-foreground hover:bg-primary/90"
+ >
+ Sign In
+
+ )}
+
+
+
+ )
+}
diff --git a/components/holders.tsx b/components/holders.tsx
new file mode 100644
index 0000000..4860dbd
--- /dev/null
+++ b/components/holders.tsx
@@ -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(
+ `/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 (
+
+
Top Holders
+
+ {holders.map((holder, i) => (
+
+
+
{i + 1}.
+
+
+ {holder.name === 'Raydium Pool' ? (
+
+ RP
+
+ ) : holder.name === 'Bonding Curve' ? (
+
+ BC
+
+ ) : (<>
+
+ {holder.name.substring(0, 2)} >
+ )}
+
+
+ {holder.name}
+
+ {!holder.isSystem && (
+
+ )}
+ {holder.isSystem && (
+ //
β
+
+ )}
+ {holder.name === 'Bonding Curve' && (
+ //
β
+
+ )}
+
+
+
+
+ {((holder.amount / totalSupply) * 100).toFixed(2)}%
+
+
+ {(holder.amount / 1_000_000).toFixed(1)}M
+
+
+
+ ))}
+ {!holders?.length && (
+
No holders yet {`;-;`}
+ )}
+
+
+ )
+}
diff --git a/components/icons/OK.tsx b/components/icons/OK.tsx
new file mode 100644
index 0000000..8e47a14
--- /dev/null
+++ b/components/icons/OK.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgOK);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/about.tsx b/components/icons/about.tsx
new file mode 100644
index 0000000..a12a25c
--- /dev/null
+++ b/components/icons/about.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgAbout);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/add-user-male.tsx b/components/icons/add-user-male.tsx
new file mode 100644
index 0000000..d429fe9
--- /dev/null
+++ b/components/icons/add-user-male.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgAddUserMale);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/appointment-reminders.tsx b/components/icons/appointment-reminders.tsx
new file mode 100644
index 0000000..c0ca466
--- /dev/null
+++ b/components/icons/appointment-reminders.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgAppointmentReminders);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/back.tsx b/components/icons/back.tsx
new file mode 100644
index 0000000..32babc1
--- /dev/null
+++ b/components/icons/back.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgBack);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/binoculars.tsx b/components/icons/binoculars.tsx
new file mode 100644
index 0000000..bb97731
--- /dev/null
+++ b/components/icons/binoculars.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgBinoculars);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/bookmark-ribbon.tsx b/components/icons/bookmark-ribbon.tsx
new file mode 100644
index 0000000..73190ef
--- /dev/null
+++ b/components/icons/bookmark-ribbon.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgBookmarkRibbon);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/bookmark.tsx b/components/icons/bookmark.tsx
new file mode 100644
index 0000000..badf0e8
--- /dev/null
+++ b/components/icons/bookmark.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgBookmark);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/box.tsx b/components/icons/box.tsx
new file mode 100644
index 0000000..e49af60
--- /dev/null
+++ b/components/icons/box.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgBox);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/briefcase.tsx b/components/icons/briefcase.tsx
new file mode 100644
index 0000000..c1824f0
--- /dev/null
+++ b/components/icons/briefcase.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgBriefcase);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/calendar.tsx b/components/icons/calendar.tsx
new file mode 100644
index 0000000..d31ba12
--- /dev/null
+++ b/components/icons/calendar.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgCalendar);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/cancel-2.tsx b/components/icons/cancel-2.tsx
new file mode 100644
index 0000000..3156212
--- /dev/null
+++ b/components/icons/cancel-2.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgCancel2);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/cancel.tsx b/components/icons/cancel.tsx
new file mode 100644
index 0000000..855d79c
--- /dev/null
+++ b/components/icons/cancel.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgCancel);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/check-all.tsx b/components/icons/check-all.tsx
new file mode 100644
index 0000000..cb821ea
--- /dev/null
+++ b/components/icons/check-all.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgCheckAll);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/checked-2.tsx b/components/icons/checked-2.tsx
new file mode 100644
index 0000000..5008d5b
--- /dev/null
+++ b/components/icons/checked-2.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgChecked2);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/checked.tsx b/components/icons/checked.tsx
new file mode 100644
index 0000000..1b8434b
--- /dev/null
+++ b/components/icons/checked.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgChecked);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/checkmark.tsx b/components/icons/checkmark.tsx
new file mode 100644
index 0000000..55e038b
--- /dev/null
+++ b/components/icons/checkmark.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgCheckmark);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/clock.tsx b/components/icons/clock.tsx
new file mode 100644
index 0000000..b373901
--- /dev/null
+++ b/components/icons/clock.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgClock);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/close-window.tsx b/components/icons/close-window.tsx
new file mode 100644
index 0000000..0d5ae53
--- /dev/null
+++ b/components/icons/close-window.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgCloseWindow);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/combo-chart.tsx b/components/icons/combo-chart.tsx
new file mode 100644
index 0000000..0dd8317
--- /dev/null
+++ b/components/icons/combo-chart.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgComboChart);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/conference-call.tsx b/components/icons/conference-call.tsx
new file mode 100644
index 0000000..6c0cf54
--- /dev/null
+++ b/components/icons/conference-call.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgConferenceCall);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/contacts.tsx b/components/icons/contacts.tsx
new file mode 100644
index 0000000..9269601
--- /dev/null
+++ b/components/icons/contacts.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgContacts);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/cursor.tsx b/components/icons/cursor.tsx
new file mode 100644
index 0000000..f1146d9
--- /dev/null
+++ b/components/icons/cursor.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgCursor);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/delete-sign.tsx b/components/icons/delete-sign.tsx
new file mode 100644
index 0000000..4a07dda
--- /dev/null
+++ b/components/icons/delete-sign.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgDeleteSign);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/delete.tsx b/components/icons/delete.tsx
new file mode 100644
index 0000000..5dba611
--- /dev/null
+++ b/components/icons/delete.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgDelete);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/document.tsx b/components/icons/document.tsx
new file mode 100644
index 0000000..3897135
--- /dev/null
+++ b/components/icons/document.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgDocument);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/domain.tsx b/components/icons/domain.tsx
new file mode 100644
index 0000000..c131024
--- /dev/null
+++ b/components/icons/domain.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgDomain);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/download.tsx b/components/icons/download.tsx
new file mode 100644
index 0000000..0d1a6ed
--- /dev/null
+++ b/components/icons/download.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgDownload);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/downloads-2.tsx b/components/icons/downloads-2.tsx
new file mode 100644
index 0000000..a61a47e
--- /dev/null
+++ b/components/icons/downloads-2.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgDownloads2);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/edit.tsx b/components/icons/edit.tsx
new file mode 100644
index 0000000..cd1f4f6
--- /dev/null
+++ b/components/icons/edit.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgEdit);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/email.tsx b/components/icons/email.tsx
new file mode 100644
index 0000000..2f71439
--- /dev/null
+++ b/components/icons/email.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgEmail);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/empty-trush.tsx b/components/icons/empty-trush.tsx
new file mode 100644
index 0000000..80059ff
--- /dev/null
+++ b/components/icons/empty-trush.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgEmptyTrush);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/exit.tsx b/components/icons/exit.tsx
new file mode 100644
index 0000000..5731715
--- /dev/null
+++ b/components/icons/exit.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgExit);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/expand-arrow.tsx b/components/icons/expand-arrow.tsx
new file mode 100644
index 0000000..a9fbfb6
--- /dev/null
+++ b/components/icons/expand-arrow.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgExpandArrow);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/external-link.tsx b/components/icons/external-link.tsx
new file mode 100644
index 0000000..c1500bd
--- /dev/null
+++ b/components/icons/external-link.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgExternalLink);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/facebook-new.tsx b/components/icons/facebook-new.tsx
new file mode 100644
index 0000000..b6db53b
--- /dev/null
+++ b/components/icons/facebook-new.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgFacebookNew);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/facebook.tsx b/components/icons/facebook.tsx
new file mode 100644
index 0000000..67ed4ea
--- /dev/null
+++ b/components/icons/facebook.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgFacebook);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/file.tsx b/components/icons/file.tsx
new file mode 100644
index 0000000..ff8a02d
--- /dev/null
+++ b/components/icons/file.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgFile);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/filled-trash.tsx b/components/icons/filled-trash.tsx
new file mode 100644
index 0000000..810fbe2
--- /dev/null
+++ b/components/icons/filled-trash.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgFilledTrash);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/folder-invoices-19.tsx b/components/icons/folder-invoices-19.tsx
new file mode 100644
index 0000000..fecc525
--- /dev/null
+++ b/components/icons/folder-invoices-19.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgFolderInvoices19);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/folder-invoices.tsx b/components/icons/folder-invoices.tsx
new file mode 100644
index 0000000..891b27c
--- /dev/null
+++ b/components/icons/folder-invoices.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgFolderInvoices);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/for-you.tsx b/components/icons/for-you.tsx
new file mode 100644
index 0000000..3c15cb3
--- /dev/null
+++ b/components/icons/for-you.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgForYou);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/forward.tsx b/components/icons/forward.tsx
new file mode 100644
index 0000000..af2cfe0
--- /dev/null
+++ b/components/icons/forward.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgForward);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/gemini-ai.tsx b/components/icons/gemini-ai.tsx
new file mode 100644
index 0000000..0ed257d
--- /dev/null
+++ b/components/icons/gemini-ai.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgGeminiAi);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/gmail.tsx b/components/icons/gmail.tsx
new file mode 100644
index 0000000..27538c3
--- /dev/null
+++ b/components/icons/gmail.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgGmail);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/google-logo.tsx b/components/icons/google-logo.tsx
new file mode 100644
index 0000000..d70736a
--- /dev/null
+++ b/components/icons/google-logo.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgGoogleLogo);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/groups.tsx b/components/icons/groups.tsx
new file mode 100644
index 0000000..ed2a965
--- /dev/null
+++ b/components/icons/groups.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgGroups);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/hand-cursor.tsx b/components/icons/hand-cursor.tsx
new file mode 100644
index 0000000..8bc0e8f
--- /dev/null
+++ b/components/icons/hand-cursor.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgHandCursor);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/home.tsx b/components/icons/home.tsx
new file mode 100644
index 0000000..a0ce6e7
--- /dev/null
+++ b/components/icons/home.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgHome);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/idea.tsx b/components/icons/idea.tsx
new file mode 100644
index 0000000..2c0f73d
--- /dev/null
+++ b/components/icons/idea.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgIdea);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/image-file.tsx b/components/icons/image-file.tsx
new file mode 100644
index 0000000..9ba79ff
--- /dev/null
+++ b/components/icons/image-file.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgImageFile);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/index.ts b/components/icons/index.ts
new file mode 100644
index 0000000..b654e21
--- /dev/null
+++ b/components/icons/index.ts
@@ -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';
diff --git a/components/icons/info.tsx b/components/icons/info.tsx
new file mode 100644
index 0000000..0a51455
--- /dev/null
+++ b/components/icons/info.tsx
@@ -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 & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgInfo);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/instagram-new.tsx b/components/icons/instagram-new.tsx
new file mode 100644
index 0000000..9c69b90
--- /dev/null
+++ b/components/icons/instagram-new.tsx
@@ -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 LgInstagramNew = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgInstagramNew);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/key.tsx b/components/icons/key.tsx
new file mode 100644
index 0000000..f8eb72c
--- /dev/null
+++ b/components/icons/key.tsx
@@ -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 LgKey = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgKey);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/like.tsx b/components/icons/like.tsx
new file mode 100644
index 0000000..98ea911
--- /dev/null
+++ b/components/icons/like.tsx
@@ -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 LgLike = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgLike);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/linkedin.tsx b/components/icons/linkedin.tsx
new file mode 100644
index 0000000..3ceb036
--- /dev/null
+++ b/components/icons/linkedin.tsx
@@ -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 LgLinkedin = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgLinkedin);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/lock.tsx b/components/icons/lock.tsx
new file mode 100644
index 0000000..c816226
--- /dev/null
+++ b/components/icons/lock.tsx
@@ -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 LgLock = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgLock);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/mailbox-closed-flag-down.tsx b/components/icons/mailbox-closed-flag-down.tsx
new file mode 100644
index 0000000..8801e90
--- /dev/null
+++ b/components/icons/mailbox-closed-flag-down.tsx
@@ -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 LgMailboxClosedFlagDown = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgMailboxClosedFlagDown);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/maintenance.tsx b/components/icons/maintenance.tsx
new file mode 100644
index 0000000..44cb6d8
--- /dev/null
+++ b/components/icons/maintenance.tsx
@@ -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 LgMaintenance = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgMaintenance);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/marker.tsx b/components/icons/marker.tsx
new file mode 100644
index 0000000..8048811
--- /dev/null
+++ b/components/icons/marker.tsx
@@ -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 LgMarker = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgMarker);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/menu.tsx b/components/icons/menu.tsx
new file mode 100644
index 0000000..44f9ebe
--- /dev/null
+++ b/components/icons/menu.tsx
@@ -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 LgMenu = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgMenu);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/music.tsx b/components/icons/music.tsx
new file mode 100644
index 0000000..23e4b21
--- /dev/null
+++ b/components/icons/music.tsx
@@ -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 LgMusic = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgMusic);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/news.tsx b/components/icons/news.tsx
new file mode 100644
index 0000000..4c98f65
--- /dev/null
+++ b/components/icons/news.tsx
@@ -0,0 +1,77 @@
+import * as React from "react";
+import type { SVGProps } from "react";
+import { Ref, forwardRef } from "react";
+interface SVGRProps {
+ title?: string;
+ titleId?: string;
+}
+const LgNews = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgNews);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/no-synchronize.tsx b/components/icons/no-synchronize.tsx
new file mode 100644
index 0000000..b659520
--- /dev/null
+++ b/components/icons/no-synchronize.tsx
@@ -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 LgNoSynchronize = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgNoSynchronize);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/opened-folder.tsx b/components/icons/opened-folder.tsx
new file mode 100644
index 0000000..296edad
--- /dev/null
+++ b/components/icons/opened-folder.tsx
@@ -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 LgOpenedFolder = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgOpenedFolder);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/phone.tsx b/components/icons/phone.tsx
new file mode 100644
index 0000000..9074bb0
--- /dev/null
+++ b/components/icons/phone.tsx
@@ -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 LgPhone = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgPhone);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/picture.tsx b/components/icons/picture.tsx
new file mode 100644
index 0000000..bed3fbb
--- /dev/null
+++ b/components/icons/picture.tsx
@@ -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 LgPicture = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgPicture);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/pinterest.tsx b/components/icons/pinterest.tsx
new file mode 100644
index 0000000..bb47e02
--- /dev/null
+++ b/components/icons/pinterest.tsx
@@ -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 LgPinterest = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgPinterest);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/plus-math.tsx b/components/icons/plus-math.tsx
new file mode 100644
index 0000000..8d1ca22
--- /dev/null
+++ b/components/icons/plus-math.tsx
@@ -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 LgPlusMath = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgPlusMath);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/plus.tsx b/components/icons/plus.tsx
new file mode 100644
index 0000000..949e7be
--- /dev/null
+++ b/components/icons/plus.tsx
@@ -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 LgPlus = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgPlus);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/puzzle.tsx b/components/icons/puzzle.tsx
new file mode 100644
index 0000000..4d5a687
--- /dev/null
+++ b/components/icons/puzzle.tsx
@@ -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 LgPuzzle = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgPuzzle);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/refresh.tsx b/components/icons/refresh.tsx
new file mode 100644
index 0000000..8ba3994
--- /dev/null
+++ b/components/icons/refresh.tsx
@@ -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 LgRefresh = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgRefresh);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/restart.tsx b/components/icons/restart.tsx
new file mode 100644
index 0000000..cf31beb
--- /dev/null
+++ b/components/icons/restart.tsx
@@ -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 LgRestart = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgRestart);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/save.tsx b/components/icons/save.tsx
new file mode 100644
index 0000000..175ec76
--- /dev/null
+++ b/components/icons/save.tsx
@@ -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 LgSave = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgSave);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/scroll.tsx b/components/icons/scroll.tsx
new file mode 100644
index 0000000..d1aa12f
--- /dev/null
+++ b/components/icons/scroll.tsx
@@ -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 LgScroll = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgScroll);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/search.tsx b/components/icons/search.tsx
new file mode 100644
index 0000000..6e470e4
--- /dev/null
+++ b/components/icons/search.tsx
@@ -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 LgSearch = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgSearch);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/secured-letter.tsx b/components/icons/secured-letter.tsx
new file mode 100644
index 0000000..b770d9a
--- /dev/null
+++ b/components/icons/secured-letter.tsx
@@ -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 LgSecuredLetter = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgSecuredLetter);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/service.tsx b/components/icons/service.tsx
new file mode 100644
index 0000000..4246133
--- /dev/null
+++ b/components/icons/service.tsx
@@ -0,0 +1,74 @@
+import * as React from "react";
+import type { SVGProps } from "react";
+import { Ref, forwardRef } from "react";
+interface SVGRProps {
+ title?: string;
+ titleId?: string;
+}
+const LgService = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgService);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/settings.tsx b/components/icons/settings.tsx
new file mode 100644
index 0000000..88be9ed
--- /dev/null
+++ b/components/icons/settings.tsx
@@ -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 LgSettings = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgSettings);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/share-2.tsx b/components/icons/share-2.tsx
new file mode 100644
index 0000000..4bf7703
--- /dev/null
+++ b/components/icons/share-2.tsx
@@ -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 LgShare2 = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgShare2);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/share-3.tsx b/components/icons/share-3.tsx
new file mode 100644
index 0000000..21ac5cd
--- /dev/null
+++ b/components/icons/share-3.tsx
@@ -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 LgShare3 = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgShare3);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/share.tsx b/components/icons/share.tsx
new file mode 100644
index 0000000..1a22cd0
--- /dev/null
+++ b/components/icons/share.tsx
@@ -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 LgShare = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgShare);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/shutdown.tsx b/components/icons/shutdown.tsx
new file mode 100644
index 0000000..2428bed
--- /dev/null
+++ b/components/icons/shutdown.tsx
@@ -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 LgShutdown = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgShutdown);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/speech-bubble.tsx b/components/icons/speech-bubble.tsx
new file mode 100644
index 0000000..1ef55f2
--- /dev/null
+++ b/components/icons/speech-bubble.tsx
@@ -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 LgSpeechBubble = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgSpeechBubble);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/star.tsx b/components/icons/star.tsx
new file mode 100644
index 0000000..d8553ca
--- /dev/null
+++ b/components/icons/star.tsx
@@ -0,0 +1,35 @@
+import * as React from "react";
+import type { SVGProps } from "react";
+import { Ref, forwardRef } from "react";
+interface SVGRProps {
+ title?: string;
+ titleId?: string;
+}
+const LgStar = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgStar);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/sun.tsx b/components/icons/sun.tsx
new file mode 100644
index 0000000..f21a476
--- /dev/null
+++ b/components/icons/sun.tsx
@@ -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 LgSun = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgSun);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/support.tsx b/components/icons/support.tsx
new file mode 100644
index 0000000..e1c9991
--- /dev/null
+++ b/components/icons/support.tsx
@@ -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 LgSupport = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgSupport);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/synchronize.tsx b/components/icons/synchronize.tsx
new file mode 100644
index 0000000..826a511
--- /dev/null
+++ b/components/icons/synchronize.tsx
@@ -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 LgSynchronize = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgSynchronize);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/toolbox.tsx b/components/icons/toolbox.tsx
new file mode 100644
index 0000000..dd5abb9
--- /dev/null
+++ b/components/icons/toolbox.tsx
@@ -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 LgToolbox = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgToolbox);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/trash.tsx b/components/icons/trash.tsx
new file mode 100644
index 0000000..724c446
--- /dev/null
+++ b/components/icons/trash.tsx
@@ -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 LgTrash = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgTrash);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/trending-up.tsx b/components/icons/trending-up.tsx
new file mode 100644
index 0000000..f33e600
--- /dev/null
+++ b/components/icons/trending-up.tsx
@@ -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 LgTrendingUp = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgTrendingUp);
+export default ForwardRef;
diff --git a/components/icons/twitter.tsx b/components/icons/twitter.tsx
new file mode 100644
index 0000000..4ac4d02
--- /dev/null
+++ b/components/icons/twitter.tsx
@@ -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 LgTwitter = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgTwitter);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/uncheck-all.tsx b/components/icons/uncheck-all.tsx
new file mode 100644
index 0000000..a81ee55
--- /dev/null
+++ b/components/icons/uncheck-all.tsx
@@ -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 LgUncheckAll = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgUncheckAll);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/unlock.tsx b/components/icons/unlock.tsx
new file mode 100644
index 0000000..0636d58
--- /dev/null
+++ b/components/icons/unlock.tsx
@@ -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 LgUnlock = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgUnlock);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/upload-2.tsx b/components/icons/upload-2.tsx
new file mode 100644
index 0000000..6e99bee
--- /dev/null
+++ b/components/icons/upload-2.tsx
@@ -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 LgUpload2 = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgUpload2);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/user-female.tsx b/components/icons/user-female.tsx
new file mode 100644
index 0000000..dd933bb
--- /dev/null
+++ b/components/icons/user-female.tsx
@@ -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 LgUserFemale = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgUserFemale);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/user-male.tsx b/components/icons/user-male.tsx
new file mode 100644
index 0000000..f949840
--- /dev/null
+++ b/components/icons/user-male.tsx
@@ -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 LgUserMale = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgUserMale);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/user.tsx b/components/icons/user.tsx
new file mode 100644
index 0000000..7556803
--- /dev/null
+++ b/components/icons/user.tsx
@@ -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 LgUser = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgUser);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/visible.tsx b/components/icons/visible.tsx
new file mode 100644
index 0000000..6f91514
--- /dev/null
+++ b/components/icons/visible.tsx
@@ -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 LgVisible = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgVisible);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/whatsapp.tsx b/components/icons/whatsapp.tsx
new file mode 100644
index 0000000..0ae254b
--- /dev/null
+++ b/components/icons/whatsapp.tsx
@@ -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 LgWhatsapp = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgWhatsapp);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/icons/youtube-play.tsx b/components/icons/youtube-play.tsx
new file mode 100644
index 0000000..f902b22
--- /dev/null
+++ b/components/icons/youtube-play.tsx
@@ -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 LgYoutubePlay = ({
+ title,
+ titleId,
+ ...props
+}: SVGProps & SVGRProps, ref: Ref) => {title ? {title} : null} ;
+const ForwardRef = forwardRef(LgYoutubePlay);
+export default ForwardRef;
\ No newline at end of file
diff --git a/components/onboarding-guard.tsx b/components/onboarding-guard.tsx
new file mode 100644
index 0000000..5061feb
--- /dev/null
+++ b/components/onboarding-guard.tsx
@@ -0,0 +1,36 @@
+'use client'
+
+import { useSession } from 'next-auth/react'
+import { useRouter, usePathname } from 'next/navigation'
+import { useEffect } from 'react'
+
+export function OnboardingGuard({ children }: { children: React.ReactNode }) {
+ const { data: session, status } = useSession()
+ const router = useRouter()
+ const pathname = usePathname()
+
+ useEffect(() => {
+ if (status === 'loading') return
+
+ if (session?.user?.isOnboarding) {
+ if (pathname !== '/onboarding') {
+ router.push('/onboarding')
+ }
+ } else {
+ if (pathname === '/onboarding') {
+ router.push('/')
+ }
+ }
+ }, [session, status, pathname, router])
+
+ //! while checking, we might want to return null to block content rendering,
+ //! but that might cause flicker
+ //! for now, we render children, but the redirect will happen fast
+ //! if we really want to block, we can render nothing if the check fails
+
+ if (status === 'authenticated' && session?.user?.isOnboarding && pathname !== '/onboarding') {
+ return null;
+ }
+
+ return <>{children}>
+}
diff --git a/components/session-provider.tsx b/components/session-provider.tsx
new file mode 100644
index 0000000..d9385a4
--- /dev/null
+++ b/components/session-provider.tsx
@@ -0,0 +1,9 @@
+'use client'
+
+import React from "react"
+
+import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react'
+
+export function SessionProvider({ children }: { children: React.ReactNode }) {
+ return {children}
+}
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
new file mode 100644
index 0000000..55c2f6e
--- /dev/null
+++ b/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/components/trade-history.tsx b/components/trade-history.tsx
new file mode 100644
index 0000000..9688ba9
--- /dev/null
+++ b/components/trade-history.tsx
@@ -0,0 +1,245 @@
+'use client'
+
+import { useEffect, useState, useMemo } from 'react'
+import useSWR from 'swr'
+import { Trade } from '@/types'
+import { Socket } from 'socket.io-client'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import { Button } from '@/components/ui/button'
+import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
+import { LgFolderInvoices, LgExternalLink } from './icons'
+import { UserBadges } from '@/components/ui/user-badges'
+
+const fetcher = (url: string) => fetch(url).then((res) => res.json())
+
+interface TradeHistoryProps {
+ coinId: string
+ ticker: string
+ socket?: Socket | null
+ limit?: number
+}
+
+function groupTrades(trades: Trade[]) {
+ if (!trades) return []
+ const grouped: any[] = []
+
+ trades.forEach((trade) => {
+ const lastGroup = grouped[grouped.length - 1]
+ const tradeDate = new Date(trade.timestamp)
+
+ const isSameMinute = lastGroup &&
+ new Date(lastGroup.timestamp).getMinutes() === tradeDate.getMinutes() &&
+ new Date(lastGroup.timestamp).getHours() === tradeDate.getHours() &&
+ new Date(lastGroup.timestamp).getDate() === tradeDate.getDate()
+
+ if (lastGroup && lastGroup.type === trade.type && isSameMinute) {
+ lastGroup.count += 1
+ lastGroup.amount += trade.amount
+ lastGroup.total += trade.total
+ if (!lastGroup.images.includes(trade.userImage)) {
+ lastGroup.images.push(trade.userImage)
+ }
+ if (lastGroup.username !== trade.username) {
+ lastGroup.isMultiUser = true
+ }
+ } else {
+ grouped.push({
+ ...trade,
+ count: 1,
+ images: [trade.userImage],
+ isMultiUser: false
+ })
+ }
+ })
+ return grouped
+}
+
+export function TradeHistory({ coinId, ticker, socket, limit }: TradeHistoryProps) {
+ const { data: rawTrades, isLoading, mutate } = useSWR(
+ `/api/trades?coinId=${coinId}`,
+ fetcher
+ )
+
+ const trades = useMemo(() => {
+ const grouped = groupTrades(rawTrades || [])
+ return limit ? grouped.slice(0, limit) : grouped
+ }, [rawTrades, limit])
+
+ useEffect(() => {
+ if (!socket) return
+
+ const handleNewTrade = (trade: any) => {
+ mutate((currentTrades) => {
+ if (!currentTrades) return [trade]
+ if (currentTrades.some(t => t._id === trade._id)) return currentTrades
+
+ return [trade, ...currentTrades]
+ }, false)
+ }
+
+ socket.on(`trade:${coinId}`, handleNewTrade)
+
+ return () => {
+ socket.off(`trade:${coinId}`, handleNewTrade)
+ }
+ }, [socket, coinId, mutate])
+
+ if (isLoading) {
+ return (
+
+ )
+ }
+
+ if (!trades || trades.length === 0) {
+ return (
+
+
+
No trades have been made yet.
+
+ )
+ }
+
+ return (
+
+
+
+ Recent Trades
+
+ {trades.length}
+
+
+
+ {trades.length > 5 && (
+
+
+
+ View All
+
+
+
+
+ Trade History
+
+ All recorded trades for ${ticker}
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ )
+}
+
+function TradeTable({ trades, ticker }: { trades: any[], ticker: string }) {
+ return (
+
+
+
+ User
+ Type
+ Total (SOL)
+ {ticker}
+ Date
+
+
+
+ {trades.map((trade) => (
+
+
+
+
+ {trade.images.slice(0, 3).map((img: string, i: number) => (
+
+
+ ??
+
+ ))}
+ {trade.images.length > 3 && (
+
+ +{trade.images.length - 3}
+
+ )}
+
+
+
+ {trade.isMultiUser ? 'Various' : trade.username}
+
+ {!trade.isMultiUser && (
+
+ )}
+
+
+
+
+
+ {trade.type === 'mined' ? 'MINED' : trade.type}
+ {trade.count > 1 && (
+
+ x{trade.count}
+
+ )}
+
+
+
+ {trade.total.toFixed(3)}
+
+
+ {formatNumber(trade.amount)}
+
+
+ {new Date(trade.timestamp).toLocaleString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+
+ ))}
+
+
+ )
+}
+
+function formatNumber(num: number) {
+ if (num >= 1000000) return `${(num / 1000000).toFixed(2)}M`
+ if (num >= 1000) return `${(num / 1000).toFixed(2)}K`
+ return num.toLocaleString(undefined, { maximumFractionDigits: 2 })
+}
diff --git a/components/trade-panel.tsx b/components/trade-panel.tsx
new file mode 100644
index 0000000..005c156
--- /dev/null
+++ b/components/trade-panel.tsx
@@ -0,0 +1,399 @@
+'use client'
+
+import { useState } from 'react'
+import { useSession } from 'next-auth/react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Coin, User } from '@/types'
+import { mutate } from 'swr'
+import { Socket } from 'socket.io-client'
+import { LgLock } from '@/components/icons'
+import { ArrowRightLeft, Pickaxe } from 'lucide-react'
+import useSWR from 'swr'
+import { useEffect } from 'react'
+import { toast } from 'sonner'
+
+interface TradePanelProps {
+ coin: Coin
+ userBalance: number
+ userHolding: number
+ socket?: Socket | null
+}
+
+export function TradePanel({ coin, userBalance, userHolding, socket }: TradePanelProps) {
+ const { data: session } = useSession()
+ const [tradeType, setTradeType] = useState<'buy' | 'sell'>('buy')
+ const [amount, setAmount] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState('')
+ const [inputMode, setInputMode] = useState<'token' | 'sol'>('token')
+ const [minerLoading, setMinerLoading] = useState(false)
+ const [currentTime, setCurrentTime] = useState(Date.now())
+
+ // Update current time every second for live mining calculation
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentTime(Date.now())
+ }, 1000)
+ return () => clearInterval(interval)
+ }, [])
+
+ // Fetch user data to check for miners
+ const { data: userData } = useSWR(
+ session ? '/api/user' : null,
+ (url: string) => fetch(url).then((res) => res.json())
+ )
+
+ const userMiners = userData?.miners?.filter(m => m.coinId === coin._id && m.isActive) || []
+
+ // Calculate live mined tokens from all miners (1000 tokens per second per miner)
+ const liveMinedTokens = userMiners.reduce((total, miner) => {
+ const timeDiffMs = currentTime - new Date(miner.lastCollectedAt).getTime()
+ const timeDiffSeconds = Math.floor(timeDiffMs / 1000)
+ return total + (timeDiffSeconds * 1000)
+ }, 0)
+
+ const vSol = coin.virtualSolReserves || 30
+ const vTokens = coin.virtualTokenReserves || 1073000000
+ const k = vSol * vTokens
+
+ const getCostForTokens = (tokens: number) => {
+ if (tokens <= 0) return 0
+ // ! ensure we don't divide by zero or negative
+ const newVTokens = vTokens - tokens
+ if (newVTokens <= 0) return Infinity
+ const newVSol = k / newVTokens
+ return newVSol - vSol
+ }
+
+ const getTokensForSol = (solAmount: number) => {
+ if (solAmount <= 0) return 0
+ const newVSol = vSol + solAmount
+ const newVTokens = k / newVSol
+ return vTokens - newVTokens
+ }
+
+ const numAmount = parseFloat(amount) || 0
+
+ // Calculate displayed totals and API payload values
+ let tradePayloadAmount = 0
+ let estimatedTotalSol = 0 // For display
+
+ if (tradeType === 'buy') {
+ if (inputMode === 'sol') {
+ // Input is SOL
+ estimatedTotalSol = numAmount
+ tradePayloadAmount = getTokensForSol(numAmount)
+ } else {
+ // Input is Tokens
+ tradePayloadAmount = numAmount
+ estimatedTotalSol = getCostForTokens(numAmount)
+ }
+ } else {
+ // Sell (Input is always Tokens in this simplified UI)
+ tradePayloadAmount = numAmount
+ estimatedTotalSol = (vSol - (k / (vTokens + numAmount))) // Sell payout
+ }
+
+ //? reserve 0.015 SOL for gas/tx fees to play it safe
+ const GAS_BUFFER = 0.015
+ const safeBalance = Math.max(0, userBalance - GAS_BUFFER)
+
+ //? we wil calculate max buy using inverse curve
+ const maxBuyTokens = getTokensForSol(safeBalance)
+ const maxSell = userHolding
+
+ const handleTrade = async () => {
+ if (!session) return
+
+ setError('')
+ setLoading(true)
+
+ try {
+ const response = await fetch('/api/trade', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ coinId: coin._id,
+ type: tradeType,
+ amount: tradePayloadAmount, // Send standard Token amount
+ }),
+ })
+
+ const data = await response.json()
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Trade failed')
+ }
+
+ if (socket && data.trade) {
+ socket.emit('new_trade', data.trade)
+ }
+
+ setAmount('')
+ mutate('/api/user')
+ mutate(`/api/coins/${coin._id}`)
+ } catch (err: any) {
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleBuyMiner = async () => {
+ if (!session) return
+
+ setMinerLoading(true)
+ try {
+ const response = await fetch('/api/miner/buy', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ coinId: coin._id }),
+ })
+
+ const data = await response.json()
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Failed to buy miner')
+ }
+
+ toast.success(data.message)
+ // Force immediate re-fetch of user data and wait for it
+ await mutate('/api/user', undefined, { revalidate: true })
+ mutate(`/api/coins/${coin._id}`)
+ } catch (err: any) {
+ toast.error(err.message)
+ } finally {
+ setMinerLoading(false)
+ }
+ }
+
+ const handleCollectMiner = async () => {
+ if (!session) return
+
+ setMinerLoading(true)
+ try {
+ const response = await fetch('/api/miner/collect', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ coinId: coin._id }),
+ })
+
+ const data = await response.json()
+
+ if (!response.ok) {
+ throw new Error(data.error || 'Failed to collect mined tokens')
+ }
+
+ toast.success(data.message)
+ mutate('/api/user')
+ mutate(`/api/coins/${coin._id}`)
+ mutate(`/api/trades?coinId=${coin._id}`)
+ } catch (err: any) {
+ toast.error(err.message)
+ } finally {
+ setMinerLoading(false)
+ }
+ }
+
+ const setPercentage = (percentage: number) => {
+ if (tradeType === 'sell') {
+ if (percentage === 1) {
+ setAmount(maxSell.toString())
+ return
+ }
+ const val = Math.floor((maxSell * percentage) * 10000) / 10000
+ setAmount(val.toString())
+ return;
+ }
+
+ // Buying
+ if (inputMode === 'sol') {
+ // Percentage of SOL Balance
+ const val = Math.floor((safeBalance * percentage) * 10000) / 10000
+ setAmount(val.toString())
+ } else {
+ // Percentage of Max Tokens
+ if (percentage === 1) {
+ setAmount(maxBuyTokens.toString())
+ return
+ }
+ const val = Math.floor((maxBuyTokens * percentage) * 10000) / 10000
+ setAmount(val.toString())
+ }
+ }
+
+ const toggleInputMode = () => {
+ setAmount('')
+ setInputMode(prev => prev === 'token' ? 'sol' : 'token')
+ }
+
+ if (!session) {
+ return (
+
+
+
+
Sign in to Trade
+
+ Connect your Discord account to start trading
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Trade Type Toggle */}
+
+ { setTradeType('buy'); setInputMode('token'); setAmount(''); }}
+ className={`flex-1 rounded-md py-2.5 text-sm font-medium transition-all ${
+ tradeType === 'buy'
+ ? 'bg-primary text-primary-foreground shadow-sm'
+ : 'text-muted-foreground hover:text-foreground'
+ }`}
+ >
+ Buy
+
+ { setTradeType('sell'); setInputMode('token'); setAmount(''); }}
+ className={`flex-1 rounded-md py-2.5 text-sm font-medium transition-all ${
+ tradeType === 'sell'
+ ? 'bg-destructive text-destructive-foreground shadow-sm'
+ : 'text-muted-foreground hover:text-foreground'
+ }`}
+ >
+ Sell
+
+
+
+ {/* Balance Info */}
+
+
+ {tradeType === 'buy' ? 'Available Balance' : 'Your Holdings'}
+
+
+ {tradeType === 'buy'
+ ? <>{userBalance.toFixed(4)} >
+ : `${userHolding.toLocaleString()} ${coin.ticker}`
+ }
+
+
+
+ {/* Amount Input */}
+
+
+
+ Amount ({inputMode === 'sol' ? 'SOL' : `$${coin.ticker}`})
+
+ {tradeType === 'buy' && (
+
+
+ Switch to {inputMode === 'sol' ? coin.ticker : 'SOL'}
+
+ )}
+
+
+ setAmount(e.target.value)}
+ placeholder="0"
+ className="h-12 bg-muted/30 pr-16 font-mono text-lg"
+ />
+
+ {inputMode === 'sol' ? 'SOL' : coin.ticker}
+
+
+
+
+ {/* Quick Amount Buttons */}
+
+ {[25, 50, 75, 100].map((pct) => (
+ setPercentage(pct / 100)}
+ className="rounded-lg border border-border/50 py-2 text-sm font-medium text-muted-foreground transition-colors hover:border-primary/50 hover:text-primary"
+ >
+ {pct}%
+
+ ))}
+
+
+ {/* Total */}
+
+
+ {tradeType === 'buy' ? 'Est. Cost' : 'Est. Payout'}
+
+
+ {estimatedTotalSol.toFixed(6)}
+
+
+
+
+ {/* Error */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Trade Button */}
+
userBalance + 0.00000001 : numAmount > maxSell)}
+ className={`h-12 w-full text-base font-medium ${
+ tradeType === 'buy'
+ ? 'bg-primary hover:bg-primary/90'
+ : 'bg-destructive hover:bg-destructive/90'
+ }`}
+ >
+ {loading ? (
+
+ ) : (
+ <>
+ {tradeType === 'buy' ? 'Buy' : 'Sell'} {tradeType === 'buy' && inputMode === 'sol' ? 'Tokens' : `$${coin.ticker}`}
+ >
+ )}
+
+
+ {/* Price Impact Warning */}
+ {estimatedTotalSol > coin.liquidity * 0.1 && (
+
+ This trade may have a high price impact due to low liquidity.
+
+ )}
+
+ {/* Miner Section (disabled) */}
+ {session && (
+
+
+
+
${coin.ticker} Miner
+
+
+
+
Miners are temporarily disabled
+
+ Buying and collecting miners is currently unavailable. You have been refunded.
+
+
+ Collect
+ +1
+
+
+
+ )}
+
+ )
+}
diff --git a/components/trading-chart.tsx b/components/trading-chart.tsx
new file mode 100644
index 0000000..d152f11
--- /dev/null
+++ b/components/trading-chart.tsx
@@ -0,0 +1,247 @@
+'use client'
+
+// gpt idk how to do ts
+
+import { useEffect, useRef, useState, useMemo } from 'react'
+import { createChart, ColorType, IChartApi, Time } from 'lightweight-charts'
+import { PricePoint } from '@/types'
+
+interface TradingChartProps {
+ priceHistory: PricePoint[]
+ currentPrice: number
+ solPrice?: number
+}
+
+function aggregateToCandles(data: { time: number; value: number }[], periodSeconds = 60 * 5) {
+ if (data.length === 0) return []
+
+ const candles = []
+
+ let currentCandleStartTime = Math.floor(data[0].time / periodSeconds) * periodSeconds
+ let currentOpen = data[0].value
+ let currentHigh = data[0].value
+ let currentLow = data[0].value
+ let currentClose = data[0].value
+
+ for (let i = 0; i < data.length; i++) {
+ const point = data[i]
+ if (point.time < currentCandleStartTime + periodSeconds) {
+ currentHigh = Math.max(currentHigh, point.value)
+ currentLow = Math.min(currentLow, point.value)
+ currentClose = point.value
+ } else {
+ candles.push({
+ time: currentCandleStartTime as Time,
+ open: currentOpen,
+ high: currentHigh,
+ low: currentLow,
+ close: currentClose,
+ })
+
+ currentOpen = currentClose
+ currentCandleStartTime = Math.floor(point.time / periodSeconds) * periodSeconds
+ currentHigh = point.value
+ currentLow = point.value
+ currentClose = point.value
+ }
+ }
+
+ candles.push({
+ time: currentCandleStartTime as Time,
+ open: currentOpen,
+ high: currentHigh,
+ low: currentLow,
+ close: currentClose,
+ })
+
+ return candles
+}
+
+export function TradingChart({ priceHistory, currentPrice, solPrice = 0 }: TradingChartProps) {
+ const chartContainerRef = useRef(null)
+ const chartRef = useRef(null)
+ const [showUsd, setShowUsd] = useState(true)
+
+ const baseData = useMemo(() => {
+ return priceHistory.map(point => ({
+ time: (new Date(point.timestamp).getTime() / 1000),
+ value: showUsd ? point.price * (solPrice || 1) : point.price
+ })).sort((a, b) => (a.time as number) - (b.time as number))
+ }, [priceHistory, showUsd, solPrice])
+
+ const displayData = useMemo(() => {
+ const data = [...baseData]
+ if (data.length > 0) {
+ const lastTime = data[data.length - 1].time
+ const now = Math.floor(Date.now() / 1000)
+ const currentVal = showUsd ? currentPrice * (solPrice || 1) : currentPrice
+
+ if (now > lastTime) {
+ data.push({ time: now, value: currentVal })
+ } else {
+ data[data.length - 1].value = currentVal
+ }
+ } else {
+ const now = Math.floor(Date.now() / 1000)
+ const currentVal = showUsd ? currentPrice * (solPrice || 1) : currentPrice
+ data.push({ time: now, value: currentVal })
+ }
+ return data
+ }, [baseData, currentPrice, solPrice, showUsd])
+
+
+ const startPrice = displayData[0]?.value || 0
+ const endPrice = displayData[displayData.length - 1]?.value || 0
+ const priceChange = startPrice !== 0 ? ((endPrice - startPrice) / startPrice) * 100 : 0
+ const isPositive = priceChange >= 0
+
+ const displayPrice = endPrice
+
+ const formatPrice = (price: number) => {
+ if (price === 0) return '0.00'
+ if (price < 0.00000001) return price.toFixed(12)
+ if (price < 0.000001) return price.toFixed(9)
+ if (price < 0.001) return price.toFixed(7)
+ if (price < 1) return price.toFixed(5)
+ return price.toFixed(3)
+ }
+
+ useEffect(() => {
+ if (!chartContainerRef.current) return
+
+ const handleResize = () => {
+ chartRef.current?.applyOptions({ width: chartContainerRef.current!.clientWidth })
+ }
+
+ const chart = createChart(chartContainerRef.current, {
+ layout: {
+ background: { type: ColorType.Solid, color: 'transparent' },
+ textColor: '#9ca3af',
+ },
+ width: chartContainerRef.current.clientWidth,
+ height: 500,
+ grid: {
+ vertLines: { color: 'rgba(42, 46, 57, 0.1)' },
+ horzLines: { color: 'rgba(42, 46, 57, 0.1)' },
+ },
+ rightPriceScale: {
+ borderColor: 'rgba(197, 203, 206, 0.1)',
+ },
+ timeScale: {
+ borderColor: 'rgba(197, 203, 206, 0.1)',
+ timeVisible: true,
+ secondsVisible: false,
+ rightOffset: 12,
+ barSpacing: 6,
+ },
+ crosshair: {
+ mode: 1, // @type CrosshairMode.Normal
+ }
+ })
+
+ // Create Candle Series
+ let series: any;
+
+ try {
+ // Standard v3/v4 way
+ series = (chart as any).addCandlestickSeries({
+ upColor: '#10b981',
+ downColor: '#ef4444',
+ borderVisible: false,
+ wickUpColor: '#10b981',
+ wickDownColor: '#ef4444',
+ priceFormat: {
+ type: 'custom',
+ formatter: (price: number) => {
+ if (price < 0.000001) return price.toFixed(9)
+ if (price < 0.001) return price.toFixed(6)
+ if (price < 1) return price.toFixed(4)
+ return price.toFixed(2)
+ },
+ minMove: 0.000000001,
+ }
+ })
+
+ // Aggregate data into candles (5 min intervals? or appropriate based on history length)
+ // If history is short (new coin), use small interval (1 min)
+ // If long, use longer.
+ const duration = displayData.length > 0 ? displayData[displayData.length - 1].time - displayData[0].time : 0
+ const interval = duration < 3600 ? 60 : 300 // 1 min vs 5 min
+
+ const candleData = aggregateToCandles(displayData, interval)
+ series.setData(candleData)
+
+ // Removed fitContent to avoid "zoomed all the way in" look on load
+ // chart.timeScale().fitContent()
+ chartRef.current = chart
+
+ } catch (e) {
+ console.error("Failed to chart series:", e)
+ }
+
+ window.addEventListener('resize', handleResize)
+
+ return () => {
+ window.removeEventListener('resize', handleResize)
+ chart.remove()
+ }
+ }, [displayData, isPositive])
+
+ return (
+
+
+
+
+
+ {priceHistory.length > 0 ? new Date(priceHistory[0].timestamp).toLocaleDateString() : 'Today'}
+ {' '}-{' '}
+ Live
+
+ setShowUsd(!showUsd)}
+ className="rounded bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground hover:bg-muted/80 hover:text-foreground"
+ >
+ Switch to {showUsd ? 'SOL' : 'USD'}
+
+
+
+
+ {showUsd ? (
+ '$'
+ ) : (
+
+ )}
+ {formatPrice(displayPrice)}
+
+
+ {isPositive ? 'β' : 'β'}{Math.abs(priceChange).toFixed(2)}%
+
+
+
+ MCAP: {showUsd ? '$' : (
+
+ )}
+ {formatPrice(displayPrice * 1_000_000_000)} {showUsd ? 'USD' : 'SOL'}
+ {/* !! note: this is a rough estimation based on 1b supply for display only! */}
+
+
+
+
+
+
+ )
+}
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
new file mode 100644
index 0000000..e538a33
--- /dev/null
+++ b/components/ui/accordion.tsx
@@ -0,0 +1,66 @@
+'use client'
+
+import * as React from 'react'
+import * as AccordionPrimitive from '@radix-ui/react-accordion'
+import { ChevronDownIcon } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180',
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..9704452
--- /dev/null
+++ b/components/ui/alert-dialog.tsx
@@ -0,0 +1,157 @@
+'use client'
+
+import * as React from 'react'
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
+
+import { cn } from '@/lib/utils'
+import { buttonVariants } from '@/components/ui/button'
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+