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

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# Nuxt 3 Minimal Starter
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

5
app/app.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

BIN
app/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

6
app/assets/logo.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

10
app/assets/settings.scss Normal file
View File

@ -0,0 +1,10 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
// $color-pack: false
// );

View File

@ -0,0 +1,82 @@
<script setup lang="ts">
const items = shallowRef([
{
title: 'Vuetify Documentation',
icon: `$vuetify`,
href: 'https://vuetifyjs.com/',
},
{
title: 'Vuetify Support',
icon: 'mdi-shield-star-outline',
href: 'https://support.vuetifyjs.com/',
},
{
title: 'Vuetify X',
icon: ['M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z'],
href: 'https://x.com/vuetifyjs',
},
{
title: 'Vuetify GitHub',
icon: `mdi-github`,
href: 'https://github.com/vuetifyjs/vuetify',
},
{
title: 'Vuetify Discord',
icon: ['M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z'],
href: 'https://community.vuetifyjs.com/',
},
{
title: 'Vuetify Reddit',
icon: `mdi-reddit`,
href: 'https://reddit.com/r/vuetifyjs',
},
])
</script>
<template>
<v-footer
app
height="40"
>
<NuxtLink
v-for="item in items"
:key="item.title"
class="d-inline-block mx-2 social-link"
:href="item.href"
rel="noopener noreferrer"
target="_blank"
:title="item.title"
>
<v-icon
:icon="item.icon"
:size="item.icon === '$vuetify' ? 24 : 16"
/>
</NuxtLink>
<div
class="text-caption text-disabled"
style="position: absolute; right: 16px;"
>
&copy; 2016-{{ (new Date()).getFullYear() }} <span class="d-none d-sm-inline-block">Vuetify, LLC</span>
<NuxtLink
class="text-decoration-none on-surface"
href="https://vuetifyjs.com/about/licensing/"
rel="noopener noreferrer"
target="_blank"
>
MIT License
</NuxtLink>
</div>
</v-footer>
</template>
<style scoped lang="sass">
.social-link :deep(.v-icon)
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
text-decoration: none
transition: .2s ease-in-out
&:hover
color: rgba(25, 118, 210, 1)
</style>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
const { bgClass, gradClass, roundnessClass, contentClass, as } = withDefaults(defineProps<{
bgClass?: string;
gradClass?: string;
roundnessClass?: string;
contentClass?: string;
as?: string;
}>(),{
bgClass: "bg-white dark:bg-zinc-900",
gradClass: "bg-gradient-to-r from-yellow-400 to-pink-400 opacity-30",
roundnessClass: "rounded-2xl",
contentClass: "",
as: "div"
})
</script>
<template>
<component :is="as" :class="$cls('group relative p-8', roundnessClass)" v-bind="$attrs">
<div :class="$cls('absolute -inset-px', gradClass, roundnessClass)" aria-hidden="true"></div>
<div :class="$cls('absolute inset-0', bgClass, roundnessClass)" aria-hidden="true"></div>
<div class="relative z-10" :class="contentClass">
<slot />
</div>
</component>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { SunIcon, MoonIcon } from "@heroicons/vue/20/solid";
const colorMode = useColorMode();
const onClick = () =>
colorMode.value === "light"
? (colorMode.preference = "dark")
: (colorMode.preference = "light");
</script>
<template>
<button
aria-label="Color Mode"
class="border rounded-full px-2 py-2 text-zinc-500 border-zinc-500 hover:bg-white hover:text-zinc-900 hover:border-zinc-900 active:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-700 focus-visible:ring-opacity-75"
@click="onClick"
>
<ColorScheme placeholder="...">
<template v-if="colorMode.value === 'dark'">
<SunIcon name="dark-mode" class="w-4 h-4" />
<span class="sr-only">Dark Mode</span>
</template>
<template v-else>
<MoonIcon name="light-mode" class="w-4 h-4" />
<span class="sr-only">Light Mode</span>
</template>
</ColorScheme>
</button>
</template>

View File

@ -0,0 +1,7 @@
<template>
<div class="sm:px-8">
<div class="mx-auto max-w-8xl lg:px-8">
<slot />
</div>
</div>
</template>

10
app/components/Footer.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<v-container>
<div class="dark:text-zinc-500 pb-16 mt-16">
<div class="text-center">
<!-- copyright -->
<p class="text-sm">&copy; {{ new Date().getFullYear() }} <NuxtLink to="/" class="text-zinc-500 hover:text-zinc-400">Oscar Mattern</NuxtLink>, All rights reserved.</p>
</div>
</div>
</v-container>
</template>

60
app/components/Header.vue Normal file
View File

@ -0,0 +1,60 @@
<script setup lang="ts">
const links = [
{ name: "Home", to: "/" },
{ name: "Galleries", to: "/galleries" },
{ name: "Stories", to: "/stories" },
{ name: "Hire me", to: "/hire-me" },
] as { name: string; to: string }[];
onMounted(() => {
window.addEventListener("scroll", onScroll);
});
onBeforeUnmount(() => {
window.removeEventListener("scroll", onScroll);
});
const showHeader = ref(true);
const lastScrollPosition = ref(0);
const onScroll = () => {
const currentScrollPosition =
window.pageYOffset || document.documentElement.scrollTop;
// Momentum scrolling on iOS can cause the scroll position to be negative
if (currentScrollPosition < 0) return;
// add 60px delay
if (Math.abs(currentScrollPosition - lastScrollPosition.value) < 60) return;
// show if scrolling up
showHeader.value = currentScrollPosition < lastScrollPosition.value;
lastScrollPosition.value = currentScrollPosition;
};
</script>
<template>
<div
:class="[
showHeader ? 'translate-y-0' : '-translate-y-full',
'transform-gpu transition-transform duration-500 sticky top-0 z-50',
]"
>
<v-container class="pt-4 lg:pt-10">
<div class="flex items-center justify-between">
<Logo />
<div
class="border border-zinc-300/50 dark:border-zinc-900/60 rounded-full pl-2 lg:pl-4 pr-2 py-2 backdrop-blur-lg bg-zinc-100/50 dark:bg-zinc-800/50"
>
<div class="flex items-center gap-4">
<div class="hidden lg:block">
<NavLinks :links="links" />
</div>
<div class="lg:hidden">
<NavLinksMobile :links="links" />
</div>
<ColorModeSwitch />
</div>
</div>
</div>
</v-container>
</div>
</template>

View File

@ -0,0 +1,90 @@
<template>
<v-container class="fill-height" max-width="900">
<div>
<v-img
class="mb-4"
height="150"
src="~/assets/logo.png"
/>
<div class="mb-8 text-center">
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
</div>
<v-row>
<v-col cols="12">
<v-card
class="py-4"
color="surface-variant"
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
prepend-icon="mdi-rocket-launch-outline"
rounded="lg"
variant="tonal"
>
<template #image>
<v-img position="top right" />
</template>
<template #title>
<h2 class="text-h5 font-weight-bold">
Get started
</h2>
</template>
<template #subtitle>
<div class="text-subtitle-1">
Change this page by updating <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>components/HelloWorld.vue</v-kbd>.
</div>
</template>
</v-card>
</v-col>
<v-col v-for="link in links" :key="link.href" cols="6">
<v-card
append-icon="mdi-open-in-new"
class="py-4"
color="surface-variant"
:href="link.href"
:prepend-icon="link.icon"
rel="noopener noreferrer"
rounded="lg"
:subtitle="link.subtitle"
target="_blank"
:title="link.title"
variant="tonal"
/>
</v-col>
</v-row>
</div>
</v-container>
</template>
<script setup>
const links = [
{
href: 'https://vuetifyjs.com/',
icon: 'mdi-text-box-outline',
subtitle: 'Learn about all things Vuetify in our documentation.',
title: 'Documentation',
},
{
href: 'https://vuetifyjs.com/introduction/why-vuetify/#feature-guides',
icon: 'mdi-star-circle-outline',
subtitle: 'Explore available framework Features.',
title: 'Features',
},
{
href: 'https://vuetifyjs.com/components/all',
icon: 'mdi-widgets-outline',
subtitle: 'Discover components in the API Explorer.',
title: 'Components',
},
{
href: 'https://discord.vuetifyjs.com',
icon: 'mdi-account-group-outline',
subtitle: 'Connect with Vuetify developers.',
title: 'Community',
},
]
</script>

View File

@ -0,0 +1,116 @@
<script setup lang="ts"></script>
<template>
<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="/img/home/grid-01.jpg"
alt="Alt text"
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="/img/home/grid-02.jpg"
alt="Alt text"
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="/img/home/grid-03.jpg"
alt="Alt text"
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="/img/home/grid-07.jpg"
alt="Alt text"
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="/img/home/grid-04.jpg"
alt="Alt text"
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="/img/home/grid-05.jpg"
alt="Alt text"
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="/img/home/grid-06.jpg"
alt="Alt text"
format="webp"
loading="lazy"
/>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import {ArrowLongRightIcon} from "@heroicons/vue/20/solid"
const { to } = withDefaults(defineProps<{
to: string;
}>(),{
to: "/",
})
</script>
<template>
<NuxtLink :to="to" class="group/arrowlink flex items-center gap-2 text-gray-500 hover:text-gray-900 dark:text-zinc-300 dark:hover:text-zinc-100">
<span><slot/></span>
<ArrowLongRightIcon class="w-5 h-5 transition transform-gpu group-hover/arrowlink:translate-x-1" />
</NuxtLink>
</template>

18
app/components/Logo.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<NuxtLink
to="/"
class="group flex items-center gap-4 text-zinc-700 dark:text-zinc-400 hover:text-zinc-900 focus:outline-none focus-visible:ring-offset-2 ring-offset-zinc-900 focus:rounded-full focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
<div class="rounded-full border-2 border-white dark:border-zinc-400 shadow-md group-hover:shadow-xl transition-shadow duration-100 shrink-0">
<NuxtImg
class="inline-block h-14 w-14 rounded-full"
src="/img/logo.jpg"
alt="Oscar Mattern"
format="webp"
width="100"
height="100"
/>
</div>
<span class="uppercase tracking-widest hidden md:inline-flex shrink-1 sr-only">Oscar Mattern</span>
</NuxtLink>
</template>

View File

@ -0,0 +1,130 @@
<script setup lang="ts">
import type { Ref } from "vue";
import { nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch } from "vue";
type Column = number[];
interface Props {
items: unknown[];
columnWidth?: number;
gap?: number;
rtl?: boolean;
ssrColumns?: number;
scrollContainer?: HTMLElement | null;
}
const props = withDefaults(defineProps<Props>(), {
columnWidth: 400,
gap: 0,
rtl: false,
ssrColumns: 0,
scrollContainer: null,
});
const emit = defineEmits<{
(event: "redraw"): void;
(event: "redrawSkip"): void;
}>();
const { columnWidth, items, gap, rtl, ssrColumns, scrollContainer } =
toRefs(props);
const columns = ref<Column[]>([]);
const wall = ref<HTMLDivElement>() as Ref<HTMLDivElement>;
function columnCount(): number {
const count = Math.floor(
(wall.value.getBoundingClientRect().width + gap.value) /
(columnWidth.value + gap.value)
);
return count > 0 ? count : 1;
}
function createColumns(count: number): Column[] {
return [...new Array(count)].map(() => []);
}
if (ssrColumns.value > 0) {
const newColumns = createColumns(ssrColumns.value);
items.value.forEach((_: unknown, i: number) =>
newColumns[i % ssrColumns.value]!.push(i)
);
columns.value = newColumns;
}
async function fillColumns(itemIndex: number) {
if (itemIndex >= items.value.length) {
return;
}
await nextTick();
const columnDivs = [...wall.value.children] as HTMLDivElement[];
if (rtl.value) {
columnDivs.reverse();
}
const target = columnDivs.reduce((prev, curr) =>
curr.getBoundingClientRect().height < prev.getBoundingClientRect().height
? curr
: prev
);
columns.value[+target.dataset.index!]!.push(itemIndex);
await fillColumns(itemIndex + 1);
}
async function redraw(force = false) {
if (columns.value.length === columnCount() && !force) {
emit("redrawSkip");
return;
}
columns.value = createColumns(columnCount());
const scrollTarget = scrollContainer?.value;
const scrollY = scrollTarget ? scrollTarget.scrollTop : window.scrollY;
await fillColumns(0);
scrollTarget
? scrollTarget.scrollBy({ top: scrollY - scrollTarget.scrollTop })
: window.scrollTo({ top: scrollY });
emit("redraw");
}
const resizeObserver =
typeof ResizeObserver === "undefined"
? undefined
: new ResizeObserver(() => redraw());
onMounted(() => {
redraw();
resizeObserver?.observe(wall.value);
});
onBeforeUnmount(() => resizeObserver?.unobserve(wall.value));
watch([items, rtl], () => redraw(true));
watch([columnWidth, gap], () => redraw());
</script>
<template>
<div
ref="wall"
class="masonry-wall"
:style="{ display: 'flex', gap: `${gap}px` }"
>
<div
v-for="(column, columnIndex) in columns"
:key="columnIndex"
class="masonry-column"
:data-index="columnIndex"
:style="{
display: 'flex',
'flex-basis': '0px',
'flex-direction': 'column',
'flex-grow': 1,
gap: `${gap}px`,
height: ['-webkit-max-content', '-moz-max-content', 'max-content'] as any,
'min-width': 0
}"
>
<div v-for="itemIndex in column" :key="itemIndex" class="masonry-item">
<slot :item="items[itemIndex]" :index="itemIndex">
{{ items[itemIndex] }}
</slot>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
const { links } = defineProps<{
links: { name: string; to: string }[];
}>();
</script>
<template>
<ul class="flex items-center gap-4">
<NuxtLink
:to="link.to"
:class="[
$route.path === link.to? 'text-gradient': 'text-zinc-700 dark:text-zinc-400',
'px-2 hover:text-zinc-900 dark:hover:text-zinc-200'
]"
v-for="(link, index) in links"
:key="`navlinks-${index}`"
>
{{ link.name }}
</NuxtLink>
</ul>
</template>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { Bars2Icon } from "@heroicons/vue/20/solid";
const { links } = defineProps<{
links: { name: string; to: string }[];
}>();
</script>
<template>
<nuxt-ui-dropdown as="div" class="inline-block text-left z-10">
<div>
<nuxt-ui-dropdown-toggle class="border rounded-full px-2 py-2 text-zinc-500 border-zinc-500 hover:bg-white hover:text-zinc-900 hover:border-zinc-900 active:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-700 focus-visible:ring-opacity-75"
>
<span class="sr-only">Menu</span>
<Bars2Icon class="h-4 w-4" aria-hidden="true" />
</nuxt-ui-dropdown-toggle>
</div>
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<nuxt-ui-dropdown-menu
class="absolute right-0 mt-4 w-56 origin-top-right divide-y divide-zinc-100 dark:divide-zinc-700 rounded-xl bg-white dark:bg-black shadow-lg ring-1 ring-black dark:ring-white ring-opacity-5 dark:ring-opacity-5 focus:outline-none"
>
<div class="px-2 py-2 w-full">
<nuxt-ui-dropdown-item
v-for="(link, index) in links"
:key="index"
v-slot="{ close }"
>
<NuxtLink
:class="[
$route.path === link.to
? 'bg-zinc-200 dark:bg-zinc-900 text-zinc-900 dark:text-zinc-200'
: 'text-zinc-900 dark:text-zinc-200',
'group block w-full items-center rounded-xl text-sm',
]"
:to="link.to"
>
<span @click.native="close" class="truncate px-4 py-2 block">{{
link.name
}}</span>
</NuxtLink>
</nuxt-ui-dropdown-item>
</div>
</nuxt-ui-dropdown-menu>
</transition>
</nuxt-ui-dropdown>
</template>

View File

@ -0,0 +1,35 @@
<script setup>
import PhotoSwipeLightbox from "photoswipe/lightbox";
import "photoswipe/style.css";
const gallery = ref(null);
const slots = useSlots();
const children = slots.default() ? slots.default()[0].children : false;
const childrenType = children ? children[0].type : false;
let lightbox;
onMounted(() => {
if (!lightbox && childrenType) {
lightbox = new PhotoSwipeLightbox({
gallery: gallery.value,
children: "a",
pswpModule: () => import("photoswipe"),
});
lightbox.init();
}
});
onUnmounted(() => {
if (lightbox) {
lightbox.destroy();
lightbox = null;
}
});
</script>
<template>
<div ref="gallery">
<slot />
</div>
</template>

View File

@ -0,0 +1 @@
<template>hi</template>

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>

41
app/layouts/default.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<article class="min-h-screen flex flex-col items-stretch">
<!-- ring -->
<div class="fixed inset-0 flex justify-center sm:px-8">
<div class="flex w-full max-w-7xl lg:px-8">
<div
class="w-full bg-white ring-1 ring-zinc-200/50 dark:bg-zinc-900 dark:ring-zinc-500/20"
/>
</div>
</div>
<!-- background -->
<v-container class="relative">
<div class="absolute top-0 right-0">
<NuxtImg
src="img/bg-glow.png"
aria-hidden="true"
class="w-[44rem]"
format="webp"
width="944"
height="586"
/>
</div>
</v-container>
<!-- header -->
<Header />
<!-- main -->
<main class="maya-prose">
<v-container>
<slot />
</v-container>
</main>
<!-- footer -->
<Footer class="mt-auto" />
</article>
</template>

124
app/pages/index.vue Normal file
View File

@ -0,0 +1,124 @@
<template>
<v-container fluid>
<!-- Hero Section -->
<v-row justify="center" class="text-center my-16">
<v-col cols="12" md="8">
<h1 class="text-h3 font-weight-bold mb-4">
Inspire Emotion and Evoke Storytelling through the Art of Photography.
</h1>
<p class="text-body-1">
Explore my photography portfolio and see the world through my creative lens.
Contact me if you would like to hire me.
</p>
</v-col>
</v-row>
<!-- Hero Image Grid -->
<v-row dense class="mb-16">
<v-col
v-for="(image, index) in heroImages"
:key="index"
cols="12"
sm="6"
md="4"
>
<v-img
:src="image.src"
:alt="image.alt"
:width="image.width"
:height="image.height"
class="rounded-lg elevation-2"
cover
/>
</v-col>
</v-row>
<!-- About Me Section -->
<v-row class="align-center my-16">
<v-col cols="12" md="5">
<v-img
src="/img/home/personal-photo.webp"
alt="Oscar Mattern"
width="100%"
height="auto"
class="rounded-lg"
cover
/>
</v-col>
<v-col cols="12" md="7">
<h2 class="text-h4 font-weight-bold mb-4">About Me</h2>
<p class="text-body-1 mb-4">
I am a photographer based in New York City. I specialize in
<strong>landscape, travel, and portrait photography</strong>. I have been
photographing for over 10 years and have been published in many magazines and
newspapers. I am available for hire for weddings, events, and portraits.
</p>
<p class="text-body-1">
I am a passionate photographer with a keen eye for capturing the beauty of life
through the art of photography.
</p>
</v-col>
</v-row>
<!-- Awards Section -->
<v-row class="my-12">
<v-col cols="12">
<h2 class="text-h5 font-weight-bold mb-6">Awards</h2>
<v-timeline side="end" dense>
<v-timeline-item
v-for="(award, index) in awards"
:key="index"
dot-color="primary"
size="small"
>
<strong>{{ award.title }} ({{ award.year }})</strong><br />
<span class="text-body-2">{{ award.description }}</span>
</v-timeline-item>
</v-timeline>
</v-col>
</v-row>
<!-- CTA Hire Me Section -->
<v-row justify="center" class="text-center mt-16">
<v-col cols="12" md="8">
<h2 class="text-h4 font-weight-bold mb-4">Questions or collaboration ideas?</h2>
<p class="text-body-1 mb-6">
I would love to hear from you! Whether you have <strong>questions</strong> about
my work, have <strong>collaboration ideas</strong>, or are interested in
<strong>hiring me</strong> for a project, don't hesitate to reach out.
</p>
<p class="text-body-1">I usually respond within hours!</p>
</v-col>
</v-row>
</v-container>
</template>
<script setup>
const heroImages = [
{ src: '/img/home/hero-grid-01.webp', alt: 'Image 1', width: 1600, height: 2400 },
{ src: '/img/home/hero-grid-02.webp', alt: 'Image 2', width: 1600, height: 2199 },
{ src: '/img/home/hero-grid-03.webp', alt: 'Image 3', width: 1600, height: 2400 },
{ src: '/img/home/hero-grid-07.webp', alt: 'Image 4', width: 1600, height: 2400 },
{ src: '/img/home/hero-grid-05.webp', alt: 'Image 5', width: 1600, height: 2000 },
{ src: '/img/home/hero-grid-06.webp', alt: 'Image 6', width: 1600, height: 2400 },
{ src: '/img/home/hero-grid-04.webp', alt: 'Image 7', width: 1600, height: 2400 },
]
const awards = [
{
title: 'Best Landscape Photographer',
year: '2019',
description: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quod.',
},
{
title: 'Best Portrait Photographer',
year: '2018',
description: 'Lorem ipsum dolor sit amet.',
},
{
title: 'Best Travel Photographer',
year: '2017',
description: 'Lorem ipsum dolor sit amet.',
},
]
</script>

8
app/plugins/vuetify.ts Normal file
View File

@ -0,0 +1,8 @@
export default defineNuxtPlugin(nuxtApp => {
// check https://vuetify-nuxt-module.netlify.app/guide/nuxt-runtime-hooks.html
nuxtApp.hook('vuetify:before-create', options => {
if (import.meta.client) {
console.log('vuetify:before-create', options)
}
})
})

39
assets/css/main.css Normal file
View File

@ -0,0 +1,39 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities{
.text-gradient{
@apply text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-pink-600;
}
.maya-prose{
@apply prose max-w-none lg:prose-lg;
@apply prose-zinc dark:prose-invert;
@apply prose-img:rounded-xl;
@apply prose-a:decoration-wavy prose-a:decoration-yellow-800/70 prose-a:text-zinc-900 prose-a:dark:decoration-yellow-200/50 prose-a:dark:text-zinc-100 hover:dark:prose-a:text-yellow-200;
@apply prose-h1:font-display prose-h1:font-thin prose-h1:mb-24;
}
.maya-prose-story{
@apply prose mx-auto lg:prose-lg;
@apply prose-zinc dark:prose-invert;
@apply prose-img:rounded-xl;
@apply prose-a:decoration-wavy prose-a:decoration-yellow-800/70 prose-a:text-zinc-900 prose-a:dark:decoration-yellow-200/50 prose-a:dark:text-zinc-100 hover:dark:prose-a:text-yellow-200;
@apply prose-h1:font-display prose-h1:font-thin prose-h1:mb-24;
}
}
.pt-fade-up-enter-active,
.pt-fade-up-leave-active {
transition: all 0.4s cubic-bezier(0, 0, 0.48, 0.99), opacity 0.2s ease-out;
}
.pt-fade-up-enter-from {
transition: all 0.1s ease-out;
opacity: 0;
transform: translate3d(0, 30%, 0);
}
.pt-fade-up-leave-to {
transition: all 0.1s ease-out;
opacity: 0;
transform: translate3d(0, 10%, 0);
}

6
eslint.config.mjs Normal file
View File

@ -0,0 +1,6 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)

68
nuxt.config.ts Normal file
View File

@ -0,0 +1,68 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
import tailwindcss from "@tailwindcss/vite";
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
css: ['~/assets/css/main.css'],
vite: {
plugins: [
tailwindcss(),
],
},
future: {
compatibilityVersion: 4
},
modules: [
'@nuxt/eslint',
'@nuxt/fonts',
'@nuxt/icon',
'@nuxt/image',
'@nuxt/scripts',
'vuetify-nuxt-module',
],
ssr: true,
// when enabling ssr option you need to disable inlineStyles and maybe devLogs
features: {
inlineStyles: false,
devLogs: false,
},
build: {
transpile: ['vuetify'],
},
vite: {
ssr: {
noExternal: ['vuetify'],
},
},
css: [],
vuetify: {
moduleOptions: {
// check https://nuxt.vuetifyjs.com/guide/server-side-rendering.html
ssrClientHints: {
reloadOnFirstRequest: false,
viewportSize: true,
prefersColorScheme: false,
prefersColorSchemeOptions: {
useBrowserThemeOnly: false,
},
},
// /* If customizing sass global variables ($utilities, $reset, $color-pack, $body-font-family, etc) */
// disableVuetifyStyles: true,
styles: {
configFile: 'assets/settings.scss',
},
},
},
})

14915
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "vuetify-project",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"prepare": "nuxt prepare",
"typecheck": "nuxt typecheck",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@nuxt/eslint": "1.3.0",
"@nuxt/fonts": "0.11.2",
"@nuxt/icon": "1.12.0",
"@nuxt/image": "1.10.0",
"@nuxt/scripts": "0.11.6",
"@tailwindcss/vite": "^4.1.5",
"@unhead/vue": "^2.0.3",
"eslint": "^9.0.0",
"nuxt": "^3.17.2",
"vue": "latest",
"vuetify": "^3.8.1"
},
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72",
"devDependencies": {
"@mdi/font": "^7.4.47",
"@nuxt/fonts": "^0.11.1",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"sass-embedded": "^1.86.3",
"tailwindcss": "^4.1.5",
"typescript": "^5.6.3",
"vue-tsc": "^2.1.6",
"vuetify-nuxt-module": "^0.18.6"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/img/bg-glow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
public/img/bg-glow2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Some files were not shown because too many files have changed in this diff Show More