maybe a fork of sparrowhe's "bluesky circle" webapp, to frontend only?
at main 6.5 kB view raw
1'use client' 2 3import { useState, useRef, useEffect } from 'react' 4// import Turnstile, { useTurnstile } from "react-turnstile"; 5 6import { Button } from "@/components/ui/button" 7import { Input } from "@/components/ui/input" 8import { Label } from "@/components/ui/label" 9import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" 10import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" 11import { AlertCircle, Github, ExternalLink } from "lucide-react" 12 13import "./App.css" 14 15 16const fantasicLoadingText = [ 17 'Finding Elon Musk...', 18 'Generating BlueSky...', 19 'Creating Circle...', 20 'Uninstalling X...', 21 'Banning by Elon Musk...', 22 'Launching Rocket...', 23] 24 25export default function Component() { 26 // const turnstile = useTurnstile() 27 const [handle, setHandle] = useState('') 28 const [captchaKey,] = useState('') 29 const [imageData, setImageData] = useState<string | null>(null) 30 const [backgroundColor, setBackgroundColor] = useState('#0085ff') 31 const [isLoading, setIsLoading] = useState(false) 32 const [loadingText, setLoadingText] = useState('Generating...') 33 const [error, setError] = useState<string | null>(null) 34 const canvasRef = useRef<HTMLCanvasElement>(null) 35 36 const handleSubmit = async (e: React.FormEvent) => { 37 e.preventDefault() 38 setIsLoading(true) 39 setError(null) 40 setImageData(null) 41 // turnstile.reset() 42 try { 43 const response = await fetch(`/generate`, { 44 method: 'POST', 45 body: new URLSearchParams({ handle, captchaKey }), 46 headers: { 47 'Content-Type': 'application/x-www-form-urlencoded', 48 }, 49 }) 50 51 if (!response.ok) { 52 throw new Error(`HTTP error! status: ${response.status}`) 53 } 54 55 const blob = await response.blob() 56 const reader = new FileReader() 57 reader.onloadend = () => { 58 const base64data = reader.result as string 59 setImageData(base64data) 60 } 61 reader.readAsDataURL(blob) 62 } catch (error) { 63 console.error('Error fetching image:', error) 64 setError('Failed to generate image. Please try again.') 65 } finally { 66 setIsLoading(false) 67 } 68 } 69 70 useEffect(() => { 71 if (isLoading) { 72 const interval = setInterval(() => { 73 setLoadingText(fantasicLoadingText[Math.floor(Math.random() * fantasicLoadingText.length)]) 74 }, 1000) 75 return () => clearInterval(interval) 76 } 77 }, [isLoading]) 78 79 useEffect(() => { 80 if (imageData && canvasRef.current) { 81 const canvas = canvasRef.current 82 const ctx = canvas.getContext('2d') 83 const img = new Image() 84 img.onload = () => { 85 canvas.width = img.width 86 canvas.height = img.height 87 ctx!.fillStyle = backgroundColor 88 ctx!.fillRect(0, 0, canvas.width, canvas.height) 89 ctx!.drawImage(img, 0, 0) 90 } 91 img.src = imageData 92 } 93 }, [imageData, backgroundColor]) 94 95 const handleSave = () => { 96 if (canvasRef.current) { 97 const link = document.createElement('a') 98 link.download = `${handle}-bluesky-circlr.png` 99 link.href = canvasRef.current.toDataURL() 100 link.click() 101 } 102 } 103 104 return ( 105 <div className="w-full max-w-md mx-auto"> 106 <Card className="w-full max-w-md mx-auto"> 107 <CardHeader> 108 <CardTitle>BlueSky Circle Generator</CardTitle> 109 </CardHeader> 110 <CardContent> 111 <form onSubmit={handleSubmit} className="space-y-4"> 112 <div className="space-y-2"> 113 <Label htmlFor="handle">BlueSky Handle</Label> 114 <Input 115 id="handle" 116 value={handle} 117 onChange={(e) => setHandle(e.target.value)} 118 onBlur={(e) => { 119 // remove @ from handle 120 setHandle(e.target.value.replace('@', '')) 121 // remove invalid characters 122 setHandle(e.target.value.replace(/[^a-zA-Z0-9_\-.]/g, '')) 123 }} 124 pattern='@{0,1}[a-zA-Z0-9_\-.]*' 125 placeholder="Enter BlueSky Handle" 126 required 127 /> 128 </div> 129 <Button type="submit" disabled={isLoading}> 130 {isLoading ? loadingText : 'Generate Image'} 131 </Button> 132 {/* <Turnstile 133 sitekey="1x00000000000000000000AA" 134 onVerify={(token) => { 135 setCaptchaKey(token) 136 }} 137 /> */} 138 </form> 139 {error && ( 140 <Alert variant="destructive" className="mt-4"> 141 <AlertCircle className="h-4 w-4" /> 142 <AlertTitle>Error</AlertTitle> 143 <AlertDescription>{error}</AlertDescription> 144 </Alert> 145 )} 146 {imageData && ( 147 <div className="mt-4 space-y-4"> 148 <div className="relative aspect-square w-full"> 149 <canvas 150 ref={canvasRef} 151 className="absolute inset-0 w-full h-full" 152 /> 153 </div> 154 </div> 155 )} 156 </CardContent> 157 {imageData && ( 158 <CardFooter> 159 <div className="flex w-full max-w-sm items-center space-x-2"> 160 <Input 161 id="backgroundColor" 162 type="color" 163 value={backgroundColor} 164 className="color-input" 165 onChange={(e) => setBackgroundColor(e.target.value)} 166 /> 167 <Button onClick={handleSave}> 168 Save Image 169 </Button> 170 </div> 171 </CardFooter> 172 )} 173 <footer className="mt-8 text-sm text-gray-500 space-y-1"> 174 <div className="flex items-center justify-center space-x-4"> 175 <a href="https://github.com/sparrowhe/bluesky-circle" className="flex items-center text-primary hover:text-primary/60 transition-colors"> 176 <Github className="w-4 h-4 mr-1" /> 177 Source Code 178 </a> 179 <a href="https://github.com/users/sparrowhe/projects/1" className="flex items-center text-primary hover:text-primary/60 transition-colors"> 180 <ExternalLink className="w-4 h-4 mr-1" /> 181 Roadmap 182 </a> 183 </div> 184 <p className="text-center pb-2"> 185 Designed and developed by <a href="https://bsky.app/profile/sparrow.0x0e.top">SparrowHe</a> with 186 </p> 187 </footer> 188 </Card> 189 </div> 190 ) 191}