446 lines
11 KiB
Svelte
446 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { persisted } from '$lib/helpers/localStore';
|
|
import { fade, slide } from 'svelte/transition';
|
|
import TextSplit from './TextSplit.svelte';
|
|
import ImageFrame from './ImageFrame.svelte';
|
|
import Button from './button.svelte';
|
|
import InputRange from './InputRange.svelte';
|
|
import { goto } from '$app/navigation';
|
|
import Loader from './Loader.svelte';
|
|
|
|
let data = persisted<{
|
|
name: string;
|
|
showNameLengthError: boolean;
|
|
nameAccepted?: boolean;
|
|
|
|
provideAdelsTitel?: boolean;
|
|
adelsTitel?: string;
|
|
showAdelsTitelLengthError: boolean;
|
|
adelsTitelAccepted?: boolean;
|
|
adelsTitelSuggestions?: string[];
|
|
|
|
providePortrait?: boolean;
|
|
portraitUrl?: string;
|
|
portraitHairType?: 'straight' | 'curly' | 'bald';
|
|
portraitHairColor?: 'red' | 'brown' | 'blonde' | 'black' | 'grey' | 'white';
|
|
portraitHairLength?: 'short' | 'medium' | 'long';
|
|
portraitSkinColor?: 'very light' | 'light' | 'medium' | 'dark' | 'very dark';
|
|
portraitAccepted?: boolean;
|
|
portraitPublic?: boolean;
|
|
|
|
confidence?: number;
|
|
confidenceAccepted?: boolean;
|
|
|
|
createPersonality?: boolean;
|
|
}>('data', {
|
|
name: '',
|
|
showNameLengthError: false,
|
|
nameAccepted: false,
|
|
adelsTitel: undefined,
|
|
showAdelsTitelLengthError: false,
|
|
adelsTitelSuggestions: [],
|
|
provideAdelsTitel: undefined,
|
|
portraitPublic: true,
|
|
confidence: undefined,
|
|
confidenceAccepted: false,
|
|
createPersonality: undefined
|
|
});
|
|
|
|
$: if ($data.name.length > 99) {
|
|
$data.showNameLengthError = true;
|
|
}
|
|
|
|
$: if ($data?.adelsTitel?.length > 99) {
|
|
$data.showAdelsTitelLengthError = true;
|
|
}
|
|
|
|
let loadingAdelsTitel = false;
|
|
async function fetchAdelsTitel() {
|
|
if (loadingAdelsTitel) return;
|
|
loadingAdelsTitel = true;
|
|
|
|
const res = await fetch('/api/ai/name/' + $data.name);
|
|
const json = await res.json();
|
|
$data.adelsTitelSuggestions = json;
|
|
loadingAdelsTitel = false;
|
|
}
|
|
|
|
let loadingPortrait = false;
|
|
async function fetchPortrait() {
|
|
if (loadingPortrait) return;
|
|
loadingPortrait = true;
|
|
|
|
const res = await fetch(`/api/ai/image/${$data.adelsTitel || $data.name}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
hairType: $data.portraitHairType,
|
|
hairColor: $data.portraitHairColor,
|
|
hairLength: $data.portraitHairLength,
|
|
skinColor: $data.portraitSkinColor,
|
|
name: $data.name
|
|
})
|
|
});
|
|
|
|
const json = await res.json();
|
|
|
|
$data.portraitUrl = json.url;
|
|
loadingPortrait = false;
|
|
}
|
|
|
|
let isSubmitting = false;
|
|
async function submit() {
|
|
if (isSubmitting) return;
|
|
isSubmitting = true;
|
|
await fetch(`/api/invites`, {
|
|
method: 'POST',
|
|
body: JSON.stringify($data)
|
|
});
|
|
localStorage.clear();
|
|
localStorage.setItem('last-name', $data?.adelsTitel || '');
|
|
isSubmitting = false;
|
|
goto('/gallery');
|
|
}
|
|
</script>
|
|
|
|
<div class="wrapper">
|
|
<section in:slide={{ delay: 500 }}>
|
|
<TextSplit content="Wie lauten Euer Vor- und Nachname, edler Gast?" />
|
|
<input placeholder="Name" type="text" bind:value={$data.name} />
|
|
|
|
{#if $data.name.length > 99}
|
|
<p class="error">
|
|
Wir bitten um Entschuldigung, aber dieser Name ist zu lang. (maximal 100 Zeichen)
|
|
</p>
|
|
{:else if $data.showNameLengthError}
|
|
<p class="hint">{$data.name.length}/100 Zeichen</p>
|
|
{/if}
|
|
{#if !$data.nameAccepted}
|
|
<button
|
|
disabled={$data.name.length > 99}
|
|
on:click={() => {
|
|
$data.nameAccepted = true;
|
|
}}>Name speichern</button
|
|
>
|
|
{/if}
|
|
</section>
|
|
|
|
{#if ($data.name && $data.nameAccepted) || $data.confidence !== undefined}
|
|
<section in:slide={{ delay: 0, duration: 500 }}>
|
|
<TextSplit
|
|
content="Wie sicher dürfen wir mit Eurem glanzvollen Erscheinen rechnen?"
|
|
delay={0}
|
|
/>
|
|
<div in:fade={{ delay: 1200 }}>
|
|
<InputRange bind:value={$data.confidence} />
|
|
</div>
|
|
{#if !$data.confidenceAccepted}
|
|
<button
|
|
on:click={() => {
|
|
$data.confidenceAccepted = true;
|
|
}}>ok</button
|
|
>
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
|
|
{#if $data.confidence !== undefined && $data.confidenceAccepted && $data.createPersonality === undefined}
|
|
<section in:slide out:slide>
|
|
<TextSplit
|
|
content="Möchtet Ihr, dass wir eine alternative Persönlichkeit für Euch erschaffen?"
|
|
/>
|
|
<div>
|
|
<button
|
|
on:click={() => {
|
|
$data.createPersonality = true;
|
|
}}>Ja</button
|
|
>
|
|
<button
|
|
on:click={() => {
|
|
$data.createPersonality = false;
|
|
}}>Nein</button
|
|
>
|
|
</div>
|
|
{#if $data.adelsTitel}
|
|
{$data.adelsTitel}
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
|
|
{#if $data.createPersonality === true}
|
|
<section in:fade out:slide>
|
|
{#if $data.provideAdelsTitel === undefined}
|
|
<TextSplit content="Möchtet Ihr einen würdigen Adelsnamen für Euch in Erwägung ziehen?" />
|
|
<div>
|
|
<button
|
|
on:click={() => {
|
|
fetchAdelsTitel();
|
|
$data.provideAdelsTitel = true;
|
|
}}
|
|
>
|
|
ja</button
|
|
>
|
|
<button
|
|
on:click={() => {
|
|
$data.adelsTitel = '';
|
|
$data.provideAdelsTitel = true;
|
|
}}
|
|
>
|
|
ich habe bereits einen
|
|
</button>
|
|
<button
|
|
on:click={() => {
|
|
$data.adelsTitel = '';
|
|
$data.provideAdelsTitel = false;
|
|
}}
|
|
>
|
|
nein</button
|
|
>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if loadingAdelsTitel}
|
|
<div style="display: flex; gap: 20px; align-items: center;">
|
|
<Loader />
|
|
<p>Adelsnamen werden geschrieben</p>
|
|
</div>
|
|
{:else if typeof $data.adelsTitel === 'string'}
|
|
<TextSplit content="Euer Adelsname" />
|
|
<input placeholder="Name" type="text" bind:value={$data.adelsTitel} />
|
|
{#if $data.adelsTitel.length > 98}
|
|
<p class="error">
|
|
Wir bitten um Entschuldigung, aber dieser Titel ist zu lang. (maximal 100 Zeichen)
|
|
</p>
|
|
{:else if $data.showAdelsTitelLengthError}
|
|
<p class="hint">{$data.adelsTitel.length}/100 Zeichen</p>
|
|
{/if}
|
|
{#if !$data.adelsTitelAccepted}
|
|
<button
|
|
disabled={$data.adelsTitel.length > 99}
|
|
on:click={() => {
|
|
$data.adelsTitelAccepted = true;
|
|
}}>akzeptieren</button
|
|
>
|
|
<button
|
|
on:click={() => {
|
|
$data.adelsTitel = undefined;
|
|
fetchAdelsTitel();
|
|
}}>neue vorschläge</button
|
|
>
|
|
{/if}
|
|
{:else if $data.adelsTitelSuggestions?.length}
|
|
<p>Adelsname Vorschläge</p>
|
|
<hr />
|
|
{#each $data.adelsTitelSuggestions as suggestion}
|
|
<button
|
|
on:click={() => {
|
|
$data.adelsTitel = suggestion;
|
|
}}>{suggestion}</button
|
|
>
|
|
{/each}
|
|
<hr />
|
|
<button
|
|
on:click={() => {
|
|
fetchAdelsTitel();
|
|
}}>neue vorschläge</button
|
|
>
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
|
|
{#if $data.adelsTitel && $data.adelsTitelAccepted && $data.providePortrait !== false}
|
|
<section transition:slide class="portrait-frame" class:loaded={!!$data.portraitUrl}>
|
|
{#if $data.providePortrait === undefined}
|
|
<TextSplit
|
|
content="Sollten unsere begabten Künstler ein majestätisches Porträt von Euch anfertigen?"
|
|
/>
|
|
<div>
|
|
<button
|
|
on:click={() => {
|
|
$data.providePortrait = true;
|
|
}}>ja</button
|
|
>
|
|
<button
|
|
on:click={() => {
|
|
$data.providePortrait = true;
|
|
}}>false</button
|
|
>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if $data.providePortrait && !loadingPortrait && !$data.portraitUrl}
|
|
<p>
|
|
Wir werden {$data.adelsTitel || $data.name} mit
|
|
<select placeholder="Typ" bind:value={$data.portraitSkinColor}>
|
|
<option value="very light">sehr heller</option>
|
|
<option value="light">heller</option>
|
|
<option value="medium">medium</option>
|
|
<option value="dark">dunkler</option>
|
|
<option value="very dark">sehr dunkler</option>
|
|
</select>
|
|
Haut und
|
|
<select placeholder="Typ" bind:value={$data.portraitHairType}>
|
|
<option value="straight">glatten</option>
|
|
<option value="curly">lockigen</option>
|
|
<option value="wavy">welligen</option>
|
|
</select>
|
|
<select placeholder="Länge" bind:value={$data.portraitHairLength}>
|
|
<option value="long">langen</option>
|
|
<option value="medium">medium-langen</option>
|
|
<option value="short">kurzen</option>
|
|
</select>,
|
|
<select placeholder="Farbe" bind:value={$data.portraitHairColor}>
|
|
<option value="red">roten</option>
|
|
<option value="black">schwarzen</option>
|
|
<option value="blond">blonden</option>
|
|
<option value="brown">braunen</option>
|
|
</select> Haaren zeichnen
|
|
</p>
|
|
<button on:click={() => fetchPortrait()}> porträt malen (~15 Sekunden)</button>
|
|
{:else if loadingPortrait}
|
|
<div style="display: flex; gap: 20px; align-items: center;">
|
|
<Loader />
|
|
<p>Euer edles Antlitz wird gemalt</p>
|
|
</div>
|
|
{:else if $data.portraitUrl}
|
|
<div in:slide={{ duration: 2000 }} class="portrait">
|
|
<ImageFrame src={$data.portraitUrl} alt="portrait" />
|
|
|
|
{#if !$data.portraitAccepted}
|
|
<button
|
|
on:click={() => {
|
|
$data.portraitAccepted = true;
|
|
}}>Porträt annehmen</button
|
|
>
|
|
<button
|
|
on:click={() => {
|
|
$data.portraitUrl = undefined;
|
|
}}>Neues Porträt anfordern</button
|
|
>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</section>
|
|
{/if}
|
|
|
|
{#if $data.portraitAccepted}
|
|
{#if isSubmitting}
|
|
<section in:slide style="border: none;">
|
|
<div style="display: flex; gap: 20px; align-items: center;">
|
|
<Loader />
|
|
<p>Einladung wird abgeschickt</p>
|
|
</div>
|
|
</section>
|
|
{:else}
|
|
<div class="button-wrapper">
|
|
<Button on:click={() => submit()}>Einladung versenden</Button>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.button-wrapper {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding-block: 20px;
|
|
margin: 0 auto;
|
|
width: 99%;
|
|
max-width: 500px;
|
|
padding-block: 50px;
|
|
font-size: 2em;
|
|
margin-bottom: 200px;
|
|
}
|
|
|
|
p.error {
|
|
background: red;
|
|
color: white;
|
|
padding: 9px;
|
|
border-radius: 5px;
|
|
font-size: 0.5em;
|
|
font-weight: 200;
|
|
font-family: sans-serif;
|
|
}
|
|
|
|
p.hint {
|
|
font-size: 0.6em;
|
|
font-family: sans-serif;
|
|
font-weight: 200;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.portrait-frame.loaded {
|
|
margin-top: 100px;
|
|
border: none;
|
|
}
|
|
|
|
button {
|
|
border: none;
|
|
border-radius: 2px;
|
|
padding: 5px 9px;
|
|
margin-right: 10px;
|
|
background: #866831;
|
|
color: black;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:disabled {
|
|
opacity: 0.4;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 50px;
|
|
padding-bottom: 200px;
|
|
}
|
|
.portrait {
|
|
width: 100%;
|
|
}
|
|
input[type='text'] {
|
|
font-family: 'Parisienne', cursive;
|
|
background: transparent;
|
|
border: none;
|
|
color: white;
|
|
width: 100%;
|
|
font-size: 1em;
|
|
margin: 0 auto;
|
|
border-bottom: 1px solid white;
|
|
box-sizing: border-box;
|
|
}
|
|
input[type='text']::after {
|
|
content: '';
|
|
background: white;
|
|
height: 1px;
|
|
width: 100%;
|
|
}
|
|
|
|
select {
|
|
background: transparent;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px;
|
|
border-bottom: 1px solid white;
|
|
}
|
|
|
|
select > option {
|
|
color: black;
|
|
}
|
|
section {
|
|
color: white;
|
|
font-family: 'Parisienne', cursive;
|
|
max-width: 500px;
|
|
padding: 20px;
|
|
margin: 0 auto;
|
|
font-size: 2em;
|
|
width: 70%;
|
|
border-width: 30px;
|
|
border-style: solid;
|
|
border-image: url(/border_b.svg);
|
|
border-image-repeat: stretch;
|
|
border-image-slice: 100%;
|
|
border-image-slice: 24% 23%;
|
|
}
|
|
</style>
|