fun 'init
This commit is contained in:
commit
8ed56ab084
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
4
next.config.mjs
Normal file
4
next.config.mjs
Normal file
@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
6172
package-lock.json
generated
Normal file
6172
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "solana-mama-generator",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fal-ai/client": "^1.2.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@solana/spl-token": "^0.3.8",
|
||||
"@solana/web3.js": "^1.78.4",
|
||||
"next": "13.5.4",
|
||||
"openai": "^4.77.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "13.5.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
67
src/app/api/flux/route.ts
Normal file
67
src/app/api/flux/route.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { fal } from "@fal-ai/client"
|
||||
|
||||
if (!process.env.FAL_AI_KEY) {
|
||||
throw new Error('FAL_AI_KEY is not configured in environment variables')
|
||||
}
|
||||
|
||||
// Configure fal client
|
||||
fal.config({
|
||||
credentials: process.env.FAL_AI_KEY
|
||||
})
|
||||
|
||||
// Consistent image size for all generations
|
||||
const IMAGE_WIDTH: number = 1024
|
||||
const IMAGE_HEIGHT: number = 1024
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const { prompt, modelId } = await req.json()
|
||||
|
||||
if (!prompt || !modelId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Prompt and modelId are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
console.log('Generating with Flux model:', modelId)
|
||||
console.log('Prompt:', prompt)
|
||||
|
||||
const result = await fal.subscribe(modelId, {
|
||||
input: {
|
||||
prompt: prompt,
|
||||
image_size: {
|
||||
width: IMAGE_WIDTH,
|
||||
height: IMAGE_HEIGHT
|
||||
},
|
||||
},
|
||||
logs: true,
|
||||
onQueueUpdate: (update) => {
|
||||
if (update.status === "IN_PROGRESS") {
|
||||
console.log('Generation progress:', update.logs.map((log) => log.message))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
console.log('Flux generation result:', result)
|
||||
|
||||
// Extract the image URL from the response
|
||||
const imageUrl = result.data?.images?.[0]?.url
|
||||
|
||||
if (!imageUrl) {
|
||||
console.error('No image URL in response:', result)
|
||||
throw new Error('No image URL in response')
|
||||
}
|
||||
|
||||
return NextResponse.json({ imageUrl })
|
||||
} catch (error) {
|
||||
console.error('Flux generation error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to generate image' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
75
src/app/globals.css
Normal file
75
src/app/globals.css
Normal file
@ -0,0 +1,75 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgb(var(--background-start-rgb)),
|
||||
rgb(var(--background-end-rgb))
|
||||
);
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Custom animations */
|
||||
@keyframes bounce-delay {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1.0); }
|
||||
}
|
||||
|
||||
.delay-100 {
|
||||
animation-delay: 100ms;
|
||||
}
|
||||
|
||||
.delay-200 {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
|
||||
/* Reset default focus styles */
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for textareas */
|
||||
textarea {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(139, 92, 246, 0.5) rgba(17, 24, 39, 0.1);
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-track {
|
||||
background: rgba(17, 24, 39, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
textarea::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(139, 92, 246, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Ensure proper backdrop-filter support */
|
||||
@supports (backdrop-filter: blur(12px)) {
|
||||
.backdrop-blur-lg {
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure proper text-gradient support */
|
||||
@supports (-webkit-background-clip: text) or (background-clip: text) {
|
||||
.bg-clip-text {
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
}
|
22
src/app/layout.tsx
Normal file
22
src/app/layout.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Mark\'s Meme Market',
|
||||
description: 'Use MTM to generate memes',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
147
src/app/page.tsx
Normal file
147
src/app/page.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import WalletHeader from '../components/WalletHeader'
|
||||
import AIServiceCard from '../components/AIServiceCard'
|
||||
import { generateWithFlux, FluxGenerationResult, FLUX_MODELS } from '../services/fluxService'
|
||||
import { processMTMPayment } from '../services/paymentService'
|
||||
|
||||
interface WalletState {
|
||||
connected: boolean
|
||||
publicKey: string | null
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
solflare: any; // Or use a more specific type if available
|
||||
}
|
||||
}
|
||||
|
||||
const Page: React.FC = (): React.ReactElement => {
|
||||
const [walletState, setWalletState] = useState<WalletState>({
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
})
|
||||
|
||||
const connectWallet = async (): Promise<void> => {
|
||||
try {
|
||||
if (typeof window === 'undefined' || !window.solflare) {
|
||||
throw new Error('Solflare wallet not found! Please install it first.')
|
||||
}
|
||||
|
||||
await window.solflare.connect()
|
||||
|
||||
if (!window.solflare.publicKey) {
|
||||
throw new Error('Failed to connect to wallet')
|
||||
}
|
||||
|
||||
const publicKey: string = window.solflare.publicKey.toString()
|
||||
setWalletState({
|
||||
connected: true,
|
||||
publicKey,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Wallet connection error:', error)
|
||||
setWalletState({
|
||||
connected: false,
|
||||
publicKey: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleFluxGeneration = (modelId: string, cost: number) => {
|
||||
return async (prompt: string): Promise<FluxGenerationResult> => {
|
||||
if (!walletState.connected || !walletState.publicKey || !window.solflare) {
|
||||
return { error: 'Wallet not connected' }
|
||||
}
|
||||
|
||||
// Process payment first
|
||||
const paymentResult = await processMTMPayment(
|
||||
walletState.publicKey,
|
||||
cost,
|
||||
window.solflare
|
||||
)
|
||||
|
||||
if (!paymentResult.success) {
|
||||
return { error: paymentResult.error }
|
||||
}
|
||||
|
||||
// Then generate image with specified model
|
||||
return generateWithFlux(prompt, modelId)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full flex flex-col items-center bg-gradient-to-b from-slate-950 via-orange-950 to-slate-950">
|
||||
<div className="container max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-amber-500">
|
||||
Mark's Meme Market
|
||||
</h1>
|
||||
<p className="text-orange-200 text-lg mb-8">
|
||||
Use MTM to generate memes
|
||||
</p>
|
||||
|
||||
<WalletHeader
|
||||
isConnected={walletState.connected}
|
||||
publicKey={walletState.publicKey}
|
||||
onConnect={connectWallet}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Flux Models Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{FLUX_MODELS.map((model) => (
|
||||
<AIServiceCard
|
||||
key={model.modelId}
|
||||
title={model.name}
|
||||
description={model.description}
|
||||
tokenCost={model.cost}
|
||||
isWalletConnected={walletState.connected}
|
||||
onGenerate={handleFluxGeneration(model.modelId, model.cost)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Coming Soon Card - with purple theme for contrast */}
|
||||
<div className="relative bg-purple-900/30 backdrop-blur-lg rounded-2xl shadow-xl border border-purple-700/50 overflow-hidden group hover:shadow-purple-500/20 transition-all duration-300">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-600/10 to-pink-500/10 opacity-50"></div>
|
||||
<div className="relative p-6 flex flex-col h-full">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-500">
|
||||
Coming Soon
|
||||
</h3>
|
||||
<p className="mt-2 text-purple-200">
|
||||
New AI model integration in development. Stay tuned for more amazing features!
|
||||
</p>
|
||||
<div className="mt-2 inline-block px-3 py-1 bg-purple-500/20 rounded-full">
|
||||
<span className="text-purple-200 text-sm">TBD</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
disabled
|
||||
className="w-full bg-gradient-to-r from-purple-500/50 to-pink-500/50
|
||||
text-white/50 font-semibold py-4 px-6 rounded-xl
|
||||
cursor-not-allowed opacity-50"
|
||||
>
|
||||
Coming Soon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-12 text-center text-slate-400">
|
||||
<p className="text-sm">
|
||||
Powered by Mark • Use at your own risk
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
132
src/components/AIServiceCard.tsx
Normal file
132
src/components/AIServiceCard.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface AIServiceCardProps {
|
||||
title: string
|
||||
description: string
|
||||
tokenCost: number
|
||||
isWalletConnected: boolean
|
||||
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }>
|
||||
}
|
||||
|
||||
interface GenerationState {
|
||||
loading: boolean
|
||||
processing: boolean
|
||||
imageUrl: string | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
title,
|
||||
description,
|
||||
tokenCost,
|
||||
isWalletConnected,
|
||||
onGenerate
|
||||
}) => {
|
||||
const [inputText, setInputText] = useState<string>('')
|
||||
const [generationState, setGenerationState] = useState<GenerationState>({
|
||||
loading: false,
|
||||
processing: false,
|
||||
imageUrl: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const handleGenerate = async (): Promise<void> => {
|
||||
if (!inputText || !isWalletConnected) return
|
||||
|
||||
setGenerationState({
|
||||
...generationState,
|
||||
loading: true,
|
||||
error: null,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await onGenerate(inputText)
|
||||
|
||||
if (result.error) {
|
||||
setGenerationState({
|
||||
...generationState,
|
||||
loading: false,
|
||||
error: result.error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (result.imageUrl) {
|
||||
setGenerationState({
|
||||
loading: false,
|
||||
processing: false,
|
||||
imageUrl: result.imageUrl,
|
||||
error: null,
|
||||
})
|
||||
} else {
|
||||
throw new Error('No image URL received')
|
||||
}
|
||||
} catch (error) {
|
||||
setGenerationState({
|
||||
...generationState,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Generation failed',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full bg-slate-900/50 backdrop-blur-lg rounded-2xl shadow-xl border border-orange-800/50 mb-8 hover:shadow-orange-500/20 transition-all duration-300">
|
||||
<div className="p-6">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-amber-500">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-slate-300 mt-2">{description}</p>
|
||||
<div className="mt-2 inline-block px-3 py-1 bg-amber-500/20 rounded-full text-amber-200 text-sm">
|
||||
Cost: {tokenCost} MTM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
placeholder="Enter your prompt here..."
|
||||
disabled={!isWalletConnected}
|
||||
className="w-full bg-slate-950/80 text-slate-200 border border-orange-900 rounded-xl p-4
|
||||
placeholder-slate-500 focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20
|
||||
focus:outline-none min-h-[120px] transition-all duration-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
rows={4}
|
||||
/>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={!isWalletConnected || generationState.loading || !inputText}
|
||||
className="w-full bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600
|
||||
hover:to-amber-600 text-white font-semibold py-4 px-6 rounded-xl
|
||||
transition-all duration-200 shadow-lg hover:shadow-amber-500/25
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
|
||||
>
|
||||
{generationState.loading ? 'Processing...' : `Pay ${tokenCost} MTM & Generate`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{generationState.error && (
|
||||
<div className="mt-4 bg-red-900/20 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center">
|
||||
{generationState.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{generationState.imageUrl && (
|
||||
<div className="mt-4">
|
||||
<img
|
||||
src={generationState.imageUrl}
|
||||
alt="Generated content"
|
||||
className="w-full h-auto rounded-xl shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIServiceCard
|
131
src/components/TextGenerationCard.tsx
Normal file
131
src/components/TextGenerationCard.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface TextGenerationCardProps {
|
||||
title: string
|
||||
description: string
|
||||
tokenCost: number
|
||||
isWalletConnected: boolean
|
||||
onGenerate: (prompt: string) => Promise<{ textResponse?: string, error?: string }>
|
||||
}
|
||||
|
||||
interface GenerationState {
|
||||
loading: boolean
|
||||
processing: boolean
|
||||
textResponse: string | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
const TextGenerationCard: React.FC<TextGenerationCardProps> = ({
|
||||
title,
|
||||
description,
|
||||
tokenCost,
|
||||
isWalletConnected,
|
||||
onGenerate
|
||||
}) => {
|
||||
const [inputText, setInputText] = useState<string>('')
|
||||
const [generationState, setGenerationState] = useState<GenerationState>({
|
||||
loading: false,
|
||||
processing: false,
|
||||
textResponse: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const handleGenerate = async (): Promise<void> => {
|
||||
if (!inputText || !isWalletConnected) return
|
||||
|
||||
setGenerationState({
|
||||
...generationState,
|
||||
loading: true,
|
||||
error: null,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await onGenerate(inputText)
|
||||
|
||||
if (result.error) {
|
||||
setGenerationState({
|
||||
...generationState,
|
||||
loading: false,
|
||||
error: result.error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (result.textResponse) {
|
||||
setGenerationState({
|
||||
loading: false,
|
||||
processing: false,
|
||||
textResponse: result.textResponse,
|
||||
error: null,
|
||||
})
|
||||
} else {
|
||||
throw new Error('No response received')
|
||||
}
|
||||
} catch (error) {
|
||||
setGenerationState({
|
||||
...generationState,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Generation failed',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
|
||||
<div className="p-6">
|
||||
<div className="mb-4">
|
||||
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-600">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-2">{description}</p>
|
||||
<div className="mt-2 inline-block px-3 py-1 bg-green-500/20 rounded-full text-green-300 text-sm">
|
||||
Cost: {tokenCost} MTM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
placeholder="Enter your question here..."
|
||||
disabled={!isWalletConnected}
|
||||
className="w-full bg-gray-900/50 text-gray-100 border border-gray-700 rounded-xl p-4
|
||||
placeholder-gray-500 focus:border-green-500 focus:ring-2 focus:ring-green-500/20
|
||||
focus:outline-none min-h-[120px] transition-all duration-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
rows={4}
|
||||
/>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={!isWalletConnected || generationState.loading || !inputText}
|
||||
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600
|
||||
hover:to-emerald-600 text-white font-semibold py-4 px-6 rounded-xl
|
||||
transition-all duration-200 shadow-lg hover:shadow-green-500/25
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
|
||||
>
|
||||
{generationState.loading ? 'Processing...' : `Pay ${tokenCost} MTM & Generate`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{generationState.error && (
|
||||
<div className="mt-4 bg-red-900/20 border border-red-500/20 text-red-400 px-4 py-3 rounded-xl text-center">
|
||||
{generationState.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{generationState.textResponse && (
|
||||
<div className="mt-6 bg-gray-900/50 rounded-xl p-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-green-400 mb-3">Response:</h3>
|
||||
<div className="text-gray-300 whitespace-pre-wrap">
|
||||
{generationState.textResponse}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextGenerationCard
|
35
src/components/WalletHeader.tsx
Normal file
35
src/components/WalletHeader.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
interface WalletHeaderProps {
|
||||
isConnected: boolean
|
||||
publicKey: string | null
|
||||
onConnect: () => Promise<void>
|
||||
}
|
||||
|
||||
const WalletHeader: React.FC<WalletHeaderProps> = ({ isConnected, publicKey, onConnect }) => {
|
||||
return (
|
||||
<div className="w-full bg-slate-900/50 backdrop-blur-lg rounded-xl shadow-lg border border-orange-800/50 mb-8 p-4">
|
||||
{!isConnected ? (
|
||||
<button
|
||||
onClick={onConnect}
|
||||
className="w-full bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-600 hover:to-amber-600
|
||||
text-white font-semibold py-3 px-6 rounded-lg transition-all duration-200
|
||||
shadow-lg hover:shadow-orange-500/25"
|
||||
>
|
||||
Connect Solflare Wallet
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-slate-300">Connected Wallet</span>
|
||||
<span className="px-3 py-1 bg-amber-500/20 rounded-full text-amber-200 text-sm">
|
||||
{publicKey?.slice(0, 22)}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletHeader
|
70
src/services/fluxService.ts
Normal file
70
src/services/fluxService.ts
Normal file
@ -0,0 +1,70 @@
|
||||
export interface FluxGenerationResult {
|
||||
imageUrl?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface FluxModelConfig {
|
||||
modelId: string
|
||||
name: string
|
||||
description: string
|
||||
cost: number
|
||||
}
|
||||
|
||||
// Available Flux/fal-ai models
|
||||
export const FLUX_MODELS: FluxModelConfig[] = [
|
||||
{
|
||||
modelId: "fal-ai/flux/schnell",
|
||||
name: "Schnell",
|
||||
description: "Fast meme generator",
|
||||
cost: 2
|
||||
},
|
||||
{
|
||||
modelId: "fal-ai/recraft-v3",
|
||||
name: "Recraft",
|
||||
description: "Advanced meme generator",
|
||||
cost: 400
|
||||
},
|
||||
{
|
||||
modelId: "fal-ai/stable-diffusion-v35-large",
|
||||
name: "Marquee",
|
||||
description: "Best meme generator",
|
||||
cost: 500
|
||||
}
|
||||
]
|
||||
|
||||
export async function generateWithFlux(
|
||||
prompt: string,
|
||||
modelId: string
|
||||
): Promise<FluxGenerationResult> {
|
||||
try {
|
||||
const response = await fetch('/api/flux', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
modelId
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to generate image')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('Raw Flux response:', data)
|
||||
|
||||
if (data.imageUrl) {
|
||||
return { imageUrl: data.imageUrl }
|
||||
} else {
|
||||
console.error('Unexpected response structure:', data)
|
||||
throw new Error('Invalid response format from Flux API')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Flux generation error:', error)
|
||||
return {
|
||||
error: error instanceof Error ? error.message : 'Generation failed'
|
||||
}
|
||||
}
|
||||
}
|
149
src/services/paymentService.ts
Normal file
149
src/services/paymentService.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { Connection, PublicKey, Transaction, SystemProgram } from '@solana/web3.js'
|
||||
import {
|
||||
TOKEN_PROGRAM_ID,
|
||||
createTransferInstruction,
|
||||
getAssociatedTokenAddress,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
} from '@solana/spl-token'
|
||||
|
||||
// Constants
|
||||
const MTM_TOKEN_MINT: string = '97RggLo3zV5kFGYW4yoQTxr4Xkz4Vg2WPHzNYXXWpump'
|
||||
const PAYMENT_RECEIVER_ADDRESS: string = 'FFDx3SdAEeXrp6BTmStB4BDHpctGsaasZq4FFcowRobY'
|
||||
|
||||
// RPC Configuration
|
||||
const SOLANA_RPC_URL: string = 'https://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
|
||||
const SOLANA_WEBSOCKET_URL: string = 'wss://young-radial-orb.solana-mainnet.quiknode.pro/67612b364664616c29514e551bf5de38447ca3d4'
|
||||
|
||||
// Initialize connection with WebSocket support
|
||||
const connection = new Connection(
|
||||
SOLANA_RPC_URL,
|
||||
{
|
||||
commitment: 'confirmed',
|
||||
wsEndpoint: SOLANA_WEBSOCKET_URL,
|
||||
confirmTransactionInitialTimeout: 60000, // 60 seconds
|
||||
}
|
||||
)
|
||||
|
||||
export interface PaymentResult {
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
async function findAssociatedTokenAddress(
|
||||
walletAddress: PublicKey,
|
||||
tokenMintAddress: PublicKey
|
||||
): Promise<PublicKey> {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[
|
||||
walletAddress.toBuffer(),
|
||||
TOKEN_PROGRAM_ID.toBuffer(),
|
||||
tokenMintAddress.toBuffer(),
|
||||
],
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID
|
||||
)[0]
|
||||
}
|
||||
|
||||
export async function processMTMPayment(
|
||||
walletPublicKey: string,
|
||||
tokenAmount: number,
|
||||
solflareWallet: any
|
||||
): Promise<PaymentResult> {
|
||||
try {
|
||||
const senderPublicKey = new PublicKey(walletPublicKey)
|
||||
const mintPublicKey = new PublicKey(MTM_TOKEN_MINT)
|
||||
const receiverPublicKey = new PublicKey(PAYMENT_RECEIVER_ADDRESS)
|
||||
|
||||
console.log('Processing payment with keys:', {
|
||||
sender: senderPublicKey.toBase58(),
|
||||
mint: mintPublicKey.toBase58(),
|
||||
receiver: receiverPublicKey.toBase58(),
|
||||
})
|
||||
|
||||
// Find ATAs
|
||||
const senderATA = await findAssociatedTokenAddress(
|
||||
senderPublicKey,
|
||||
mintPublicKey
|
||||
)
|
||||
|
||||
const receiverATA = await findAssociatedTokenAddress(
|
||||
receiverPublicKey,
|
||||
mintPublicKey
|
||||
)
|
||||
|
||||
console.log('Token accounts:', {
|
||||
senderATA: senderATA.toBase58(),
|
||||
receiverATA: receiverATA.toBase58(),
|
||||
})
|
||||
|
||||
const transaction = new Transaction()
|
||||
|
||||
// Check if accounts exist
|
||||
const [senderATAInfo, receiverATAInfo] = await Promise.all([
|
||||
connection.getAccountInfo(senderATA),
|
||||
connection.getAccountInfo(receiverATA),
|
||||
])
|
||||
|
||||
// Create ATAs if they don't exist
|
||||
if (!receiverATAInfo) {
|
||||
console.log('Creating receiver token account')
|
||||
transaction.add(
|
||||
createAssociatedTokenAccountInstruction(
|
||||
senderPublicKey, // payer
|
||||
receiverATA, // ata
|
||||
receiverPublicKey, // owner
|
||||
mintPublicKey // mint
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!senderATAInfo) {
|
||||
console.log('Creating sender token account')
|
||||
transaction.add(
|
||||
createAssociatedTokenAccountInstruction(
|
||||
senderPublicKey, // payer
|
||||
senderATA, // ata
|
||||
senderPublicKey, // owner
|
||||
mintPublicKey // mint
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Add transfer instruction
|
||||
transaction.add(
|
||||
createTransferInstruction(
|
||||
senderATA, // from
|
||||
receiverATA, // to
|
||||
senderPublicKey, // owner
|
||||
BigInt(tokenAmount * (10 ** 6)) // amount
|
||||
)
|
||||
)
|
||||
|
||||
const latestBlockhash = await connection.getLatestBlockhash('confirmed')
|
||||
transaction.recentBlockhash = latestBlockhash.blockhash
|
||||
transaction.feePayer = senderPublicKey
|
||||
|
||||
console.log('Sending transaction...')
|
||||
const { signature } = await solflareWallet.signAndSendTransaction(transaction)
|
||||
console.log('Transaction sent:', signature)
|
||||
|
||||
const confirmation = await connection.confirmTransaction({
|
||||
signature,
|
||||
blockhash: latestBlockhash.blockhash,
|
||||
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
||||
}, 'confirmed')
|
||||
|
||||
if (confirmation.value.err) {
|
||||
console.error('Transaction error:', confirmation.value.err)
|
||||
throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`)
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Payment error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Payment failed'
|
||||
}
|
||||
}
|
||||
}
|
29
tailwind.config.ts
Normal file
29
tailwind.config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
animation: {
|
||||
'bounce-delay': 'bounce-delay 1.4s infinite ease-in-out both',
|
||||
},
|
||||
keyframes: {
|
||||
'bounce-delay': {
|
||||
'0%, 80%, 100%': { transform: 'scale(0)' },
|
||||
'40%': { transform: 'scale(1.0)' },
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
export default config
|
34
tsconfig.json
Normal file
34
tsconfig.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user