Embed voting clips on landing page
This commit is contained in:
@@ -49,19 +49,35 @@ const props = defineProps<{
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-vote-card__clip">
|
||||
<span class="home-vote-card__clip-platform">{{ nom.clipPlatform }}</span>
|
||||
<a
|
||||
v-if="nom.clipUrl"
|
||||
:href="nom.clipUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
@click.stop
|
||||
>
|
||||
{{ nom.clipTitle }}
|
||||
</a>
|
||||
<span v-else>Kein geprüfter Clip für diese Kategorie hinterlegt.</span>
|
||||
<div class="home-vote-card__clip" @click.stop>
|
||||
<div class="home-vote-card__clip-header">
|
||||
<span class="home-vote-card__clip-platform">{{ nom.clipPlatform }}</span>
|
||||
<a
|
||||
v-if="nom.clipUrl"
|
||||
:href="nom.clipUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
referrerpolicy="no-referrer"
|
||||
>
|
||||
{{ nom.clipTitle }}
|
||||
</a>
|
||||
<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>
|
||||
|
||||
<template v-if="nom.showPick">
|
||||
|
||||
@@ -194,8 +194,8 @@ html,body{margin:0;padding:0;background:#160a26;}
|
||||
|
||||
.home-vote-card{
|
||||
display:grid;
|
||||
grid-template-columns:minmax(210px,1.1fr) minmax(230px,1fr) auto;
|
||||
align-items:center;
|
||||
grid-template-columns:minmax(190px,.8fr) minmax(360px,1.4fr) auto;
|
||||
align-items:start;
|
||||
gap:16px;
|
||||
padding:16px 18px;
|
||||
border:1.5px solid #eadff8;
|
||||
@@ -290,8 +290,9 @@ html,body{margin:0;padding:0;background:#160a26;}
|
||||
.home-vote-card__clip{
|
||||
min-width:0;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
flex-direction:column;
|
||||
align-items:stretch;
|
||||
gap:9px;
|
||||
padding:10px 12px;
|
||||
border-radius:16px;
|
||||
background:#fbf8ff;
|
||||
@@ -301,7 +302,14 @@ html,body{margin:0;padding:0;background:#160a26;}
|
||||
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;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
@@ -310,11 +318,27 @@ html,body{margin:0;padding:0;background:#160a26;}
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
.home-vote-card__clip a:hover{
|
||||
.home-vote-card__clip-header a:hover{
|
||||
color:#4f32a2;
|
||||
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{
|
||||
background:#fffaf0;
|
||||
border-color:#f2dfb6;
|
||||
@@ -805,6 +829,10 @@ html,body{margin:0;padding:0;background:#160a26;}
|
||||
flex-direction:column!important;
|
||||
}
|
||||
|
||||
.home-vote-card__clip-header{
|
||||
width:100%!important;
|
||||
}
|
||||
|
||||
.home-vote-card__pick{
|
||||
width:100%!important;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface HomeNomineeListItem {
|
||||
clipUrl: string | null
|
||||
clipTitle: string
|
||||
clipPlatform: string
|
||||
clipEmbedUrl: string | null
|
||||
clipEmbedTitle: string
|
||||
idx: number
|
||||
selected: boolean
|
||||
hasClip: boolean
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed, type ComputedRef, type Ref } from 'vue'
|
||||
|
||||
import { buildClipEmbed } from '../../lib/clipEmbeds'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { useAwardsStore } from '../../stores/awards'
|
||||
import type { HomeDisplayCategory, HomeInteractionModalKind } from './homeLandingTypes'
|
||||
@@ -117,6 +118,7 @@ export function useHomeModalCandidatePresentation(params: {
|
||||
const clipUrl = candidate.clipUrl?.trim() || null
|
||||
const clipTitle = candidate.clipTitle?.trim() || 'Highlight-Clip ansehen'
|
||||
const clipPlatform = clipUrl ? candidate.clipPlatform?.trim() || candidate.platform : 'Clip fehlt'
|
||||
const clipEmbed = clipUrl ? buildClipEmbed(clipUrl) : null
|
||||
return {
|
||||
name: candidate.displayName,
|
||||
handle: candidate.channelSlug,
|
||||
@@ -125,6 +127,8 @@ export function useHomeModalCandidatePresentation(params: {
|
||||
clipUrl,
|
||||
clipTitle,
|
||||
clipPlatform,
|
||||
clipEmbedUrl: clipEmbed?.src ?? null,
|
||||
clipEmbedTitle: clipEmbed?.title ?? clipTitle,
|
||||
idx,
|
||||
selected,
|
||||
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