Initial commit WIP
This commit is contained in:
25
app/components/content/AwardsItem.vue
Normal file
25
app/components/content/AwardsItem.vue
Normal 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>
|
28
app/components/content/AwardsList.vue
Normal file
28
app/components/content/AwardsList.vue
Normal 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>
|
17
app/components/content/BlogImage.vue
Normal file
17
app/components/content/BlogImage.vue
Normal 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>
|
16
app/components/content/Container.vue
Normal file
16
app/components/content/Container.vue
Normal 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>
|
34
app/components/content/GalleriesList.vue
Normal file
34
app/components/content/GalleriesList.vue
Normal 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>
|
62
app/components/content/GalleryListItem.vue
Normal file
62
app/components/content/GalleryListItem.vue
Normal 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>
|
153
app/components/content/HeroGrid.vue
Normal file
153
app/components/content/HeroGrid.vue
Normal 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>
|
12
app/components/content/HeroText.vue
Normal file
12
app/components/content/HeroText.vue
Normal 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>
|
30
app/components/content/MasonryGallery.vue
Normal file
30
app/components/content/MasonryGallery.vue
Normal 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>
|
49
app/components/content/PackagePrice.vue
Normal file
49
app/components/content/PackagePrice.vue
Normal 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>
|
47
app/components/content/PageHeader.vue
Normal file
47
app/components/content/PageHeader.vue
Normal 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>
|
56
app/components/content/SectionAboutMe.vue
Normal file
56
app/components/content/SectionAboutMe.vue
Normal 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>
|
55
app/components/content/SectionCtaHireMe.vue
Normal file
55
app/components/content/SectionCtaHireMe.vue
Normal 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>
|
12
app/components/content/SectionFaq.vue
Normal file
12
app/components/content/SectionFaq.vue
Normal 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>
|
7
app/components/content/SectionPackages.vue
Normal file
7
app/components/content/SectionPackages.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="not-prose">
|
||||
<div class="flex flex-col gap-8">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
10
app/components/content/SectionTestimonials.vue
Normal file
10
app/components/content/SectionTestimonials.vue
Normal 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>
|
31
app/components/content/StoriesList.vue
Normal file
31
app/components/content/StoriesList.vue
Normal 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>
|
52
app/components/content/StoryListItem.vue
Normal file
52
app/components/content/StoryListItem.vue
Normal 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>
|
31
app/components/content/Testimonial.vue
Normal file
31
app/components/content/Testimonial.vue
Normal 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>
|
44
app/components/content/ThreeImages.vue
Normal file
44
app/components/content/ThreeImages.vue
Normal 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>
|
Reference in New Issue
Block a user