Skip to content

Commit

Permalink
initial page for creating a new list
Browse files Browse the repository at this point in the history
  • Loading branch information
cmintey committed Jan 15, 2025
1 parent 44b9f00 commit cc73cf4
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 170 deletions.
3 changes: 3 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
"suggestions-are-disabled": "Suggestions are disabled",
"the-page-you-were-looking-for-wasnt-found": "The page you were looking for wasn't found",
"this-instance-is-invite-only": "This instance is invite only",
"unable-to-create-list": "Unable to create list",
"unable-to-delete-items": "Unable to delete items",
"unable-to-find-product-information": "Unable to find product information. You can still fill in the details manually.",
"unable-to-update-list-settings": "Unable to update list settings",
Expand All @@ -191,6 +192,7 @@
"confirm": "Confirm",
"copied": "Copied!",
"copy-to-clipboard": "Copy to clipboard",
"create": "Create",
"create-group": "Create Group",
"dismiss": "Dismiss",
"enable": "Enable",
Expand Down Expand Up @@ -276,6 +278,7 @@
"claimed-item": "{claimed, select, true {Claimed} other {Unclaimed}} item",
"create": "Create Wish",
"create-for": "Create Wish for {listOwner}",
"create-list": "Create List",
"default-sort": "Default Sort",
"delete": "Delete",
"deny": "Deny",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/IconSelector.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
}}
oninput={(e) => (search = e.currentTarget.value)}
placeholder="gift"
showClearButton={() => iconValue !== null || iconValue !== undefined}
showClearButton={() => iconValue !== null && iconValue !== undefined}
type="text"
value={iconValue}
>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/ListCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Avatar from "./Avatar.svelte";
import { t } from "svelte-i18n";
interface ListWithCounts extends Pick<List, "id" | "name" | "icon" | "iconColor"> {
interface ListWithCounts extends Partial<Pick<List, "id" | "name" | "icon" | "iconColor">> {
owner: Pick<User, "name" | "username" | "picture">;
itemCount?: number;
claimedCount?: number;
Expand Down
112 changes: 112 additions & 0 deletions src/lib/components/wishlists/ManageListForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script lang="ts">
import ListCard from "$lib/components/ListCard.svelte";
import { t } from "svelte-i18n";
import IconSelector from "$lib/components/IconSelector.svelte";
import { enhance } from "$app/forms";
import ClearableInput from "$lib/components/ClearableInput.svelte";
import { rgbToHex } from "$lib/util";
import type { List, User } from "@prisma/client";
interface ListProps extends Partial<Pick<List, "id" | "icon" | "iconColor" | "name">> {
owner: Pick<User, "name" | "username" | "picture">;
}
interface Props {
data: {
list: ListProps;
};
persistButtonName: string;
}
const { data, persistButtonName }: Props = $props();
let list = $state(data.list);
let colorElement: Element | undefined = $state();
let defaultColor: string = $derived.by(() => {
if (colorElement) {
const rgbColor = getComputedStyle(colorElement).backgroundColor;
const rgbValues = rgbColor.match(/\d+/g)?.map(Number.parseFloat);
return rgbValues ? rgbToHex(rgbValues[0], rgbValues[1], rgbValues[2]) : "";
}
return list.iconColor || "";
});
let colorValue: string | null = $state((() => defaultColor)());
$effect(() => {
if (!colorValue) colorValue = defaultColor;
});
</script>

<form
method="POST"
use:enhance={(e) => {
if (e.formData.get("iconColor") === defaultColor) {
e.formData.delete("iconColor");
}
}}
>
<div class="grid grid-cols-1 gap-4 pb-4 md:grid-cols-2">
<label class="col-span-1 md:col-span-2" for="name">
<span>{$t("auth.name")}</span>
<ClearableInput
id="name"
name="name"
class="input"
autocomplete="off"
clearButtonLabel={$t("a11y.clear-name-field")}
onValueClear={() => (list.name = null)}
placeholder={$t("wishes.wishes-for", { values: { listOwner: list.owner.name } })}
showClearButton={() => list.name !== null}
type="text"
bind:value={list.name}
/>
</label>

<label class="col-span-1 flex flex-col" for="name">
<span>{$t("general.icon-bg-color")}</span>
<div class="grid grid-cols-[auto_1fr] gap-2">
<input
id="iconColor"
name="iconColor"
class="input"
onchange={(e) => (list.iconColor = e.currentTarget?.value)}
type="color"
bind:value={colorValue}
/>
<ClearableInput
class="input"
clearButtonLabel={$t("a11y.clear-color-field")}
onValueClear={() => {
colorValue = defaultColor;
list.iconColor = defaultColor;
}}
readonly
showClearButton={() => colorValue !== defaultColor}
tabindex={-1}
type="text"
value={colorValue}
/>
</div>
</label>
<div class="col-span-1">
<IconSelector id="icon" icon={list.icon} onIconSelected={(icon) => (list.icon = icon)} />
</div>

<div class="col-span-1 md:col-span-2">
<div class="flex flex-col space-y-2">
<span>{$t("wishes.preview")}</span>
<ListCard hideCount {list} preventNavigate />
</div>
</div>
</div>

<div class="flex flex-row justify-between">
<button class="variant-ghost-secondary btn w-min" onclick={() => history.back()} type="button">
{$t("general.cancel")}
</button>
<button class="variant-filled-primary btn w-min" type="submit">
{persistButtonName}
</button>
</div>
</form>

<div bind:this={colorElement} class="bg-primary-400-500-token hidden"></div>
105 changes: 62 additions & 43 deletions src/lib/server/list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { init } from "@paralleldrive/cuid2";
import { client } from "./prisma";
import { createFilter, createSorts } from "./sort-filter-util";
import { createFilter } from "./sort-filter-util";

export interface GetItemsOptions {
filter: string | null;
Expand All @@ -11,13 +11,20 @@ export interface GetItemsOptions {
loggedInUserId: string | null;
}

export const create = async (ownerId: string, groupId: string) => {
export interface ListProperties {
name?: string | null;
icon?: string | null;
iconColor?: string | null;
}

export const create = async (ownerId: string, groupId: string, otherData?: ListProperties) => {
const cuid2 = init({ length: 10 });
return await client.list.create({
data: {
id: cuid2(),
ownerId,
groupId
groupId,
...otherData
}
});
};
Expand Down Expand Up @@ -45,13 +52,6 @@ export const getById = async (id: string) => {

export const getItems = async (listId: string, options: GetItemsOptions) => {
const filter = createFilter(options.filter);
const orderBy = createSorts(options.sort, options.sortDir);

filter.lists = {
every: {
id: listId
}
};

// In "approval" mode, don't show items awaiting approval unless the logged in user is the owner
if (
Expand All @@ -69,44 +69,63 @@ export const getItems = async (listId: string, options: GetItemsOptions) => {
};
}

const items = await client.item.findMany({
where: filter,
orderBy: orderBy,
const list = await client.list.findUnique({
where: {
id: listId,
items: {
every: filter
}
},
include: {
addedBy: {
select: {
id: true,
username: true,
name: true
}
},
pledgedBy: {
select: {
id: true,
username: true,
name: true
items: {
include: {
addedBy: {
select: {
id: true,
username: true,
name: true
}
},
pledgedBy: {
select: {
id: true,
username: true,
name: true
}
},
publicPledgedBy: {
select: {
username: true,
name: true
}
},
user: {
select: {
id: true,
username: true,
name: true
}
},
itemPrice: true
}
},
publicPledgedBy: {
select: {
username: true,
name: true
}
},
user: {
select: {
id: true,
username: true,
name: true
}
},
itemPrice: true
}
}
});

if (options.sort === "price" && options.sortDir === "asc") {
// need to re-sort when descending since Prisma can't order with nulls last
items.sort((a, b) => (a.itemPrice?.value ?? Infinity) - (b.itemPrice?.value ?? Infinity));
if (!list) {
return [];
}

const items = list?.items;

if (options.sort === "price") {
if (options.sortDir === "desc") {
items.sort((a, b) => (b.itemPrice?.value ?? -Infinity) - (a.itemPrice?.value ?? -Infinity));
} else {
items.sort((a, b) => (a.itemPrice?.value ?? Infinity) - (b.itemPrice?.value ?? Infinity));
}
} else {
items.sort((a, b) => (a.displayOrder ?? Infinity) - (b.displayOrder ?? Infinity));
}
return items;
};
26 changes: 0 additions & 26 deletions src/lib/server/sort-filter-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,3 @@ export const createFilter = (filter: string | null) => {
}
return search;
};

export const createSorts = (sort: string | null, direction: string | null) => {
let orderBy: Prisma.ItemOrderByWithRelationInput[] = [];
if (sort === "price" && direction && (direction === "asc" || direction === "desc")) {
orderBy = [
{
itemPrice: {
value: direction
}
}
];
} else {
orderBy = [
{
displayOrder: {
sort: "asc",
nulls: "last"
}
},
{
id: "asc"
}
];
}
return orderBy;
};
13 changes: 13 additions & 0 deletions src/routes/lists/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import { hash, hashItems, viewedItems } from "$lib/stores/viewed-items";
import { t } from "svelte-i18n";
import ListCard from "$lib/components/ListCard.svelte";
import { isInstalled } from "$lib/stores/is-installed";
import { goto } from "$app/navigation";
import { page } from "$app/state";
interface Props {
data: PageData;
Expand Down Expand Up @@ -35,6 +38,16 @@
{/each}
</div>

<button
class="z-90 variant-ghost-surface btn fixed right-4 h-16 w-16 rounded-full md:bottom-10 md:right-10 md:h-20 md:w-20"
class:bottom-24={$isInstalled}
class:bottom-4={!$isInstalled}
aria-label="add item"
onclick={() => goto(`${page.url.pathname}/create`)}
>
<iconify-icon height="32" icon="ion:add" width="32"></iconify-icon>
</button>

<svelte:head>
<title>{$t("wishes.lists")}</title>
</svelte:head>
8 changes: 4 additions & 4 deletions src/routes/lists/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import ItemCard from "$lib/components/wishlists/ItemCard/ItemCard.svelte";
import ClaimFilterChip from "$lib/components/wishlists/chips/ClaimFilter.svelte";
import { goto, invalidate } from "$app/navigation";
import { page } from "$app/stores";
import { page } from "$app/state";
import { onDestroy, onMount } from "svelte";
import { flip } from "svelte/animate";
import { quintOut } from "svelte/easing";
Expand Down Expand Up @@ -101,7 +101,7 @@
};
const subscribeToEvents = () => {
eventSource = new EventSource(`${$page.url.pathname}/events`);
eventSource = new EventSource(`${page.url.pathname}/events`);
eventSource.addEventListener(SSEvents.item.update, (e) => {
const message = JSON.parse(e.data) as Item;
updateItem(message);
Expand Down Expand Up @@ -211,7 +211,7 @@
{#if data.list.owner.isMe}
<div class="flex flex-row flex-wrap space-x-4">
<ReorderChip onFinalize={handleReorderFinalize} bind:reordering />
<ManageListChip onclick={() => goto(`${new URL($page.url).pathname}/manage`)} />
<ManageListChip onclick={() => goto(`${new URL(page.url).pathname}/manage`)} />
</div>
{/if}
</div>
Expand Down Expand Up @@ -310,7 +310,7 @@
class:bottom-24={$isInstalled}
class:bottom-4={!$isInstalled}
aria-label="add item"
onclick={() => goto(`${$page.url.pathname}/create-item?ref=${$page.url.pathname}`)}
onclick={() => goto(`${page.url.pathname}/create-item?ref=${page.url.pathname}`)}
>
<iconify-icon height="32" icon="ion:add" width="32"></iconify-icon>
</button>
Expand Down
Loading

0 comments on commit cc73cf4

Please sign in to comment.