Skip to content

Commit

Permalink
feat(app): save progress
Browse files Browse the repository at this point in the history
  • Loading branch information
Swepool committed Jan 2, 2025
1 parent 2f98029 commit 020b989
Show file tree
Hide file tree
Showing 21 changed files with 542 additions and 181 deletions.
26 changes: 26 additions & 0 deletions app/src/lib/components/TransferFrom/components/AssetDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import type {IntentStore} from "$lib/components/TransferFrom/transfer/intents.ts";
import type {Readable} from "svelte/store";
import type {ContextStore} from "$lib/components/TransferFrom/transfer/context.ts";
import {Button} from "$lib/components/ui/button";
interface Props {
intents: IntentStore
context: Readable<ContextStore>
onSelectAsset: () => void
}
export let intents: Props["intents"]
export let context: Props["context"]
export let onSelectAsset: Props["onSelectAsset"]
</script>

<Button
type="button"
size="sm"
variant="outline"
class="border-2 border-white min-w-[150px]"
on:click={onSelectAsset}
>
{$context?.assetInfo ?? "Select asset"}
</Button>
83 changes: 83 additions & 0 deletions app/src/lib/components/TransferFrom/components/ChainDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script lang="ts">
import type {Readable} from "svelte/store";
import type {ContextStore} from "$lib/components/TransferFrom/transfer/context.ts";
interface Props {
context: Readable<ContextStore>
kind: "source" | "destination"
dialogOpen: boolean
onChainSelect: (type: 'source' | 'destination', chain: string) => void
onClose: () => void
}
export let context: Props["context"]
export let kind: Props["kind"]
export let dialogOpen: Props["dialogOpen"]
export let onChainSelect: Props["onChainSelect"]
export let onClose: Props["onClose"]
</script>

{#if dialogOpen && $context?.chains}
<dialog
open
aria-label={`Select ${kind} chain`}
class="absolute z-50 inset-0 overflow-y-scroll p-0 bg-transparent m-0 w-full h-full animate-fade-in backdrop-blur-md"
>
<button
type="button"
class="fixed inset-0 w-full h-full bg-gradient-to-t from-black to-black/10 animate-fade-in"
on:click|self={onClose}
aria-label="Close dialog"
/>

<div class="relative z-10">
<div class="flex justify-center">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-8 max-w-4xl py-8 px-4">
{#each $context.chains as chain, i}
<button
style="animation-delay: {i * 100}ms"
on:click={() => onChainSelect(kind, chain.chain_id)}
type="button"
class="h-72 flex items-end border p-4 bg-secondary group hover:bg-accent transition-colors animate-slide-up"
>
<span class="font-supermolot uppercase font-bold text-xl text-start text-secondary-foreground group-hover:text-secondary">
{chain.display_name}
</span>
</button>
{/each}
</div>
</div>
</div>
</dialog>
{/if}

<style>
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
:global(.animate-fade-in) {
animation: fade-in 0.3s ease-out forwards;
}
:global(.animate-slide-up) {
animation: slide-up 0.4s ease-out forwards;
opacity: 0;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
export let visible = true
export let width = 0; // Pixel value
export let height = 0; // Pixel value
export let translateZ = 0;
export let rotateY = "0deg";
</script>

<div
class="absolute bg-neutral-900 flex flex-col items-center p-4 border-2 {visible ? 'opacity-100' : 'opacity-0 pointer-events-none'}}"
style={`width: ${width}px; height: ${height}px; transform: rotateY(${rotateY}) translateZ(${translateZ}px);;`}
>
<slot />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import type {IntentStore} from "$lib/components/TransferFrom/transfer/intents.ts";
import type {ValidationStoreAndMethods} from "$lib/components/TransferFrom/transfer/validation.ts";
import type {Readable} from "svelte/store";
import type {ContextStore} from "$lib/components/TransferFrom/transfer/context.ts";
import type {CubeFaces} from "$lib/components/TransferFrom/types.ts";
interface Props {
stores: {
intents: IntentStore
validation: ValidationStoreAndMethods
context: Readable<ContextStore>
}
rotateTo: (face: CubeFaces) => void
}
export let stores: Props["stores"]
export let rotateTo: Props["rotateTo"]
$: ({intents, validation, context} = stores)
</script>

<button class="font-supermolot font-bold text-lg mb-4" on:click={() => rotateTo("intentFace")}>Select asset</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import type {IntentStore} from "$lib/components/TransferFrom/transfer/intents.ts";
import type {ValidationStoreAndMethods} from "$lib/components/TransferFrom/transfer/validation.ts";
import type {Readable} from "svelte/store";
import type {ContextStore} from "$lib/components/TransferFrom/transfer/context.ts";
import type {CubeFaces} from "$lib/components/TransferFrom/types.ts";
interface Props {
stores: {
intents: IntentStore
validation: ValidationStoreAndMethods
context: Readable<ContextStore>
}
rotateTo: (face: CubeFaces) => void
select: "source" | "destination"
}
export let stores: Props["stores"]
export let rotateTo: Props["rotateTo"]
export let select: Props["select"]
$: ({intents, validation, context} = stores)
</script>

<h2 class="font-supermolot font-bold text-lg mb-4">Select chain</h2>
{#each $context.chains as chain}
<button on:click={() => rotateTo("intentFace")}>{chain.display_name}</button>
{/each}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script lang="ts">
import Direction from "$lib/components/TransferFrom/components/Direction.svelte";
import AssetDialog from "$lib/components/TransferFrom/components/AssetDialog.svelte";
import type {Readable} from "svelte/store";
import type {IntentStore} from "$lib/components/TransferFrom/transfer/intents.ts";
import type {ValidationStoreAndMethods} from "$lib/components/TransferFrom/transfer/validation.ts";
import type {ContextStore} from "$lib/components/TransferFrom/transfer/context.ts";
import type {CubeFaces} from "$lib/components/TransferFrom/types.ts";
import {Button} from "$lib/components/ui/button";
interface Props {
stores: {
intents: IntentStore
validation: ValidationStoreAndMethods
context: Readable<ContextStore>
}
rotateTo: (face: CubeFaces) => void
}
export let stores: Props["stores"]
export let rotateTo: Props["rotateTo"]
$: ({intents, validation, context} = stores)
</script>

<div class="flex flex-col justify-between w-full h-full">
<div class="flex flex-col gap-4">
<Direction
{context}
{intents}
getSourceChain={() => rotateTo("sourceFace")}
getDestinationChain={() => rotateTo("destinationFace")}
/>
<AssetDialog
{context}
{intents}
onSelectAsset={() => rotateTo("assetsFace")}
/>
<div class="flex flex-col gap-1">
<input
id="amount"
type="number"
name="amount"
minlength={1}
maxlength={64}
required={true}
disabled={false}
autocorrect="off"
placeholder="0.00"
spellcheck="false"
autocomplete="off"
inputmode="decimal"
data-field="amount"
autocapitalize="none"
pattern="^[0-9]*[.,]?[0-9]*$"
class="p-1 {$validation.errors.amount ? 'border-red-500' : ''}"
value={$intents.amount}
on:input={event => intents.updateField('amount', event)}
/>
{#if $validation.errors.amount}
<span class="text-red-500 text-sm">{$validation.errors.amount}</span>
{/if}
</div>

<div class="flex flex-col gap-1">
<input
type="text"
id="receiver"
name="receiver"
required={true}
disabled={false}
autocorrect="off"
spellcheck="false"
autocomplete="off"
data-field="receiver"
class="p-1 disabled:bg-black/30 {$validation.errors.receiver ? 'border-red-500' : ''}"
placeholder="Enter destination address"
value={$intents.receiver}
on:input={event => intents.updateField('receiver', event)}
/>
{#if $validation.errors.receiver}
<span class="text-red-500 text-sm">{$validation.errors.receiver}</span>
{/if}
</div>
</div>
<Button on:click={() => rotateTo("verifyFace")}>Transfer</Button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import type {IntentStore} from "$lib/components/TransferFrom/transfer/intents.ts";
import type {ValidationStoreAndMethods} from "$lib/components/TransferFrom/transfer/validation.ts";
import type {Readable} from "svelte/store";
import type {ContextStore} from "$lib/components/TransferFrom/transfer/context.ts";
import type {CubeFaces} from "$lib/components/TransferFrom/types.ts";
interface Props {
stores: {
intents: IntentStore
validation: ValidationStoreAndMethods
context: Readable<ContextStore>
}
rotateTo: (face: CubeFaces) => void
}
export let stores: Props["stores"]
export let rotateTo: Props["rotateTo"]
$: ({intents, validation, context} = stores)
</script>

<button on:click={() => rotateTo("intentFace")}>Transfer</button>
98 changes: 98 additions & 0 deletions app/src/lib/components/TransferFrom/components/Cube/index.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script lang="ts">
import type {CubeFaces} from "$lib/components/TransferFrom/types.ts";
import FaceWrapper from "$lib/components/TransferFrom/components/Cube/FaceWrapper.svelte";
import {deviceWidth} from "$lib/utilities/device.ts";
let currentRotation = {x: 0, y: 0};
const facePositions = {
intentFace: 0,
chainsFace: -90,
verifyFace: -180,
assetsFace: -270,
sourceFace: -90,
destinationFace: -90,
} as const;
let currentVisibleFace: "source" | "destination" = "source";
$: currentVisibleFace = "source";
function findShortestRotation(current: number, target: number): number {
const revolution = Math.floor(current / 360) * 360;
const normalizedTarget = target + revolution;
let diff = normalizedTarget - current;
if (Math.abs(diff) > 180) {
diff = diff > 0 ? diff - 360 : diff + 360;
}
return current + diff;
}
function rotateTo(face: CubeFaces) {
console.log('rotate to: ', face)
const targetRotation = facePositions[face];
// Calculate the new Y rotation
const newY = findShortestRotation(currentRotation.y, targetRotation);
currentRotation = {x: 0, y: newY};
// Update visibility state
if (face === "sourceFace") {
currentVisibleFace = "source";
} else if (face === "destinationFace") {
currentVisibleFace = "destination";
}
}
//If we want to be specific we can set each w
$: width =
$deviceWidth >= 1536 ? 400 : // 2xl breakpoint
$deviceWidth >= 1280 ? 400 : // xl breakpoint
$deviceWidth >= 1024 ? 400 : // lg breakpoint
$deviceWidth >= 768 ? 400 : // md breakpoint
$deviceWidth >= 640 ? 400 : // sm breakpoint
300; // Default for smaller screens
$: height = width * 1.5;
$: translateZ = width / 2;
</script>

<div class="h-screen w-full flex items-center justify-center bg-black perspective-[2000px]">
<div
class="relative transform-style-preserve-3d transition-transform duration-1000"
style={`width: ${width}px; height: ${height}px; transform: rotateX(${currentRotation.x}deg) rotateY(${currentRotation.y}deg)`}
>
<FaceWrapper {width} {height} {translateZ} visible rotateY={"0deg"}>
<slot name="intent" {rotateTo}/>
</FaceWrapper>

<!--Source and destination is on the same degree, we just hide one depending on clicked intent.-->
<!--By doing this we can "layer" faces and reuse the rotation-->
<FaceWrapper {width} {height} {translateZ} visible={currentVisibleFace === 'source'} rotateY={"90deg"}>
<slot name="source" {rotateTo}/>
</FaceWrapper>

<FaceWrapper {width} {height} {translateZ} visible={currentVisibleFace === 'destination'} rotateY={"90deg"}>
<slot name="destination" {rotateTo}/>
</FaceWrapper>

<FaceWrapper {width} {height} {translateZ} visible rotateY={"270deg"}>
<slot name="assets" {rotateTo}/>
</FaceWrapper>

<FaceWrapper {width} {height} {translateZ} visible rotateY={"180deg"}>
<slot name="transfer" {rotateTo}/>
</FaceWrapper>
</div>
</div>

<style>
.perspective-\[2000px\] {
perspective: 2000px;
}
.transform-style-preserve-3d {
transform-style: preserve-3d;
}
</style>
Loading

0 comments on commit 020b989

Please sign in to comment.