diff --git a/src/i18n/en.json b/src/i18n/en.json index 4b15ef4d..372e33b5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -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", @@ -191,6 +192,7 @@ "confirm": "Confirm", "copied": "Copied!", "copy-to-clipboard": "Copy to clipboard", + "create": "Create", "create-group": "Create Group", "dismiss": "Dismiss", "enable": "Enable", @@ -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", diff --git a/src/lib/components/IconSelector.svelte b/src/lib/components/IconSelector.svelte index 0df63e17..fa4f13ff 100644 --- a/src/lib/components/IconSelector.svelte +++ b/src/lib/components/IconSelector.svelte @@ -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} > diff --git a/src/lib/components/ListCard.svelte b/src/lib/components/ListCard.svelte index 9fb63a28..d7bd464d 100644 --- a/src/lib/components/ListCard.svelte +++ b/src/lib/components/ListCard.svelte @@ -3,7 +3,7 @@ import Avatar from "./Avatar.svelte"; import { t } from "svelte-i18n"; - interface ListWithCounts extends Pick { + interface ListWithCounts extends Partial> { owner: Pick; itemCount?: number; claimedCount?: number; diff --git a/src/lib/components/wishlists/ManageListForm.svelte b/src/lib/components/wishlists/ManageListForm.svelte new file mode 100644 index 00000000..bb019a40 --- /dev/null +++ b/src/lib/components/wishlists/ManageListForm.svelte @@ -0,0 +1,112 @@ + + +
{ + if (e.formData.get("iconColor") === defaultColor) { + e.formData.delete("iconColor"); + } + }} +> +
+ + + +
+ (list.icon = icon)} /> +
+ +
+
+ {$t("wishes.preview")} + +
+
+
+ +
+ + +
+
+ + diff --git a/src/lib/server/list.ts b/src/lib/server/list.ts index 1f10a234..44fb531d 100644 --- a/src/lib/server/list.ts +++ b/src/lib/server/list.ts @@ -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; @@ -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 } }); }; @@ -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 ( @@ -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; }; diff --git a/src/lib/server/sort-filter-util.ts b/src/lib/server/sort-filter-util.ts index 2093503c..047e135e 100644 --- a/src/lib/server/sort-filter-util.ts +++ b/src/lib/server/sort-filter-util.ts @@ -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; -}; diff --git a/src/routes/lists/+page.svelte b/src/routes/lists/+page.svelte index f3025c14..2e16b2d2 100644 --- a/src/routes/lists/+page.svelte +++ b/src/routes/lists/+page.svelte @@ -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; @@ -35,6 +38,16 @@ {/each} + + {$t("wishes.lists")} diff --git a/src/routes/lists/[id]/+page.svelte b/src/routes/lists/[id]/+page.svelte index 8b5ded56..7dd47122 100644 --- a/src/routes/lists/[id]/+page.svelte +++ b/src/routes/lists/[id]/+page.svelte @@ -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"; @@ -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); @@ -211,7 +211,7 @@ {#if data.list.owner.isMe}
- goto(`${new URL($page.url).pathname}/manage`)} /> + goto(`${new URL(page.url).pathname}/manage`)} />
{/if} @@ -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}`)} > diff --git a/src/routes/lists/[id]/manage/+page.svelte b/src/routes/lists/[id]/manage/+page.svelte index 88759257..636b3653 100644 --- a/src/routes/lists/[id]/manage/+page.svelte +++ b/src/routes/lists/[id]/manage/+page.svelte @@ -1,36 +1,18 @@ -
{ - if (e.formData.get("iconColor") === defaultColor) { - e.formData.delete("iconColor"); - } - }} -> -
- - - -
- (list.icon = icon)} /> -
- -
-
- {$t("wishes.preview")} - -
-
-
- -
- - -
-
- - + {$t("wishes.manage-list")} diff --git a/src/routes/lists/create/+page.server.ts b/src/routes/lists/create/+page.server.ts new file mode 100644 index 00000000..373580cc --- /dev/null +++ b/src/routes/lists/create/+page.server.ts @@ -0,0 +1,73 @@ +import { getConfig } from "$lib/server/config"; +import { getActiveMembership } from "$lib/server/group-membership"; +import { error, fail, redirect } from "@sveltejs/kit"; +import type { Actions, PageServerLoad } from "./$types"; +import { getFormatter } from "$lib/i18n"; +import { trimToNull } from "$lib/util"; +import { getListPropertiesSchema } from "$lib/validations"; +import { create } from "$lib/server/list"; + +export const load = (async ({ locals, url }) => { + const user = locals.user; + if (!user) { + redirect(302, `/login?ref=${url.pathname + url.search}`); + } + + const activeMembership = await getActiveMembership(user); + const config = await getConfig(activeMembership.groupId); + if (config.listMode === "registry") { + redirect(302, "/wishlists/me"); + } + + return { + list: { + name: null, + icon: null, + owner: { + name: user.name, + username: user.username, + picture: user.picture || null + } + } + }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + default: async ({ request, locals }) => { + const $t = await getFormatter(); + if (!locals.user) { + error(401, $t("errors.unauthenticated")); + } + + const activeMembership = await getActiveMembership(locals.user); + + const form = await request.formData(); + const listPropertiesSchema = getListPropertiesSchema(); + const listProperties = listPropertiesSchema.safeParse({ + name: form.get("name"), + icon: form.get("icon"), + iconColor: form.get("iconColor") + }); + if (listProperties.error) { + return fail(422, { + success: false, + errors: listProperties.error.format() + }); + } + + let list; + try { + const data = { + name: trimToNull(listProperties.data.name), + icon: trimToNull(listProperties.data.icon), + iconColor: trimToNull(listProperties.data.iconColor) + }; + list = await create(locals.user.id, activeMembership.groupId, data); + } catch (e) { + console.log("Unable to create list", e); + return fail(500, { success: false }); + } + + return redirect(302, `/lists/${list.id}`); + } +}; diff --git a/src/routes/lists/create/+page.svelte b/src/routes/lists/create/+page.svelte new file mode 100644 index 00000000..46122b03 --- /dev/null +++ b/src/routes/lists/create/+page.svelte @@ -0,0 +1,28 @@ + + + + + + {$t("wishes.create-list")} +