Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ceremony): contributors #2980

Merged
merged 17 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions ceremony/src/lib/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { get, post } from "$lib/client/http.ts"
import { user } from "$lib/stores/user.svelte.ts"
import { getQueuePayloadId } from "$lib/supabase/queries.ts"
import type { ClientState, ContributeBody } from "$lib/client/types.ts"
import { supabase } from "$lib/supabase/client.ts"

export const start = async (): Promise<ClientState | undefined> => {
const userId = user?.session?.user.id
const email = user?.session?.user?.email
const { data: session, error: sessionError } = await supabase.auth.refreshSession()

if (sessionError) {
console.error("Error refreshing session:", sessionError)
return
}

const userId = session.session?.user.id
const email = session.session?.user?.email

if (!userId) {
console.log("User not logged in")
Expand All @@ -27,7 +34,7 @@ export const start = async (): Promise<ClientState | undefined> => {
const contributeBody: Partial<ContributeBody> = {
payloadId: data.payload_id,
contributorId: userId,
jwt: user?.session?.access_token,
jwt: session.session?.access_token,
supabaseProject: import.meta.env.VITE_SUPABASE_URL,
apiKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
bucket: import.meta.env.VITE_BUCKET_ID,
Expand Down
2 changes: 1 addition & 1 deletion ceremony/src/lib/components/Blink.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function startRandomBlinking() {

$effect(() => {
if (sleep) {
eye = "-"
eye = "×"
clearInterval(blinkInterval)
} else {
eye = "0"
Expand Down
55 changes: 32 additions & 23 deletions ceremony/src/lib/components/Ceremony.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import H3 from "$lib/components/typography/H3.svelte"
import H2 from "$lib/components/typography/H2.svelte"
import Install from "$lib/components/Install.svelte"
import { start } from "$lib/client"
import H4 from "$lib/components/typography/H4.svelte"
import { AddressForm, type ValidState } from "$lib/components/address"
import Blink from "$lib/components/Blink.svelte"
import Tweet from "$lib/components/Tweet.svelte"
import SwimLoad from "$lib/components/SwimLoad.svelte"
import { getNumberSuffix } from "$lib/utils/utils.ts"
import Text from "$lib/components/typography/Text.svelte"
import Status from "$lib/components/Status.svelte"

type Props = {
contributor: ContributorState
Expand All @@ -31,52 +32,60 @@ let addressValidState: ValidState = $state("PENDING")
<div class="p-8 w-full flex items-center justify-center flex-col">

{#if contributor.state === 'inQueue'}
<H1>You are <span class="!text-union-accent-500">{contributor.queueState.position}<span
class="lowercase">{getNumberSuffix(contributor.queueState.position)}</span> </span> in queue</H1>
<SwimLoad max={100} current={50}/>
<H2>Queue length: <span class="text-union-accent-500">{contributor.queueState.count}</span></H2>
<H3>Waiting time: <span class="text-union-accent-500">{contributor.queueState.estimatedTime} minutes</span> (est.).
</H3>

{#if contributor.clientState === 'offline'}
<Install/>
{/if}

<Status {contributor} />

<div class="border p-8 w-full max-w-4xl flex flex-col items-center">
<H1 class="mb-6">You are <span class="!text-union-accent-500">{contributor.queueState.position}<span
class="lowercase">{getNumberSuffix(contributor.queueState.position)}</span> </span> in queue</H1>

<SwimLoad max={contributor.queueState.count} current={contributor.queueState.position}/>

<div class="mb-4 text-center">
<H2>Queue length: <span class="text-union-accent-500">{contributor.queueState.count}</span></H2>
<H3>Waiting time: <span class="text-union-accent-500">{contributor.queueState.estimatedTime} minutes</span>
(est.).
</H3>
</div>

<div class="text-center">
<H2 class="mb-2">Get your nft</H2>
<AddressForm class="" onValidation={result => (addressValidState = result)}/>
</div>
</div>


{:else if contributor.state === 'contribute'}
<Status {contributor} />
<H1>Starting contribution...</H1>

{:else if contributor.state === 'contributing'}
<Status {contributor} />
<H1>Contributing...</H1>

{:else if contributor.state === 'verifying'}
<Status {contributor} />
<H1>Verifying your contribution...</H1>

{:else if contributor.state === 'contributed'}

<div class="flex flex-col justify-center items-center gap-4">
<H1>Thank you! Your contribution is completed.</H1>
<H2>Get your nft</H2>
<AddressForm class="" onValidation={result => (addressValidState = result)}/>
<Tweet tweetText="0____0"/>
<Tweet/>
</div>

<!--Your turn but no client-->
{:else if contributor.state === 'noClient'}

<Status {contributor} />
<H1>No client. Cannot start contribution.</H1>
<Install/>

{:else}
<H1>Not able to contribute at this time</H1>

{/if}

</div>


<div class="absolute bottom-10 flex flex-col px-8 text-center gap-4">
<H4>
<Blink
loading={contributor.state === 'contributing'}
sleep={contributor.clientState === 'offline'}
/>
</H4>
<H4>{contributor.clientState}</H4>
</div>
68 changes: 68 additions & 0 deletions ceremony/src/lib/components/Code.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script lang="ts">
import { callJoinQueue } from "$lib/supabase"
import { toast } from "svelte-sonner"
import type { ContributorState } from "$lib/stores/state.svelte.ts"
import Button from "$lib/components/Button.svelte"

type Props = {
contributor: ContributorState
}

let { contributor }: Props = $props()

let words: Array<string> = $state(new Array(6).fill(""))
let code = $derived(normalizeString(words))
let codeLoading = $state(false)

function handlePaste(e: ClipboardEvent): void {
e.preventDefault()
const pastedText: string = e.clipboardData?.getData("text") || ""
const pastedWords: Array<string> = pastedText.split(/\s+/).slice(0, 6)
words = [...pastedWords, ...new Array(6 - pastedWords.length).fill("")]
}

function normalizeString(words: Array<string>): string {
return words
.map(word => word.trim().toLowerCase())
.join("")
.replace(/[^a-z0-9]/gi, "")
}

async function handleCodeJoin() {
codeLoading = true
try {
console.log(code)
const codeOk = await callJoinQueue(code)
if (codeOk) {
contributor.setAllowanceState("hasRedeemed")
toast.success("Code successfully redeemed")
} else {
toast.error("The code is not valid")
}
} catch (error) {
console.error("Error redeeming code:", error)
toast.error("An error occurred while redeeming the code")
} finally {
codeLoading = false
words = new Array(6).fill("")
}
}
</script>


<div class="flex gap-2 max-w-4xl flex-wrap justify-center mb-8">
{#each words as word, index}
<input
bind:value={words[index]}
onpaste={handlePaste}
class="bg-transparent border-b border-white w-20 text-center text-union-accent-500 outline-none focus:ring-0 focus:border-union-accent-500"
style="--tw-ring-color: transparent;"
/>
{#if index !== words.length - 1}
<div class="text-union-accent-500"><p>-</p></div>
{/if}
{/each}
</div>
<Button loading={codeLoading} type="button" onclick={handleCodeJoin}>
USE CODE
</Button>
85 changes: 85 additions & 0 deletions ceremony/src/lib/components/Contribution.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<script lang="ts">
import { getUserContribution } from "$lib/supabase"
import Spinner from "$lib/components/Spinner.svelte"
import H2 from "$lib/components/typography/H2.svelte"
import Button from "$lib/components/Button.svelte"
import { toast } from "svelte-sonner"
import { page } from "$app/stores"

type Props = {
hash: string
}

let { hash }: Props = $props()

function hexToUint8Array(hexString: string) {
return new Uint8Array(hexString.match(/.{1,2}/g)?.map(byte => Number.parseInt(byte, 16)) || [])
}

function uint8ArrayToUtf8(bytes: Uint8Array) {
return new TextDecoder().decode(bytes)
}

function decodeHexString(hexString: string) {
return uint8ArrayToUtf8(hexToUint8Array(hexString))
}

async function copyToClipboard(text: string, label: string) {
try {
await navigator.clipboard.writeText(text)
toast.success(`Copied ${label}!`)
} catch (err) {
console.error("Failed to copy text: ", err)
toast.error(`Failed to copy ${label} to clipboard.`)
}
}

const imagePath = "/images/ceremony.png"
let imageUrl = $derived(new URL(imagePath, $page.url.origin).href)
</script>

<svelte:head>
<meta property="og:type" content="Website"/>
<meta property="og:site_name" content="Union Ceremony"/>
<meta property="og:locale" content="en"/>
<meta property="og:image:type" content="image/png"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="675"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:site" content="@union_build"/>
<meta name="twitter:creator" content="@union_build"/>
<meta property="og:image" content={imageUrl}/>
<meta property="og:image:secure_url" content={imageUrl}/>
<meta name="twitter:image" content={imageUrl}/>
</svelte:head>

{#await getUserContribution(hash)}
<Spinner class="size-5 text-union-accent-500"/>
{:then contribution}
{#if contribution}
<div class="flex flex-col items-start gap-1 px-3 py-2">
<div>
<H2>Contributor: <span class="!text-union-accent-500">{contribution.user_name}</span></H2>
</div>

<div class="flex flex-col gap-4">
<div>
<H2 class="mb-2">Public key</H2>
<pre class="text-white whitespace-pre-wrap bg-neutral-800 p-4 mb-4">{decodeHexString(contribution.public_key)}</pre>
<Button onclick={() => copyToClipboard(decodeHexString(contribution.public_key), "public key")}>Copy
Public
key
</Button>
</div>

<div>
<H2 class="mb-2">Signature</H2>
<pre class="text-white whitespace-pre-wrap bg-neutral-800 p-4 mb-4">{decodeHexString(contribution.signature)}</pre>
<Button onclick={() => copyToClipboard(decodeHexString(contribution.signature), "signature")}>Copy
Signature
</Button>
</div>
</div>
</div>
{/if}
{/await}
46 changes: 46 additions & 0 deletions ceremony/src/lib/components/Contributions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
import Spinner from "$lib/components/Spinner.svelte"
import Text from "$lib/components/typography/Text.svelte"
import { getContributions } from "$lib/supabase"

let intervalId: NodeJS.Timeout | number
let contributions = $state()

async function loadContributions() {
contributions = await getContributions()
}

$effect(() => {
loadContributions()
intervalId = setInterval(loadContributions, 1000 * 5)

return () => {
if (intervalId) clearInterval(intervalId)
}
})
</script>
{#if contributions}

<div class="flex flex-col items-center h-svh overflow-y-auto pb-24 pt-36 w-full">
<div class="w-full h-48 bg-gradient-to-b via-black from-black to-transparent absolute top-0"></div>
<div class="flex flex-col items-center max-w-md">
<div class="rounded-full border-[2px] h-8 w-8"></div>
<div class="h-24 w-[2px] bg-white"></div>
{#each contributions as contribution, index }
<a href="/contributions?hash={contribution.public_key_hash}" class="flex items-center gap-4 w-full">

<Text>{(index + 1) * 10}M</Text>
<div class="text-white flex gap-1 items-center border-white border px-3 py-2 w-full">
<img class="size-7" src={contribution.avatar_url} alt="">
<Text class="uppercase max-w-48 truncate">{contribution.user_name}</Text>
</div>
</a>
{#if index !== contributions.length - 1}
<div class="h-12 w-[2px] bg-white"></div>
{/if}
{/each}
</div>
</div>
{:else}
<Spinner class="size-5 text-union-accent-500"/>
{/if}
Loading