Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ref, computed, onMounted } from 'vue';

import Header from './components/Header.vue';
import Hero from './components/Hero.vue';
import VideoCarousel from './components/VideoCarousel.vue';
import VideoDetailsDialog from './components/VideoDetailsDialog.vue';
import catalogData from './catalogData';
import { navigateToPlayerByVideoId } from './shared/navigation';


const modalDetailOpen = ref(false);
const selectedVideoId = ref<string | null>(null);
const loading = ref(true);
const catalog = ref<any | null>(null);
const byId = ref<Record<string, any>>({});

function openDetails(id: string) {
selectedVideoId.value = id;
Expand All @@ -22,19 +26,31 @@ function handleShowDetails() {
}

const selectedVideo = computed(() =>
selectedVideoId.value ? (catalogData as any).byId[selectedVideoId.value] : null
selectedVideoId.value ? byId.value[selectedVideoId.value] : null
);

const relatedVideos = computed(() => {
const current = selectedVideo.value as any;
if (!current || !current.relatedIds) return [] as any[];
return current.relatedIds.map((id: string) => (catalogData as any).byId[id]).filter(Boolean);
return current.relatedIds.map((id: string) => byId.value[id]).filter(Boolean);
});

function handleRedirectPlayer() {
if ((window as any)?.shellNavigate) {
(window as any).shellNavigate("/player");
onMounted(async () => {
const delay = new Promise((res) => setTimeout(res, 1200));
const [data] = await Promise.all([catalogData, delay]);
console.log(data);
catalog.value = catalogData;
const idx: Record<string, any> = {};
for (const c of data.carousels) {
for (const v of c.videos) idx[v.id] = v;
}
byId.value = idx;
loading.value = false;
});

function handleRedirectPlayer() {
// Usa o vídeo do hero (id: '1')
navigateToPlayerByVideoId('1');
}
</script>

Expand All @@ -55,9 +71,34 @@ function handleRedirectPlayer() {
<div class="relative z-10">
<Header />
<main class="z-10">
<Hero @on-show-details="handleShowDetails" @on-watch-now="handleRedirectPlayer" />
<VideoCarousel v-for="carousel in (catalogData as any).carousels" :key="carousel.id" class="mt-4"
:title="carousel.title" :videos="carousel.videos" @select="openDetails" />
<Hero v-if="!loading" @on-show-details="handleShowDetails" @on-watch-now="handleRedirectPlayer" />
<!-- Hero skeleton -->
<section v-else class="relative h-[85vh] flex items-end pt-20 pb-16">
<div class="absolute inset-0 bg-zinc-800/30 animate-pulse" />
<div class="relative z-10 px-8 max-w-3xl w-full">
<div class="h-8 w-28 mb-6 rounded-full bg-white/10" />
<div class="h-16 w-80 mb-4 rounded bg-white/10" />
<div class="h-10 w-64 mb-6 rounded bg-white/10" />
<div class="h-20 w-full max-w-xl mb-8 rounded bg-white/10" />
<div class="flex gap-4">
<div class="h-12 w-40 rounded bg-white/10" />
<div class="h-12 w-40 rounded bg-white/10" />
</div>
</div>
</section>
<template v-if="!loading && catalog">
<VideoCarousel
v-for="carousel in catalog.carousels"
:key="carousel.id"
class="mt-4"
:title="carousel.title"
:videos="carousel.videos"
@select="openDetails"
/>
</template>
<template v-else>
<VideoCarousel v-for="i in 3" :key="i" class="mt-4" title="" :videos="[]" :loading="true" />
</template>
</main>

</div>
Expand Down
4 changes: 3 additions & 1 deletion src/catalogData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type CatalogVideo = {
technologies?: string[];
learnings?: string[];
relatedIds?: string[];
manifestUrl?: string; // URL do manifest.remote.json para buscar thumbnail e duration real
};

export type CatalogCarousel = {
Expand Down Expand Up @@ -75,7 +76,8 @@ export const catalogData: CatalogData = {
'Compartilhamento de componentes',
'Gestão de dependências'
],
relatedIds: ['2', '3', '5']
relatedIds: ['2', '3', '5'],
manifestUrl: 'https://d2ov2y0lwp0e6t.cloudfront.net/videos/video2/manifest.remote.json'
},
{
id: '2',
Expand Down
37 changes: 32 additions & 5 deletions src/components/VideoCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
@mouseleave="() => isHovered = false" @click="() => emit('select', id)">
<div
class="relative aspect-video rounded-lg overflow-hidden bg-gradient-to-br from-violet-900/20 to-cyan-900/20 border border-white/5">
<img :src="thumbnail" :alt="title"
<img :src="displayThumbnail" :alt="title"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110" />

<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-60" />

<div class="absolute top-2 right-2 px-2 py-1 bg-black/70 backdrop-blur-sm rounded text-xs">
{{ duration }}
{{ displayDuration }}
</div>

<div class="absolute top-2 left-2 px-2 py-1 backdrop-blur-sm rounded text-xs" :style="{
Expand All @@ -27,7 +27,7 @@
<button class="px-4 py-2 rounded-lg transition-all duration-300 flex items-center gap-2 shadow-lg" :style="{
backgroundImage: 'linear-gradient(120deg, #71bedb 30%, #f4f468)',
boxShadow: '0 10px 15px -3px rgba(113, 190, 219, 0.3)'
}" @mouseenter="(e: any) => e.currentTarget.style.boxShadow = '0 10px 15px -3px rgba(113, 190, 219, 0.5)'"
}" @click.stop="handleWatch" @mouseenter="(e: any) => e.currentTarget.style.boxShadow = '0 10px 15px -3px rgba(113, 190, 219, 0.5)'"
@mouseleave="(e: any) => e.currentTarget.style.boxShadow = '0 10px 15px -3px rgba(113, 190, 219, 0.3)'">
<Play class="w-4 h-4 text-black" />
<span class="text-sm text-black">Assistir</span>
Expand All @@ -47,21 +47,48 @@

<script setup lang="ts">
import { Play, Plus, ThumbsUp } from 'lucide-vue-next';
import { ref } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { loadManifestData, formatDuration } from '../utils/manifestLoader';
import { navigateToPlayerByVideoId } from '../shared/navigation';

interface VideoCardProps {
id: string;
title: string;
thumbnail: string;
duration: string;
category: string;
manifestUrl?: string;
}

const isHovered = ref(false);
const manifestThumbnail = ref<string | null>(null);
const manifestDuration = ref<string | null>(null);

const props = defineProps<VideoCardProps>();
const emit = defineEmits<{ (e: 'select', id: string): void }>();

const { id, title, thumbnail, duration, category } = props as any;
const { id, title, thumbnail, duration, category, manifestUrl } = props as any;

const displayThumbnail = computed(() => manifestThumbnail.value || thumbnail);

const displayDuration = computed(() => manifestDuration.value || duration);

function handleWatch() {
navigateToPlayerByVideoId(id);
}

onMounted(async () => {
if (manifestUrl) {
const manifestData = await loadManifestData(manifestUrl);
if (manifestData) {
if (manifestData.thumbnailUrl) {
manifestThumbnail.value = manifestData.thumbnailUrl;
}
if (manifestData.duration) {
manifestDuration.value = formatDuration(manifestData.duration);
}
}
}
});

</script>
43 changes: 30 additions & 13 deletions src/components/VideoCarousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{{ title }}
</h2>

<div class="relative px-8" ref="emblaWrapperRef">
<div class="relative px-8" ref="emblaWrapperRef">

<button
class="absolute left-4 top-1/2 -translate-y-1/2 z-10 flex items-center justify-center rounded-full bg-black/60 backdrop-blur-md p-2 hover:bg-black/80 disabled:opacity-30 disabled:cursor-not-allowed"
Expand All @@ -19,16 +19,30 @@
<ChevronRight class="w-6 h-6" />
</button>

<div class="overflow-hidden" ref="emblaRef">
<div class="overflow-hidden" ref="emblaRef">
<div class="flex gap-4 will-change-transform select-none touch-pan-y" style="backface-visibility:hidden">
<div v-for="video in videos" :key="video.id" class="px-2 flex-shrink-0 min-w-0" :style="{
flex: `0 0 calc(100% / var(--slides-per-view, ${slidesPerView}))`,
maxWidth: `calc(100% / var(--slides-per-view, ${slidesPerView}))`,
}">
<VideoCard v-bind="video" @select="(id) => emit('select', id)" />
</div>
</div>
</div>
<template v-if="!loading">
<div v-for="video in videos" :key="video.id" class="px-2 flex-shrink-0 min-w-0" :style="{
flex: `0 0 calc(100% / var(--slides-per-view, ${slidesPerView}))`,
maxWidth: `calc(100% / var(--slides-per-view, ${slidesPerView}))`,
}">
<VideoCard v-bind="video" @select="(id) => emit('select', id)" />
</div>
</template>
<template v-else>
<div v-for="i in skeletonCount" :key="i" class="px-2 flex-shrink-0 min-w-0" :style="{
flex: `0 0 calc(100% / var(--slides-per-view, ${slidesPerView}))`,
maxWidth: `calc(100% / var(--slides-per-view, ${slidesPerView}))`,
}">
<div class="relative aspect-video rounded-lg overflow-hidden border border-white/5 bg-white/5 animate-pulse">
<div class="absolute inset-0 bg-gradient-to-br from-zinc-800/40 to-zinc-700/20" />
<div class="absolute bottom-2 left-2 h-4 w-20 rounded bg-white/10" />
<div class="absolute bottom-2 right-2 h-4 w-12 rounded bg-white/10" />
</div>
</div>
</template>
</div>
</div>
</div>

</div>
Expand All @@ -45,17 +59,19 @@ interface Video {
thumbnail: string;
duration: string;
category: string;
manifestUrl?: string;
}

interface VideoCarouselProps {
title: string;
videos: Video[];
title: string;
videos: Video[];
loading?: boolean;
}

const props = defineProps<VideoCarouselProps>();
const emit = defineEmits<{ (e: 'select', id: string): void }>();

const { title, videos } = props;
const { title, videos, loading = false } = props as any;

const [emblaRef, emblaApi] = emblaCarouselVue({
loop: false,
Expand All @@ -66,6 +82,7 @@ const canPrev = ref(false);
const canNext = ref(false);
const slidesPerView = ref(5);
const slidesToScroll = ref(3);
const skeletonCount = ref(10);

const emblaRootEl = ref<HTMLElement | null>(null);
const emblaWrapperRef = ref<HTMLElement | null>(null);
Expand Down
9 changes: 8 additions & 1 deletion src/components/VideoDetailsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
X,
} from 'lucide-vue-next';
import VideoCard from './VideoCard.vue';
import { navigateToPlayerByVideoId } from '../shared/navigation';

const props = defineProps<{
open: boolean,
Expand Down Expand Up @@ -57,6 +58,12 @@ onBeforeUnmount(() => {
});

const relatedVideos = computed(() => props.related || []);

function handleWatch() {
if (props.video?.id) {
navigateToPlayerByVideoId(props.video.id);
}
}
</script>

<template>
Expand Down Expand Up @@ -139,7 +146,7 @@ const relatedVideos = computed(() => props.related || []);
'linear-gradient(120deg, #71bedb 30%, #f4f468)',
boxShadow:
'0 20px 25px -5px rgba(113, 190, 219, 0.3), 0 10px 10px -5px rgba(113, 190, 219, 0.2)',
}" @mouseenter="
}" @click="handleWatch" @mouseenter="
(e: any) =>
(e.currentTarget.style.boxShadow =
'0 20px 25px -5px rgba(113, 190, 219, 0.5), 0 10px 10px -5px rgba(113, 190, 219, 0.3)')
Expand Down
47 changes: 47 additions & 0 deletions src/shared/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { VideoHash } from './types';

export function generateVideoHash(videoId: string): VideoHash {
let hash = 0;
const str = `video_${videoId}`;

for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}

const positiveHash = Math.abs(hash);
return `v${positiveHash.toString(36)}`;
}

/**
* Valida se uma string é um hash válido
*
* @param hash - String a ser validada
* @returns True se for um hash válido
*/
export function isValidVideoHash(hash: string): boolean {
return typeof hash === 'string' && hash.length > 0 && hash.startsWith('v');
}

/**
* Extrai o hash de uma URL de query string
*
* @param url - URL completa ou query string
* @returns Hash se encontrado, null caso contrário
*/
export function extractHashFromUrl(url?: string): VideoHash | null {
if (!url) {
const params = new URLSearchParams(window.location.search);
const hash = params.get('v');
return hash && isValidVideoHash(hash) ? hash : null;
}

try {
const urlObj = new URL(url, window.location.origin);
const hash = urlObj.searchParams.get('v');
return hash && isValidVideoHash(hash) ? hash : null;
} catch {
return null;
}
}
37 changes: 37 additions & 0 deletions src/shared/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Módulo compartilhado de dados de vídeo
* Este módulo será exposto via Module Federation
*/

// Tipos
export type {
VideoManifest,
VideoMetadata,
VideoData,
VideoRegistry,
VideoHash,
} from './types';

// Utilitários de hash
export {
generateVideoHash,
isValidVideoHash,
extractHashFromUrl,
} from './hash';

// Registro de vídeos
export {
getVideoByHash,
getVideoById,
getHashByVideoId,
getVideoRegistry,
updateVideoMetadata,
upsertVideo,
} from './registry';

// Navegação
export {
navigateToPlayer,
navigateToPlayerByVideoId,
navigateToPlayerByHash,
} from './navigation';
Loading