Skip to content

Commit

Permalink
feat(svelte-ui): add collection specific page
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholaschiang committed Nov 25, 2024
1 parent 2e94f53 commit 9abd0fc
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 18 deletions.
1 change: 1 addition & 0 deletions svelte-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"dependencies": {
"@supabase/sql-formatter": "^4.0.3",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"drizzle-orm": "^0.36.4",
"lucide-svelte": "^0.460.1",
"pg": "^8.13.1"
Expand Down
32 changes: 31 additions & 1 deletion svelte-ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions svelte-ui/src/lib/components/Header.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
let { children } = $props()
</script>
<header
class="flex h-10 items-center gap-2 border-b border-gray-200 px-6 dark:border-gray-800 text-lg lowercase sticky top-0 bg-white/80 dark:bg-gray-950/80 backdrop-blur-xl z-10"
>
{@render children()}
</header>
7 changes: 7 additions & 0 deletions svelte-ui/src/lib/components/Subtitle.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
let { children } = $props()
</script>

<p class="text-gray-400 dark:text-gray-500">
{@render children()}
</p>
3 changes: 3 additions & 0 deletions svelte-ui/src/lib/formatDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const { format: formatDate } = new Intl.DateTimeFormat(undefined, {
dateStyle: "long",
})
14 changes: 14 additions & 0 deletions svelte-ui/src/lib/formatLevelName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function formatLevelName(level: string): string {
switch (level) {
case 'BESPOKE':
return 'Bespoke'
case 'COUTURE':
return 'Couture'
case 'HANDMADE':
return 'Handmade'
case 'RTW':
return 'RTW'
default:
throw new Error(`Unknown level: ${level}`)
}
}
28 changes: 28 additions & 0 deletions svelte-ui/src/lib/formatLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const LOCATION_TO_NAME: Record<string, string> = {
NEW_YORK: 'New York, United States',
LONDON: 'London, England',
MILAN: 'Milan, Italy',
PARIS: 'Paris, France',
TOKYO: 'Tokyo, Japan',
BERLIN: 'Berlin, Germany',
FLORENCE: 'Florence, Italy',
LOS_ANGELES: 'Los Angeles, United States',
MADRID: 'Madrid, Spain',
COPENHAGEN: 'Copenhagen, Denmark',
SHANGHAI: 'Shanghai, China',
AUSTRALIA: 'Australia',
STOCKHOLM: 'Stockholm, Sweden',
MEXICO: 'Mexico',
MEXICO_CITY: 'Mexico City, Mexico',
KIEV: 'Kiev, Ukraine',
TBILISI: 'Tbilisi, Georgia',
SEOUL: 'Seoul, South Korea',
RUSSIA: 'Russia',
UKRAINE: 'Ukraine',
SAO_PAOLO: 'Sao Paolo, Brazil',
BRIDAL: 'Bridal',
}

export function formatLocation(location: string): string {
return LOCATION_TO_NAME[location] ?? location
}
22 changes: 22 additions & 0 deletions svelte-ui/src/lib/formatSeasonName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Capitalizes the first letter of each word in the given string. Lowercases all
* the other letters in each string (e.g. "RESORT 2024" -> "Resort 2024").
* @param str The string to capitalize.
* @returns The capitalized string.
*/
function formatCaps(sentence: string): string {
return sentence
.split(' ')
.map((w) => `${w.charAt(0).toUpperCase()}${w.slice(1).toLowerCase()}`)
.join(' ')
}

/**
* Get the user-friendly season name from a season object (e.g. "Resort 2024").
* @param season The season object.
* @returns The user-friendly season name.
*/
export function formatSeasonName(season: { name: string; year: number }) {
const name = formatCaps(season.name.replace('_', ' ')).replace(' ', '-')
return `${name} ${season.year}`
}
42 changes: 26 additions & 16 deletions svelte-ui/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
<script lang="ts">
import { Search } from "lucide-svelte"
import { formatDate } from "$lib/formatDate"
import { formatLocation } from "$lib/formatLocation"
import Header from "$lib/components/Header.svelte"
import Subtitle from "$lib/components/Subtitle.svelte"
let { data } = $props()
let form: HTMLFormElement
let value = $state("")
const { format } = new Intl.DateTimeFormat(undefined, { dateStyle: "long" })
$effect(() => {
value = new URLSearchParams(location.search).get("q") ?? ""
})
type Collection = Awaited<typeof data.collections>[number]
const sort = (a: Collection, b: Collection) =>
new Date(b.collectionDate ?? new Date()).valueOf() -
new Date(a.collectionDate ?? new Date()).valueOf()
</script>

<header
class="flex h-10 items-center gap-6 border-b border-gray-200 px-6 dark:border-gray-800"
>
<h1 class="text-lg">collections</h1>
</header>
<Header>
<a href="/">Collections</a>
</Header>
<div class="flex flex-col gap-6 p-6">
<form
data-sveltekit-replacestate
Expand All @@ -31,6 +37,7 @@
class="w-0 grow border-0 bg-transparent px-0 focus:ring-0"
placeholder="Search..."
oninput={() => form.requestSubmit()}
bind:value
/>
</form>
<div class="flex flex-col gap-2">
Expand All @@ -42,12 +49,15 @@
<p>Error: {error.message}</p>
{/await}
</div>
<ul
<div
class="grid gap-x-2 gap-y-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6"
>
{#await data.collections then collections}
{#each collections.sort(sort) as collection (collection.collectionId)}
<li class="flex flex-col gap-2 text-xs">
<a
href="/collections/{collection.collectionId}"
class="flex flex-col gap-2 text-xs"
>
<div
class="flex aspect-person w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
>
Expand All @@ -68,18 +78,18 @@
<div>
<h2>{collection.collectionName}</h2>
{#if collection.collectionDate}
<p class="text-gray-400 dark:text-gray-500">
{format(new Date(collection.collectionDate))}
</p>
<Subtitle>
{formatDate(new Date(collection.collectionDate))}
</Subtitle>
{/if}
{#if collection.collectionLocation}
<p class="text-gray-400 dark:text-gray-500">
{collection.collectionLocation}
</p>
<Subtitle>
{formatLocation(collection.collectionLocation)}
</Subtitle>
{/if}
</div>
</li>
</a>
{/each}
{/await}
</ul>
</div>
</div>
28 changes: 28 additions & 0 deletions svelte-ui/src/routes/collections/[collectionId]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { eq } from "drizzle-orm"
import * as tables from "$lib/server/db/schema/tables"
import { db } from "$lib/server/db"

export async function load({ params }) {
return {
collection: db.query.collection
.findMany({
where: eq(tables.collection.id, params.collectionId),
with: {
brand: true,
season: true,
articles: {
with: {
user_authorId: true,
publication: true,
},
},
looks: {
with: {
images: true,
},
},
},
})
.then((collections) => collections[0]),
}
}
114 changes: 114 additions & 0 deletions svelte-ui/src/routes/collections/[collectionId]/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script lang="ts">
import { ExternalLink, ChevronRight } from "lucide-svelte"
import { formatDate } from "$lib/formatDate"
import { formatSeasonName } from "$lib/formatSeasonName"
import { formatLevelName } from "$lib/formatLevelName"
import { formatLocation } from "$lib/formatLocation"
import Header from "$lib/components/Header.svelte"
import Subtitle from "$lib/components/Subtitle.svelte"
let { data } = $props()
</script>

<Header>
<a href="/">Collections</a>
<ChevronRight class="h-4 w-4 text-gray-200 dark:text-gray-800" />
{#await data.collection then collection}
<a href="/?q={encodeURIComponent(collection.brand.name)}"
>{collection.brand.name}</a
>
<ChevronRight class="h-4 w-4 text-gray-200 dark:text-gray-800" />
<a href="/?q={encodeURIComponent(formatSeasonName(collection.season))}"
>{formatSeasonName(collection.season)}
{formatLevelName(collection.level)}</a
>
<div class="flex items-center gap-1">
{#if collection.date}
<span class="text-sm text-gray-400 dark:text-gray-500"
>({formatDate(new Date(collection.date))})</span
>
{/if}
{#if collection.location}
<span class="text-sm text-gray-400 dark:text-gray-500"
>({formatLocation(collection.location)})</span
>
{/if}
</div>
{/await}
</Header>
<div class="flex h-full flex-col xl:flex-row">
<div class="flex flex-col gap-6 p-6 xl:w-0 xl:grow">
{#await data.collection}
<p>Loading...</p>
{:then collection}
<div
class="grid gap-x-2 gap-y-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-2 2xl:grid-cols-3"
>
{#each collection.looks as look (look.id)}
<a
href={look.images[0].url}
target="_blank"
rel="noopener noreferrer"
class="flex flex-col gap-2 text-xs"
>
<div
class="flex aspect-person w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
>
{#if look.images[0]?.url}
<img
loading="lazy"
decoding="async"
src={look.images[0].url}
alt="Look {look.number} of {collection.name}"
class="flex h-full w-full items-center justify-center object-cover"
/>
{:else}
<span class="text-gray-200 dark:text-gray-700"
>No look images to show</span
>
{/if}
</div>
<p>Look {look.number}</p>
</a>
{/each}
</div>
{/await}
</div>
{#await data.collection then collection}
{#if collection.articles.length}
<div class="flex flex-col gap-12 p-6 xl:pr-12">
{#each collection.articles as article (article.id)}
<div class="flex flex-col gap-4 text-sm">
<div>
<h2 class="flex items-center gap-2">
<span
>By <a
href={article.user_authorId?.url}
target="_blank"
rel="noopener noreferrer"
class="underline">{article.user_authorId?.name}</a
>
for
<a
href={article.url}
target="_blank"
rel="noopener noreferrer"
class="underline">{article.publication.name}</a
></span
>
<ExternalLink class="h-4 w-4" />
</h2>
{#if article.writtenAt}
<Subtitle>{formatDate(new Date(article.writtenAt))}</Subtitle>
{/if}
</div>
<article class="prose prose-sm prose-zinc dark:prose-invert">
{@html article.content}
</article>
</div>
{/each}
</div>
{/if}
{/await}
</div>
Loading

0 comments on commit 9abd0fc

Please sign in to comment.