feat: add create and message page UI
Co-authored-by: 이지은 <ijieun@ijieun-ui-MacBookPro.local>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ Thumbs.db
|
|||||||
# Vite
|
# Vite
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
# Local notes (not pushed)
|
||||||
|
docs/
|
||||||
|
|||||||
71
src/lib/components/ui/create/BudgetSlider.svelte
Normal file
71
src/lib/components/ui/create/BudgetSlider.svelte
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script>
|
||||||
|
let { budget = $bindable(50_000) } = $props();
|
||||||
|
|
||||||
|
const min = 10_000;
|
||||||
|
const max = 150_000;
|
||||||
|
const step = 5_000;
|
||||||
|
|
||||||
|
const fillPercent = $derived(((budget - min) / (max - min)) * 100);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs tracking-[0.2em] text-muted uppercase">Budget</p>
|
||||||
|
<p class="mt-2 text-3xl font-semibold tracking-tight">
|
||||||
|
₩{budget.toLocaleString('ko-KR')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="relative h-6">
|
||||||
|
<div class="absolute top-1/2 right-0 left-0 h-px -translate-y-1/2 bg-placeholder"></div>
|
||||||
|
<div
|
||||||
|
class="absolute top-1/2 left-0 h-px -translate-y-1/2 bg-subtle"
|
||||||
|
style="width: {fillPercent}%"
|
||||||
|
></div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
|
{step}
|
||||||
|
bind:value={budget}
|
||||||
|
aria-label="Budget"
|
||||||
|
class="budget-slider absolute inset-0 w-full cursor-pointer appearance-none bg-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-xs text-muted">
|
||||||
|
<span>₩10,000</span>
|
||||||
|
<span>₩150,000+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.budget-slider::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--color-pill);
|
||||||
|
border: none;
|
||||||
|
margin-top: -0.4375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-slider::-moz-range-thumb {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--color-pill);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-slider::-webkit-slider-runnable-track {
|
||||||
|
height: 1px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-slider::-moz-range-track {
|
||||||
|
height: 1px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
src/lib/components/ui/create/ContextForm.svelte
Normal file
66
src/lib/components/ui/create/ContextForm.svelte
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<script>
|
||||||
|
import OptionGroup from './OptionGroup.svelte';
|
||||||
|
import BudgetSlider from './BudgetSlider.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
who = $bindable(null),
|
||||||
|
whatFor = $bindable(null),
|
||||||
|
style = $bindable(null),
|
||||||
|
budget = $bindable(50_000)
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const hasAnySelection = $derived(who !== null || whatFor !== null || style !== null);
|
||||||
|
|
||||||
|
const whoOptions = ['Friend', 'Family', 'Partner', 'Teacher', 'Others'];
|
||||||
|
const whatForOptions = ['Birthday', 'Anniversary', 'Thanks', 'Daily'];
|
||||||
|
const styleOptions = ['Feminine', 'Masculine', 'Neutral'];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-1 flex-col justify-center px-6 py-10 md:px-12 lg:px-16 lg:py-16">
|
||||||
|
<header class="mb-10 space-y-3 lg:mb-14">
|
||||||
|
{#if !hasAnySelection}
|
||||||
|
<h1 class="text-3xl leading-relaxed font-light text-muted md:text-4xl lg:text-[2.75rem]">
|
||||||
|
Who are we making flowers for?
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-muted">Pick a few details below</p>
|
||||||
|
{:else}
|
||||||
|
<h1 class="text-3xl leading-tight font-light text-muted md:text-4xl lg:text-[2.75rem]">
|
||||||
|
{#if whatFor}
|
||||||
|
A <span class="font-semibold text-ink underline decoration-ink/30 underline-offset-4"
|
||||||
|
>{whatFor}</span
|
||||||
|
>
|
||||||
|
bouquet for
|
||||||
|
{:else}
|
||||||
|
A bouquet for
|
||||||
|
{/if}
|
||||||
|
{#if who}
|
||||||
|
<span class="font-semibold text-ink underline decoration-ink/30 underline-offset-4"
|
||||||
|
>{who}</span
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<span class="text-muted">...</span>
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-muted">
|
||||||
|
{style ?? '—'} | ₩{budget.toLocaleString('ko-KR')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="space-y-8 lg:space-y-10">
|
||||||
|
<OptionGroup label="Who" options={whoOptions} selected={who} onchange={(v) => (who = v)} />
|
||||||
|
<OptionGroup
|
||||||
|
label="What for"
|
||||||
|
options={whatForOptions}
|
||||||
|
selected={whatFor}
|
||||||
|
onchange={(v) => (whatFor = v)}
|
||||||
|
/>
|
||||||
|
<OptionGroup
|
||||||
|
label="Style"
|
||||||
|
options={styleOptions}
|
||||||
|
selected={style}
|
||||||
|
onchange={(v) => (style = v)}
|
||||||
|
/>
|
||||||
|
<BudgetSlider bind:budget />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
21
src/lib/components/ui/create/OptionGroup.svelte
Normal file
21
src/lib/components/ui/create/OptionGroup.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
let { label, options, selected, onchange } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-sm tracking-[0.2em] text-muted uppercase">{label}</p>
|
||||||
|
<div class="flex flex-wrap gap-x-5 gap-y-2">
|
||||||
|
{#each options as option (option)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => onchange(option)}
|
||||||
|
class={[
|
||||||
|
'text-xl tracking-wide transition-colors',
|
||||||
|
selected === option ? 'text-ink' : 'text-muted hover:text-ink'
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
34
src/lib/components/ui/message/MessageForm.svelte
Normal file
34
src/lib/components/ui/message/MessageForm.svelte
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script>
|
||||||
|
import MessagePresetList from './MessagePresetList.svelte';
|
||||||
|
|
||||||
|
let { message = $bindable('') } = $props();
|
||||||
|
|
||||||
|
const presets = [
|
||||||
|
'Happy Birthday!',
|
||||||
|
'Thank you for always being there',
|
||||||
|
'I love you',
|
||||||
|
"I'm proud of you",
|
||||||
|
'Congratulations!',
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedPreset = $derived(presets.find((preset) => preset === message) ?? null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-1 flex-col justify-center px-6 py-10 md:px-12 lg:px-16 lg:py-16">
|
||||||
|
<header class="mb-8 lg:mb-10">
|
||||||
|
<textarea
|
||||||
|
bind:value={message}
|
||||||
|
placeholder="Write something from your heart"
|
||||||
|
rows="1"
|
||||||
|
aria-label="Write your message"
|
||||||
|
class="w-full resize-none border-none bg-transparent text-3xl leading-relaxed font-light text-ink placeholder:text-muted focus:outline-none md:text-4xl lg:text-[2.75rem]"
|
||||||
|
></textarea>
|
||||||
|
<div class="mt-6 border-b border-pill lg:mt-3"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<MessagePresetList
|
||||||
|
options={presets}
|
||||||
|
selected={selectedPreset}
|
||||||
|
onchange={(preset) => (message = preset)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
19
src/lib/components/ui/message/MessagePresetList.svelte
Normal file
19
src/lib/components/ui/message/MessagePresetList.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script>
|
||||||
|
// OptionGroup과 같은 선택 버튼 스타일 — message는 세로 목록
|
||||||
|
let { options, selected, onchange } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex max-w-md flex-col gap-y-8">
|
||||||
|
{#each options as option (option)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => onchange(option)}
|
||||||
|
class={[
|
||||||
|
'text-left text-xl tracking-wide transition-colors',
|
||||||
|
selected === option ? 'text-ink' : 'text-muted hover:text-ink'
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -1 +1,42 @@
|
|||||||
<h1>/create page</h1>
|
<script>
|
||||||
|
import Header from '$lib/components/ui/Header.svelte';
|
||||||
|
import Artwork from '$lib/components/ui/Artwork/Artwork.svelte';
|
||||||
|
import ContextForm from '$lib/components/ui/create/ContextForm.svelte';
|
||||||
|
|
||||||
|
let who = $state(null);
|
||||||
|
let whatFor = $state(null);
|
||||||
|
let style = $state(null);
|
||||||
|
let budget = $state(50_000);
|
||||||
|
|
||||||
|
const hasAnySelection = $derived(who !== null || whatFor !== null || style !== null);
|
||||||
|
|
||||||
|
const artworkTitle = $derived.by(() => {
|
||||||
|
if (!hasAnySelection) return 'Title';
|
||||||
|
const occasion = whatFor ? `A ${whatFor} bouquet for` : 'A bouquet for';
|
||||||
|
return `${occasion} ${who ?? '...'}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const artworkDescription = $derived(
|
||||||
|
hasAnySelection
|
||||||
|
? `${style ?? '—'} style · ₩${budget.toLocaleString('ko-KR')} budget`
|
||||||
|
: 'Description Description Description'
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
upload와 같은 2열 레이아웃: 좌측 Artwork 고정, 우측 ContextForm.
|
||||||
|
선택값이 바뀌면 create1 → create2 헤드라인·요약이 반응형으로 전환됩니다.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="flex h-dvh flex-col overflow-x-hidden bg-surface text-ink lg:h-screen lg:overflow-hidden"
|
||||||
|
>
|
||||||
|
<Header step={1} total={6} />
|
||||||
|
|
||||||
|
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||||
|
<Artwork title={artworkTitle} description={artworkDescription} />
|
||||||
|
|
||||||
|
<section class="relative flex min-h-0 flex-1 flex-col lg:overflow-y-auto">
|
||||||
|
<ContextForm bind:who bind:whatFor bind:style bind:budget />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|||||||
29
src/routes/message/+page.svelte
Normal file
29
src/routes/message/+page.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
import Header from '$lib/components/ui/Header.svelte';
|
||||||
|
import Artwork from '$lib/components/ui/Artwork/Artwork.svelte';
|
||||||
|
import MessageForm from '$lib/components/ui/message/MessageForm.svelte';
|
||||||
|
|
||||||
|
let message = $state('');
|
||||||
|
|
||||||
|
const artworkTitle = $derived(message ? 'Your message' : 'Title');
|
||||||
|
|
||||||
|
const artworkDescription = $derived(message || 'Description Description Description');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
create / upload와 같은 2열 레이아웃.
|
||||||
|
우측 MessageForm에서 프리셋 메시지를 pill 버튼으로 선택합니다.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="flex h-dvh flex-col overflow-x-hidden bg-surface text-ink lg:h-screen lg:overflow-hidden"
|
||||||
|
>
|
||||||
|
<Header step={4} total={6} />
|
||||||
|
|
||||||
|
<main class="flex min-h-0 flex-1 flex-col lg:flex-row">
|
||||||
|
<Artwork title={artworkTitle} description={artworkDescription} />
|
||||||
|
|
||||||
|
<section class="relative flex min-h-0 flex-1 flex-col lg:overflow-y-auto">
|
||||||
|
<MessageForm bind:message />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user