Skip to content

Commit 282c1af

Browse files
kevincadorseferturanvladjerca
committed
feat(list): compact my lists hub cards
Co-authored-by: seferturan <seferturan85@gmail.com> Co-authored-by: Vlad Jerca <vlad.jerca@gmail.com> fix(list): use logical inset-inline-start in smart list posters
1 parent ce5a377 commit 282c1af

5 files changed

Lines changed: 409 additions & 45 deletions

File tree

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,49 @@
11
<script lang="ts">
2-
import SmartListIcon from "$lib/components/icons/SmartListIcon.svelte";
32
import SectionList from "$lib/components/lists/section-list/SectionList.svelte";
43
import type { DiscoverMode } from "$lib/features/filters/models/DiscoverMode";
54
import * as m from "$lib/features/i18n/messages.ts";
65
import { UrlBuilder } from "$lib/utils/url/UrlBuilder";
76
import CtaItem from "../components/cta/CtaItem.svelte";
8-
import ListsHeader from "../user/_internal/ListsHeader.svelte";
97
import CreateSmartListButton from "./_internal/CreateSmartListButton.svelte";
10-
import SmartListRenderer from "./SmartListRenderer.svelte";
8+
import SmartListSummaryItem from "./_internal/SmartListSummaryItem.svelte";
119
import { useSmartLists } from "./useSmartLists";
1210
1311
const { mode }: { mode: DiscoverMode } = $props();
1412
15-
const { list } = $derived(useSmartLists({ mode }));
13+
const { list, isLoading } = $derived(useSmartLists({ mode }));
1614
1715
const drilldown = $derived({
1816
href: UrlBuilder.lists.smart.all(),
1917
label: m.button_label_view_all_lists(),
2018
source: { id: "smart-lists" },
21-
mode: $list.length === 0 ? ("disabled" as const) : ("default" as const),
19+
mode: "always" as const,
2220
});
2321
</script>
2422

2523
{#snippet actions()}
2624
<CreateSmartListButton />
2725
{/snippet}
2826

29-
{#if $list.length === 0}
30-
<SectionList
31-
id={{
32-
scope: "smart-lists",
33-
}}
34-
items={[]}
35-
title="Smart Lists"
36-
--height-list="var(--height-poster-list-sm)"
37-
{drilldown}
38-
{actions}
39-
>
40-
{#snippet item(_)}{/snippet}
41-
{#snippet empty()}
42-
<CtaItem cta={{ type: "smart-list" }} variant="placeholder" />
43-
{/snippet}
44-
</SectionList>
45-
{:else}
46-
<div class="trakt-smart-lists">
47-
<ListsHeader title="Smart Lists" {actions} {drilldown}>
48-
{#snippet icon()}
49-
<SmartListIcon />
50-
{/snippet}
51-
</ListsHeader>
52-
53-
<SmartListRenderer mode={mode ?? "media"} />
54-
</div>
55-
{/if}
27+
<SectionList
28+
id={{
29+
scope: "smart-lists",
30+
}}
31+
items={$list}
32+
title={m.list_title_smart_lists()}
33+
--height-list="var(--height-lists-list)"
34+
--height-override-list={$list.length === 0
35+
? "var(--height-poster-list-sm)"
36+
: undefined}
37+
{drilldown}
38+
{actions}
39+
>
40+
{#snippet item(list)}
41+
<SmartListSummaryItem {list} />
42+
{/snippet}
5643

57-
<style>
58-
.trakt-smart-lists {
59-
display: flex;
60-
flex-direction: column;
61-
gap: var(--gap-micro);
62-
}
63-
</style>
44+
{#snippet empty()}
45+
{#if !$isLoading}
46+
<CtaItem cta={{ type: "smart-list" }} variant="placeholder" />
47+
{/if}
48+
{/snippet}
49+
</SectionList>
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
<script lang="ts">
2+
import Card from "$lib/components/card/Card.svelte";
3+
import MovieIcon from "$lib/components/icons/MovieIcon.svelte";
4+
import ShowIcon from "$lib/components/icons/ShowIcon.svelte";
5+
import Link from "$lib/components/link/Link.svelte";
6+
import * as m from "$lib/features/i18n/messages.ts";
7+
import CrossOriginImage from "$lib/features/image/components/CrossOriginImage.svelte";
8+
import type { SmartList } from "$lib/requests/queries/users/smartListQuery.ts";
9+
import { toTranslatedGenre } from "$lib/utils/formatting/string/toTranslatedGenre";
10+
import { UrlBuilder } from "$lib/utils/url/UrlBuilder";
11+
import SmartListActions from "./SmartListActions.svelte";
12+
import { useSmartListPreview } from "./useSmartListPreview";
13+
14+
const { list }: { list: SmartList } = $props();
15+
16+
const href = $derived(UrlBuilder.lists.smart.view(list.id));
17+
const { posters } = $derived(useSmartListPreview(list));
18+
const filterSummary = $derived(getFilterSummary(list));
19+
20+
function targetLabel(target: SmartList["target"]) {
21+
switch (target) {
22+
case "trending":
23+
return m.list_title_trending();
24+
case "popular":
25+
return m.list_title_most_popular();
26+
case "anticipated":
27+
return m.list_title_most_anticipated();
28+
case "unknown":
29+
return m.list_title_smart_lists();
30+
}
31+
}
32+
33+
function splitValues(value: string): string[] {
34+
return value.split(",").map((item) => item.trim()).filter(Boolean);
35+
}
36+
37+
function toTitleCase(value: string): string {
38+
return value
39+
.replaceAll("-", " ")
40+
.replaceAll("_", " ")
41+
.replace(/\b\w/g, (character) => character.toUpperCase());
42+
}
43+
44+
function formatRange(
45+
value: string,
46+
formatter: (range: { min: number; max: number }) => string,
47+
): string {
48+
const [minValue, maxValue] = value.split("-");
49+
50+
if (!minValue || !maxValue) {
51+
return formatPlainRange(value);
52+
}
53+
54+
const min = Number(minValue);
55+
const max = Number(maxValue);
56+
57+
if (Number.isNaN(min) || Number.isNaN(max)) {
58+
return value;
59+
}
60+
61+
return formatter({ min, max });
62+
}
63+
64+
function formatPlainRange(value: string): string {
65+
const [min, max] = value.split("-");
66+
67+
if (min && max) {
68+
return `${min}-${max}`;
69+
}
70+
71+
if (min) {
72+
return `${min}+`;
73+
}
74+
75+
if (max) {
76+
return `Up to ${max}`;
77+
}
78+
79+
return value;
80+
}
81+
82+
function formatPercentRange(value: string, label: string): string {
83+
const [min, max] = value.split("-");
84+
85+
if (min && max) {
86+
return `${label} ${min}-${max}%`;
87+
}
88+
89+
if (min) {
90+
return `${label} ${min}%+`;
91+
}
92+
93+
if (max) {
94+
return `${label} up to ${max}%`;
95+
}
96+
97+
return `${label} ${value}`;
98+
}
99+
100+
function formatFilter(key: string, value: string): string[] {
101+
if (value.length === 0) {
102+
return [];
103+
}
104+
105+
switch (key) {
106+
case "genres":
107+
case "subgenres":
108+
return splitValues(value).map((genre) => toTranslatedGenre(genre));
109+
case "years":
110+
return [formatRange(value, m.advanced_filter_label_release_year)];
111+
case "runtimes":
112+
return [formatRange(value, m.advanced_filter_label_runtime)];
113+
case "ratings":
114+
return [formatPercentRange(value, "Trakt")];
115+
case "imdb_ratings":
116+
return [`IMDb ${formatPlainRange(value)}`];
117+
case "rt_meters":
118+
return [formatPercentRange(value, "RT")];
119+
case "rt_user_meters":
120+
return [formatPercentRange(value, "RT Audience")];
121+
case "certifications":
122+
return splitValues(value).map((certification) =>
123+
certification.toUpperCase()
124+
);
125+
case "countries":
126+
return splitValues(value).map((country) => country.toUpperCase());
127+
case "statuses":
128+
case "watchnow":
129+
return splitValues(value).map(toTitleCase);
130+
case "ignore_watched":
131+
return value === "true" ? [m.header_hide_watched()] : [];
132+
case "ignore_watchlisted":
133+
return value === "true" ? [m.header_hide_watchlisted()] : [];
134+
default:
135+
return [`${toTitleCase(key)} ${value}`];
136+
}
137+
}
138+
139+
function getFilterSummary(list: SmartList): string {
140+
const filters = Object.entries(list.params)
141+
.flatMap(([key, value]) => formatFilter(key, value));
142+
143+
return [targetLabel(list.target), ...filters].join(", ");
144+
}
145+
</script>
146+
147+
<Card
148+
--width-card="min(var(--width-list-card), 85vw)"
149+
--height-card="var(--height-list-card)"
150+
>
151+
<div class="trakt-smart-list-summary">
152+
<div class="trakt-smart-list-header">
153+
<div class="trakt-smart-list-icon" aria-hidden="true">
154+
{#if list.type === "movie"}
155+
<MovieIcon />
156+
{:else}
157+
<ShowIcon />
158+
{/if}
159+
</div>
160+
161+
<div class="trakt-smart-list-title">
162+
<Link {href}>
163+
<p class="secondary bold ellipsis">{list.title}</p>
164+
</Link>
165+
<p class="secondary small ellipsis">
166+
{filterSummary}
167+
</p>
168+
</div>
169+
170+
<SmartListActions {list} />
171+
</div>
172+
173+
<Link {href} color="inherit">
174+
<div
175+
class="trakt-smart-list-posters"
176+
style="--poster-count: {$posters.length}"
177+
>
178+
{#each $posters as poster, index (`${list.id}_poster_${index}`)}
179+
<div class="poster-wrapper" style="--poster-index: {index}">
180+
<CrossOriginImage
181+
src={poster}
182+
alt={m.image_alt_list_preview_poster({ title: list.title })}
183+
/>
184+
</div>
185+
{/each}
186+
</div>
187+
</Link>
188+
</div>
189+
</Card>
190+
191+
<style lang="scss">
192+
@use "$style/scss/mixins/index" as *;
193+
194+
.trakt-smart-list-summary {
195+
--smart-list-summary-background: color-mix(
196+
in srgb,
197+
var(--color-card-background) 88%,
198+
var(--color-foreground)
199+
);
200+
201+
height: var(--height-list-card);
202+
box-sizing: border-box;
203+
204+
display: flex;
205+
flex-direction: column;
206+
gap: var(--gap-m);
207+
208+
padding: var(--ni-12);
209+
210+
outline: var(--border-thickness-xs) solid transparent;
211+
transition: outline-color var(--transition-increment) ease-in-out;
212+
213+
background-color: var(--smart-list-summary-background);
214+
border-radius: var(--border-radius-m);
215+
}
216+
217+
.trakt-smart-list-header {
218+
display: flex;
219+
align-items: center;
220+
gap: var(--gap-xs);
221+
222+
min-width: 0;
223+
}
224+
225+
.trakt-smart-list-icon {
226+
display: flex;
227+
align-items: center;
228+
justify-content: center;
229+
230+
width: var(--ni-40);
231+
height: var(--ni-40);
232+
flex-shrink: 0;
233+
234+
color: var(--color-text-secondary);
235+
background-color: color-mix(
236+
in srgb,
237+
var(--color-card-background) 78%,
238+
var(--color-foreground)
239+
);
240+
border-radius: var(--border-radius-s);
241+
242+
:global(svg) {
243+
width: var(--ni-24);
244+
height: var(--ni-24);
245+
}
246+
}
247+
248+
.trakt-smart-list-title {
249+
display: grid;
250+
min-width: 0;
251+
flex-grow: 1;
252+
253+
:global(.trakt-link) {
254+
min-width: 0;
255+
text-decoration: none;
256+
}
257+
}
258+
259+
.trakt-smart-list-posters {
260+
--poster-width: var(--ni-120);
261+
--poster-height: var(--ni-180);
262+
263+
height: var(--poster-height);
264+
width: 100%;
265+
266+
display: flex;
267+
flex-shrink: 0;
268+
269+
position: relative;
270+
271+
counter-reset: number;
272+
}
273+
274+
.poster-wrapper {
275+
--poster-index: 0;
276+
277+
--poster-overlap: calc(var(--poster-width) / 5);
278+
--total-poster-width: calc(
279+
(var(--poster-width) - var(--poster-overlap)) * var(--poster-count)
280+
);
281+
--poster-spread-width: min(100%, var(--total-poster-width));
282+
--poster-offset: calc(
283+
(var(--poster-spread-width) - var(--poster-width)) /
284+
max(1, var(--poster-count) - 1)
285+
);
286+
287+
position: absolute;
288+
inset-inline-start: calc(var(--poster-offset) * var(--poster-index));
289+
290+
height: var(--poster-height);
291+
width: var(--poster-width);
292+
293+
box-shadow: var(--shadow-floating);
294+
border-radius: var(--border-radius-m);
295+
overflow: hidden;
296+
297+
:global(img) {
298+
width: 100%;
299+
height: 100%;
300+
}
301+
}
302+
303+
@include for-mouse() {
304+
:global(.trakt-card-content:hover) .trakt-smart-list-summary {
305+
outline-color: var(--color-card-border-hover);
306+
}
307+
}
308+
</style>

0 commit comments

Comments
 (0)