Skip to content

Commit 0cc4117

Browse files
TV Content Page Updates (#148)
* wip * Update async data call in all.vue * Remove unused imports in tv.ts * Update TV episode component to use TV URL for image source and format TV date * Add formatTvDate function to utils/dates.ts * Refactor formatDate function to use formatTvDate * Remove unused import in index.vue * cleanup * Add schema.org markup for video metadata * Add website.svg social icon * Add provider option to DirectusImageProps and update Nuxt Image configuration * Add TVByline and TVReactions components * Add visitor ID tracking for TV pages * fix popper hydration issues * update TVShow to accept URL * Add recommendations section to TV episode page * fix css warnings * fix padding for transcript * add divider between content and meta
1 parent 8063379 commit 0cc4117

File tree

9 files changed

+614
-39
lines changed

9 files changed

+614
-39
lines changed

assets/svg/social/website.svg

+1
Loading

components/Base/DirectusImage.vue

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ export interface DirectusImageProps {
1212
1313
width?: number;
1414
height?: number;
15+
provider?: string;
1516
}
1617
17-
defineProps<DirectusImageProps>();
18+
withDefaults(defineProps<DirectusImageProps>(), {
19+
provider: 'directus',
20+
});
1821
</script>
1922

2023
<template>
21-
<NuxtImg :src="uuid" :alt="alt" :width="width" :height="height" format="auto" loading="lazy" />
24+
<NuxtImg :src="uuid" :alt="alt" :width="width" :height="height" format="auto" loading="lazy" :provider />
2225
</template>

components/Block/Showcase.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ loop();
6363
.block-showcase {
6464
@container (width > 50rem) {
6565
display: grid;
66-
align-items: start;
66+
align-items: flex-start;
6767
grid-template-columns: repeat(v-bind(sections), 1fr);
6868
gap: var(--space-8);
6969
}

components/Tv/TVByline.vue

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<script setup lang="ts">
2+
interface BaseBylineProps {
3+
name?: string;
4+
title?: string;
5+
image?: string;
6+
links?: [
7+
{
8+
service: string;
9+
url: string;
10+
},
11+
];
12+
}
13+
14+
defineProps<BaseBylineProps>();
15+
</script>
16+
17+
<template>
18+
<address rel="author" class="base-byline">
19+
<BaseDirectusImage
20+
v-if="image"
21+
:width="44"
22+
:height="44"
23+
class="avatar"
24+
:uuid="image"
25+
:alt="name ?? ''"
26+
provider="directusTv"
27+
/>
28+
29+
<div>
30+
<p v-if="name" class="name">{{ name }}</p>
31+
<p v-if="title" class="title">{{ title }}</p>
32+
<div v-if="links" class="share-icons">
33+
<template v-for="{ service, url } in links" :key="service">
34+
<NuxtLink :href="url" target="_blank">
35+
<img :src="dynamicAsset(`/svg/social/${service}.svg`)" :alt="service" />
36+
</NuxtLink>
37+
</template>
38+
</div>
39+
</div>
40+
</address>
41+
</template>
42+
43+
<style scoped lang="scss">
44+
.base-byline {
45+
--color: var(--foreground);
46+
--title-color: var(--gray-400);
47+
--text-shadow: none;
48+
49+
color: var(--color);
50+
display: flex;
51+
gap: var(--space-2);
52+
font-style: normal;
53+
align-items: flex-start;
54+
55+
.avatar {
56+
border-radius: var(--rounded-full);
57+
inline-size: var(--space-11);
58+
block-size: var(--space-11);
59+
object-fit: cover;
60+
background: var(--gray-100);
61+
}
62+
63+
div {
64+
text-shadow: var(--text-shadow);
65+
}
66+
67+
.name,
68+
.title {
69+
margin: 0;
70+
}
71+
72+
.title {
73+
color: var(--title-color);
74+
font-size: var(--font-size-sm);
75+
line-height: var(--line-height-sm);
76+
text-wrap: balance;
77+
}
78+
79+
.share-icons {
80+
display: flex;
81+
align-items: center;
82+
gap: var(--space-5);
83+
84+
img {
85+
margin-block-start: var(--space-1);
86+
width: var(--space-6);
87+
height: auto;
88+
filter: brightness(1);
89+
transition: filter var(--duration-150) var(--ease-out);
90+
91+
&:hover {
92+
transition: none;
93+
filter: brightness(50);
94+
}
95+
}
96+
}
97+
}
98+
</style>

components/Tv/TVReactions.vue

+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
<script setup>
2+
import { createItem, updateItem } from '@directus/sdk';
3+
4+
const { $directusTv } = useNuxtApp();
5+
const ariaId = useId();
6+
7+
const props = defineProps({
8+
episodeId: {
9+
type: String,
10+
required: true,
11+
},
12+
});
13+
14+
const visitorId = useCookie('visitor_id');
15+
16+
const loading = ref(false);
17+
const error = ref(null);
18+
const success = ref(false);
19+
20+
const reaction = reactive({
21+
comments: '',
22+
});
23+
24+
const textarea = ref(null);
25+
26+
const isOpen = ref(false);
27+
28+
const ratingMessages = {
29+
dislike: 'Woof! 🤦‍♂️ How can we do better?',
30+
like: 'Nice! 👍 Anything we can improve upon?',
31+
love: `Awesome! The whole team is rejoicing in celebration! 🥳🎉🎊 Anything you'd like to say to them?`,
32+
};
33+
34+
async function submitReaction(type) {
35+
isOpen.value = true;
36+
loading.value = true;
37+
38+
if (type) {
39+
reaction.type = type;
40+
}
41+
42+
try {
43+
if (reaction.id) {
44+
const response = await $directusTv.request(
45+
updateItem('tv_episode_reactions', reaction.id, {
46+
episode: props.episodeId,
47+
type: reaction.type,
48+
comments: reaction.comments,
49+
visitor_id: visitorId.value,
50+
}),
51+
);
52+
53+
if (response.comments) {
54+
success.value = true;
55+
56+
await setTimeout(() => {
57+
isOpen.value = false;
58+
}, 2000);
59+
}
60+
} else {
61+
const data = await $directusTv.request(
62+
createItem('tv_episode_reactions', {
63+
episode: props.episodeId,
64+
type: reaction.type,
65+
comments: reaction.comments,
66+
visitor_id: visitorId.value,
67+
}),
68+
);
69+
70+
reaction.id = data.id;
71+
}
72+
} catch (e) {
73+
error.value = e;
74+
} finally {
75+
loading.value = false;
76+
}
77+
}
78+
79+
watch(isOpen, (value) => {
80+
if (value) {
81+
setTimeout(() => {
82+
textarea.value.focus();
83+
}, 100);
84+
}
85+
});
86+
87+
onKeyStroke('Escape', () => {
88+
isOpen.value = false;
89+
});
90+
</script>
91+
<template>
92+
<div>
93+
<VDropdown :aria-id class="reactions" :triggers="[]" :shown="isOpen" :auto-hide="false">
94+
<button
95+
v-tooltip="`I do not like this`"
96+
class="feedback-button"
97+
:aria-pressed="reaction.type === 'dislike'"
98+
@click="() => submitReaction('dislike')"
99+
>
100+
<BaseIcon name="thumb_down" />
101+
</button>
102+
<button
103+
v-tooltip="'I like this'"
104+
class="feedback-button"
105+
:aria-pressed="reaction.type === 'like'"
106+
@click="() => submitReaction('like')"
107+
>
108+
<BaseIcon name="thumb_up" />
109+
</button>
110+
<button
111+
v-tooltip="'I freaking love this'"
112+
:aria-pressed="reaction.type === 'love'"
113+
class="feedback-button"
114+
@click="() => submitReaction('love')"
115+
>
116+
<BaseIcon name="favorite" />
117+
</button>
118+
<template #popper>
119+
<ThemeProvider variant="dark">
120+
<div class="popover">
121+
<template v-if="!success">
122+
<p>{{ ratingMessages[reaction.type] }}</p>
123+
<textarea ref="textarea" v-model="reaction.comments" class="input" placeholder="Give us your feedback" />
124+
<BaseButton
125+
type="button"
126+
:loading="loading"
127+
:disabled="loading"
128+
color="primary"
129+
label="Send Your Feedback"
130+
@click="submitReaction(reaction.type)"
131+
/>
132+
</template>
133+
<template v-else-if="error">
134+
<p>Whoops! There was an error submitting your feedback.</p>
135+
</template>
136+
<template v-else-if="success">
137+
<p>Thank you for your feedback!</p>
138+
</template>
139+
<button class="close-button" @click="() => (isOpen = false)">
140+
<BaseIcon name="close" />
141+
</button>
142+
</div>
143+
</ThemeProvider>
144+
</template>
145+
</VDropdown>
146+
</div>
147+
</template>
148+
149+
<style lang="scss" scoped>
150+
.reactions {
151+
display: flex;
152+
align-content: center;
153+
border-radius: var(--rounded-full);
154+
background: var(--gray-100);
155+
padding: var(--space-1);
156+
gap: var(--space-1);
157+
}
158+
159+
.feedback-button {
160+
transition: background-color 0.2s;
161+
background: none;
162+
border: none;
163+
cursor: pointer;
164+
padding: var(--space-2);
165+
&:hover {
166+
background: var(--gray-200);
167+
}
168+
border-radius: var(--rounded-full);
169+
&[aria-pressed='true'] {
170+
background: var(--primary-500);
171+
color: var(--white);
172+
}
173+
}
174+
175+
.popover {
176+
position: relative;
177+
width: 350px;
178+
padding: var(--space-5);
179+
background-color: var(--gray-100);
180+
border-radius: var(--rounded-xl);
181+
border: 2px solid var(--gray-200);
182+
display: flex;
183+
flex-direction: column;
184+
gap: var(--space-2);
185+
box-shadow: var(--shadow-lg);
186+
187+
button {
188+
width: auto;
189+
}
190+
191+
.close-button {
192+
position: absolute;
193+
top: var(--space-2);
194+
right: var(--space-2);
195+
background: none;
196+
border: none;
197+
cursor: pointer;
198+
padding: var(--space-2);
199+
border-radius: var(--rounded-full);
200+
&:hover {
201+
background: var(--gray-200);
202+
}
203+
}
204+
}
205+
206+
.input {
207+
color: var(--gray-900);
208+
width: 100%;
209+
height: 100px;
210+
border-radius: 4px;
211+
padding: 0.375rem 0.75rem;
212+
background-color: var(--gray-50);
213+
border: 1px solid var(--gray-200);
214+
&:focus {
215+
border-color: var(--primary);
216+
outline: none;
217+
box-shadow: 0px 0px var(--space-1) 0px var(--primary-100);
218+
}
219+
}
220+
</style>
221+
222+
<style lang="css">
223+
.v-popper--theme-dropdown {
224+
.v-popper__inner {
225+
background-color: transparent !important;
226+
border: none !important;
227+
border-radius: 6px;
228+
}
229+
230+
.v-popper__arrow-container {
231+
.v-popper__arrow-outer {
232+
border-color: #334155;
233+
}
234+
.v-popper__arrow-inner {
235+
border-color: #334155;
236+
}
237+
}
238+
}
239+
</style>

components/Tv/TVShow.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<NuxtLink :to="`/tv/${slug}`" class="show">
2+
<NuxtLink :to="url ? url : `/tv/${slug}`" class="show">
33
<div class="tile" :style="`background-image: url(${directusUrl}/assets/${tile}?width=600)`">
44
<span v-if="overlay">{{ overlay }}</span>
55
</div>
@@ -16,6 +16,7 @@ const {
1616
const directusUrl = process.env.DIRECTUS_TV_URL || tvUrl;
1717
1818
defineProps({
19+
url: String,
1920
slug: String,
2021
tile: String,
2122
title: String,

layouts/tv.vue

+9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
useHead({
33
bodyAttrs: { class: 'tv' },
44
});
5+
6+
// Generate a unique visitor ID for each user of TV pages to track reactions
7+
const visitorId = useCookie('visitor_id');
8+
9+
if (!visitorId.value) {
10+
const id = useId();
11+
12+
visitorId.value = id;
13+
}
514
</script>
615

716
<template>

0 commit comments

Comments
 (0)