From 282c1aff558b062b54defc5c525ac0b73e7f809b Mon Sep 17 00:00:00 2001 From: Kevin Cador Date: Mon, 22 Jun 2026 15:28:55 +0200 Subject: [PATCH 1/3] feat(list): compact my lists hub cards Co-authored-by: seferturan Co-authored-by: Vlad Jerca fix(list): use logical inset-inline-start in smart list posters --- .../sections/lists/smart/SmartLists.svelte | 64 ++-- .../_internal/SmartListSummaryItem.svelte | 308 ++++++++++++++++++ .../smart/_internal/useSmartListPreview.ts | 48 +++ .../sections/lists/user/PersonalLists.svelte | 21 +- .../routes/users/[user]/lists/+page.svelte | 13 +- 5 files changed, 409 insertions(+), 45 deletions(-) create mode 100644 projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte create mode 100644 projects/client/src/lib/sections/lists/smart/_internal/useSmartListPreview.ts diff --git a/projects/client/src/lib/sections/lists/smart/SmartLists.svelte b/projects/client/src/lib/sections/lists/smart/SmartLists.svelte index 2e3b778d4b..7f7bf1e9b3 100644 --- a/projects/client/src/lib/sections/lists/smart/SmartLists.svelte +++ b/projects/client/src/lib/sections/lists/smart/SmartLists.svelte @@ -1,24 +1,22 @@ @@ -26,38 +24,26 @@ {/snippet} -{#if $list.length === 0} - - {#snippet item(_)}{/snippet} - {#snippet empty()} - - {/snippet} - -{:else} -
- - {#snippet icon()} - - {/snippet} - - - -
-{/if} + + {#snippet item(list)} + + {/snippet} - + {#snippet empty()} + {#if !$isLoading} + + {/if} + {/snippet} + diff --git a/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte b/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte new file mode 100644 index 0000000000..601df8e6e4 --- /dev/null +++ b/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte @@ -0,0 +1,308 @@ + + + +
+
+ + +
+ +

{list.title}

+ +

+ {filterSummary} +

+
+ + +
+ + +
+ {#each $posters as poster, index (`${list.id}_poster_${index}`)} +
+ +
+ {/each} +
+ +
+
+ + diff --git a/projects/client/src/lib/sections/lists/smart/_internal/useSmartListPreview.ts b/projects/client/src/lib/sections/lists/smart/_internal/useSmartListPreview.ts new file mode 100644 index 0000000000..296734e31c --- /dev/null +++ b/projects/client/src/lib/sections/lists/smart/_internal/useSmartListPreview.ts @@ -0,0 +1,48 @@ +import { map, of } from 'rxjs'; +import type { SmartList } from '$lib/requests/queries/users/smartListQuery.ts'; +import { useAnticipatedList } from '../../anticipated/useAnticipatedList.ts'; +import { usePopularList } from '../../popular/usePopularList.ts'; +import { useTrendingList } from '../../trending/useTrendingList.ts'; + +const previewLimit = 8; + +function smartListToPreviewQuery(list: SmartList) { + const params = { + limit: previewLimit, + search: list.params, + type: list.type, + }; + + switch (list.target) { + case 'trending': + return useTrendingList(params); + case 'popular': + return usePopularList(params); + case 'anticipated': + return useAnticipatedList(params); + case 'unknown': + return undefined; + } +} + +export function useSmartListPreview(list: SmartList) { + const query = smartListToPreviewQuery(list); + + if (!query) { + return { + isLoading: of(false), + posters: of([]), + }; + } + + return { + isLoading: query.isLoading, + posters: query.list.pipe( + map((items) => + items + .map((item) => item.poster.url.thumb) + .slice(0, previewLimit) + ), + ), + }; +} diff --git a/projects/client/src/lib/sections/lists/user/PersonalLists.svelte b/projects/client/src/lib/sections/lists/user/PersonalLists.svelte index dccec18d0e..c3f52494d2 100644 --- a/projects/client/src/lib/sections/lists/user/PersonalLists.svelte +++ b/projects/client/src/lib/sections/lists/user/PersonalLists.svelte @@ -19,12 +19,19 @@ import UserList from "./UserList.svelte"; const previewLimit = 3; + type PersonalListsDisplay = "auto" | "compact"; const { type, slug, mode, - }: { type: PersonalListType; slug: string; mode?: DiscoverMode } = $props(); + display = "auto", + }: { + type: PersonalListType; + slug: string; + mode?: DiscoverMode; + display?: PersonalListsDisplay; + } = $props(); const { list: lists, @@ -35,13 +42,20 @@ const { isMe } = $derived(useIsMe(slug)); const variant = $derived.by(() => { + if (display === "compact") { + return "summary"; + } + const shouldShowSummary = $hasNextPage || $lists.length === 0 || $lists.length > previewLimit; return shouldShowSummary ? "summary" : "preview"; }); const isMine = $derived(type === "personal" && $isMe); - const isPresentable = $derived(isMine || (!$isLoading && $lists.length > 0)); + const isPresentable = $derived( + (display === "compact" ? $isMe : isMine) || + (!$isLoading && $lists.length > 0), + ); const title = $derived.by(() => { switch (type) { @@ -111,6 +125,7 @@ href: UrlBuilder.lists.all(slug, type), label: m.button_label_view_all_lists(), source: { id: "personal-lists", type }, + mode: "always", }} --height-list="var(--height-lists-list)" --height-override-list={$lists.length === 0 @@ -148,6 +163,6 @@ .trakt-lists-preview { display: flex; flex-direction: column; - gap: var(--gap-micro); + gap: var(--content-gap); } diff --git a/projects/client/src/routes/users/[user]/lists/+page.svelte b/projects/client/src/routes/users/[user]/lists/+page.svelte index 15d25f17e4..0c41a8406e 100644 --- a/projects/client/src/routes/users/[user]/lists/+page.svelte +++ b/projects/client/src/routes/users/[user]/lists/+page.svelte @@ -9,6 +9,7 @@ import TraktPageCoverSetter from "$lib/sections/layout/TraktPageCoverSetter.svelte"; import SmartLists from "$lib/sections/lists/smart/SmartLists.svelte"; import PersonalLists from "$lib/sections/lists/user/PersonalLists.svelte"; + import type { PersonalListType } from "$lib/sections/lists/user/models/PersonalListType.ts"; import WatchList from "$lib/sections/lists/watchlist/WatchList.svelte"; import NavbarStateSetter from "$lib/sections/navbar/NavbarStateSetter.svelte"; import { DEFAULT_SHARE_COVER } from "$lib/utils/assets"; @@ -21,6 +22,12 @@ const { user } = useUser(); const { isMe } = $derived(useIsMe(params.user)); + + const personalListTypes: PersonalListType[] = [ + "personal", + "liked", + "collaboration", + ]; - - - + {#each personalListTypes as type (type)} + + {/each} From f19309b70277ae35b3b906007df3a2a783f8a5d3 Mon Sep 17 00:00:00 2001 From: seferturan Date: Tue, 14 Jul 2026 21:15:57 +0200 Subject: [PATCH 2/3] refactor(list): extracts common list summary card --- .../lists/components/ListSummaryCard.svelte | 61 ++++++++++ .../list-summary/ListSummaryItem.svelte | 57 +-------- .../_internal/SmartListSummaryItem.svelte | 108 ++++++------------ 3 files changed, 102 insertions(+), 124 deletions(-) create mode 100644 projects/client/src/lib/sections/lists/components/ListSummaryCard.svelte diff --git a/projects/client/src/lib/sections/lists/components/ListSummaryCard.svelte b/projects/client/src/lib/sections/lists/components/ListSummaryCard.svelte new file mode 100644 index 0000000000..89c46a2de2 --- /dev/null +++ b/projects/client/src/lib/sections/lists/components/ListSummaryCard.svelte @@ -0,0 +1,61 @@ + + + +
+ {@render children()} +
+
+ + diff --git a/projects/client/src/lib/sections/lists/components/list-summary/ListSummaryItem.svelte b/projects/client/src/lib/sections/lists/components/list-summary/ListSummaryItem.svelte index 8667ac6af6..b613cdbf53 100644 --- a/projects/client/src/lib/sections/lists/components/list-summary/ListSummaryItem.svelte +++ b/projects/client/src/lib/sections/lists/components/list-summary/ListSummaryItem.svelte @@ -1,7 +1,7 @@ - -
- - -
-
- - + + + + diff --git a/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte b/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte index 601df8e6e4..0ce9a9853c 100644 --- a/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte +++ b/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte @@ -1,11 +1,11 @@ - -
-
- - -
- -

{list.title}

- -

- {filterSummary} -

-
- - + +
+ - -
- {#each $posters as poster, index (`${list.id}_poster_${index}`)} -
- -
- {/each} -
- -
- - - From 170b0622eafab4f20c2f1b020eea504958c6977e Mon Sep 17 00:00:00 2001 From: seferturan Date: Wed, 15 Jul 2026 09:46:08 +0200 Subject: [PATCH 3/3] fix(list): internationalize and improve range filter summaries Adds new i18n messages for upper-bound-only range filters (e.g., "Up to 90"). Introduces locale-aware percentage formatting for all smart list range filter summaries, ensuring percentage values are displayed correctly according to the user's locale. Changes from ranges to be worded to kill bidi risks. --- projects/client/i18n/meta/en.json | 69 +++++++++++++++++++ .../_internal/SmartListSummaryItem.svelte | 34 +++++++-- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/projects/client/i18n/meta/en.json b/projects/client/i18n/meta/en.json index 29aa170b20..d81daa9c00 100644 --- a/projects/client/i18n/meta/en.json +++ b/projects/client/i18n/meta/en.json @@ -6963,6 +6963,75 @@ } } }, + "list_summary_range_up_to": { + "default": "Up to {value}", + "description": "Smart list filter summary for an upper-bound-only range, e.g. 'Up to 90'.", + "variables": { + "value": { + "type": "string" + } + } + }, + "list_summary_range_labeled_up_to": { + "default": "{label} up to {value}", + "description": "Smart list filter summary for a labeled upper-bound-only range, e.g. 'Trakt up to 80%'.", + "variables": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "list_summary_range_between": { + "default": "{min} to {max}", + "description": "Smart list filter summary for a range with both bounds, e.g. '60 to 80'.", + "variables": { + "min": { + "type": "string" + }, + "max": { + "type": "string" + } + } + }, + "list_summary_range_labeled_between": { + "default": "{label} {min} to {max}", + "description": "Smart list filter summary for a labeled range with both bounds, e.g. 'Trakt 60% to 80%'.", + "variables": { + "label": { + "type": "string" + }, + "min": { + "type": "string" + }, + "max": { + "type": "string" + } + } + }, + "list_summary_range_from": { + "default": "{value} and up", + "description": "Smart list filter summary for a lower-bound-only range, e.g. '60 and up'.", + "variables": { + "value": { + "type": "string" + } + } + }, + "list_summary_range_labeled_from": { + "default": "{label} {value} and up", + "description": "Smart list filter summary for a labeled lower-bound-only range, e.g. 'Trakt 60% and up'.", + "variables": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "warning_smart_lists_limit": { "default": "You've reached your smart list limit", "description": "Warning text shown when a user has reached their smart list limit and attempts to create a new smart list." diff --git a/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte b/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte index 0ce9a9853c..e7737e6410 100644 --- a/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte +++ b/projects/client/src/lib/sections/lists/smart/_internal/SmartListSummaryItem.svelte @@ -2,10 +2,12 @@ import MovieIcon from "$lib/components/icons/MovieIcon.svelte"; import ShowIcon from "$lib/components/icons/ShowIcon.svelte"; import Link from "$lib/components/link/Link.svelte"; + import { languageTag } from "$lib/features/i18n/index.ts"; import * as m from "$lib/features/i18n/messages.ts"; import CrossOriginImage from "$lib/features/image/components/CrossOriginImage.svelte"; import type { SmartList } from "$lib/requests/queries/users/smartListQuery.ts"; import ListSummaryCard from "$lib/sections/lists/components/ListSummaryCard.svelte"; + import { toPercentage } from "$lib/utils/formatting/number/toPercentage.ts"; import { toTranslatedGenre } from "$lib/utils/formatting/string/toTranslatedGenre"; import { UrlBuilder } from "$lib/utils/url/UrlBuilder"; import SmartListActions from "./SmartListActions.svelte"; @@ -65,33 +67,53 @@ const [min, max] = value.split("-"); if (min && max) { - return `${min}-${max}`; + return m.list_summary_range_between({ min, max }); } if (min) { - return `${min}+`; + return m.list_summary_range_from({ value: min }); } if (max) { - return `Up to ${max}`; + return m.list_summary_range_up_to({ value: max }); } return value; } + function toRatingPercentage(value: string): string { + const numeric = Number(value); + + if (Number.isNaN(numeric)) { + return value; + } + + return toPercentage(numeric / 100, languageTag()); + } + function formatPercentRange(value: string, label: string): string { const [min, max] = value.split("-"); if (min && max) { - return `${label} ${min}-${max}%`; + return m.list_summary_range_labeled_between({ + label, + min: toRatingPercentage(min), + max: toRatingPercentage(max), + }); } if (min) { - return `${label} ${min}%+`; + return m.list_summary_range_labeled_from({ + label, + value: toRatingPercentage(min), + }); } if (max) { - return `${label} up to ${max}%`; + return m.list_summary_range_labeled_up_to({ + label, + value: toRatingPercentage(max), + }); } return `${label} ${value}`;