Embed voting clips on landing page
This commit is contained in:
@@ -49,19 +49,35 @@ const props = defineProps<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="home-vote-card__clip">
|
<div class="home-vote-card__clip" @click.stop>
|
||||||
<span class="home-vote-card__clip-platform">{{ nom.clipPlatform }}</span>
|
<div class="home-vote-card__clip-header">
|
||||||
<a
|
<span class="home-vote-card__clip-platform">{{ nom.clipPlatform }}</span>
|
||||||
v-if="nom.clipUrl"
|
<a
|
||||||
:href="nom.clipUrl"
|
v-if="nom.clipUrl"
|
||||||
target="_blank"
|
:href="nom.clipUrl"
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
referrerpolicy="no-referrer"
|
rel="noopener noreferrer"
|
||||||
@click.stop
|
referrerpolicy="no-referrer"
|
||||||
>
|
>
|
||||||
{{ nom.clipTitle }}
|
{{ nom.clipTitle }}
|
||||||
</a>
|
</a>
|
||||||
<span v-else>Kein geprüfter Clip für diese Kategorie hinterlegt.</span>
|
<span v-else>Kein geprüfter Clip für diese Kategorie hinterlegt.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
v-if="nom.clipEmbedUrl"
|
||||||
|
class="home-vote-card__clip-player"
|
||||||
|
:src="nom.clipEmbedUrl"
|
||||||
|
:title="nom.clipEmbedTitle"
|
||||||
|
loading="lazy"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture; web-share"
|
||||||
|
allowfullscreen
|
||||||
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p v-else-if="nom.clipUrl" class="home-vote-card__clip-fallback">
|
||||||
|
Dieser Clip kann nicht eingebettet werden, ist aber über den Link erreichbar.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="nom.showPick">
|
<template v-if="nom.showPick">
|
||||||
|
|||||||
@@ -194,8 +194,8 @@ html,body{margin:0;padding:0;background:#160a26;}
|
|||||||
|
|
||||||
.home-vote-card{
|
.home-vote-card{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns:minmax(210px,1.1fr) minmax(230px,1fr) auto;
|
grid-template-columns:minmax(190px,.8fr) minmax(360px,1.4fr) auto;
|
||||||
align-items:center;
|
align-items:start;
|
||||||
gap:16px;
|
gap:16px;
|
||||||
padding:16px 18px;
|
padding:16px 18px;
|
||||||
border:1.5px solid #eadff8;
|
border:1.5px solid #eadff8;
|
||||||
@@ -290,8 +290,9 @@ html,body{margin:0;padding:0;background:#160a26;}
|
|||||||
.home-vote-card__clip{
|
.home-vote-card__clip{
|
||||||
min-width:0;
|
min-width:0;
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
flex-direction:column;
|
||||||
gap:10px;
|
align-items:stretch;
|
||||||
|
gap:9px;
|
||||||
padding:10px 12px;
|
padding:10px 12px;
|
||||||
border-radius:16px;
|
border-radius:16px;
|
||||||
background:#fbf8ff;
|
background:#fbf8ff;
|
||||||
@@ -301,7 +302,14 @@ html,body{margin:0;padding:0;background:#160a26;}
|
|||||||
font-weight:700;
|
font-weight:700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-vote-card__clip a{
|
.home-vote-card__clip-header{
|
||||||
|
min-width:0;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-vote-card__clip-header a{
|
||||||
min-width:0;
|
min-width:0;
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
text-overflow:ellipsis;
|
text-overflow:ellipsis;
|
||||||
@@ -310,11 +318,27 @@ html,body{margin:0;padding:0;background:#160a26;}
|
|||||||
text-decoration:none;
|
text-decoration:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-vote-card__clip a:hover{
|
.home-vote-card__clip-header a:hover{
|
||||||
color:#4f32a2;
|
color:#4f32a2;
|
||||||
text-decoration:underline;
|
text-decoration:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-vote-card__clip-player{
|
||||||
|
width:100%;
|
||||||
|
aspect-ratio:16/9;
|
||||||
|
border:0;
|
||||||
|
border-radius:12px;
|
||||||
|
background:#181226;
|
||||||
|
box-shadow:0 10px 26px rgba(63,53,86,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-vote-card__clip-fallback{
|
||||||
|
margin:0;
|
||||||
|
color:#8b779f;
|
||||||
|
font-size:12px;
|
||||||
|
line-height:1.45;
|
||||||
|
}
|
||||||
|
|
||||||
.home-vote-card--missing-clip .home-vote-card__clip{
|
.home-vote-card--missing-clip .home-vote-card__clip{
|
||||||
background:#fffaf0;
|
background:#fffaf0;
|
||||||
border-color:#f2dfb6;
|
border-color:#f2dfb6;
|
||||||
@@ -805,6 +829,10 @@ html,body{margin:0;padding:0;background:#160a26;}
|
|||||||
flex-direction:column!important;
|
flex-direction:column!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-vote-card__clip-header{
|
||||||
|
width:100%!important;
|
||||||
|
}
|
||||||
|
|
||||||
.home-vote-card__pick{
|
.home-vote-card__pick{
|
||||||
width:100%!important;
|
width:100%!important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export interface HomeNomineeListItem {
|
|||||||
clipUrl: string | null
|
clipUrl: string | null
|
||||||
clipTitle: string
|
clipTitle: string
|
||||||
clipPlatform: string
|
clipPlatform: string
|
||||||
|
clipEmbedUrl: string | null
|
||||||
|
clipEmbedTitle: string
|
||||||
idx: number
|
idx: number
|
||||||
selected: boolean
|
selected: boolean
|
||||||
hasClip: boolean
|
hasClip: boolean
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { computed, type ComputedRef, type Ref } from 'vue'
|
import { computed, type ComputedRef, type Ref } from 'vue'
|
||||||
|
|
||||||
|
import { buildClipEmbed } from '../../lib/clipEmbeds'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { useAwardsStore } from '../../stores/awards'
|
import { useAwardsStore } from '../../stores/awards'
|
||||||
import type { HomeDisplayCategory, HomeInteractionModalKind } from './homeLandingTypes'
|
import type { HomeDisplayCategory, HomeInteractionModalKind } from './homeLandingTypes'
|
||||||
@@ -117,6 +118,7 @@ export function useHomeModalCandidatePresentation(params: {
|
|||||||
const clipUrl = candidate.clipUrl?.trim() || null
|
const clipUrl = candidate.clipUrl?.trim() || null
|
||||||
const clipTitle = candidate.clipTitle?.trim() || 'Highlight-Clip ansehen'
|
const clipTitle = candidate.clipTitle?.trim() || 'Highlight-Clip ansehen'
|
||||||
const clipPlatform = clipUrl ? candidate.clipPlatform?.trim() || candidate.platform : 'Clip fehlt'
|
const clipPlatform = clipUrl ? candidate.clipPlatform?.trim() || candidate.platform : 'Clip fehlt'
|
||||||
|
const clipEmbed = clipUrl ? buildClipEmbed(clipUrl) : null
|
||||||
return {
|
return {
|
||||||
name: candidate.displayName,
|
name: candidate.displayName,
|
||||||
handle: candidate.channelSlug,
|
handle: candidate.channelSlug,
|
||||||
@@ -125,6 +127,8 @@ export function useHomeModalCandidatePresentation(params: {
|
|||||||
clipUrl,
|
clipUrl,
|
||||||
clipTitle,
|
clipTitle,
|
||||||
clipPlatform,
|
clipPlatform,
|
||||||
|
clipEmbedUrl: clipEmbed?.src ?? null,
|
||||||
|
clipEmbedTitle: clipEmbed?.title ?? clipTitle,
|
||||||
idx,
|
idx,
|
||||||
selected,
|
selected,
|
||||||
hasClip: Boolean(clipUrl),
|
hasClip: Boolean(clipUrl),
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
export type ClipEmbedKind = 'twitch' | 'youtube'
|
||||||
|
|
||||||
|
export interface ClipEmbed {
|
||||||
|
kind: ClipEmbedKind
|
||||||
|
src: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildClipEmbed(clipUrl: string, parentHost = currentParentHost()): ClipEmbed | null {
|
||||||
|
const url = parseUrl(clipUrl)
|
||||||
|
if (!url) return null
|
||||||
|
|
||||||
|
const twitchSlug = parseTwitchClipSlug(url)
|
||||||
|
if (twitchSlug && parentHost) {
|
||||||
|
const params = new URLSearchParams({ clip: twitchSlug, parent: parentHost })
|
||||||
|
return {
|
||||||
|
kind: 'twitch',
|
||||||
|
src: `https://clips.twitch.tv/embed?${params.toString()}`,
|
||||||
|
title: 'Twitch Clip Player',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const youtubeId = parseYoutubeVideoId(url)
|
||||||
|
if (youtubeId) {
|
||||||
|
return {
|
||||||
|
kind: 'youtube',
|
||||||
|
src: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(youtubeId)}`,
|
||||||
|
title: 'YouTube Video Player',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentParentHost() {
|
||||||
|
if (typeof window === 'undefined') return ''
|
||||||
|
return window.location.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUrl(value: string) {
|
||||||
|
try {
|
||||||
|
return new URL(value)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTwitchClipSlug(url: URL) {
|
||||||
|
const host = normalizeHost(url.hostname)
|
||||||
|
const segments = pathSegments(url.pathname)
|
||||||
|
|
||||||
|
if (host === 'clips.twitch.tv') {
|
||||||
|
return sanitizeSlug(segments[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host === 'twitch.tv' && segments.length >= 3 && segments[1] === 'clip') {
|
||||||
|
return sanitizeSlug(segments[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseYoutubeVideoId(url: URL) {
|
||||||
|
const host = normalizeHost(url.hostname)
|
||||||
|
const segments = pathSegments(url.pathname)
|
||||||
|
|
||||||
|
if (host === 'youtu.be') {
|
||||||
|
return sanitizeYoutubeId(segments[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host === 'youtube.com') {
|
||||||
|
if (segments[0] === 'watch') {
|
||||||
|
return sanitizeYoutubeId(url.searchParams.get('v'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments[0] === 'embed' || segments[0] === 'shorts' || segments[0] === 'live') {
|
||||||
|
return sanitizeYoutubeId(segments[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHost(hostname: string) {
|
||||||
|
return hostname.toLowerCase().replace(/^www\./, '').replace(/^m\./, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathSegments(pathname: string) {
|
||||||
|
return pathname.split('/').map((item) => item.trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeSlug(value: string | null | undefined) {
|
||||||
|
const slug = value?.trim()
|
||||||
|
return slug && /^[A-Za-z0-9_-]+$/.test(slug) ? slug : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeYoutubeId(value: string | null | undefined) {
|
||||||
|
const id = value?.trim()
|
||||||
|
return id && /^[A-Za-z0-9_-]{6,}$/.test(id) ? id : null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user