Initial commit WIP

This commit is contained in:
Mike Conrad
2025-05-08 19:18:45 -04:00
commit 09cbc3f5a3
235 changed files with 25013 additions and 0 deletions

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
defineProps({
title: {
type: String,
required: false,
},
description: {
type: String,
required: false,
},
year: {
type: String,
required: false,
},
});
</script>
<template>
<li class="dark:text-zinc-400 flex flex-col gap-1">
<p class="flex items-baseline gap-2">
<span class="font-display text-2xl text-zinc-500 dark:text-zinc-500">{{ year }}</span>
<span><strong class="font-semibold dark:text-zinc-200">{{ title }}</strong></span>
</p>
<span class="text-sm">{{ description }}</span>
</li>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
interface Award {
title?: string;
year?: string;
description?: string;
}
defineProps({
titleText: {
type: String,
required: false,
},
awards: {
type: Array as PropType<Award[]>,
required: false,
},
});
</script>
<template>
<div class="not-prose flex flex-col gap-4 lg:w-1/2">
<h3 class="uppercase tracking-widest font-light text-zinc-900 dark:text-orange-600">
{{ titleText }}
</h3>
<ul class="space-y-4">
<AwardsItem v-for="(item, index) in awards" :key="index" :title="item.title" :year="item.year" :description="item.description" />
</ul>
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
const { src, alt } = defineProps<{
src: string;
alt?: string;
}>();
</script>
<template>
<NuxtImg
v-if="src"
placeholder
sizes="sm:100vw md:70vw"
class="w-full"
:src="src"
:alt="alt"
loading="lazy"
/>
</template>

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
const { innerClass } = defineProps<{
innerClass?: string;
}>();
</script>
<template>
<div class="sm:px-8">
<div class="mx-auto max-w-7xl lg:px-8">
<div class="relative px-4 sm:px-8 lg:px-12" :class="innerClass">
<div class="mx-auto max-w-2xl lg:max-w-5xl">
<slot />
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import { withTrailingSlash } from "ufo";
const props = defineProps({
path: {
type: String,
default: "galleries",
},
});
const { data: _galleries } = await useAsyncData(
"galleries",
async () =>
await queryContent(withTrailingSlash(props.path)).find()
);
const galleries = computed(() => _galleries.value || [])
</script>
<template>
<div v-if="galleries?.length" class="not-prose grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<GalleryListItem
v-for="(gallery, index) in galleries"
:key="index"
:gallery="gallery"
/>
</div>
<div v-else>
<p class="">
No galleries found.
</p>
</div>
</template>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import type { Image } from "~/../types/image";
type Gallery = {
_path: string;
title: string;
description?: string;
cover?: Image;
};
const props = defineProps({
gallery: {
type: Object as PropType<Gallery>,
required: true,
validator: (value: Gallery) => {
if (value?._path && value.title) {
return true;
}
return false;
},
},
});
</script>
<template>
<NuxtLink :to="gallery._path" class="group">
<div
class="relative w-full overflow-hidden rounded-lg aspect-[3/2] md:aspect-[2/3] dark:bg-zinc-800"
>
<NuxtImg
:src="gallery.cover?.src || 'img/placeholder.jpg'"
:alt="gallery.cover?.alt || gallery.title"
:width="gallery.cover?.width"
:height="gallery.cover?.height"
class="h-full w-full object-cover object-center group-hover:opacity-75"
sizes="sm:100vw md:50vw lg:30vw"
loading="lazy"
placeholder
/>
<div class="absolute bottom-0 w-full p-4 grid grid-cols-4 gap-3" v-if="gallery?.images?.length">
<div
v-for="thumbnail, index in gallery.images.slice(0, 4)"
:key="index"
class="col-span-1 aspect-square w-full rounded-lg overflow-hidden group-hover:opacity-75 dark:bg-zinc-800"
>
<NuxtImg
:src="thumbnail.src"
alt="thumbnail.alt"
class="h-full w-full object-cover object-center"
loading="lazy"
sizes="sm:70px md:75px"
placeholder
/>
</div>
</div>
</div>
<div
class="mt-4 flex items-center justify-between text-base font-medium dark:text-zinc-200"
>
<h3>{{ gallery.title }}</h3>
</div>
</NuxtLink>
</template>

View File

@ -0,0 +1,153 @@
<script setup lang="ts">
import type { PropType } from "vue";
import type { Image } from "../../types/image";
defineProps({
image1: {
type: Object as PropType<Image>,
required: false,
},
image2: {
type: Object as PropType<Image>,
required: false,
},
image3: {
type: Object as PropType<Image>,
required: false,
},
image4: {
type: Object as PropType<Image>,
required: false,
},
image5: {
type: Object as PropType<Image>,
required: false,
},
image6: {
type: Object as PropType<Image>,
required: false,
},
image7: {
type: Object as PropType<Image>,
required: false,
},
});
const defaultImage = "img/placeholder.jpg";
</script>
<template>
<div class="not-prose mt-16 sm:mt-24 content-visibility-visible contain-intrinsic-size-[auto_600px]">
<div class="flex items-center justify-center gap-5 py-4 sm:gap-6 relative z-20">
<!-- col1 -->
<div class="hidden lg:flex flex-col gap-5 sm:gap-6" v-parallax data-rellax-speed="3">
<div class="relative aspect-[2/3] w-36 lg:w-52 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800">
<NuxtImg
placeholder
sizes="sm:100vw md:50vw lg:220px"
class="absolute inset-0 h-full w-full object-cover"
:src="image1?.src ? image1.src : defaultImage"
:alt="image1?.alt ? image1.alt : 'No alt text'"
:width="image1?.width ? image1.width : 1"
:height="image1?.height ? image1.height : 1"
format="webp"
loading="lazy"
/>
</div>
</div>
<!-- col2 -->
<div class="flex flex-col gap-5 sm:gap-6" v-parallax data-rellax-speed="1">
<div class="relative aspect-[4/3] w-44 md:w-52 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800">
<NuxtImg
placeholder
sizes="sm:100vw md:50vw lg:220px"
class="absolute inset-0 h-full w-full object-cover"
:src="image2?.src ? image2.src : defaultImage"
:alt="image2?.alt ? image2.alt : 'No alt text'"
:width="image2?.width ? image2.width : 1"
:height="image2?.height ? image2.height : 1"
format="webp"
loading="lazy"
/>
</div>
<div class="relative aspect-[3/4] w-44 md:w-52 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800">
<NuxtImg
placeholder
sizes="sm:100vw md:50vw lg:220px"
class="absolute inset-0 h-full w-full object-cover"
:src="image3?.src ? image3.src : defaultImage"
:alt="image3?.alt ? image3.alt : 'No alt text'"
:width="image3?.width ? image3.width : 1"
:height="image3?.height ? image3.height : 1"
format="webp"
loading="lazy"
/>
</div>
</div>
<!-- col3 -->
<div class="flex flex-col gap-5 sm:gap-6" v-parallax data-rellax-speed="0">
<div class="relative aspect-[2/3] w-72 md:w-80 lg:w-96 flex-none overflow-hidden rounded-2xl bg-zinc-100 dark:bg-zinc-800">
<NuxtImg
placeholder
sizes="sm:100vw md:50vw lg:380px"
class="absolute inset-0 h-full w-full object-cover"
:src="image4?.src ? image4.src : defaultImage"
:alt="image4?.alt ? image4.alt : 'No alt text'"
:width="image4?.width ? image4.width : 1"
:height="image4?.height ? image4.height : 1"
format="webp"
loading="lazy"
/>
</div>
</div>
<!-- col4 -->
<div class="flex flex-col gap-5 sm:gap-6" v-parallax data-rellax-speed="1">
<div class="relative aspect-[3/4] w-44 md:w-52 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800">
<NuxtImg
placeholder
sizes="sm:100vw md:50vw lg:220px"
class="absolute inset-0 h-full w-full object-cover"
:src="image5?.src ? image5.src : defaultImage"
:alt="image5?.alt ? image5.alt : 'No alt text'"
:width="image5?.width ? image5.width : 1"
:height="image5?.height ? image5.height : 1"
format="webp"
loading="lazy"
/>
</div>
<div class="relative aspect-[4/3] w-44 md:w-52 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800">
<NuxtImg
placeholder
sizes="sm:100vw md:50vw lg:220px"
class="absolute inset-0 h-full w-full object-cover"
:src="image6?.src ? image6.src : defaultImage"
:alt="image6?.alt ? image6.alt : 'No alt text'"
:width="image6?.width ? image6.width : 1"
:height="image6?.height ? image6.height : 1"
format="webp"
loading="lazy"
/>
</div>
</div>
<!-- col5 -->
<div class="hidden lg:flex flex-col gap-5 sm:gap-6" v-parallax data-rellax-speed="3">
<div class="relative aspect-[2/3] w-44 lg:w-52 flex-none overflow-hidden rounded-xl bg-zinc-100 dark:bg-zinc-800">
<NuxtImg
placeholder
sizes="sm:100vw md:50vw lg:220px"
class="absolute inset-0 h-full w-full object-cover"
:src="image7?.src ? image7.src : defaultImage"
:alt="image7?.alt ? image7.alt : 'No alt text'"
:width="image7?.width ? image7.width : 1"
:height="image7?.height ? image7.height : 1"
format="webp"
loading="lazy"
/>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,12 @@
<template>
<div class="not-prose lg:max-w-3xl mx-auto pt-8 lg:pt-44 content-visibility-auto contain-intrinsic-size-[auto_400px]">
<div class="flex flex-col gap-9">
<h1 class="text-center font-display font-light leading-snug text-3xl md:text-4xl lg:text-5xl text-zinc-700 dark:text-zinc-300">
<slot> Inspire Emotion and Evoke Storytelling through the Art of Photography. </slot>
</h1>
<p class="text-zinc-700 dark:text-zinc-400 text-center text-sm lg:text-base px-0 md:px-14 lg:px-24">
<slot name="description"> Explore my photography portfolio and see the world through my creative lens. Contact me if you would like to hire me. </slot>
</p>
</div>
</div>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import type { Image } from "~/../types/image";
interface itemPropsT {
item: Image;
index: number;
}
type Props = {
images: Image[];
};
defineProps<Props>();
</script>
<template>
<div class="not-prose">
<PhotoSwipe>
<div>
<MasonryWall :items="images" :ssr-columns="1" :column-width="300" :gap="32" class="grid grid-cols-2 lg:grid-cols-3 gap-8">
<template #default="{ item }: itemPropsT">
<a class="photoswipe-item rounded-xl overflow-hidden block dark:bg-zinc-800 bg-zinc-200" :href="$img(item.src, { width: 1600 })" data-cropped="true" :data-pswp-width="item.width" :data-pswp-height="item.height">
<NuxtImg :src="item.src" alt="Some image" sizes="sm:90vw md:50vw lg:30vw" class="w-full h-full object-cover object-center" :width="item.width" :height="item.height" loading="lazy" />
</a>
</template>
</MasonryWall>
</div>
</PhotoSwipe>
</div>
</template>

View File

@ -0,0 +1,49 @@
<template>
<div class="not-prose">
<div class="mx-auto max-w-2xl rounded-3xl ring-1 ring-zinc-200 dark:ring-zinc-50/5 lg:mx-0 lg:flex lg:max-w-none lg:items-stretch">
<div class="p-8 sm:p-10 lg:flex-auto">
<h3 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-zinc-200">{{ title }}</h3>
<p v-if="description" class="mt-6 text-base leading-7 text-zinc-600 dark:text-zinc-500">{{ description }}</p>
<ul role="list" class="mt-8 grid grid-cols-1 gap-4 text-sm leading-6 text-zinc-600 dark:text-zinc-400">
<li v-for="feature in includedFeatures" :key="feature" class="flex gap-x-2">
<Icon name="heroicons:check" class="h-6 w-5 flex-none text-yellow-600" />
{{ feature }}
</li>
</ul>
</div>
<div class="-mt-2 p-2 lg:mt-0 lg:w-full lg:max-w-sm lg:flex-shrink-0">
<div
class="relative overflow-hidden rounded-2xl py-10 text-center ring-1 ring-inset ring-zinc-900/5 lg:flex lg:flex-col lg:justify-center lg:py-16 lg:h-full"
:class="[image ? 'bg-zinc-200 dark:bg-zinc-800' : 'bg-zinc-100 dark:bg-zinc-800/30']"
>
<div v-if="image" class="absolute inset-0 mix-blend-overlay">
<NuxtImg :src="image.src" :alt="image.alt" :width="image.width" :height="image.height" class="w-full h-full object-cover" />
</div>
<div class="mx-auto max-w-xs px-8 z-10">
<p class="flex items-baseline justify-center gap-x-2">
<span class="text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100">{{ price }}</span>
<span class="text-sm font-semibold leading-6 tracking-wide text-zinc-800 dark:text-zinc-200">{{ currency }}</span>
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Image } from "~/../types/image";
interface Props {
title: string;
description?: string;
includedFeatures: string[];
price: string;
currency?: string;
image?: Image;
}
withDefaults(defineProps<Props>(), {
description: "",
currency: "USD",
});
</script>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
defineProps({
title: {
type: String,
required: true,
},
description: {
type: String,
required: false,
},
orientation: {
type: String,
required: false,
default: "left",
},
});
</script>
<template>
<div class="not-prose relative pt-14 pb-10 lg:mt-32 lg:pb-24">
<div class="absolute top-0 left-0 pointer-events-none w-full text-clip overflow-hidden" :class="[orientation === 'center' ? 'flex justify-center' : '']" v-parallax data-rellax-speed="4">
<span class="text-[9rem] lg:text-[10rem] font-display text-zinc-900 dark:text-zinc-50 opacity-2 truncate">{{ title }}</span>
</div>
<div>
<template v-if="orientation === 'left'">
<div class="max-w-xl">
<h1 class="font-thin font-display text-5xl text-gradient leading-tighter w-max max-w-full">{{ title }}</h1>
</div>
<div class="max-w-2xl">
<p class="mt-6 lg:mt-9 dark:text-zinc-500">
<slot name="description">{{ description }}</slot>
</p>
</div>
</template>
<template v-if="orientation === 'center'">
<div class="max-w-xl mx-auto flex justify-center">
<h1 class="font-thin font-display text-5xl text-gradient leading-tighter text-center w-max">{{ title }}</h1>
</div>
<div class="max-w-2xl mx-auto">
<p class="mt-6 lg:mt-9 dark:text-zinc-500 text-center">
<slot name="description">{{ description }}</slot>
</p>
</div>
</template>
</div>
</div>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import type { Image } from "../../types/image";
defineProps({
firstname: {
type: String,
required: false,
},
lastname: {
type: String,
required: false,
},
photo: {
type: Object as PropType<Image>,
required: false,
},
});
</script>
<template>
<div class="not-prose mt-16 lg:mt-48 content-visibility-visible contain-intrinsic-size-[auto_750px]">
<div class="grid grid-cols-1 lg:grid-cols-7 gap-8">
<div class="col-span-4 flex flex-col gap-8 order-2 lg:order-1">
<h2 class="text-4xl lg:text-6xl font-display text-zinc-800 dark:text-zinc-200">
{{ firstname }} <br />
{{ lastname }}
</h2>
<div class="dark:text-zinc-400 flex flex-col gap-4">
<slot name="description"></slot>
</div>
<NuxtImg src="/img/home/sign.png" alt="signature" class="h-8 mr-auto" />
</div>
<div v-parallax
data-rellax-xs-speed="0"
data-rellax-mobile-speed="0"
data-rellax-tablet-speed="0"
data-parallax-speed="-1" data-rellax-percentage="0.5" class="col-span-3 order-1 lg:order-2 pl-2 pt-2">
<div class="lg:absolute aspect-square lg:aspect-[2/3] flex-none overflow-hidden bg-zinc-100 dark:bg-zinc-800 w-32 lg:w-72 rounded-2xl rotate-3">
<NuxtImg
placeholder
:src="photo?.src ? photo.src : 'img/placeholder.jpg'"
:alt="photo?.alt ? photo.alt : 'Oscar Mattern'"
:width="photo?.width ? photo.width : 1"
:height="photo?.height ? photo.height : 1"
format="webp"
loading="lazy"
class="h-full w-full object-cover"
sizes="sm:50vw md:50vw lg:30vw"
/>
</div>
</div>
</div>
<hr class="my-10 h-px border-0 bg-zinc-200 dark:bg-zinc-800" />
<slot name="extra"></slot>
</div>
</template>

View File

@ -0,0 +1,55 @@
<template>
<div class="not-prose">
<div class="relative px-4 py-12 lg:py-24 rounded-2xl overflow-hidden border border-zinc-200/50 dark:border-yellow-100/5 shadow-sm dark:shadow-2xl">
<div class="">
<h2 class="font-display font-thin text-4xl max-w-2xl mx-auto text-center text-zinc-900 dark:text-gradient">
<slot name="title">Questions or collaboration ideas?</slot>
</h2>
<div class="flex flex-col gap-4 mt-8 max-w-lg mx-auto text-center text-zinc-600 dark:text-zinc-400 text-sm">
<slot name="description">
<p>I would love to hear from you! Whether you have questions about my work, have collaboration ideas, or are interested in hiring me for a project, don't hesitate to reach out.</p>
<p>I usually respond within hours!</p>
</slot>
</div>
<ul class="mt-16 flex flex-col gap-4 lg:gap-12 lg:flex-row items-center justify-center max-w-2xl mx-auto">
<li>
<NuxtLink to="mailto:oscar@mattern.com" class="flex items-center gap-2 text-sm text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100">
<Icon name="heroicons:envelope-20-solid" size="1.8rem" />
<span>oscar@mattern.com</span>
</NuxtLink>
</li>
<li>
<NuxtLink to="https://instagram.org" class="flex items-center gap-2 text-sm text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100">
<Icon name="fe:instagram" size="1.8rem" />
<span>oscarphotography</span>
</NuxtLink>
</li>
<li>
<NuxtLink to="https://instagram.org" class="flex items-center gap-2 text-sm text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100">
<Icon name="fe:twitter" size="1.8rem" />
<span>oscarphotography</span>
</NuxtLink>
</li>
</ul>
</div>
<svg viewBox="0 0 1024 1024" class="hidden dark:block absolute top-1/2 left-1/2 h-[64rem] w-[64rem] -translate-x-1/2 pointer-events-none" aria-hidden="true">
<circle cx="512" cy="512" r="512" fill="url(#759c1415-0410-454c-8f7c-9a820de03641)" fill-opacity="0.7"></circle>
<defs>
<radialGradient id="759c1415-0410-454c-8f7c-9a820de03641" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(512 512) rotate(90) scale(512)">
<stop stop-color="#18181B"></stop>
<stop offset="1" stop-color="#333626" stop-opacity="0"></stop>
</radialGradient>
</defs>
</svg>
<svg viewBox="0 0 1024 1024" class="dark:hidden absolute top-1/2 left-1/2 h-[64rem] w-[64rem] -translate-x-1/2 pointer-events-none" aria-hidden="true">
<circle cx="512" cy="512" r="512" fill="url(#759c1415-0410-454c-8f7c-9a820de03641)" fill-opacity="0.7"></circle>
<defs>
<radialGradient id="759c1415-0410-454c-8f7c-9a820de03641" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(512 512) rotate(90) scale(512)">
<stop stop-color="#F0F0F0"></stop>
<stop offset="1" stop-color="#F3E0AD" stop-opacity="0"></stop>
</radialGradient>
</defs>
</svg>
</div>
</div>
</template>

View File

@ -0,0 +1,12 @@
<template>
<div class="not-prose mt-32">
<div class="divide-y divide-zinc-900/10 dark:divide-zinc-100/10">
<h2 class="text-2xl font-bold leading-10 tracking-tight text-zinc-900 dark:text-zinc-100">
<slot name="title">Frequently asked questions</slot>
</h2>
<dl class="mt-10 space-y-6 divide-y divide-zinc-900/10 dark:divide-zinc-100/10">
<slot name="items" />
</dl>
</div>
</div>
</template>

View File

@ -0,0 +1,7 @@
<template>
<div class="not-prose">
<div class="flex flex-col gap-8">
<slot/>
</div>
</div>
</template>

View File

@ -0,0 +1,10 @@
<template>
<div class="mt-24">
<div class="text-center">
<slot name="title" />
</div>
<div class="not-prose -mt-8 sm:-mx-4 sm:columns-2 sm:text-[0]">
<slot name="items" />
</div>
</div>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import { withTrailingSlash } from "ufo";
const props = defineProps({
path: {
type: String,
default: "stories",
},
});
const { data: _stories } = await useAsyncData(
"stories",
async () =>
await queryContent(withTrailingSlash(props.path)).sort({ date: -1 }).find()
);
const stories = computed(() => _stories.value || []);
</script>
<template>
<div
v-if="stories?.length"
class="not-prose grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
<StoryListItem v-for="story in stories" :key="story._path" :story="story" />
</div>
<div v-else>
<p class="">No Stories found.</p>
</div>
</template>

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import type { Image } from "~/../types/image";
type Story = {
_path: string;
title: string;
description?: string;
cover?: Image;
date?: string;
tags?: string[];
};
defineProps({
story: {
type: Object as PropType<Story>,
required: true,
validator: (value: Story) =>{
if (value?._path && value?.title) {
return true;
}
return false;
}
},
});
</script>
<template>
<NuxtLink :to="story._path" class="group">
<div
class="relative w-full overflow-hidden rounded-lg sm:aspect-[3/2] md:aspect-square lg:aspect-[2/3] dark:bg-zinc-800"
>
<NuxtImg
:src="story.cover?.src || 'img/placeholder.jpg'"
:alt="story.cover?.alt || story.title"
:width="story.cover?.width || 2"
:height="story.cover?.height || 3"
class="h-full w-full object-cover object-center group-hover:opacity-75"
sizes="sm:100vw md:50vw lg:30vw"
loading="lazy"
placeholder
/>
</div>
<div
class="mt-4 flex items-center justify-between text-base font-medium dark:text-zinc-300"
>
<h3>{{ story.title }}</h3>
</div>
<p v-if="story.date" class="mt-1 text-sm italic dark:text-zinc-500">
{{ formatDate(story.date) }}
</p>
</NuxtLink>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
import type { Image } from '~/../types/image';
defineProps({
quote: {
type: String,
required: false,
},
name: {
type: String,
required: false,
},
image: {
type: Object as PropType<Image>,
required: false,
},
});
</script>
<template>
<div class="pt-8 sm:inline-block sm:w-full sm:px-4">
<figure class="rounded-2xl bg-zinc-50 dark:bg-zinc-800/5 p-6 text-sm leading-6 dark:border dark:border-zinc-800/50">
<blockquote class="text-zinc-900 dark:text-zinc-400">
<p>{{ quote }}</p>
</blockquote>
<figcaption class="mt-6 flex items-center gap-x-4">
<NuxtImg class="h-10 w-10 rounded-full bg-zinc-50 dark:bg-zinc-800 object-cover" :src="image?.src ? image.src : '/img/placeholder.jpg'" :alt="image?.alt ? image.alt : 'Placeholder'" :width="image?.width ? image.width : 80" :height="image?.height ? image.height : 80" />
<div class="font-semibold text-zinc-900 dark:text-zinc-300">{{ name }}</div>
</figcaption>
</figure>
</div>
</template>

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
const { src1, src2, src3, alt1, alt2, alt3 } = defineProps<{
src1: string;
alt1: string;
src2: string;
alt2: string;
src3: string;
alt3: string;
}>();
</script>
<template>
<div class="not-prose pb-8 grid lg:grid-cols-3 gap-3 lg:gap-6 aspect-[1] sm:aspect-[4/3] lg:aspect-[4/3] lg:-mr-12 lg:-ml-12">
<div
class="col-span-2 row-span-2 dark:bg-gray-800 rounded-md lg:rounded-xl overflow-hidden"
>
<NuxtImg
:src="src1"
:alt="alt1"
loading="lazy"
class="h-full w-full object-cover object-center"
sizes="sm:100vw md:50vw"
/>
</div>
<div class="col-span-1 dark:bg-gray-800 rounded-md lg:rounded-xl overflow-hidden">
<NuxtImg
:src="src2"
:alt="alt2"
loading="lazy"
class="h-full w-full object-cover object-center"
sizes="sm:100vw md:30vw"
/>
</div>
<div class="col-span-1 dark:bg-gray-800 rounded-md lg:rounded-xl overflow-hidden">
<NuxtImg
:src="src3"
:alt="alt3"
loading="lazy"
class="h-full w-full object-cover object-center"
sizes="sm:100vw md:30vw"
/>
</div>
</div>
</template>