pummmp.fun/app/create/page.tsx
2026-07-04 12:49:09 -07:00

412 lines
14 KiB
TypeScript

'use client'
import React from "react"
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useSession, signIn } from 'next-auth/react'
import { Header } from '@/components/header'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { LgGeminiAi, LgLock, LgInfo } from '@/components/icons'
export default function CreateCoinPage() {
const router = useRouter()
const { data: session } = useSession()
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [formData, setFormData] = useState({
name: '',
ticker: '',
description: '',
image: '',
website: '',
twitter: '',
telegram: '',
initialBuy: 0,
useUserImage: false,
verified: false,
boosted: false,
})
const [initialBuyInput, setInitialBuyInput] = useState('')
const [initialBuyMode, setInitialBuyMode] = useState<'sol' | 'tokens'>('sol')
React.useEffect(() => {
const amount = parseFloat(initialBuyInput) || 0;
if (amount <= 0) {
setFormData(prev => ({ ...prev, initialBuy: 0 }))
return;
}
if (initialBuyMode === 'sol') {
setFormData(prev => ({ ...prev, initialBuy: amount }))
} else {
//! Bonding curve math (Constant Product)
//! k = 32,190,000,000
//! virtualSolReserves = 30
//! virtualTokenReserves = 1,073,000,000
const vTokens = 1073000000;
const vSol = 30;
const k = vTokens * vSol;
const tokensToBuy = amount;
//! cap buy at 80% of supply to avoid explosion
if (tokensToBuy >= vTokens * 0.8) {
return;
}
const newVTokens = vTokens - tokensToBuy;
const newVSol = k / newVTokens;
const costSol = newVSol - vSol;
setFormData(prev => ({ ...prev, initialBuy: costSol }))
}
}, [initialBuyInput, initialBuyMode])
React.useEffect(() => {
if (session?.user?.image) {
setFormData(prev => ({ ...prev, useUserImage: true }))
}
}, [session])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!session) {
signIn('discord')
return
}
setError('')
setLoading(true)
try {
const response = await fetch('/api/coins', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to create coin')
}
router.push(`/coin/${data._id}`)
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
const previewImage = formData.useUserImage && session?.user?.image
? session.user.image
: `https://api.dicebear.com/7.x/identicon/svg?seed=${formData.ticker || 'preview'}`
return (
<div className="min-h-screen bg-background">
<Header />
<main className="mx-auto max-w-2xl px-4 py-12">
{/* Header */}
<div className="mb-10 text-center">
<div className="mb-4 inline-flex items-center justify-center rounded-2xl bg-accent/20 p-2">
<img src="/logo.svg" className="h-20 w-20" alt="Logo" />
</div>
<h1 className="mb-3 text-3xl font-bold">Launch Your Memecoin</h1>
<p className="text-muted-foreground">
Create a new coin and make some MULA...
</p>
</div>
{!session ? (
<div className="rounded-2xl border border-border bg-card p-8 text-center">
<LgLock className="mx-auto mb-4 h-12 w-12 opacity-50" />
<h2 className="mb-2 text-xl font-semibold">Sign In Required</h2>
<p className="mb-6 text-muted-foreground">
Connect your Discord account to create coins
</p>
<Button
onClick={() => signIn('discord')}
className="gap-2 bg-[#5865F2] hover:bg-[#4752C4]"
>
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
Sign in with Discord
</Button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Preview Card */}
<div className="rounded-2xl border border-border bg-card p-6">
<h3 className="mb-4 text-sm font-medium text-muted-foreground">Preview</h3>
<div className="flex items-center gap-4">
<img
src={previewImage || "/placeholder.svg"}
alt="Preview"
className="h-16 w-16 rounded-xl bg-muted object-cover"
/>
<div>
<p className="text-xl font-semibold">
{formData.name || 'Coin Name'}
</p>
<p className="text-muted-foreground">
${formData.ticker || 'TICKER'}
</p>
</div>
</div>
{/* Image Source Toggle */}
{session?.user?.image && (
<div className="mt-4 flex items-center gap-2">
<input
type="checkbox"
id="useUserImage"
checked={formData.useUserImage}
onChange={(e) => setFormData({ ...formData, useUserImage: e.target.checked })}
className="rounded border-gray-300"
/>
<label htmlFor="useUserImage" className="text-sm text-foreground">
Use my Discord profile picture as icon
</label>
</div>
)}
</div>
{/* Form Fields */}
<div className="space-y-4 rounded-2xl border border-border bg-card p-6">
<div className="space-y-2">
<Label htmlFor="name">Coin Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="e.g. Doge Moon"
className="h-12 bg-muted/30"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="ticker">Ticker Symbol</Label>
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground">
$
</span>
<Input
id="ticker"
value={formData.ticker}
onChange={(e) => setFormData({ ...formData, ticker: e.target.value.toUpperCase() })}
placeholder="MOON"
className="h-12 bg-muted/30 pl-8 uppercase"
maxLength={10}
required
/>
</div>
<p className="text-xs text-muted-foreground">
3-10 characters, letters only
</p>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description (Optional)</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Tell us about your coin..."
className="bg-muted/30"
/>
</div>
<div className="space-y-2">
<Label htmlFor="website">Website (Optional)</Label>
<Input
id="website"
value={formData.website}
onChange={(e) => setFormData({ ...formData, website: e.target.value })}
placeholder="https://..."
className="h-12 bg-muted/30"
/>
</div>
<div className="space-y-2">
<Label htmlFor="twitter">Twitter (Optional)</Label>
<Input
id="twitter"
value={formData.twitter}
onChange={(e) => setFormData({ ...formData, twitter: e.target.value })}
placeholder="https://x.com/..."
className="h-12 bg-muted/30"
/>
</div>
<div className="space-y-2">
<Label htmlFor="telegram">Telegram (Optional)</Label>
<Input
id="telegram"
value={formData.telegram}
onChange={(e) => setFormData({ ...formData, telegram: e.target.value })}
placeholder="https://t.me/..."
className="h-12 bg-muted/30"
/>
</div>
{/* Options Section */}
<div className="space-y-4 pt-4 border-t border-border">
<h3 className="text-lg font-semibold">Options</h3>
{/* Verified Coin Toggle - Only for verified users */}
{(session?.user as any)?.verified && (
<div className="flex items-center justify-between rounded-xl border border-border bg-muted/20 p-4">
<div className="space-y-0.5">
<Label className="text-base">Verified Coin</Label>
<p className="text-sm text-muted-foreground">
Mark this coin as verified immediately
</p>
</div>
<Switch
checked={formData.verified}
onCheckedChange={(checked) => setFormData({ ...formData, verified: checked })}
/>
</div>
)}
{/* Boost Toggle */}
<div className="flex items-center justify-between rounded-xl border border-yellow-500/20 bg-yellow-500/5 p-4">
<div className="space-y-0.5">
<Label className="text-base flex items-center gap-2">
Start Boosted
<span className="rounded bg-yellow-500/20 px-1.5 py-0.5 text-xs text-yellow-500">
+1 SOL
</span>
</Label>
<p className="text-sm text-muted-foreground">
Pin your coin to the top for 24 hours immediately upon launch.
</p>
</div>
<Switch
checked={formData.boosted}
onCheckedChange={(checked) => setFormData({ ...formData, boosted: checked })}
/>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="initialBuy">Initial Buy</Label>
<div className="flex items-center gap-2 rounded-lg bg-muted p-1">
<button
type="button"
onClick={() => setInitialBuyMode('sol')}
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
initialBuyMode === 'sol'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
SOL
</button>
<button
type="button"
onClick={() => setInitialBuyMode('tokens')}
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
initialBuyMode === 'tokens'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
Tokens
</button>
</div>
</div>
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 flex items-center justify-center text-muted-foreground">
{initialBuyMode === 'sol' ? (
<img src="/solana.svg" className="h-4 w-4 opacity-80" alt="SOL" />
) : (
<span className="text-xs font-bold text-muted-foreground">T</span>
)}
</span>
<Input
id="initialBuy"
type="number"
step={initialBuyMode === 'sol' ? "0.01" : "1000"}
min="0"
value={initialBuyInput}
onChange={(e) => setInitialBuyInput(e.target.value)}
placeholder={initialBuyMode === 'sol' ? "0.00" : "Number of tokens"}
className="h-12 bg-muted/30 pl-10"
/>
{initialBuyMode === 'tokens' && formData.initialBuy > 0 && (
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-muted-foreground">
{formData.initialBuy.toFixed(4)} SOL
</div>
)}
</div>
<p className="text-xs text-muted-foreground">
{initialBuyMode === 'sol'
? "Enter the amount of SOL you want to spend."
: "Enter the number of tokens to buy. The price increases as you buy more!"}
</p>
</div>
</div>
{/* Info Box */}
<div className="flex gap-3 rounded-xl bg-primary/10 p-4">
<LgInfo className="h-5 w-5 shrink-0" />
<div className="text-sm">
<p className="font-medium text-primary">Creation Fee: 0.02 SOL</p>
<p className="text-muted-foreground">
Your coin launches with 1 billion tokens.
Cost: 0.02 SOL + Initial Buy Amount.
</p>
</div>
</div>
{/* Error */}
{error && (
<div className="flex gap-3 rounded-xl bg-destructive/10 p-4">
<LgInfo className="h-5 w-5 shrink-0 text-destructive" />
<p className="text-sm text-destructive">{error}</p>
</div>
)}
{/* Submit Button */}
<Button
type="submit"
disabled={loading || !formData.name || !formData.ticker}
className="h-12 w-full text-base font-medium"
>
{loading ? (
<div className="flex items-center gap-2">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-current border-t-transparent" />
Launching...
</div>
) : (
<div className="flex items-center gap-2">
{/* Icon removed or replaced */}
Launch Coin (Cost: {(0.02 + (formData.boosted ? 1 : 0) + (formData.initialBuy || 0)).toFixed(2)} SOL)
</div>
)}
</Button>
</form>
)}
</main>
</div>
)
}