Initial commit: IndeeHub decentralized streaming platform

Built a complete Netflix-style streaming interface for IndeeHub's decentralized media platform with real film content.

Features:
- Vue 3 + TypeScript + Vite setup with hot module reloading
- Netflix-inspired UI with hero section and horizontal scrolling content rows
- Glass morphism design system with custom Tailwind configuration
- 20+ real IndeeHub films organized into 6 categories (Bitcoin, Documentaries, Drama, etc.)
- Full-featured video player component with custom controls
- Mobile-responsive design with bottom navigation
- Nostr integration ready (nostr-tools, relay pool, NIP-71 support)
- Pinia state management for content
- MCP tools configured (Filesystem, Memory, Nostr, Puppeteer)

Components:
- Browse.vue: Main streaming interface with hero and content rows
- ContentRow.vue: Horizontal scrolling film cards with navigation arrows
- VideoPlayer.vue: Custom video player with play/pause, seek, volume, fullscreen
- MobileNav.vue: Bottom tab navigation for mobile devices

Tech Stack:
- Frontend: Vue 3 (Composition API), TypeScript
- Build: Vite 7
- Styling: Tailwind CSS with custom theme
- State: Pinia 3
- Router: Vue Router 4.6
- Protocol: Nostr (nostr-tools 2.22)

Design:
- 4px grid spacing system
- Glass morphism UI components
- Netflix-style hero section with featured content
- Smooth animations and hover effects
- Mobile-first responsive breakpoints
- Dark theme with custom color palette

Content:
- 20+ IndeeHub films with titles, descriptions, categories
- Bitcoin documentaries: God Bless Bitcoin, Dirty Coin, Searching for Satoshi
- Independent films and documentaries
- Working Unsplash CDN images for thumbnails and backdrops

Ready for deployment to Umbrel, Start9, and Archy nodes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dorian
2026-02-02 22:19:47 +00:00
commit 0bb1bcc5f9
50 changed files with 8278 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
<template>
<div class="content-row">
<h2 class="text-xl md:text-2xl font-semibold text-white mb-4 px-6">
{{ title }}
</h2>
<div class="relative group">
<!-- Scroll Left Button -->
<button
v-if="canScrollLeft"
@click="scrollLeft"
class="absolute left-0 top-0 bottom-0 z-10 w-12 bg-black/50 hover:bg-black/70 backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity"
>
<svg class="w-8 h-8 mx-auto" fill="currentColor" viewBox="0 0 24 24">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
</svg>
</button>
<!-- Content Slider -->
<div
ref="sliderRef"
class="flex gap-2 overflow-x-auto scrollbar-hide scroll-smooth px-6"
@scroll="handleScroll"
>
<div
v-for="content in contents"
:key="content.id"
class="content-card flex-shrink-0 w-[200px] md:w-[280px]"
@click="$emit('content-click', content)"
>
<img
:src="content.thumbnail"
:alt="content.title"
class="w-full aspect-[2/3] object-cover rounded-lg"
loading="lazy"
/>
<div class="mt-2">
<h3 class="text-sm font-medium text-white truncate">{{ content.title }}</h3>
<p class="text-xs text-white/60 truncate">{{ content.description }}</p>
</div>
</div>
</div>
<!-- Scroll Right Button -->
<button
v-if="canScrollRight"
@click="scrollRight"
class="absolute right-0 top-0 bottom-0 z-10 w-12 bg-black/50 hover:bg-black/70 backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity"
>
<svg class="w-8 h-8 mx-auto" fill="currentColor" viewBox="0 0 24 24">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</svg>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import type { Content } from '../types/content'
interface Props {
title: string
contents: Content[]
}
defineProps<Props>()
defineEmits<{
'content-click': [content: Content]
}>()
const sliderRef = ref<HTMLElement | null>(null)
const canScrollLeft = ref(false)
const canScrollRight = ref(true)
const handleScroll = () => {
if (!sliderRef.value) return
const { scrollLeft, scrollWidth, clientWidth } = sliderRef.value
canScrollLeft.value = scrollLeft > 0
canScrollRight.value = scrollLeft < scrollWidth - clientWidth - 10
}
const scrollLeft = () => {
if (!sliderRef.value) return
sliderRef.value.scrollBy({ left: -600, behavior: 'smooth' })
}
const scrollRight = () => {
if (!sliderRef.value) return
sliderRef.value.scrollBy({ left: 600, behavior: 'smooth' })
}
onMounted(() => {
if (sliderRef.value) {
sliderRef.value.addEventListener('scroll', handleScroll)
handleScroll()
}
})
onUnmounted(() => {
if (sliderRef.value) {
sliderRef.value.removeEventListener('scroll', handleScroll)
}
})
</script>
<style scoped>
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
</style>