Cards were feeling too large at 5 per row. Updated calc to fit 6 cards uniformly across ContentRow and Browse (My List sections). Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
6.7 KiB
Vue
212 lines
6.7 KiB
Vue
<template>
|
||
<div class="content-row">
|
||
<h2 class="content-row-title text-xl md:text-2xl font-bold text-white mb-4 px-4 uppercase">
|
||
{{ title }}
|
||
</h2>
|
||
|
||
<div class="relative group">
|
||
<!-- Scroll Left Button -->
|
||
<button
|
||
v-if="canScrollLeft"
|
||
@click="scrollLeft"
|
||
class="scroll-nav-button hidden md:flex items-center justify-center absolute left-0 top-0 bottom-0 z-10 w-12 transition-all"
|
||
>
|
||
<svg class="w-8 h-8" 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-8 overflow-x-auto overflow-y-visible scrollbar-hide scroll-smooth px-4 pt-6 pb-8"
|
||
@scroll="handleScroll"
|
||
>
|
||
<div
|
||
v-for="content in contents"
|
||
:key="content.id"
|
||
class="content-card flex-shrink-0 w-[200px] card-desktop-6 group/card cursor-pointer"
|
||
@click="$emit('content-click', content)"
|
||
>
|
||
<div class="glass-card rounded-lg p-1.5 transition-all duration-300 relative">
|
||
<img
|
||
:src="content.thumbnail"
|
||
:alt="content.title"
|
||
class="w-full aspect-[2/3] object-contain rounded-md bg-neutral-900"
|
||
loading="lazy"
|
||
/>
|
||
<!-- Social Indicators -->
|
||
<div class="absolute bottom-3 left-3 flex items-center gap-2">
|
||
<span class="social-badge" v-if="getReactionCount(content.id) > 0">
|
||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"/></svg>
|
||
{{ getReactionCount(content.id) }}
|
||
</span>
|
||
<span class="social-badge" v-if="getCommentCount(content.id) > 0">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
|
||
{{ getCommentCount(content.id) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="mt-2">
|
||
<h3 class="text-base md:text-xl font-semibold md:font-bold text-white truncate">{{ content.title }}</h3>
|
||
<p class="text-base text-white/60 truncate hidden md:block">{{ content.description }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Scroll Right Button -->
|
||
<button
|
||
v-if="canScrollRight"
|
||
@click="scrollRight"
|
||
class="scroll-nav-button hidden md:flex items-center justify-center absolute right-0 top-0 bottom-0 z-10 w-12 transition-all"
|
||
>
|
||
<svg class="w-8 h-8" 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]
|
||
}>()
|
||
|
||
// Social counts are now fetched from the relay when the detail modal opens.
|
||
// We no longer show mock badges on cards -- the real data lives on the relay.
|
||
function getReactionCount(_contentId: string): number {
|
||
return 0
|
||
}
|
||
|
||
function getCommentCount(_contentId: string): number {
|
||
return 0
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
.content-row-title {
|
||
background: linear-gradient(to right, #fafafa, #9ca3af);
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
letter-spacing: 0.05em; /* 5% character spacing */
|
||
}
|
||
|
||
.scroll-nav-button {
|
||
background: rgba(0, 0, 0, 0.35);
|
||
backdrop-filter: blur(24px);
|
||
-webkit-backdrop-filter: blur(24px);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
box-shadow:
|
||
0 8px 24px rgba(0, 0, 0, 0.45),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||
color: rgba(255, 255, 255, 0.8);
|
||
opacity: 0.7;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.scroll-nav-button:hover {
|
||
background: rgba(0, 0, 0, 0.45);
|
||
border-color: rgba(255, 255, 255, 0.15);
|
||
opacity: 1;
|
||
color: rgba(255, 255, 255, 1);
|
||
box-shadow:
|
||
0 12px 32px rgba(0, 0, 0, 0.6),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.glass-card {
|
||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.group\/card:hover .glass-card {
|
||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2), 0 0 20px rgba(255, 255, 255, 0.05);
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
/* Show exactly 6 cards on desktop.
|
||
Total horizontal padding: section px-4 (32px) + slider px-4 (32px) = 64px.
|
||
Gaps between 6 cards: 5 × 2rem (gap-8) = 160px.
|
||
Card width = (viewport - 64px padding - 160px gaps) / 6 */
|
||
@media (min-width: 768px) {
|
||
.card-desktop-6 {
|
||
width: calc((100vw - 14rem) / 6);
|
||
max-width: 300px;
|
||
}
|
||
}
|
||
|
||
.social-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
padding: 2px 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
background: rgba(0, 0, 0, 0.6);
|
||
backdrop-filter: blur(12px);
|
||
-webkit-backdrop-filter: blur(12px);
|
||
border-radius: 6px;
|
||
}
|
||
</style>
|