Files
indee-demo/src/components/ContentRow.vue
Dorian 989dd75a84 style: change content grid from 5 to 6 cards per row on desktop
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>
2026-02-13 22:56:35 +00:00

212 lines
6.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>