feat: add upload page UI (moodboard + SNS feed)
* feat: add upload page initial UI * feat: add moodboard and SNS feed upload layouts * fix: improve mobile upload page layout
This commit is contained in:
19
src/lib/components/ui/Artwork/Artwork.svelte
Normal file
19
src/lib/components/ui/Artwork/Artwork.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
// The exhibited artwork — always shown on the left, acting like a step indicator.
|
||||
import Vase from './Vase.svelte';
|
||||
import DescriptionCard from './DescriptionCard.svelte';
|
||||
|
||||
let { title = 'Title', description = 'Description Description Description' } = $props();
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="flex w-full shrink-0 flex-col border-b border-line lg:min-h-0 lg:w-[44%] lg:shrink-0 lg:overflow-y-auto lg:border-r lg:border-b-0"
|
||||
>
|
||||
<!-- mobile: compact row · desktop: centered column -->
|
||||
<div
|
||||
class="mx-auto flex w-full max-w-100 flex-row items-center gap-12 px-6 py-5 lg:flex-1 lg:flex-col lg:items-center lg:justify-center lg:gap-10 lg:px-6 lg:py-12"
|
||||
>
|
||||
<Vase />
|
||||
<DescriptionCard {title} {description} />
|
||||
</div>
|
||||
</section>
|
||||
8
src/lib/components/ui/Artwork/DescriptionCard.svelte
Normal file
8
src/lib/components/ui/Artwork/DescriptionCard.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script>
|
||||
let { title = 'Title', description = 'Description Description Description' } = $props();
|
||||
</script>
|
||||
|
||||
<div class="min-w-0 flex-1 border border-line-strong px-4 py-3 lg:w-54 lg:flex-none lg:px-6 lg:py-5">
|
||||
<h3 class="text-sm">{title}</h3>
|
||||
<p class="mt-2 text-xs leading-snug">{description}</p>
|
||||
</div>
|
||||
11
src/lib/components/ui/Artwork/Vase.svelte
Normal file
11
src/lib/components/ui/Artwork/Vase.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
import vaseIllustration from '$lib/assets/vase-illustration.svg';
|
||||
</script>
|
||||
|
||||
<img
|
||||
src={vaseIllustration}
|
||||
alt=""
|
||||
class="mx-auto h-auto w-full max-w-24 shrink-0 sm:max-w-28 lg:max-w-75"
|
||||
width="320"
|
||||
height="452"
|
||||
/>
|
||||
21
src/lib/components/ui/Header.svelte
Normal file
21
src/lib/components/ui/Header.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
// `step` is 1-based; the matching dot is highlighted as the current step.
|
||||
let { step = 1, total = 6 } = $props();
|
||||
|
||||
const dots = $derived(Array.from({ length: total }, (_, i) => i));
|
||||
</script>
|
||||
|
||||
<header class="flex items-center justify-between border-b border-line px-6 py-5 md:px-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="size-4 shrink-0 bg-placeholder"></span>
|
||||
<span class="text-lg tracking-wide">AI Florist</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 sm:gap-4">
|
||||
{#each dots as dot (dot)}
|
||||
<span
|
||||
class={['rounded-[1px]', dot === step - 1 ? 'size-2.5 bg-subtle' : 'size-2 bg-placeholder']}
|
||||
></span>
|
||||
{/each}
|
||||
</div>
|
||||
</header>
|
||||
62
src/lib/components/ui/upload/MoodboardGrid.svelte
Normal file
62
src/lib/components/ui/upload/MoodboardGrid.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import UploadTile from './UploadTile.svelte';
|
||||
|
||||
// One reference image per category, laid out as an interlocking collage
|
||||
// (offset seams, varied heights) instead of equal quarters.
|
||||
const tiles = [
|
||||
{ key: 'color', label: 'Color', aspect: 'aspect-4/5' },
|
||||
{ key: 'season', label: 'Season', aspect: 'aspect-4/3' },
|
||||
{ key: 'character', label: 'Character', aspect: 'aspect-4/3' },
|
||||
{ key: 'location', label: 'Location', aspect: 'aspect-4/5' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="moodboard w-full min-h-0 flex-1">
|
||||
{#each tiles as tile (tile.key)}
|
||||
<UploadTile
|
||||
label={tile.label}
|
||||
class="tile tile-{tile.key} h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto {tile.aspect}"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.moodboard {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.moodboard {
|
||||
grid-template-rows: repeat(5, 1fr);
|
||||
grid-template-areas:
|
||||
'color season'
|
||||
'color season'
|
||||
'color location'
|
||||
'character location'
|
||||
'character location';
|
||||
min-height: 34rem;
|
||||
}
|
||||
|
||||
:global(.tile-color) {
|
||||
grid-area: color;
|
||||
}
|
||||
|
||||
:global(.tile-season) {
|
||||
grid-area: season;
|
||||
}
|
||||
|
||||
:global(.tile-character) {
|
||||
grid-area: character;
|
||||
}
|
||||
|
||||
:global(.tile-location) {
|
||||
grid-area: location;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
45
src/lib/components/ui/upload/SnsFeedUpload.svelte
Normal file
45
src/lib/components/ui/upload/SnsFeedUpload.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script>
|
||||
import UploadTile from './UploadTile.svelte';
|
||||
|
||||
// Two SNS feed screenshots. On desktop they fill the panel edge-to-edge in
|
||||
// a staggered composition (one raised on the right, one dropped on the
|
||||
// left); below that they fall back to a simple side-by-side / stacked grid.
|
||||
</script>
|
||||
|
||||
<div class="feed w-full min-h-0 flex-1">
|
||||
<UploadTile class="tile-one h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto aspect-4/5" />
|
||||
<UploadTile class="tile-two h-full min-h-0 w-full max-lg:aspect-auto lg:aspect-auto aspect-4/5" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feed {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.feed {
|
||||
grid-template-rows: repeat(5, 1fr);
|
||||
grid-template-areas:
|
||||
'. two'
|
||||
'one two'
|
||||
'one two'
|
||||
'one two'
|
||||
'one .';
|
||||
min-height: 34rem;
|
||||
}
|
||||
|
||||
:global(.tile-one) {
|
||||
grid-area: one;
|
||||
}
|
||||
|
||||
:global(.tile-two) {
|
||||
grid-area: two;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
src/lib/components/ui/upload/UploadTile.svelte
Normal file
66
src/lib/components/ui/upload/UploadTile.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
// A single click-to-upload slot: a light bordered placeholder when empty,
|
||||
// the chosen image (cover) when filled. Layout (size / grid placement) is
|
||||
// supplied by the parent via `class` and `style` so the same tile works in
|
||||
// both the moodboard and the SNS feed.
|
||||
let { label = null, class: klass = '', style = '' } = $props();
|
||||
|
||||
let preview = $state(null);
|
||||
|
||||
function pick(event) {
|
||||
const file = event.currentTarget.files?.[0];
|
||||
if (!file) return;
|
||||
if (preview) URL.revokeObjectURL(preview);
|
||||
preview = URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (preview) URL.revokeObjectURL(preview);
|
||||
});
|
||||
</script>
|
||||
|
||||
<label
|
||||
class={[
|
||||
'group relative flex cursor-pointer items-center justify-center overflow-hidden bg-track transition-colors',
|
||||
!preview && 'border border-line hover:border-line-strong',
|
||||
klass
|
||||
]}
|
||||
{style}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="sr-only"
|
||||
aria-label={label ? `Add a ${label} image` : 'Add an image'}
|
||||
onchange={pick}
|
||||
/>
|
||||
|
||||
{#if preview}
|
||||
<img src={preview} alt={label ?? ''} class="h-full w-full object-cover" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/45 to-transparent"></div>
|
||||
{#if label}
|
||||
<span class="absolute bottom-3 left-4 text-sm tracking-[0.15em] text-surface uppercase"
|
||||
>{label}</span
|
||||
>
|
||||
{/if}
|
||||
<span
|
||||
class="absolute top-3 right-3 rounded-full bg-black/40 px-2.5 py-1 text-xs text-surface opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
Change
|
||||
</span>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center gap-3 text-subtle transition-transform group-hover:scale-105"
|
||||
>
|
||||
<span
|
||||
class="flex size-10 items-center justify-center rounded-full border border-current text-xl leading-none"
|
||||
aria-hidden="true">+</span
|
||||
>
|
||||
{#if label}
|
||||
<span class="text-sm tracking-[0.15em] uppercase">{label}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
Reference in New Issue
Block a user