Skip to content

Commit

Permalink
Add product categories (#351)
Browse files Browse the repository at this point in the history
* Initial work for managing categories

* Separate categories into it's own page

* Wrap up shop categories
  • Loading branch information
ptlthg authored Jan 10, 2025
1 parent 22c9c80 commit 8ba06dc
Show file tree
Hide file tree
Showing 23 changed files with 3,427 additions and 2,021 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"prettier-plugin-tailwindcss": "^0.6.9",
"svelte": "^5.1.13",
"svelte-check": "^4.0.6",
"svelte-dnd-action": "^0.9.54",
"svelte-preprocess": "^6.0.3",
"svelte-sonner": "^0.3.28",
"svelte-ux": "^0.75.4",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

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

16 changes: 10 additions & 6 deletions src/components/monetization/product-card.svelte
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
<script lang="ts">
import type { components } from '$lib/api/api';
import { cn } from '$lib/utils';
import ProductPrice from './product-price.svelte';
import Package from 'lucide-svelte/icons/package';
interface Props {
product: components['schemas']['ProductDto'];
class?: string;
}
let { product }: Props = $props();
let { product, class: className = '' }: Props = $props();
let image = $derived(product.thumbnail?.url ?? '');
</script>

<a
class="m-1 inline-block max-w-64 rounded-md bg-primary-foreground shadow-primary hover:drop-shadow-lg"
class={cn('inline-block max-w-64 rounded-md bg-primary-foreground shadow-primary hover:drop-shadow-lg', className)}
href="/shop/{product.id}"
>
<div class="flex min-w-0 flex-col items-center justify-start gap-4">
<div class="flex min-w-0 flex-col items-center justify-start">
<div class="grid min-h-32 w-full items-center justify-center rounded-md drop-shadow-lg">
{#if image}
<img src={image} alt={product.name} class="h-32 w-32 rounded-sm object-cover" />
{:else}
<Package size={64} />
{/if}
<div class="absolute bottom-0 right-0 mb-1 mr-2 rounded-md shadow-primary-foreground drop-shadow-md">
<ProductPrice {product} />
</div>
</div>
<div class="flex flex-row justify-between gap-2 px-2 pb-2 pt-1">
<p class="overflow-hidden text-ellipsis pr-4 text-xl">{product.name}</p>
<ProductPrice {product} />
<div class="flex flex-row items-start justify-center gap-2 p-2 px-1">
<p class="overflow-hidden text-ellipsis text-xl">{product.name}</p>
</div>
</div>
</a>
6 changes: 3 additions & 3 deletions src/components/monetization/product-price.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@
</script>

<p
class="h-fit rounded-md bg-green-600 px-4 py-1 text-center font-semibold leading-none text-white dark:bg-green-700 {free
class="h-fit rounded-md bg-green-600 px-4 pb-1 pt-0.5 text-center font-semibold leading-none text-white dark:bg-green-700 {free
? 'pb-1.5'
: ''}"
>
{#if free}
Free
{:else}
<span class="whitespace-nowrap">{dollars} USD</span>
<span class="whitespace-nowrap leading-none">{dollars} USD</span>
{#if product.isSubscription}<br />
<span class="whitespace-nowrap text-xs">per month</span>
<span class="whitespace-nowrap text-xs leading-none">per month</span>
{/if}
{/if}
</p>
172 changes: 105 additions & 67 deletions src/components/monetization/product.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,90 +9,128 @@
import TicketX from 'lucide-svelte/icons/ticket-x';
import Replace from 'lucide-svelte/icons/replace';
import ScrollText from 'lucide-svelte/icons/scroll-text';
import Check from 'lucide-svelte/icons/check';
import X from 'lucide-svelte/icons/x';
import { cn } from '$lib/utils';
import type { Snippet } from 'svelte';
interface Props {
product: components['schemas']['ProductDto'];
class?: string;
children?: Snippet;
showPublished?: boolean;
showFeatures?: boolean;
}
let { product }: Props = $props();
let { product, class: className = '', children, showPublished, showFeatures = true }: Props = $props();
let features = $derived(product.features ?? {});
let styles = $derived(product.weightStyles ?? []);
</script>

<div class="m-1 inline-block rounded-md bg-primary-foreground p-4">
<div class={cn('m-1 inline-block rounded-md bg-primary-foreground p-4', className)}>
<div class="flex items-center justify-between">
<div class="flex min-w-0 flex-shrink items-center justify-start gap-4">
<div class="flex min-w-0 flex-shrink items-center justify-start gap-2">
{#if product.thumbnail?.url}
<img src={product.thumbnail.url} alt={product.name} class="h-8 w-8 rounded-sm object-cover" />
{/if}
<div class="flex flex-col justify-start gap-2">
<div class="flex {showFeatures ? 'flex-col' : 'flex-row'} justify-start gap-2">
{#if !showFeatures}
{@render available()}
{/if}
<p class="overflow-hidden text-ellipsis whitespace-nowrap pr-4 text-xl">{product.name}</p>
<div class="flex flex-row items-center gap-1">
{#if styles.length}
<ProductFeature>
{#snippet icon()}
<Image size={16} />
{/snippet}
<p class="font-semibold">Unlocks weight styles:</p>
{#each styles as style}
<p class="text-sm font-semibold">"{style.name}"</p>
{/each}
</ProductFeature>
{/if}
{#if features.embedColors?.length}
<ProductFeature>
{#snippet icon()}
<Palette size={16} />
{/snippet}
<p class="font-semibold">Unlocks embed colors:</p>
{#each features.embedColors as color}
<div class="flex flex-row items-center gap-1">
<div class="h-4 w-4 rounded-sm" style="background-color: #{color}"></div>
<span class="text-sm font-semibold leading-none">#{color}</span>
</div>
{/each}
</ProductFeature>
{/if}
{#if features.badgeId}
<ProductFeature>
{#snippet icon()}
<Tag size={16} />
{/snippet}
<p class="font-semibold">Unlocks a badge!</p>
</ProductFeature>
{/if}
{#if features.hideShopPromotions}
<ProductFeature>
{#snippet icon()}
<TicketX size={16} />
{/snippet}
<p class="font-semibold">Hides shop promotions!</p>
</ProductFeature>
{/if}
{#if features.weightStyleOverride}
<ProductFeature>
{#snippet icon()}
<Replace size={16} />
{/snippet}
<p class="font-semibold">Apply your weight style on everyone!</p>
</ProductFeature>
{/if}
{#if features.moreInfoDefault}
<ProductFeature>
{#snippet icon()}
<ScrollText size={16} />
{/snippet}
<p class="font-semibold">More info in weight command by default!</p>
</ProductFeature>
{/if}
</div>
{#if showFeatures}
<div class="flex flex-row items-center gap-1">
{@render available()}
{#if styles.length}
<ProductFeature>
{#snippet icon()}
<Image size={16} />
{/snippet}
<p class="font-semibold">Unlocks weight styles:</p>
{#each styles as style}
<p class="text-sm font-semibold">"{style.name}"</p>
{/each}
</ProductFeature>
{/if}
{#if features.embedColors?.length}
<ProductFeature>
{#snippet icon()}
<Palette size={16} />
{/snippet}
<p class="font-semibold">Unlocks embed colors:</p>
{#each features.embedColors as color}
<div class="flex flex-row items-center gap-1">
<div class="h-4 w-4 rounded-sm" style="background-color: #{color}"></div>
<span class="text-sm font-semibold leading-none">#{color}</span>
</div>
{/each}
</ProductFeature>
{/if}
{#if features.badgeId}
<ProductFeature>
{#snippet icon()}
<Tag size={16} />
{/snippet}
<p class="font-semibold">Unlocks a badge!</p>
</ProductFeature>
{/if}
{#if features.hideShopPromotions}
<ProductFeature>
{#snippet icon()}
<TicketX size={16} />
{/snippet}
<p class="font-semibold">Hides shop promotions!</p>
</ProductFeature>
{/if}
{#if features.weightStyleOverride}
<ProductFeature>
{#snippet icon()}
<Replace size={16} />
{/snippet}
<p class="font-semibold">Apply your weight style on everyone!</p>
</ProductFeature>
{/if}
{#if features.moreInfoDefault}
<ProductFeature>
{#snippet icon()}
<ScrollText size={16} />
{/snippet}
<p class="font-semibold">More info in weight command by default!</p>
</ProductFeature>
{/if}
</div>
{/if}
</div>
</div>
<div class="flex min-w-0 items-center justify-end gap-4">
<Button href="/shop/{product.id}" class="m-1" variant="ghost">
<ExternalLink />
</Button>
<div class="flex min-w-0 items-center justify-end gap-2">
{#if children}
{@render children()}
{:else}
<Button href="/shop/{product.id}" class="m-1" variant="ghost">
<ExternalLink />
</Button>
{/if}
</div>
</div>
</div>

{#snippet available()}
{#if showPublished}
{#if product.available}
<ProductFeature>
{#snippet icon()}
<Check size={16} />
{/snippet}
<p class="font-semibold">Available</p>
</ProductFeature>
{:else}
<ProductFeature>
{#snippet icon()}
<X size={16} />
{/snippet}
<p class="font-semibold">Not available</p>
</ProductFeature>
{/if}
{/if}
{/snippet}
4 changes: 4 additions & 0 deletions src/content/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ export const ADMIN_NAV_PAGES = [
title: 'Badges',
href: '/admin/badges',
},
{
title: 'Categories',
href: '/admin/categories',
},
{
title: 'Products',
href: '/admin/products',
Expand Down
Loading

0 comments on commit 8ba06dc

Please sign in to comment.