maybe a fork of sparrowhe's "bluesky circle" webapp, to frontend only?
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}