this repo has no description
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