this repo has no description
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 2f09d27a1cefb30ea4803195d26acf8692e07668 286 lines 13 kB view raw
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <meta property="og:title" content="at://giveaways 🎉"> 7 <meta property="og:image" content="/images/cover.jpg"> 8 <meta property="og:description" content="Host a giveaway from a Bluesky post."> 9 10 <title>at://giveaways 🎉</title> 11 <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/> 12 <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> 13 <script src="https://unpkg.com/alpinejs" defer></script> 14 15 <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> 16 <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> 17 <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> 18 <link rel="manifest" href="/site.webmanifest"> 19 20 <script type="module"> 21 import { 22 CompositeHandleResolver, 23 DohJsonHandleResolver, 24 WellKnownHandleResolver 25 } from 'https://esm.sh/@atcute/identity-resolver'; 26 27 const handleResolver = new CompositeHandleResolver({ 28 strategy: 'race', 29 methods: { 30 dns: new DohJsonHandleResolver({dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query'}), 31 http: new WellKnownHandleResolver(), 32 }, 33 }); 34 35 window.resolveHandle = async (handle) => await handleResolver.resolve(handle); 36 37 </script> 38 39 <script> 40 41 const constellationEndpoint = 'https://constellation.microcosm.blue'; 42 43 async function callConstellationEndpoint(target, collection, path, cursor = null) { 44 try { 45 const url = new URL(`${constellationEndpoint}/links/distinct-dids`); 46 url.searchParams.append('target', target); 47 url.searchParams.append('collection', collection); 48 url.searchParams.append('path', path); 49 if (cursor) { 50 url.searchParams.append('cursor', cursor); 51 } 52 const response = await fetch(url); 53 if (!response.ok) { 54 throw new Error(`HTTP error! Status: ${response.status}`); 55 } 56 57 return await response.json(); 58 } catch (error) { 59 console.error('Error calling constellation endpoint:', error); 60 throw error; 61 } 62 } 63 64 function isValidHttpUrl(string) { 65 let url; 66 try { 67 url = new URL(string); 68 } catch (_) { 69 return false; 70 } 71 return url.protocol === "http:" || url.protocol === "https:"; 72 } 73 74 document.addEventListener('alpine:init', () => { 75 Alpine.data('giveaway', () => ({ 76 //Form input 77 post_url: '', 78 winner_count: 1, 79 likes_only: true, 80 reposts_only: false, 81 likes_and_reposts: false, 82 83 error: '', 84 loading: false, 85 winners: [], 86 showResults: false, 87 88 validateCheckBoxes(event) { 89 const targetId = event.target.id; 90 this.likes_only = targetId === 'likes'; 91 this.reposts_only = targetId === 'reposts_only'; 92 this.likes_and_reposts = targetId === 'likes_and_reposts'; 93 }, 94 async runGiveaway() { 95 this.error = ''; 96 this.loading = true; 97 this.winners = []; 98 this.showResults = false; 99 100 try { 101 //Form validation 102 if (this.winner_count < 1) { 103 this.error = 'SOMEBODY has to win'; 104 return; 105 } 106 107 if (!this.likes_only && !this.reposts_only && !this.likes_and_reposts) { 108 this.error = 'Well, you have to pick some way for them to win'; 109 return; 110 } 111 112 let atUri = ''; 113 if (this.post_url.startsWith('at://')) { 114 atUri = this.post_url; 115 } else { 116 //More checks to make sure it's a bsky url 117 if (!this.post_url.startsWith('https://bsky.app/')) { 118 this.error = 'Link to the Bluesky post or at uri please'; 119 return; 120 } 121 const postSplit = this.post_url.split('/'); 122 if (postSplit.length < 7) { 123 this.error = 'Invalid Bluesky post URL. Should look like https://bsky.app/profile/baileytownsend.dev/post/3lbq7o74fcc2d'; 124 return; 125 } 126 try { 127 const handle = postSplit[4]; 128 const recordKey = postSplit[6]; 129 130 let did = await window.resolveHandle(handle); 131 atUri = `at://${did}/app.bsky.feed.post/${recordKey}`; 132 133 } catch (e) { 134 console.log(e); 135 this.error = e.message; 136 return; 137 } 138 } 139 140 141 // Determine which collections to fetch based on user selection 142 const collections = []; 143 if (this.likes_only || this.likes_and_reposts) { 144 collections.push('app.bsky.feed.like'); 145 } 146 if (this.reposts_only || this.likes_and_reposts) { 147 collections.push('app.bsky.feed.repost'); 148 } 149 150 // Path to extract the subject URI 151 const path = '.subject.uri'; 152 153 // Fetch data for each collection 154 const results = []; 155 let cursor = null; 156 for (const collection of collections) { 157 console.log(`Fetching ${collection} data...`); 158 const response = await callConstellationEndpoint(atUri, collection, path); 159 console.log(`${collection} response:`, response); 160 if (response && response.linking_dids) { 161 cursor = response.cursor; 162 results.push(...response.linking_dids); 163 } 164 } 165 166 // Remove duplicates if fetching both likes and reposts 167 const uniqueDids = [...new Set(results)]; 168 console.log('Unique DIDs:', uniqueDids); 169 170 // Select winners 171 if (uniqueDids.length === 0) { 172 this.error = 'No participants found for this post'; 173 return; 174 } 175 176 const winnerCount = Math.min(this.winner_count, uniqueDids.length); 177 178 // Randomly select winners 179 for (let i = 0; i < winnerCount; i++) { 180 const randomIndex = Math.floor(Math.random() * uniqueDids.length); 181 this.winners.push(uniqueDids[randomIndex]); 182 // Remove the winner to avoid duplicates 183 uniqueDids.splice(randomIndex, 1); 184 } 185 186 console.log('Winners:', this.winners); 187 this.showResults = true; 188 189 } catch (error) { 190 console.error('Error in runGiveaway:', error); 191 this.error = `Error fetching data: ${error.message}`; 192 } finally { 193 this.loading = false; 194 } 195 } 196 197 })) 198 }) 199 </script> 200</head> 201 202 203<body> 204<div class="hero bg-base-200 min-h-screen"> 205 <div class="hero-content flex-col "> 206 <div class="text-center"> 207 <h1 class="text-5xl font-bold">at://giveaways 🎉</h1> 208 <p class="py-6"> 209 Pick which Bluesky post you want to use for a giveaway. 210 </p> 211 <div>uses <a class="link" href="https://constellation.microcosm.blue/">constellation 212 🌌</a> 213 powered 214 by 215 <a href="https://microcosm.blue" class="link"><span 216 style="color: rgb(243, 150, 169);">m</span><span style="color: rgb(244, 156, 92);">i</span><span 217 style="color: rgb(199, 176, 76);">c</span><span style="color: rgb(146, 190, 76);">r</span><span 218 style="color: rgb(78, 198, 136);">o</span><span style="color: rgb(81, 194, 182);">c</span><span 219 style="color: rgb(84, 190, 215);">o</span><span style="color: rgb(143, 177, 241);">s</span><span 220 style="color: rgb(206, 157, 241);">m</span></a> 221 </div> 222 </div> 223 <div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl"> 224 <div class="card-body" x-data="giveaway"> 225 <form x-on:submit.prevent="await runGiveaway()"> 226 <fieldset class="fieldset"> 227 <label for="post_url" class="label">Post Url</label> 228 <input x-model="post_url" id="post_url" type="text" class="input" 229 placeholder="https://bsky.app/profile/baileytownsend.dev/post/3lbq7o74fcc2d"/> 230 <label for="winner_count" class="label">How many winners?</label> 231 <input x-model="winner_count" id="winner_count" type="number" class="input" value="1"/> 232 <fieldset class="fieldset bg-base-100 border-base-300 rounded-box w-64 border p-4"> 233 <legend class="fieldset-legend">Winning options</legend> 234 <label class="label"> 235 <input x-model="likes_only" x-on:change="validateCheckBoxes($event)" 236 id="likes" 237 type="checkbox" 238 checked="checked" 239 class="checkbox"/> 240 Likes only 241 </label> 242 <label class="label"> 243 <input x-model="reposts_only" x-on:change="validateCheckBoxes($event)" id="reposts_only" 244 type="checkbox" 245 class="checkbox"/> 246 Reposts only 247 </label> 248 <label class="label"> 249 <input x-model="likes_and_reposts" x-on:change="validateCheckBoxes($event)" 250 id="likes_and_reposts" 251 type="checkbox" 252 class="checkbox"/> 253 Likes & Reposts 254 </label> 255 256 </fieldset> 257 <span x-show="error" x-text="error" class="text-red-500 text-lg font-bold"></span> 258 <button type="submit" class="btn btn-neutral mt-4" x-bind:disabled="loading"> 259 <span x-show="!loading">I choose you!</span> 260 <span x-show="loading" class="loading loading-spinner"></span> 261 </button> 262 </fieldset> 263 </form> 264 265 <!-- Results Section --> 266 <div x-show="showResults" class="mt-6 p-4 bg-base-200 rounded-lg"> 267 <h3 class="text-xl font-bold mb-2">🎉 Winners 🎉</h3> 268 <p class="mb-2">Total participants: <span x-text="winners.length"></span></p> 269 <ul class="list-disc pl-5"> 270 <template x-for="winner in winners"> 271 <li class="mb-1"> 272 <span x-text="winner"></span> 273 </li> 274 </template> 275 </ul> 276 </div> 277 278 <a href="https://tangled.sh/@baileytownsend.dev/at-giveaways" class="link mt-4 block">View on <span 279 class="font-semibold italic">tangled.sh</span></a> 280 </div> 281 </div> 282 </div> 283</div> 284</body> 285</html> 286