Show portfolio videos on site cards and detail pages.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,6 +24,15 @@ const { portfolioCategories } = useNoroData()
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-t from-black/50 via-black/10 to-transparent transition duration-300 group-hover:from-black/60"
|
||||
/>
|
||||
<span
|
||||
v-if="project.video"
|
||||
class="absolute left-4 top-4 inline-flex items-center gap-1.5 rounded-full bg-black/55 px-2.5 py-1 text-[11px] font-medium text-white backdrop-blur-sm"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
ویدیو
|
||||
</span>
|
||||
<span
|
||||
class="absolute bottom-4 right-4 rounded-full bg-white/20 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm"
|
||||
>
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface PortfolioProject {
|
||||
year: string
|
||||
featured?: boolean
|
||||
image: string
|
||||
video?: string
|
||||
gallery: string[]
|
||||
client?: string
|
||||
deliverables: string[]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { isVideoUrl } from '~/utils/cms'
|
||||
|
||||
const {
|
||||
getProjectById,
|
||||
getRelatedProjects,
|
||||
@@ -27,6 +29,11 @@ const lightboxIndex = ref<number | null>(null)
|
||||
|
||||
const galleryLength = computed(() => project.value?.gallery.length ?? 0)
|
||||
|
||||
const lightboxItem = computed(() => {
|
||||
if (lightboxIndex.value === null || !project.value) return null
|
||||
return project.value.gallery[lightboxIndex.value] || null
|
||||
})
|
||||
|
||||
function openLightbox(index: number) {
|
||||
lightboxIndex.value = index
|
||||
}
|
||||
@@ -96,11 +103,22 @@ useHead({
|
||||
|
||||
<div class="grid gap-8 lg:grid-cols-2 lg:items-center">
|
||||
<div>
|
||||
<span
|
||||
class="inline-block rounded-full bg-noro-brand/10 px-3 py-1 text-xs font-medium text-noro-brand"
|
||||
>
|
||||
{{ portfolioCategories[project.category].label }}
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="inline-block rounded-full bg-noro-brand/10 px-3 py-1 text-xs font-medium text-noro-brand"
|
||||
>
|
||||
{{ portfolioCategories[project.category].label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="project.video"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-noro-text/5 px-3 py-1 text-xs font-medium text-noro-muted"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
ویدیو
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="mt-4 text-3xl font-bold text-noro-text sm:text-4xl lg:text-5xl">
|
||||
{{ project.title }}
|
||||
</h1>
|
||||
@@ -120,7 +138,17 @@ useHead({
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-noro-border bg-noro-surface shadow-lg">
|
||||
<video
|
||||
v-if="project.video"
|
||||
:src="project.video"
|
||||
:poster="project.image"
|
||||
controls
|
||||
playsinline
|
||||
preload="metadata"
|
||||
class="aspect-[16/9] w-full bg-black object-contain"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:src="project.image"
|
||||
:alt="project.title"
|
||||
class="aspect-[4/3] w-full object-cover"
|
||||
@@ -168,24 +196,35 @@ useHead({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="noro-section border-t border-noro-border bg-noro-surface/50">
|
||||
<section
|
||||
v-if="project.gallery.length"
|
||||
class="noro-section border-t border-noro-border bg-noro-surface/50"
|
||||
>
|
||||
<div class="noro-container">
|
||||
<SectionHeading
|
||||
eyebrow="Gallery"
|
||||
title="گالری تصاویر"
|
||||
title="گالری رسانه"
|
||||
description="نمایی نزدیکتر از جزئیات و خروجیهای این پروژه."
|
||||
/>
|
||||
<div class="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<button
|
||||
v-for="(image, index) in project.gallery"
|
||||
v-for="(item, index) in project.gallery"
|
||||
:key="index"
|
||||
type="button"
|
||||
class="group relative overflow-hidden rounded-2xl border border-noro-border bg-noro-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-noro-brand"
|
||||
@click="openLightbox(index)"
|
||||
>
|
||||
<video
|
||||
v-if="isVideoUrl(item)"
|
||||
:src="item"
|
||||
muted
|
||||
preload="metadata"
|
||||
class="pointer-events-none aspect-[4/3] w-full bg-black object-cover"
|
||||
/>
|
||||
<img
|
||||
:src="image"
|
||||
:alt="`${project.title} — تصویر ${index + 1}`"
|
||||
v-else
|
||||
:src="item"
|
||||
:alt="`${project.title} — رسانه ${index + 1}`"
|
||||
class="aspect-[4/3] w-full object-cover transition duration-500 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
@@ -196,7 +235,7 @@ useHead({
|
||||
<span
|
||||
class="rounded-full bg-white px-3 py-1 text-xs font-medium text-[rgb(15_20_20)] opacity-0 shadow-md transition group-hover:opacity-100"
|
||||
>
|
||||
بزرگنمایی
|
||||
{{ isVideoUrl(item) ? 'پخش ویدیو' : 'بزرگنمایی' }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -258,11 +297,11 @@ useHead({
|
||||
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="lightboxIndex !== null"
|
||||
v-if="lightboxIndex !== null && lightboxItem"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4 backdrop-blur-sm"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-label="`تصویر ${lightboxIndex + 1} از ${project.gallery.length}`"
|
||||
:aria-label="`رسانه ${lightboxIndex + 1} از ${project.gallery.length}`"
|
||||
@click.self="closeLightbox"
|
||||
>
|
||||
<button
|
||||
@@ -280,7 +319,7 @@ useHead({
|
||||
v-if="galleryLength > 1"
|
||||
type="button"
|
||||
class="absolute right-4 top-1/2 z-10 -translate-y-1/2 rounded-full bg-white/10 p-3 text-white transition hover:bg-white/20"
|
||||
aria-label="تصویر قبلی"
|
||||
aria-label="قبلی"
|
||||
@click.stop="showPrevImage"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -292,7 +331,7 @@ useHead({
|
||||
v-if="galleryLength > 1"
|
||||
type="button"
|
||||
class="absolute left-4 top-1/2 z-10 -translate-y-1/2 rounded-full bg-white/10 p-3 text-white transition hover:bg-white/20"
|
||||
aria-label="تصویر بعدی"
|
||||
aria-label="بعدی"
|
||||
@click.stop="showNextImage"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -307,9 +346,20 @@ useHead({
|
||||
{{ lightboxIndex + 1 }} / {{ galleryLength }}
|
||||
</p>
|
||||
|
||||
<video
|
||||
v-if="isVideoUrl(lightboxItem)"
|
||||
:key="lightboxItem"
|
||||
:src="lightboxItem"
|
||||
controls
|
||||
autoplay
|
||||
playsinline
|
||||
class="max-h-[90vh] max-w-full rounded-xl bg-black"
|
||||
@click.stop
|
||||
/>
|
||||
<img
|
||||
:src="project.gallery[lightboxIndex]"
|
||||
:alt="`${project.title} — تصویر ${lightboxIndex + 1}`"
|
||||
v-else
|
||||
:src="lightboxItem"
|
||||
:alt="`${project.title} — رسانه ${lightboxIndex + 1}`"
|
||||
class="max-h-[90vh] max-w-full rounded-xl object-contain"
|
||||
@click.stop
|
||||
>
|
||||
|
||||
+10
-1
@@ -45,6 +45,7 @@ export interface CmsProject {
|
||||
year?: string
|
||||
featured?: boolean
|
||||
imageUrl?: string
|
||||
videoUrl?: string
|
||||
galleryUrls?: string[]
|
||||
client?: string
|
||||
deliverables?: string[]
|
||||
@@ -69,8 +70,15 @@ export function resolveCmsUrl(path: string | undefined, apiBase: string): string
|
||||
return path.startsWith('/') ? `${base}${path}` : `${base}/${path}`
|
||||
}
|
||||
|
||||
export function isVideoUrl(url?: string): boolean {
|
||||
if (!url) return false
|
||||
const clean = url.split('?')[0]?.split('#')[0] || ''
|
||||
return /\.(mp4|webm|ogg|ogv|mov)$/i.test(clean)
|
||||
}
|
||||
|
||||
export function mapProject(project: CmsProject, apiBase: string): PortfolioProject {
|
||||
const image = resolveCmsUrl(project.imageUrl, apiBase)
|
||||
const video = resolveCmsUrl(project.videoUrl, apiBase)
|
||||
const gallery = (project.galleryUrls?.length ? project.galleryUrls : [project.imageUrl])
|
||||
.filter(Boolean)
|
||||
.map((url) => resolveCmsUrl(url, apiBase))
|
||||
@@ -83,7 +91,8 @@ export function mapProject(project: CmsProject, apiBase: string): PortfolioProje
|
||||
longDescription: project.longDescription || project.description,
|
||||
year: project.year || '',
|
||||
featured: project.featured,
|
||||
image: image || gallery[0] || '',
|
||||
image: image || gallery.find((url) => !isVideoUrl(url)) || '',
|
||||
video: video || undefined,
|
||||
gallery: gallery.length ? gallery : image ? [image] : [],
|
||||
client: project.client,
|
||||
deliverables: project.deliverables || []
|
||||
|
||||
Reference in New Issue
Block a user