Closed Bewinxed closed 1 year ago
Can you post the +page.svelte
code as well? Nothing looks wrong here directly.
Might be a bit long but here
<script lang="ts">
import InputProgress from 'src/components/forms/InputProgress.svelte';
import type { Snapshot } from './$types';
import { toasts } from 'svelte-toasts';
$: snapshotData = {
form: $form,
lastClickedSection: $lastClickedSection
};
function mergeObjectsRecursive(
target: Record<string, unknown>,
source: Record<string, unknown>
): Record<string, unknown> {
for (const key in source) {
if (
typeof target[key] === 'object' &&
target[key] !== null &&
typeof source[key] === 'object' &&
source[key] !== null
) {
mergeObjectsRecursive(
target[key] as Record<string, unknown>,
source[key] as Record<string, unknown>
);
} else if (target[key] === undefined || target[key] === null || target[key] === '') {
target[key] = source[key];
}
}
return target;
}
export const snapshot: Snapshot = {
capture: () => snapshotData,
restore: (value) => {
toasts.add({
title: 'Restored',
description: 'Your form has been restored from a previous session.',
type: 'success',
placement: 'top-right'
});
console.log(value);
if (!$form.id) {
$form = value.form;
} else {
$form = mergeObjectsRecursive($form, value.form);
}
$lastClickedSection = value.lastClickedSection;
}
};
import {
type ListingProject,
PricingUnits,
ProjectTypes,
ListingPurposes
} from 'src/types/listing';
import { Accordion, AccordionItem } from '@skeletonlabs/skeleton';
import Svelecte from 'svelecte';
import {
Avatar,
InputChip,
ProgressBar,
ProgressRadial,
RangeSlider
} from '@skeletonlabs/skeleton';
import Select from 'svelte-select';
import type {
CollectionNameMappingItem,
CollectionSocialMetadata,
CollectionStatsItem
} from '../../types/helloMoon';
let filterText = '';
let value: any;
let items: CollectionNameMappingItem[] = [];
let searchable = true;
let placeholder = '🔍 Type to find your collection...';
// VALIDATION
// $: {
// if ($form.purpose.includes('takeoverPartial')) {
// $form.partialTakeoverPercentage = undefined;
// }
// }
let loadOptions = async function (filterText: string) {
return new Promise((resolve, reject) => {
setTimeout(async () => {
if (filterText.length < 3) {
return resolve([]);
}
const response = await fetch(`/api/project/search?collectionName=${filterText}`);
if (!response.ok) {
console.log(
`Error fetching collection name mapping: ${response.status} ${response.statusText}`
);
return [];
}
const newItems = await response.json();
return resolve(
newItems.map((item: CollectionNameMappingItem) => {
return {
label: item.collectionName,
value: item.helloMoonCollectionId
};
})
);
}, 100);
});
};
import type { PageData } from './$types';
import { page } from '$app/stores';
import { superForm } from 'sveltekit-superforms/client';
import SuperDebug from 'sveltekit-superforms/client/SuperDebug.svelte';
import { Listing, ListingPurpose } from 'src/types/listing';
import { slide } from 'svelte/transition';
import { Collection } from 'mongodb';
import TechStackForm from 'src/components/TechStackForm.svelte';
import SocialsForm from 'src/components/SocialsForm.svelte';
import Icon from '@iconify/svelte';
import AddressesForm from 'src/components/AddressesForm.svelte';
import { writable } from 'svelte/store';
export let data: PageData;
// Client API:
const { form, errors, enhance, delayed, message, constraints } = superForm(data.form, {
scrollToError: 'smooth',
autoFocusOnError: 'detect',
errorSelector: '[data-invalid]'
});
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && filterText.length > 0) {
alert('ENTER PRESSED: ' + filterText);
}
}
async function getSocialMetadata() {
console.log('getting social metadata for');
const response = await fetch(`/api/project/${$form.project.helloMoonCollectionId}/metadata`);
if (!response.ok) {
console.log(
`Error fetching collection social metadata: ${response.status} ${response.statusText}`
);
}
const metadata = (await response.json()) as CollectionSocialMetadata;
// const metadata = {
// collectionName: 'DeGods',
// helloMoonCollectionId: '040de757c0d2b75dcee999ddd47689c4',
// narrative: 'A collection of 10,000 of the most degenerate gods in the universe.',
// external_url: 'https://degods.com/',
// symbol: null,
// description: null,
// image: 'https://metadata.degods.com/g/9999-dead.png',
// twitter: null,
// discord: null,
// website: null,
// categories: null,
// isUnverifiedCollection: false
// };
const updatedValues: ListingProject = {
helloMoonCollectionId: metadata.helloMoonCollectionId,
name: metadata.collectionName,
website: metadata.external_url,
description: metadata.narrative,
image: metadata.image
};
// fill form data
$form.project = { ...$form.project, ...updatedValues };
if (metadata.twitter) {
$form.project.socials.twitter = {
network: 'twitter',
url: metadata.twitter
};
}
if (metadata.discord) {
$form.project.socials.discord = {
network: 'discord',
url: metadata.discord
};
}
return metadata;
}
async function getStats() {
console.log('getting stats', $form.project);
const response = await fetch(`/api/project/${$form.project.helloMoonCollectionId}/stats`);
if (!response.ok) {
console.log(`Error fetching collection stats: ${response.status} ${response.statusText}`);
}
const stats = (await response.json()) as CollectionStatsItem;
const updatedValues = {
floorPriceAtListing: parseInt(stats.floorPriceLamports) / 1000000000,
launchDate: new Date(stats.startTime * 1000).toLocaleDateString('en-US'),
launchPrice: parseInt(stats.mintPriceMode),
ownersAtListing: parseInt(stats.currentOwnerCount),
volumeAtListing: parseInt(stats.volume) / 1000000000,
listingsAtListing: parseInt(stats.listingCount),
supply: parseInt(stats.supply)
};
// fill form data
$form.project = { ...$form.project, ...updatedValues };
return stats;
}
const listingPurposes = {
[ListingPurposes.enum.takeoverFull]: {
label: 'Full Takeover',
description: 'I want to sell my entire project.',
icon: 'mdi:account-group'
},
[ListingPurposes.enum.takeoverPartial]: {
label: 'Partial Takeover',
description: 'I want to sell a portion of my project.',
icon: 'mdi:account-group-outline'
},
[ListingPurposes.enum.funding]: {
label: 'Funding',
description: 'I want to raise funds for my project.',
icon: 'mdi:cash-multiple'
},
[ListingPurposes.enum.others]: {
label: 'Something Else',
description: 'I want to list for another reason.',
icon: 'mdi:help-circle-outline'
}
};
// FORM PROGRESS
// initialize the active index to -1, indicating no active accordion
const lastClickedSection = writable(-1);
let formProgress1 = 0;
let formProgress2 = 0;
let formProgress3 = 0;
</script>
<svelte:window on:keydown={handleKeydown} />
<!-- search bar -->
<div class="flex flex-col m-8 space-y-4">
<h1>New Listing</h1>
<!-- FORM START LETS GOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO -->
<form class="flex flex-col gap-y-8" method="POST" use:enhance>
<div class="flex flex-col border rounded-lg p-4 space-y-4 gap-y-4">
<div id="accordion" class="space-y-12">
<div
id="accordion-item"
class="accordion-item"
on:click={() => {
lastClickedSection.set(1);
console.log('clicked', $lastClickedSection);
}}
on:keydown={() => {
lastClickedSection.set(1);
console.log('clicked', $lastClickedSection);
}}
>
<div class="flex flex-row mb-4">
<div class="w-full flex flex-row place-content-between">
<div class="flex flex-row place-items-center space-x-2">
<Icon icon="mdi:bulletin-board" />
<h5>Listing Details</h5>
</div>
<div>
<ProgressRadial
class="h-12 w-12"
value={formProgress1}
font={150}
stroke={100}
meter="stroke-primary-500"
track="stroke-primary-500/30">{formProgress1}%</ProgressRadial
>
</div>
</div>
</div>
<div class="border rounded-lg p-4 space-y-4">
<label class="label">
Reason for listing? <br /><br />
<div class="logo-cloud flex flex-row gap-0.5 border">
{#each Object.entries(ListingPurpose) as [purposeValue, purposeLabel]}
<button
class="logo-item p-2 text-left h-16 w-1/4 {$form.purpose.includes(purposeValue)
? '!bg-primary-600'
: ''}"
data-purpose={purposeValue}
on:click|preventDefault={() => {
if ($form.purpose.includes(purposeValue)) {
$form.purpose = $form.purpose.filter((p) => p !== purposeValue);
} else {
$form.purpose.push(purposeValue);
$form.purpose = [...new Set($form.purpose)];
}
if (!$form.purpose.includes('takeoverPartial')) {
$form.partialTakeoverPercentage = undefined;
}
}}
>
<span> <Icon icon={listingPurposes[purposeValue].icon} /></span>
<span> {listingPurposes[purposeValue].label}</span>
</button>
{/each}
</div>
{#if $errors.purpose}<span class="invalid">{$errors.purpose}</span>{/if}
</label>
{#if $form.purpose.includes('takeoverPartial')}
<div transition:slide>
<RangeSlider
class="range"
name="partialTakeoverPercentage"
data-invalid={$errors.partialTakeoverPercentage}
bind:value={$form.partialTakeoverPercentage}
type="range"
max={100}
ticked
>
<div class="flex justify-between items-center">
<div class="font-bold">Takeover %</div>
<div class="badge variant-filled-primary text-white">
{$form.partialTakeoverPercentage}
</div>
</div>
</RangeSlider>
</div>
{/if}
<div class="w-full grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="space-y-1" for="project-search">
<span class="label">Asking Price</span>
<blockquote>Make sure you select the right currency</blockquote>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<div class="input-group-shim"><i class="fa-solid fa-dollar-sign" />$</div>
<InputProgress
{errors}
placeholder="Asking Price"
bind:progress={formProgress1}
bind:value={$form.price}
progressAmount={2}
name="price"
type="number"
/>
<select
class="select"
name="priceUnit"
data-invalid={$errors.priceUnit}
bind:value={$form.priceUnit}
>
{#each PricingUnits as priceUnit}
<option value={priceUnit}>{priceUnit}</option>
{/each}
{#if $errors.priceUnit}<span class="invalid">{$errors.priceUnit}</span>{/if}
</select>
</div>
{#if $errors.price}<span class="invalid">{$errors.price}</span>{/if}
</label>
<label class="space-y-1" for="project-search">
<span class="label">What does the price include?</span>
<!-- helper text -->
<blockquote>Select from below or add your own</blockquote>
<br />
<Svelecte
options={['Project IP', 'Treasury', 'Source Code']}
creatable={true}
multiple={true}
bind:value={$form.priceIncludes}
placeholder="Type Multiple Items"
/>
</label>
<label class="col-span-2"
>Reason for listing <br /><br />
<textarea
class="textarea"
name="reason"
data-invalid={$errors.reason}
bind:value={$form.reason}
/>
</label>
<!-- listing headline -->
<label for="headline" class="col-span-2">
<span class="label">Listing Title <br /><br /></span>
<InputProgress
{errors}
placeholder="e.g. 'Looking to sell 50% of my project'"
bind:progress={formProgress1}
bind:value={$form.headline}
progressAmount={5}
name="headline"
type="text"
/>
{#if $errors.headline}<span class="invalid">{$errors.headline}</span>{/if}
<blockquote>{$form.headline}</blockquote>
</label>
</div>
</div>
</div>
<!-- ... -->
<div
id="accordion-item"
on:click={() => {
lastClickedSection.set(2);
console.log('clicked', $lastClickedSection);
}}
on:keydown={() => {
lastClickedSection.set(2);
console.log('clicked', $lastClickedSection);
}}
>
<div class="flex flex-row mb-4">
<div class="w-full flex flex-row place-content-between">
<div class="flex flex-row place-items-center space-x-2">
<Icon icon="ph:projector-screen-chart" />
<h5>Project Info</h5>
</div>
<div>
<ProgressRadial
class="h-12 w-12"
value={formProgress2}
font={150}
stroke={100}
meter="stroke-primary-500"
track="stroke-primary-500/30">{formProgress2}%</ProgressRadial
>
</div>
</div>
</div>
<div id="content">
<div class="border rounded-lg p-4 space-y-4">
<h4 class="font-semibold">Project Details</h4>
{#if $form.purpose}
<label>
What kind of project are you listing? <br /><br />
<select
on:input={(e) => {
$form.project.categories = Array.from(e.target.selectedOptions).map(
(o) => o.value
);
}}
class="select"
name="project.categories"
multiple
size="3"
data-invalid={$errors.project}
>
{#each ProjectTypes as projectType}
<option value={projectType}>{projectType}</option>
{/each}
</select>
{#if $errors.project}<span class="invalid">{$errors.purpose}</span>{/if}
</label>
<div class="space-y-4" transition:slide>
<InputProgress
{errors}
name="helloMoonCollectionId"
type="hidden"
bind:value={$form.project.helloMoonCollectionId}
/>
{#if $errors.project}<span class="invalid">{$errors.project}</span>{/if}
{#if ($form.project?.categories ?? []).includes('NFT')}
<label class="space-y-1" for="project-search">
<h3 class="label">We can autofill your project's info</h3>
<br />
<Svelecte
resetOnBlur={true}
fetchResetOnBlur={true}
valueAsObject
minQuery={3}
placeholder="🔍 Type to find your project"
on:change={(e) => {
console.log('change', e.detail);
$form.project.collectionName = e.detail.label;
$form.project.helloMoonCollectionId = e.detail.value;
getSocialMetadata();
getStats();
}}
fetch={loadOptions}
/>
</label>
{#if $form.project.helloMoonCollectionId && !$form.project}
{#await getSocialMetadata()}
<div transition:slide class="p-4 space-y-2">
<ProgressBar label="Progress Bar" />
<p>Autofilling project data</p>
</div>
{:then}
{#if $form.project}
<!-- post card -->
<div
class="flex bg-surface-500 shadow-lg rounded-lg md:mx-auto my-56 max-w-md md:max-w-2xl "
>
<!--horizantil margin is just for display-->
<div class="flex items-start px-4 py-6">
<Avatar
width="w-24"
src={$form.project.image}
class="rounded-full object-cover mr-4 shadow"
/>
<div class="">
<div class="flex items-center justify-between">
<h3 class="text-lg font-bold -mt-1">
{$form.project.collectionName}
</h3>
<small class="text-sm">{new Date().toLocaleDateString()}</small>
</div>
<p class="font-bold text-ellipsis">
{$form.project.description}
</p>
{#await getStats()}
<div transition:slide class="p-4 space-y-2">
<ProgressBar label="Progress Bar" />
<p>Loading latest stats...</p>
</div>
{:then stats}
<div class="mt-4 flex items-center">
<div class="flex flex-col mr-2 text-sm">
Supply
<span>{parseInt(stats.supply)}</span>
</div>
<div class="flex flex-col mr-2 text-sm ">
Floor Price
<span>{parseInt(stats.floorPriceLamports) / 1000000000}</span>
</div>
<div class="flex mr-2 text-primary-100 text-sm">
<svg
fill="none"
viewBox="0 0 24 24"
class="w-4 h-4 mr-1"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
/>
</svg>
<a
href={$form.project.website}
target="_blank"
rel="noreferrer"><span>website </span></a
>
</div>
</div>
{/await}
</div>
</div>
</div>
{/if}
{/await}
{/if}
{/if}
</div>
{/if}
<div class="w-full grid grid-cols-1 md:grid-cols-2 gap-4">
<hr class="col-span-2" />
<div class="flex flex-row space-x-2 col-span-2">
<Icon icon="material-symbols:info-outline" />
<h5>Project Info</h5>
</div>
<!-- replicate above -->
<InputProgress
{errors}
name="helloMoonCollectionId"
type="hidden"
bind:value={$form.project.helloMoonCollectionId}
bind:progress={formProgress2}
progressAmount={2}
required
/>
<label for="collectionName"
>Project Name:
<!-- replicate above -->
<InputProgress
{errors}
name="collectionName"
type="text"
bind:value={$form.project.collectionName}
bind:progress={formProgress2}
progressAmount={2}
required
/>
</label>
<label for="website"
>Website:
<!-- replicate above -->
<InputProgress
{errors}
name="website"
type="url"
bind:value={$form.project.website}
bind:progress={formProgress2}
progressAmount={2}
required
/>
</label>
<label class="label"
>Launch Date:
<input
name="launchDate"
data-invalid={$errors?.project}
class="input"
type="date"
placeholder="Enter the project's launch date"
bind:value={$form.project.launchDate}
required
/>
</label>
<label for="description" class="col-span-2"
>Description:
<textarea
name="description"
data-invalid={$errors?.project}
class="textarea"
placeholder="Enter the project's description"
bind:value={$form.project.description}
required
/>
</label>
</div>
<div class="col-span-2 space-y-4 w-full grid grid-cols-1 md:grid-cols-2 gap-4">
<hr class="col-span-2" />
<div class="flex flex-row space-x-2 col-span-2">
<Icon icon="fluent-mdl2:financial" />
<h6>Project Financials</h6>
</div>
<label for="floorPriceAtListing"
>Floor Price at Listing:
<input
name="floorPriceAtListing"
data-invalid={$errors?.project}
class="input"
type="number"
readonly
placeholder="Enter the floor price at listing"
bind:value={$form.project.floorPriceAtListing}
min="0"
/>
</label>
<label for="supply"
>Supply:
<input
name="supply"
data-invalid={$errors?.project}
class="input"
type="number"
readonly
placeholder="Enter the supply"
bind:value={$form.project.supply}
min="1"
required
/>
</label>
<label for="supply"
>Current Holders:
<input
name="supply"
data-invalid={$errors?.project}
class="input"
type="number"
readonly
placeholder="Enter the supply"
bind:value={$form.project.ownersAtListing}
min="1"
required
/>
</label>
<label for="volume">
Volume:
<input
name="volume"
data-invalid={$errors?.project}
class="input"
type="number"
readonly
placeholder="Enter the volume"
bind:value={$form.project.volumeAtListing}
min="0"
/>
</label>
</div>
</div>
</div>
</div>
</div>
<div
id="accordion-item"
class="accordion-item border rounded-lg p-4 space-y-4"
on:click={() => {
lastClickedSection.set(3);
console.log('clicked', $lastClickedSection);
}}
on:keydown={() => {
lastClickedSection.set(3);
console.log('clicked', $lastClickedSection);
}}
>
<div class="flex flex-row mb-4">
<div class="w-full flex flex-row place-content-between">
<div class="flex flex-row place-items-center space-x-2">
<Icon icon="iconoir:profile-circle" />
<h5>Project Links</h5>
</div>
<div>
<ProgressRadial
class="h-12 w-12"
value={formProgress3}
font={150}
stroke={100}
meter="stroke-primary-500"
track="stroke-primary-500/30">{formProgress3}%</ProgressRadial
>
</div>
</div>
</div>
<div id="content">
<div class="col-span-2">
<div class="flex flex-row space-x-2">
<h6>Social Links</h6>
</div>
<blockquote>Add the social links of your project</blockquote>
</div>
<div class="col-span-2 transition-all duration-700">
<SocialsForm {form} />
</div>
<hr class="col-span-2" />
<div class="col-span-2">
<div class="flex flex-row space-x-2">
<Icon icon="ph:stack" />
<h6>Tech Stack</h6>
</div>
<blockquote>Add the technologies used in your website/app/etc...</blockquote>
</div>
<div class="col-span-2 transition-all duration-700">
<TechStackForm {form} />
</div>
<hr class="col-span-2" />
<div class="col-span-2">
<div class="flex flex-row space-x-2">
<Icon icon="carbon:wallet" />
<h6>Project Addresses</h6>
</div>
<blockquote>Add your royalty/treasury/other important addresses</blockquote>
</div>
<div class="col-span-2 transition-all duration-700">
<AddressesForm {form} />
</div>
<hr class="col-span-2" />
</div>
</div>
</div>
<button type="submit" class="btn bg-green-600">Submit</button>
{#if $delayed}Working...{/if}
{#if $message}
<h3 class:invalid={$page.status >= 400}>{$message}</h3>
{/if}
</form>
<SuperDebug data={$form} />
<!-- show result card -->
</div>
<style>
:global(.svelecte-control) {
--sv-bg: rgb(var(--color-surface-700)) !important;
--sv-color: white !important;
--sv-border: 1px solid rgb(var(--color-surface-500)) !important;
--sv-item-selected-bg: rgb(var(--color-surface-700)) !important;
--sv-item-active-bg: rgb(var(--color-surface-700)) !important;
--sv-item-btn-bg: rgb(var(--color-surface-700)) !important;
--sv-item-btn-bg-hover: rgb(var(--color-surface-700)) !important;
--sv-item-highlight-color: rgb(var(--color-surface-700)) !important;
--sv-item-color: white !important;
--sv-item-selected-color: white !important;
--sv-item-border: 1px solid rgb(var(--color-surface-500)) !important;
--sv-color: rgb(var(--text-primary-100)) !important;
--sv-placeholder-color: white !important;
--sv-highlight-bg: rgb(var(--color-surface-100)) !important;
--sv-highlight-color: rgb(var(--color-surface-700)) !important;
/* --sv-active-border: 1px solid red !important;
--sv-border-color: red !important;
--sv-disabled-border-color: red !important;
--sv-item-color: black !important; */
--sv-item-active-color: red !important;
/* --sv-item-active-bg: red !important; */
}
.invalid {
color: red;
}
</style>
Quite long, yes. :) I see nothing obviously wrong with how you use the library, could it be something with the snapshot code, that sets $form
, and may do that after a post?
I'll try removing it and seeing what happens then get back to you, in a few hours
On Sun, 12 Mar 2023 at 7:31 PM Andreas Söderlund @.***> wrote:
Quite long, yes. :) I see nothing obviously wrong with how you use the library, could it be something with the snapshot code, that sets $form, and may do that after a post?
— Reply to this email directly, view it on GitHub https://github.com/ciscoheat/sveltekit-superforms/issues/27#issuecomment-1465242241, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACFY5BOGDMYWYQRYNJVDGZLW3X27TANCNFSM6AAAAAAVYGLQKI . You are receiving this because you authored the thread.Message ID: @.***>
Ok, good luck. You could try clearing out most things except SuperDebug
and see if you get the form data back, or if it goes missing during the server roundtrip?
Apparently when the form is invalid the invalid fields are cleared, and since some of the invalid fields are inside the nested object, the whole nested object gets wiped :(
edit: forgot to remove this ``` // fill form data $form.project = { ...$form.project, ...updatedValues };
removed the line at the end and now it doesn't get cleared!
I passed the nested project as another form so that i can use the constraints from there
form invalid errors { user: [ 'String must contain at least 1 character(s)' ], financials: [ 'Required', 'Required' ], project: [ 'Required', 'Required', 'Required', 'Required', 'Required', 'Required', 'Required', 'Required', 'Required', 'Required', 'Required', 'Required', 'Required' ], overview: [ 'String must contain at least 1 character(s)' ] }
@Bewinxed Check out the announcement for v0.6, it has an example of how to use the new nested data feature: https://github.com/ciscoheat/sveltekit-superforms/discussions/41
@Bewinxed did you figure this out, so the issue can be closed?
Yes thanks a lot!
On Thu, 13 Apr 2023 at 9:24 PM Andreas Söderlund @.***> wrote:
@Bewinxed https://github.com/Bewinxed did you figure this out, so the issue can be closed?
— Reply to this email directly, view it on GitHub https://github.com/ciscoheat/sveltekit-superforms/issues/27#issuecomment-1507431302, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACFY5BNUTYXGUHHO7W6QFI3XBBAENANCNFSM6AAAAAAVYGLQKI . You are receiving this because you were mentioned.Message ID: @.***>
+page.server.ts
When the form is invalid and submitted, the whole form gets cleared :( how to prevent this?