Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "list" ADD COLUMN "hideOwner" BOOLEAN NOT NULL DEFAULT false;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ model List {
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
groupId String
public Boolean @default(false)
hideOwner Boolean @default(false)
icon String?
iconColor String?
items ListItem[]
Expand Down
9 changes: 7 additions & 2 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"a11y": {
"a-person-looking-at-an-empty-board": "A person looking at an empty board",
"cancel-editing": "cancel editing",
"claimed-progress": "Claimed progress",
"clear-color-field": "clear color field",
"clear-icon-field": "clear icon field",
"clear-name-field": "clear name field",
Expand Down Expand Up @@ -106,10 +107,10 @@
"show-for-list-owner": "Show for list owner",
"show-for-list-owner-description": "Allow list owners to view claims on their own lists.",
"show-name": "Show name",
"show-name-publicly": "Show name publicly",
"show-name-publicly-tooltip": "When enabled, the name provided on a public claim is shown to everyone viewing the public list.",
"show-name-across-groups": "Show name across groups",
"show-name-across-groups-tooltip": "Displays the name of the user who made the claim even if the user is not in your group.",
"show-name-publicly": "Show name publicly",
"show-name-publicly-tooltip": "When enabled, the name provided on a public claim is shown to everyone viewing the public list.",
"smtp": "SMTP",
"smtp-from-email": "From Email",
"smtp-from-name": "From Name",
Expand Down Expand Up @@ -413,6 +414,7 @@
"hide-all-currencies": "hide all currencies...",
"hide-claims": "Hide claims",
"hide-description": "Hide description",
"hide-owner": "Hide Owner",
"image-url": "Image URL",
"import-error-text": "Wishlist was unable to determine the product url from the given parameters. You can still manually copy the product url and add the item using the normal process.",
"import-error-title": "Unable to import item",
Expand All @@ -424,6 +426,9 @@
"item-url": "Item URL",
"item-was-deleted": "{name} was deleted",
"item-was-removed-from-list": "{name} was removed from this list",
"items-available": "{availableCount, plural, one {# Available} other {# Available}}",
"items-claimed": "{claimedCount} of {itemCount} Claimed",
"items-requested": "{itemCount, plural, one {# Requested} other {# Requested}}",
"list-managers": "List managers",
"list-managers-tooltip": "List managers can do everything you can do including approving or denying suggestions, updating list settings, and removing items from the list. If your group is set to 'surprise' mode, any items added by a list manager will be visible to you.",
"list-view": "List View",
Expand Down
70 changes: 50 additions & 20 deletions src/lib/components/ListCard.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import type { List, User } from "$lib/generated/prisma/client";
import { Progress } from "@skeletonlabs/skeleton-svelte";
import Avatar from "./Avatar.svelte";
import { getFormatter } from "$lib/i18n";

Expand All @@ -12,15 +13,28 @@

interface Props {
hideCount?: boolean;
hideOwner?: boolean;
list: ListWithCounts;
hasNewItems?: boolean;
preventNavigate?: boolean;
}

const { hideCount = false, hasNewItems = false, list, preventNavigate = false }: Props = $props();
const {
hideCount = false,
hideOwner = false,
hasNewItems = false,
list,
preventNavigate = false
}: Props = $props();
const t = getFormatter();

let listName = $derived(list.name || $t("wishes.wishes-for", { values: { listOwner: list.owner.name } }));
let availableCount = $derived((list.itemCount ?? 0) - (list.claimedCount ?? 0));
let claimedPercent = $derived(
list.itemCount && list.itemCount > 0
? Math.min(100, Math.round(((list.claimedCount ?? 0) / list.itemCount) * 100))
: 0
);
let iconColor = $derived(list.iconColor);
let elementTag = $derived(preventNavigate ? "div" : "a");
let element: HTMLElement | undefined = $state();
Expand Down Expand Up @@ -59,30 +73,46 @@
>
<iconify-icon class="text-2xl" icon={"ion:" + (list.icon ?? "gift")}></iconify-icon>
</div>
<div class="flex flex-col space-y-1">
<span class="text-primary-900-100 line-clamp-2 text-2xl font-bold md:text-4xl" data-testid="list-name">
<div class="flex flex-col gap-1">
<span class="text-primary-900-100 line-clamp-2 text-xl font-bold md:text-2xl" data-testid="list-name">
{listName}
</span>
<div class="flex flex-row flex-wrap items-center gap-2 text-lg">
<div class="flex flex-row items-center gap-2">
<Avatar class="text-tiny size-6" user={list.owner} />
{#if !hideOwner}
<div class="grid grid-cols-[1.125rem_auto] items-center gap-2">
<Avatar class="text-tiny size-5" user={list.owner} />
<span class="text-surface-800-200" data-testid="list-owner">{list.owner.name}</span>
</div>

{#if list.itemCount !== undefined}
<span>·</span>
<div class="flex flex-row items-center gap-x-2">
<iconify-icon icon="ion:gift"></iconify-icon>
<span data-testid="item-count">
{!hideCount ? `${list.claimedCount}/` : ""}{list.itemCount}
</span>
{#if hasNewItems}
<iconify-icon
class="text-primary-800-200 size-2 opacity-40"
icon="ion:ellipse-sharp"
width="0.5rem"
></iconify-icon>
{/if}
<div class="contents" data-testid="item-count">
<div class="grid grid-cols-[1.125rem_auto_1fr] gap-2 items-center">
<iconify-icon class="justify-self-center" icon="ion:gift"></iconify-icon>
<span>
{#if hideCount}
{$t("wishes.items-requested", { values: { itemCount: list.itemCount } })}
{:else}
<strong>{$t("wishes.items-available", { values: { availableCount } })}</strong>
{/if}
</span>
{#if hasNewItems}
<iconify-icon
class="text-primary-800-200 size-2 opacity-40 self-center"
icon="ion:ellipse-sharp"
width="0.5rem"
></iconify-icon>
{/if}
</div>
{#if list.itemCount !== undefined && !hideCount}
<div class="flex flex-row items-center gap-x-2 w-full">
<Progress class="flex items-center flex-row max-w-64 shrink" max={100} value={claimedPercent}>
<Progress.Track>
<Progress.Range class="bg-primary-500" />
</Progress.Track>
<Progress.Label>
{$t("wishes.items-claimed", {
values: { claimedCount: list.claimedCount, itemCount: list.itemCount }
})}
</Progress.Label>
</Progress>
</div>
{/if}
</div>
Expand Down
14 changes: 12 additions & 2 deletions src/lib/components/wishlists/ManageListForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import ConfirmModal from "../modals/ConfirmModal.svelte";
import SelectListManagerModal from "../modals/SelectListManagerModal.svelte";

interface ListProps extends Partial<Pick<List, "id" | "icon" | "iconColor" | "name" | "public" | "description">> {
interface ListProps extends Partial<
Pick<List, "id" | "icon" | "iconColor" | "name" | "public" | "description" | "hideOwner">
> {
owner: Pick<User, "id" | "name" | "username" | "picture">;
managers: Pick<User, "id" | "name" | "username">[];
}
Expand All @@ -30,6 +32,7 @@
const formId: string = $props.id();

let list = $derived(list_);
let hideOwner = $state(list_.hideOwner ?? false);
let colorElement: Element | undefined = $state();
let defaultColor: string = $derived(
colorElement ? getComputedStyle(colorElement).backgroundColor : list.iconColor || ""
Expand Down Expand Up @@ -119,6 +122,13 @@
<IconSelector id="icon" icon={list.icon} onIconSelected={(icon) => (list.icon = icon)} />
</div>

<div class="col-span-full">
<label class="unstyled flex w-fit flex-row items-center gap-x-2" for="hideOwner">
<input id="hideOwner" name="hideOwner" class="checkbox" type="checkbox" bind:checked={hideOwner} />
<span>{$t("wishes.hide-owner")}</span>
</label>
</div>

<div class="col-span-full">
<label class="label mb-1" for="description">
<span>{$t("general.description")}</span>
Expand Down Expand Up @@ -196,7 +206,7 @@
<div class="col-span-full">
<div class="flex flex-col space-y-2">
<span>{$t("wishes.preview")}</span>
<ListCard hideCount {list} preventNavigate />
<ListCard hideCount {hideOwner} {list} preventNavigate />
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/lib/server/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ListProperties {
icon?: string | null;
iconColor?: string | null;
public?: boolean;
hideOwner?: boolean;
}

export const create = async (ownerId: string, groupId: string, otherData?: ListProperties) => {
Expand Down
1 change: 1 addition & 0 deletions src/lib/server/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const getListPropertiesSchema = () => {
icon: z.string().trim().nullable(),
iconColor: z.string().trim().nullable(),
public: z.coerce.boolean().default(false),
hideOwner: z.coerce.boolean().default(false),
description: z.string().max(10000).nullable(),
managers: z.string().array().nullable().default([])
});
Expand Down
13 changes: 9 additions & 4 deletions src/routes/lists/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const load = (async ({ url }) => {
name: true,
icon: true,
iconColor: true,
hideOwner: true,
owner: {
select: {
id: true,
Expand Down Expand Up @@ -110,6 +111,7 @@ export const load = (async ({ url }) => {
name: true,
icon: true,
iconColor: true,
hideOwner: true,
owner: {
select: {
id: true,
Expand Down Expand Up @@ -165,6 +167,7 @@ export const load = (async ({ url }) => {
name: list.name,
icon: list.icon,
iconColor: list.iconColor,
hideOwner: list.hideOwner,
owner: list.owner,
claimedCount: undefined,
itemCount: list.items.reduce((accum, { item }) => accum + (item.quantity || 1), 0),
Expand All @@ -176,10 +179,11 @@ export const load = (async ({ url }) => {
.map((list) => {
const claimedCount = list.items
.filter((it) => it.approved)
.filter(({ item }) => {
const claimedCount = item.claims.map(({ quantity }) => quantity).reduce((a, b) => a + b, 0);
return claimedCount === item.quantity;
}).length;
.reduce((accum, { item }) => {
if (item.quantity === null) return accum;
const itemClaimed = item.claims.reduce((a, { quantity }) => a + quantity, 0);
return accum + Math.min(itemClaimed, item.quantity);
}, 0);
const itemCount = list.items
.filter((it) => it.approved)
.reduce((accum, { item }) => accum + (item.quantity || 1), 0);
Expand All @@ -189,6 +193,7 @@ export const load = (async ({ url }) => {
name: list.name,
icon: list.icon,
iconColor: list.iconColor,
hideOwner: list.hideOwner,
owner: list.owner,
claimedCount,
itemCount,
Expand Down
6 changes: 3 additions & 3 deletions src/routes/lists/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@
{:else}
<div class="flex flex-col space-y-4" data-testid="list-container" in:fade>
{#each data.myLists as list (list.id)}
<ListCard hideCount {list} />
<ListCard hideCount hideOwner={list.hideOwner} {list} />
{/each}

{#each data.otherLists as list (list.id)}
{#await hasNewItems(list)}
<ListCard {list} />
<ListCard hideOwner={list.hideOwner} {list} />
{:then hasNewItems}
<ListCard {hasNewItems} {list} />
<ListCard {hasNewItems} hideOwner={list.hideOwner} {list} />
{/await}
{/each}
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/routes/lists/[id]/manage/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const load: PageServerLoad = async ({ params }) => {
icon: true,
iconColor: true,
public: true,
hideOwner: true,
owner: {
select: {
id: true,
Expand Down Expand Up @@ -90,6 +91,7 @@ export const actions: Actions = {
icon: form.get("icon"),
iconColor: form.get("iconColor"),
public: form.get("public"),
hideOwner: form.get("hideOwner"),
description: form.get("description"),
managers: form.getAll("managers")
});
Expand Down Expand Up @@ -117,6 +119,7 @@ export const actions: Actions = {
icon: trimToNull(listProperties.data.icon),
iconColor: trimToNull(listProperties.data.iconColor),
public: listProperties.data.public,
hideOwner: listProperties.data.hideOwner,
description: trimToNull(listProperties.data.description)
},
where: {
Expand Down
3 changes: 3 additions & 0 deletions src/routes/lists/create/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const load = (async () => {
picture: user.picture || null
},
description: null,
hideOwner: false,
managers: []
},
listMode: config.listMode,
Expand Down Expand Up @@ -76,6 +77,7 @@ export const actions: Actions = {
icon: form.get("icon"),
iconColor: form.get("iconColor"),
public: form.get("public"),
hideOwner: form.get("hideOwner"),
description: form.get("description")
});
if (listProperties.error) {
Expand All @@ -102,6 +104,7 @@ export const actions: Actions = {
icon: trimToNull(listProperties.data.icon),
iconColor: trimToNull(listProperties.data.iconColor),
public: listProperties.data.public,
hideOwner: listProperties.data.hideOwner,
description: trimToNull(listProperties.data.description)
};
list = await create(user.id, activeMembership.groupId, data);
Expand Down
2 changes: 1 addition & 1 deletion tests/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Features to test
- [ ] Claim
- [ ] Validate claimed name text
- [ ] Claim multiple
- [ ] Claim partial
- [x] Claim partial
- [ ] Purchase
- [ ] Item reactivity
- [ ] Create
Expand Down
28 changes: 28 additions & 0 deletions tests/modules/claim-item-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type Locator, type Page } from "@playwright/test";

export class ClaimItemModal {
private readonly modal: Locator;
private readonly quantityField: Locator;
private readonly claimButton: Locator;

constructor(page: Page) {
this.modal = page.getByRole("dialog");
this.quantityField = this.modal.getByLabel("Enter the quantity to claim");
this.claimButton = this.modal.getByRole("button", { name: "Claim" });
}

async at() {
await this.modal.waitFor({ state: "visible", timeout: 5000 });
return this;
}

async setQuantity(quantity: number) {
await this.quantityField.fill(quantity.toString());
return this;
}

async submit() {
await this.claimButton.click();
await this.modal.waitFor({ state: "detached", timeout: 5000 });
}
}
10 changes: 10 additions & 0 deletions tests/modules/item-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EditItemPage } from "../pageObjects/edit-item.page";
import { Modal } from "./modal";
import { Toast } from "./toast";
import { DeleteItemModal } from "./delete-item-modal";
import { ClaimItemModal } from "./claim-item-modal";

export class ItemCard {
private readonly card: Locator;
Expand Down Expand Up @@ -110,6 +111,15 @@ export class ItemCard {
return this;
}

async claim(quantity: number) {
await this.card.getByRole("button", { name: "Claim", exact: true }).click();
const modal = new ClaimItemModal(this.card.page());
await modal.at();
await modal.setQuantity(quantity);
await modal.submit();
return this;
}

async edit() {
await this.editButton.click();
await this.card.page().waitForURL(/\/items\/\d+\/edit/);
Expand Down
Loading
Loading