Skip to content

Commit e1e4663

Browse files
kevincadorseferturanvladjerca
authored andcommitted
feat(settings): add preview feature descriptions and improved UI/UX
Co-authored-by: seferturan <seferturan85@gmail.com> Co-authored-by: Vlad Jerca <vlad.jerca@gmail.com>
1 parent 88b2fd8 commit e1e4663

6 files changed

Lines changed: 317 additions & 35 deletions

File tree

projects/client/i18n/meta/en.json

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7053,13 +7053,78 @@
70537053
"description": "Header for the section that allows users to enable or disable preview features."
70547054
},
70557055
"description_preview_features": {
7056-
"default": "Enable or disable preview features that are in development. These features may be unstable and can be changed at any time.",
7056+
"default": "Try upcoming features before they’re broadly available. Preview Features may change as we refine them.",
70577057
"description": "Description for the preview features section."
70587058
},
70597059
"text_placeholder_preview_features": {
70607060
"default": "Currently there aren't any preview features. Check back later.",
70617061
"description": "Placeholder text for when there are no preview features available."
70627062
},
7063+
"link_label_open_preview_feature": {
7064+
"default": "Open {title}",
7065+
"description": "Aria-label for the link that opens an enabled preview feature.",
7066+
"variables": {
7067+
"title": {
7068+
"type": "string"
7069+
}
7070+
}
7071+
},
7072+
"preview_feature_title_year_in_review": {
7073+
"default": "Year In Review",
7074+
"description": "Title for the Year In Review preview feature."
7075+
},
7076+
"preview_feature_description_year_in_review": {
7077+
"default": "Preview Year in Review, now running directly in V3.",
7078+
"description": "Description for the Year In Review preview feature."
7079+
},
7080+
"preview_feature_title_month_in_review": {
7081+
"default": "Month In Review",
7082+
"description": "Title for the Month In Review preview feature."
7083+
},
7084+
"preview_feature_description_month_in_review": {
7085+
"default": "Preview Month in Review, now running directly in V3.",
7086+
"description": "Description for the Month In Review preview feature."
7087+
},
7088+
"preview_feature_title_edit_mode": {
7089+
"default": "Edit Mode",
7090+
"description": "Title for the Edit Mode preview feature."
7091+
},
7092+
"preview_feature_description_edit_mode": {
7093+
"default": "Enable controls for hiding or showing configurable sections across the app.",
7094+
"description": "Description for the Edit Mode preview feature."
7095+
},
7096+
"preview_feature_title_streaming_sync": {
7097+
"default": "Streaming Sync",
7098+
"description": "Title for the Streaming Sync preview feature."
7099+
},
7100+
"preview_feature_description_streaming_sync": {
7101+
"default": "Connect supported streaming services so watch history and ratings can sync back to Trakt automatically. The connection flow from web is still in beta.",
7102+
"description": "Description for the Streaming Sync preview feature."
7103+
},
7104+
"preview_feature_title_scoped_favorites": {
7105+
"default": "Scoped Favorites",
7106+
"description": "Title for the Scoped Favorites preview feature."
7107+
},
7108+
"preview_feature_description_scoped_favorites": {
7109+
"default": "Time-box favorite lists by showing current-year favorites on profiles and grouping the full favorites list by year.",
7110+
"description": "Description for the Scoped Favorites preview feature."
7111+
},
7112+
"preview_feature_title_social_activities": {
7113+
"default": "Social Activities",
7114+
"description": "Title for the Social Activities preview feature."
7115+
},
7116+
"preview_feature_description_social_activities": {
7117+
"default": "Show who you follow has watched, watchlisted, commented on, or rated a movie, show, or episode directly on media pages.",
7118+
"description": "Description for the Social Activities preview feature."
7119+
},
7120+
"preview_feature_title_plex_sync": {
7121+
"default": "Plex Sync",
7122+
"description": "Title for the Plex Sync preview feature."
7123+
},
7124+
"preview_feature_description_plex_sync": {
7125+
"default": "Connect Plex to sync your watch history, ratings, and library to Trakt. Includes connection, server selection, and sync tools.",
7126+
"description": "Description for the Plex Sync preview feature."
7127+
},
70637128
"header_official_links": {
70647129
"default": "Official Links",
70657130
"description": "Header for the official links section, with official links for a show or movie, like the official site, instagram, etc.."
Lines changed: 137 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,160 @@
11
<script lang="ts">
2+
import CaretRightIcon from "$lib/components/icons/CaretRightIcon.svelte";
3+
import Link from "$lib/components/link/Link.svelte";
24
import Switch from "$lib/components/toggles/Switch.svelte";
35
import { m } from "$lib/features/i18n/messages";
46
import { appendClassList } from "$lib/utils/actions/appendClassList";
57
import { FeatureFlag } from "./models/FeatureFlag";
8+
import { featureFlagDefinitions } from "./models/featureFlagDefinitions";
69
import { useFeatureFlag } from "./useFeatureFlag";
710
811
const { classList = "" }: { classList?: string } = $props();
912
1013
const { flags, setFlag } = useFeatureFlag();
11-
12-
const hasFlags = $derived(Object.keys($flags).length > 0);
13-
14-
const getKeyLabel = (key: string) =>
15-
key
16-
.split("-")
17-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
18-
.join(" ");
14+
const featureFlags = Object.values(FeatureFlag);
15+
const hasFlags = featureFlags.length > 0;
1916
</script>
2017

2118
{#if hasFlags}
22-
{#each Object.entries($flags) as [key, value] (key)}
23-
<div class="feature-flag-item" use:appendClassList={classList}>
24-
<span class="bold">{getKeyLabel(key)}</span>
25-
<Switch
26-
color="orange"
27-
label={key}
28-
checked={value}
29-
innerText={value ? "On" : "Off"}
30-
onclick={() => setFlag(key as FeatureFlag, !value)}
31-
/>
32-
</div>
33-
{/each}
19+
<div class="feature-flag-list" use:appendClassList={classList}>
20+
{#each featureFlags as key (key)}
21+
{@const value = $flags[key]}
22+
{@const definition = featureFlagDefinitions[key]}
23+
{@const Icon = definition.icon}
24+
{@const title = definition.title()}
25+
{@const description = definition.description?.() ?? ""}
26+
{@const featureLink = definition.featureLink?.()}
27+
<div class="feature-flag-item">
28+
<div class="feature-flag-icon">
29+
<Icon />
30+
</div>
31+
32+
<div class="feature-flag-copy">
33+
<span class="feature-flag-title bold">{title}</span>
34+
{#if description}
35+
<p class="feature-flag-description secondary small">
36+
{description}
37+
</p>
38+
{/if}
39+
</div>
40+
41+
<div class="feature-flag-actions">
42+
<Switch
43+
color="purple"
44+
label={title}
45+
checked={value}
46+
innerText={value ? "On" : "Off"}
47+
onclick={() => setFlag(key, !value)}
48+
/>
49+
50+
<span class="feature-flag-link" aria-hidden={!value || !featureLink}>
51+
{#if value && featureLink}
52+
<Link
53+
href={featureLink.href}
54+
label={featureLink.label()}
55+
color="inherit"
56+
>
57+
<CaretRightIcon />
58+
</Link>
59+
{/if}
60+
</span>
61+
</div>
62+
</div>
63+
{/each}
64+
</div>
3465
{:else}
3566
<p class="bold">{m.text_placeholder_preview_features()}</p>
3667
{/if}
3768

38-
<style>
69+
<style lang="scss">
70+
@use "$style/scss/mixins/index" as *;
71+
72+
.feature-flag-list {
73+
overflow: hidden;
74+
max-width: var(--ni-640);
75+
border-radius: var(--border-radius-l);
76+
background: var(--color-card-background);
77+
box-shadow: var(--shadow-base);
78+
79+
> :global(* + *) {
80+
border-top: var(--border-thickness-xxs) solid
81+
color-mix(in srgb, var(--color-foreground) 8%, transparent);
82+
}
83+
}
84+
3985
.feature-flag-item {
4086
display: flex;
4187
align-items: center;
42-
justify-content: flex-end;
43-
4488
gap: var(--gap-m);
89+
min-height: var(--ni-80);
90+
padding: var(--gap-s) var(--gap-m);
91+
box-sizing: border-box;
92+
93+
.feature-flag-icon {
94+
display: flex;
95+
flex-shrink: 0;
96+
align-items: center;
97+
justify-content: center;
98+
width: var(--ni-36);
99+
height: var(--ni-36);
100+
border-radius: var(--border-radius-m);
101+
background: color-mix(in srgb, var(--purple-500) 15%, transparent);
102+
color: var(--purple-500);
103+
104+
:global(svg) {
105+
width: var(--ni-20);
106+
height: var(--ni-20);
107+
}
108+
}
109+
110+
.feature-flag-copy {
111+
display: flex;
112+
flex: 1;
113+
flex-direction: column;
114+
gap: var(--gap-xxs);
115+
min-width: 0;
116+
}
117+
118+
.feature-flag-description {
119+
line-height: 1.3;
120+
white-space: pre-line;
121+
}
122+
123+
.feature-flag-actions {
124+
display: flex;
125+
flex-shrink: 0;
126+
align-items: center;
127+
gap: var(--gap-xs);
128+
}
129+
130+
.feature-flag-link {
131+
display: flex;
132+
align-items: center;
133+
justify-content: center;
134+
width: var(--ni-28);
135+
height: var(--ni-28);
136+
opacity: 0.35;
137+
color: var(--color-text-secondary);
138+
text-decoration: none;
139+
140+
:global(.trakt-link) {
141+
display: flex;
142+
color: inherit;
143+
text-decoration: none;
144+
}
145+
146+
:global(svg) {
147+
width: var(--ni-16);
148+
height: var(--ni-16);
149+
}
150+
}
151+
152+
@include for-mobile {
153+
align-items: flex-start;
154+
155+
.feature-flag-actions {
156+
flex-direction: column;
157+
}
158+
}
45159
}
46160
</style>

projects/client/src/lib/features/feature-flag/FeatureFlagTool.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
</ActionButton>
2424

2525
{#if $isOpen}
26-
<Drawer {onClose} title={m.header_preview_features()} hasAutoClose={false}>
26+
<Drawer
27+
{onClose}
28+
title={m.header_preview_features()}
29+
hasAutoClose={false}
30+
size="auto"
31+
>
2732
<FeatureFlagItems />
2833
</Drawer>
2934
{/if}

projects/client/src/lib/features/feature-flag/_internal/createFeatureFlagContext.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ export const FEATURE_FLAG_LOCAL_STORAGE_KEY = 'trakt-feature-flags';
99

1010
function initializeFlags() {
1111
const storedFlags = safeLocalStorage.getItem(FEATURE_FLAG_LOCAL_STORAGE_KEY);
12-
const parsedFlags = storedFlags ? JSON.parse(storedFlags) : {};
12+
const parsedFlags = storedFlags
13+
? JSON.parse(storedFlags) as Partial<Record<FeatureFlag, boolean>>
14+
: {};
15+
const featureFlags = Object.values(FeatureFlag);
1316

14-
return Object.values(FeatureFlag).reduce((acc, flag) => {
15-
acc[flag] = parsedFlags[flag] ?? false;
16-
return acc;
17-
}, {} as Record<string, boolean>);
17+
return Object.fromEntries(
18+
featureFlags.map((flag) => [flag, parsedFlags[flag] ?? false]),
19+
) as Record<FeatureFlag, boolean>;
1820
}
1921

2022
export function createFeatureFlagContext() {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import CalendarIcon from '$lib/components/icons/CalendarIcon.svelte';
2+
import EditModeIcon from '$lib/components/icons/EditModeIcon.svelte';
3+
import FavoriteIcon from '$lib/components/icons/FavoriteIcon.svelte';
4+
import PlexLibraryIcon from '$lib/components/icons/PlexLibraryIcon.svelte';
5+
import SocialIcon from '$lib/components/icons/SocialIcon.svelte';
6+
import TrackIcon from '$lib/components/icons/TrackIcon.svelte';
7+
import { m } from '$lib/features/i18n/messages.ts';
8+
import { UrlBuilder } from '$lib/utils/url/UrlBuilder.ts';
9+
import type { Component } from 'svelte';
10+
import { FeatureFlag } from './FeatureFlag.ts';
11+
12+
type FeatureFlagLink = {
13+
href: string;
14+
label: () => string;
15+
};
16+
17+
type FeatureFlagDefinition = {
18+
icon: Component;
19+
title: () => string;
20+
description?: (() => string | null) | null;
21+
featureLink?: (() => FeatureFlagLink | null) | null;
22+
};
23+
24+
type FeatureFlagDefinitions = Readonly<
25+
Record<FeatureFlag, FeatureFlagDefinition>
26+
>;
27+
28+
const currentDate = () => new Date();
29+
30+
const currentYear = () => currentDate().getFullYear();
31+
32+
const currentMonth = () => currentDate().getMonth() + 1;
33+
34+
const openFeatureLink = (href: string, title: string): FeatureFlagLink => ({
35+
href,
36+
label: () => m.link_label_open_preview_feature({ title }),
37+
});
38+
39+
export const featureFlagDefinitions: FeatureFlagDefinitions = {
40+
[FeatureFlag.YearInReview]: {
41+
icon: CalendarIcon,
42+
title: () => m.preview_feature_title_year_in_review(),
43+
description: () => m.preview_feature_description_year_in_review(),
44+
featureLink: () =>
45+
openFeatureLink(
46+
UrlBuilder.users('me').yearToDate(currentYear()),
47+
m.preview_feature_title_year_in_review(),
48+
),
49+
},
50+
[FeatureFlag.MonthInReview]: {
51+
icon: CalendarIcon,
52+
title: () => m.preview_feature_title_month_in_review(),
53+
description: () => m.preview_feature_description_month_in_review(),
54+
featureLink: () =>
55+
openFeatureLink(
56+
UrlBuilder.users('me').monthInReview(currentYear(), currentMonth()),
57+
m.preview_feature_title_month_in_review(),
58+
),
59+
},
60+
[FeatureFlag.EditMode]: {
61+
icon: EditModeIcon,
62+
title: () => m.preview_feature_title_edit_mode(),
63+
description: () => m.preview_feature_description_edit_mode(),
64+
},
65+
[FeatureFlag.StreamingServices]: {
66+
icon: TrackIcon,
67+
title: () => m.preview_feature_title_streaming_sync(),
68+
description: () => m.preview_feature_description_streaming_sync(),
69+
featureLink: () =>
70+
openFeatureLink(
71+
UrlBuilder.settings.streamingServices(),
72+
m.preview_feature_title_streaming_sync(),
73+
),
74+
},
75+
[FeatureFlag.ScopedFavorites]: {
76+
icon: FavoriteIcon,
77+
title: () => m.preview_feature_title_scoped_favorites(),
78+
description: () => m.preview_feature_description_scoped_favorites(),
79+
featureLink: () =>
80+
openFeatureLink(
81+
UrlBuilder.profile.favorites('me'),
82+
m.preview_feature_title_scoped_favorites(),
83+
),
84+
},
85+
[FeatureFlag.SocialActivities]: {
86+
icon: SocialIcon,
87+
title: () => m.preview_feature_title_social_activities(),
88+
description: () => m.preview_feature_description_social_activities(),
89+
},
90+
[FeatureFlag.PlexSync]: {
91+
icon: PlexLibraryIcon,
92+
title: () => m.preview_feature_title_plex_sync(),
93+
description: () => m.preview_feature_description_plex_sync(),
94+
featureLink: () =>
95+
openFeatureLink(
96+
UrlBuilder.settings.plex(),
97+
m.preview_feature_title_plex_sync(),
98+
),
99+
},
100+
};

0 commit comments

Comments
 (0)