Initial commit WIP
24
.gitignore
vendored
Normal 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
@ -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
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
BIN
app/assets/logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
6
app/assets/logo.svg
Normal 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
@ -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
|
||||||
|
// );
|
82
app/components/AppFooter.vue
Normal 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;"
|
||||||
|
>
|
||||||
|
© 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>
|
25
app/components/BoxGradient.vue
Normal 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>
|
27
app/components/ColorModeSwitch.vue
Normal 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>
|
7
app/components/ContainerWider.vue
Normal 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
@ -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">© {{ 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
@ -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>
|
90
app/components/HelloWorld.vue
Normal 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>
|
116
app/components/HomeImageGrid.vue
Normal 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>
|
15
app/components/LinkArrorw.vue
Normal 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
@ -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>
|
130
app/components/MasonryWall.vue
Normal 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>
|
21
app/components/NavLinks.vue
Normal 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>
|
53
app/components/NavLinksMobile.vue
Normal 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>
|
35
app/components/PhotoSwipe.vue
Normal 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>
|
1
app/components/SocialIconsList.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template>hi</template>
|
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
41
package.json
Normal 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
After Width: | Height: | Size: 4.2 KiB |
BIN
public/img/bg-glow.png
Normal file
After Width: | Height: | Size: 300 KiB |
BIN
public/img/bg-glow2.png
Normal file
After Width: | Height: | Size: 346 KiB |
BIN
public/img/galleries/gal1/afwjp6wytar25uidu64e.webp
Normal file
After Width: | Height: | Size: 211 KiB |
BIN
public/img/galleries/gal1/ajlfdemaupqqsd4j8ccw.webp
Normal file
After Width: | Height: | Size: 397 KiB |
BIN
public/img/galleries/gal1/aqe1lvyk7lykqunhefbo.webp
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
public/img/galleries/gal1/bgzdr1tp4sufw79ppdjn.webp
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
public/img/galleries/gal1/cegxhw5so9j2ytegwwkj.webp
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
public/img/galleries/gal1/dfgp7w7z2vj52ne4nl7r.webp
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
public/img/galleries/gal1/ffpqd8xd14rafd4wknrw.webp
Normal file
After Width: | Height: | Size: 191 KiB |
BIN
public/img/galleries/gal1/fzzpzq1zfvvpdkpivemr.webp
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
public/img/galleries/gal1/g7zwbsf3sp5vqjborfzr.webp
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
public/img/galleries/gal1/hzbmdxmo3zgmnhzagna5.webp
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
public/img/galleries/gal1/inha3cpx3b8nketuwleb.webp
Normal file
After Width: | Height: | Size: 214 KiB |
BIN
public/img/galleries/gal1/jdhf4fj2law6lrlfojwc.webp
Normal file
After Width: | Height: | Size: 230 KiB |
BIN
public/img/galleries/gal1/jglwqd81bpxinfuthirx.webp
Normal file
After Width: | Height: | Size: 188 KiB |
BIN
public/img/galleries/gal1/lquy5kadm1b9xgrwyxvh.webp
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
public/img/galleries/gal1/rruzp7fgm2ear7i1ts4w.webp
Normal file
After Width: | Height: | Size: 218 KiB |
BIN
public/img/galleries/gal1/siia2epwcgkevsrbsqcl.webp
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
public/img/galleries/gal1/vhiepieyevg2moptuds3.webp
Normal file
After Width: | Height: | Size: 200 KiB |
BIN
public/img/galleries/gal1/vrpfzu6oluqmgdpn0o31.webp
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
public/img/galleries/gal1/walvvts0h6dvumeulgug.webp
Normal file
After Width: | Height: | Size: 179 KiB |
BIN
public/img/galleries/gal1/wdksnzvw0seicareme7r.webp
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
public/img/galleries/gal1/wfceyqzygvhezdhikqbs.webp
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
public/img/galleries/gal1/wml7eti38naufva0dave.webp
Normal file
After Width: | Height: | Size: 98 KiB |
BIN
public/img/galleries/gal1/x19keffub3tfiz86ch2b.webp
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
public/img/galleries/gal1/xb7lzggrgi9n3rjd55bd.webp
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
public/img/galleries/gal1/z1mwk9k3vat5oh6g3r80.webp
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
public/img/galleries/gal2/ankfbvymv9cw3nlhxg6w.webp
Normal file
After Width: | Height: | Size: 115 KiB |
BIN
public/img/galleries/gal2/aqfgksdur4tidhjurjr1.webp
Normal file
After Width: | Height: | Size: 322 KiB |
BIN
public/img/galleries/gal2/bafqmzm7twoeukdkdpla.webp
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
public/img/galleries/gal2/ese8ocvfskkylryosrbq.webp
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
public/img/galleries/gal2/fbo4ueky31br9fofdueh.webp
Normal file
After Width: | Height: | Size: 191 KiB |
BIN
public/img/galleries/gal2/fsnf3vialcxfavbhalbg.webp
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
public/img/galleries/gal2/jbymykrnmgbccbv0czxo.webp
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
public/img/galleries/gal2/manqh1tqwskwrhootfrg.webp
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
public/img/galleries/gal2/mt7vbnb37mqmuob7vacf.webp
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
public/img/galleries/gal2/mubgzuvnsby0jonorusu.webp
Normal file
After Width: | Height: | Size: 298 KiB |
BIN
public/img/galleries/gal2/nadgfaxn7uuz4otztz5m.webp
Normal file
After Width: | Height: | Size: 323 KiB |
BIN
public/img/galleries/gal2/pqdezwm7um02e4nhffxb.webp
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
public/img/galleries/gal2/qoxt4ymzvurimmvxxn86.webp
Normal file
After Width: | Height: | Size: 420 KiB |
BIN
public/img/galleries/gal2/qwm4daehqvaofhgsp3cx.webp
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
public/img/galleries/gal2/qwrpgha5paixwlrokliw.webp
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
public/img/galleries/gal2/rc86yleqy1je4acxpzdz.webp
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
public/img/galleries/gal2/s7w7xultshxmymnfzden.webp
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
public/img/galleries/gal2/t1tb1gun1tuevsjto5n8.webp
Normal file
After Width: | Height: | Size: 220 KiB |
BIN
public/img/galleries/gal2/vkui6m6fdat4dc76l0os.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
public/img/galleries/gal2/y4k05bqfltdydkhpsi9x.webp
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
public/img/galleries/gal2/yhqwepbuh0ocx1uzwbay.webp
Normal file
After Width: | Height: | Size: 198 KiB |
BIN
public/img/galleries/gal2/z28qf5kigddbm7euei3j.webp
Normal file
After Width: | Height: | Size: 234 KiB |
BIN
public/img/galleries/gal2/z76ibuwoor9bfmajacol.webp
Normal file
After Width: | Height: | Size: 167 KiB |