Executable
+13
@@ -0,0 +1,13 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,2 @@
|
||||
# Public URL, used for OG Image when running nuxt generate
|
||||
NUXT_PUBLIC_SITE_URL=
|
||||
@@ -0,0 +1,34 @@
|
||||
name: ci
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node: [22]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# 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
|
||||
|
||||
# VSC
|
||||
.history
|
||||
@@ -0,0 +1,47 @@
|
||||
# Portfolio
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash [Terminal]
|
||||
npm create nuxt@latest -- -t github:nuxt-ui-templates/portfolio
|
||||
```
|
||||
|
||||
## Deploy your own
|
||||
|
||||
[](https://vercel.com/new/clone?repository-name=portfolio&repository-url=https%3A%2F%2Fgithub.com%2Fnuxt-ui-templates%2Fportfolio&demo-image=https%3A%2F%2Fui.nuxt.com%2Fassets%2Ftemplates%2Fnuxt%2Fportfolio-dark.png&demo-url=https%3A%2F%2Fportfolio-template.nuxt.dev%2F&demo-title=Nuxt%20Portfolio%20Template&demo-description=A%20sleek%20portfolio%20template%20to%20showcase%20your%20work%2C%20skills%20and%20blog%20powered%20by%20Nuxt%20Content.)
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
|
||||
## Renovate integration
|
||||
|
||||
Install [Renovate GitHub app](https://github.com/apps/renovate/installations/select_target) on your repository and you are good to go.
|
||||
@@ -0,0 +1,45 @@
|
||||
export default defineAppConfig({
|
||||
global: {
|
||||
picture: {
|
||||
dark: 'https://media.licdn.com/dms/image/v2/D4E03AQFH5BVMre2e1A/profile-displayphoto-crop_800_800/B4EZplNLbYGYAM-/0/1762634545622?e=1766016000&v=beta&t=_jK5TKTJ8agI3add11w-IRaISiFUfhtVlhaCG_Jwidw',
|
||||
light: 'https://media.licdn.com/dms/image/v2/D4E03AQFH5BVMre2e1A/profile-displayphoto-crop_800_800/B4EZplNLbYGYAM-/0/1762634545622?e=1766016000&v=beta&t=_jK5TKTJ8agI3add11w-IRaISiFUfhtVlhaCG_Jwidw',
|
||||
alt: 'My profile picture'
|
||||
},
|
||||
meetingLink: 'https://calendar.google.com/calendar/appointments/schedules/AcZssZ0BRGpbzvnsdqUpiWdhL5qiagpKYh9UF1UG3yfC_1Ga385_5-ajCy8JOnsBZJCFwd-R_si5BeT2',
|
||||
email: 'nick@kalar.codes',
|
||||
available: true
|
||||
},
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'blue',
|
||||
neutral: 'neutral'
|
||||
},
|
||||
pageHero: {
|
||||
slots: {
|
||||
container: 'py-18 sm:py-24 lg:py-32',
|
||||
title: 'mx-auto max-w-xl text-pretty text-3xl sm:text-4xl lg:text-5xl',
|
||||
description: 'mt-2 text-md mx-auto max-w-2xl text-pretty sm:text-md text-muted'
|
||||
}
|
||||
}
|
||||
},
|
||||
footer: {
|
||||
credits: `Built with Nuxt UI • © Nick Kalar ${new Date().getFullYear()}`,
|
||||
colorMode: false,
|
||||
links: [{
|
||||
'icon': 'i-simple-icons-linkedin',
|
||||
'to': 'https://www.linkedin.com/in/nickkalar/',
|
||||
'target': '_blank',
|
||||
'aria-label': 'Nick on LinkedIn'
|
||||
}, {
|
||||
'icon': 'i-simple-icons-twitter',
|
||||
'to': 'https://www.x.com/nickkalar',
|
||||
'target': '_blank',
|
||||
'aria-label': 'Nick on Twitter'
|
||||
}, {
|
||||
'icon': 'i-simple-icons-gitea',
|
||||
'to': 'https://git.kalar.codes/NickKalar',
|
||||
'target': '_blank',
|
||||
'aria-label': 'Nick on Gitea'
|
||||
}]
|
||||
}
|
||||
})
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const color = computed(() => colorMode.value === 'dark' ? '#020618' : 'white')
|
||||
|
||||
useHead({
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ key: 'theme-color', name: 'theme-color', content: color }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', href: '/favicon.ico' }
|
||||
],
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
}
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: '%s',
|
||||
ogImage: 'https://i.postimg.cc/ydfWjvKB/nick-kalar-main.png',
|
||||
twitterImage: 'https://i.postimg.cc/ydfWjvKB/nick-kalar-main.png',
|
||||
twitterCard: 'summary_large_image'
|
||||
})
|
||||
|
||||
const [{ data: navigation }, { data: files }] = await Promise.all([
|
||||
useAsyncData('navigation', () => {
|
||||
return Promise.all([
|
||||
queryCollectionNavigation('blog')
|
||||
])
|
||||
}, {
|
||||
transform: data => data.flat()
|
||||
}),
|
||||
useLazyAsyncData('search', () => {
|
||||
return Promise.all([
|
||||
queryCollectionSearchSections('blog')
|
||||
])
|
||||
}, {
|
||||
server: false,
|
||||
transform: data => data.flat()
|
||||
})
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtLayout>
|
||||
<UMain class="relative">
|
||||
<NuxtPage />
|
||||
</UMain>
|
||||
</NuxtLayout>
|
||||
|
||||
<ClientOnly>
|
||||
<LazyUContentSearch
|
||||
:files="files"
|
||||
:navigation="navigation"
|
||||
shortcut="meta_k"
|
||||
:links="navLinks"
|
||||
:fuse="{ resultLimit: 42 }"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</UApp>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
@source "../../../content/**/*";
|
||||
|
||||
@theme static {
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--ui-container: var(--container-4xl);
|
||||
|
||||
::selection {
|
||||
color: #282a30;
|
||||
background-color: #c8c8c8;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
::selection {
|
||||
color: #ffffff;
|
||||
background-color: #474747;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
const { footer } = useAppConfig()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UFooter
|
||||
class="z-10 bg-default"
|
||||
:ui="{ left: 'text-muted text-xs' }"
|
||||
>
|
||||
<template #left>
|
||||
{{ footer.credits }}
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<template v-if="footer?.links">
|
||||
<UButton
|
||||
v-for="(link, index) of footer?.links"
|
||||
:key="index"
|
||||
v-bind="{ size: 'xs', color: 'neutral', variant: 'ghost', ...link }"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</UFooter>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
defineProps<{
|
||||
links: NavigationMenuItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed top-2 sm:top-4 mx-auto left-1/2 transform -translate-x-1/2 z-10">
|
||||
<UNavigationMenu
|
||||
:items="links"
|
||||
variant="link"
|
||||
color="neutral"
|
||||
class="bg-muted/80 backdrop-blur-sm rounded-full px-2 sm:px-4 border border-muted/50 shadow-lg shadow-neutral-950/5"
|
||||
:ui="{
|
||||
link: 'px-2 py-1',
|
||||
}"
|
||||
>
|
||||
<template #list-trailing>
|
||||
<ColorModeButton />
|
||||
</template>
|
||||
</UNavigationMenu>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const nextTheme = computed(() => (colorMode.value === 'dark' ? 'light' : 'dark'))
|
||||
|
||||
const switchTheme = () => {
|
||||
colorMode.preference = nextTheme.value
|
||||
}
|
||||
|
||||
const startViewTransition = (event: MouseEvent) => {
|
||||
if (!document.startViewTransition) {
|
||||
switchTheme()
|
||||
return
|
||||
}
|
||||
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
const endRadius = Math.hypot(
|
||||
Math.max(x, window.innerWidth - x),
|
||||
Math.max(y, window.innerHeight - y)
|
||||
)
|
||||
|
||||
const transition = document.startViewTransition(() => {
|
||||
switchTheme()
|
||||
})
|
||||
|
||||
transition.ready.then(() => {
|
||||
const duration = 600
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: [
|
||||
`circle(0px at ${x}px ${y}px)`,
|
||||
`circle(${endRadius}px at ${x}px ${y}px)`
|
||||
]
|
||||
},
|
||||
{
|
||||
duration: duration,
|
||||
easing: 'cubic-bezier(.76,.32,.29,.99)',
|
||||
pseudoElement: '::view-transition-new(root)'
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<UButton
|
||||
:aria-label="`Switch to ${nextTheme} mode`"
|
||||
:icon="`i-lucide-${nextTheme === 'dark' ? 'sun' : 'moon'}`"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="rounded-full"
|
||||
@click="startViewTransition"
|
||||
/>
|
||||
<template #fallback>
|
||||
<div class="size-4" />
|
||||
</template>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
z-index: 9999;
|
||||
}
|
||||
::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
image: {
|
||||
src: string
|
||||
alt: string
|
||||
}
|
||||
index: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-white p-2 flex flex-col drop-shadow-2xl transition-transform duration-300 ease-in-out hover:scale-105 hover:rotate-0 hover:z-10"
|
||||
:class="[
|
||||
index % 2 === 0 ? '-rotate-5' : 'rotate-5',
|
||||
index % 2 === 0 ? 'hover:-translate-x-4' : 'hover:translate-x-4'
|
||||
]"
|
||||
>
|
||||
<img
|
||||
:src="image.src"
|
||||
:alt="image.alt"
|
||||
class="size-32 object-cover"
|
||||
>
|
||||
<span class="w-32 text-xs text-black font-serif font-medium text-center mt-2">
|
||||
{{ image.alt }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { IndexCollectionItem } from '@nuxt/content'
|
||||
|
||||
defineProps<{
|
||||
page: IndexCollectionItem
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageSection
|
||||
:title="page.about.title"
|
||||
:description="page.about.description"
|
||||
:ui="{
|
||||
container: '!p-0',
|
||||
title: 'text-left text-xl sm:text-xl lg:text-2xl font-medium',
|
||||
description: 'text-left mt-3 text-sm sm:text-md lg:text-sm text-muted'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import type { IndexCollectionItem } from '@nuxt/content'
|
||||
|
||||
defineProps<{
|
||||
page: IndexCollectionItem
|
||||
}>()
|
||||
|
||||
const { data: posts } = await useAsyncData('index-blogs', () =>
|
||||
queryCollection('blog').order('date', 'DESC').limit(3).all()
|
||||
)
|
||||
if (!posts.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'blogs posts not found', fatal: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageSection
|
||||
:title="page.blog.title"
|
||||
:description="page.blog.description"
|
||||
:ui="{
|
||||
container: 'px-0 !pt-0 sm:gap-6 lg:gap-8',
|
||||
title: 'text-left text-xl sm:text-xl lg:text-2xl font-medium',
|
||||
description: 'text-left mt-2 text-sm sm:text-md lg:text-sm text-muted'
|
||||
}"
|
||||
>
|
||||
<UBlogPosts
|
||||
orientation="vertical"
|
||||
class="gap-4 lg:gap-y-4"
|
||||
>
|
||||
<UBlogPost
|
||||
v-for="(post, index) in posts"
|
||||
:key="index"
|
||||
orientation="horizontal"
|
||||
variant="naked"
|
||||
v-bind="post"
|
||||
:to="post.path"
|
||||
:ui="{
|
||||
root: 'group relative lg:items-start lg:flex ring-0 hover:ring-0',
|
||||
body: '!px-0',
|
||||
header: 'hidden'
|
||||
}"
|
||||
>
|
||||
<template #footer>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="link"
|
||||
class="px-0 gap-0"
|
||||
label="Read Article"
|
||||
>
|
||||
<template #trailing>
|
||||
<UIcon
|
||||
name="i-lucide-arrow-right"
|
||||
class="size-4 text-primary transition-all opacity-0 group-hover:translate-x-1 group-hover:opacity-100"
|
||||
/>
|
||||
</template>
|
||||
</UButton>
|
||||
</template>
|
||||
</UBlogPost>
|
||||
</UBlogPosts>
|
||||
</UPageSection>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import type { IndexCollectionItem } from '@nuxt/content'
|
||||
|
||||
defineProps<{
|
||||
page: IndexCollectionItem
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageSection
|
||||
:title="page.education.title"
|
||||
:ui="{
|
||||
container: '!p-0 gap-4 sm:gap-4',
|
||||
title: 'text-left text-xl sm:text-xl lg:text-2xl font-medium',
|
||||
description: 'mt-2'
|
||||
}"
|
||||
>
|
||||
<template #description>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Motion
|
||||
v-for="(education, index) in page.education.items"
|
||||
:key="index"
|
||||
:initial="{ opacity: 0, transform: 'translateY(20px)' }"
|
||||
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
|
||||
:transition="{ delay: 0.4 + 0.2 * index }"
|
||||
:in-view-options="{ once: true }"
|
||||
class="text-muted flex items-center text-nowrap gap-2"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{{ education.date }}
|
||||
</p>
|
||||
<USeparator />
|
||||
<ULink
|
||||
class="flex items-center gap-1"
|
||||
:to="education.institution.url"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="text-sm">
|
||||
{{ education.degree }}
|
||||
</span>
|
||||
<div
|
||||
class="inline-flex items-center gap-1"
|
||||
:style="{ color: education.institution.color }"
|
||||
>
|
||||
<span class="font-medium">{{ education.institution.name }}</span>
|
||||
<img
|
||||
:src="education.institution.logo"
|
||||
:alt="education.institution.name"
|
||||
:width=32
|
||||
/>
|
||||
</div>
|
||||
</ULink>
|
||||
</Motion>
|
||||
</div>
|
||||
</template>
|
||||
</UPageSection>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import type { IndexCollectionItem } from '@nuxt/content'
|
||||
|
||||
const props = defineProps<{
|
||||
page: IndexCollectionItem
|
||||
}>()
|
||||
|
||||
const items = computed(() => {
|
||||
return props.page.faq?.categories.map((faq) => {
|
||||
return {
|
||||
label: faq.title,
|
||||
key: faq.title.toLowerCase(),
|
||||
questions: faq.questions
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const ui = {
|
||||
root: 'flex items-center gap-4 w-full',
|
||||
list: 'relative flex bg-transparent dark:bg-transparent gap-2 px-0',
|
||||
indicator: 'absolute top-[4px] duration-200 ease-out focus:outline-none rounded-lg bg-elevated/60',
|
||||
trigger: 'px-3 py-2 rounded-lg hover:bg-muted/50 data-[state=active]:text-highlighted data-[state=inactive]:text-muted',
|
||||
label: 'truncate'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageSection
|
||||
:title="page.faq.title"
|
||||
:description="page.faq.description"
|
||||
:ui="{
|
||||
container: 'px-0 !pt-0 gap-4 sm:gap-4',
|
||||
title: 'text-left text-xl sm:text-xl lg:text-2xl font-medium',
|
||||
description: 'text-left mt-2 text-sm sm:text-md lg:text-sm text-muted'
|
||||
}"
|
||||
>
|
||||
<UTabs
|
||||
:items
|
||||
orientation="horizontal"
|
||||
:ui
|
||||
>
|
||||
<template #content="{ item }">
|
||||
<UAccordion
|
||||
trailing-icon="lucide:plus"
|
||||
:items="item.questions"
|
||||
:unmount-on-hide="false"
|
||||
:ui="{
|
||||
item: 'border-none',
|
||||
trigger: 'mb-2 border-0 group px-4 transform-gpu rounded-lg bg-elevated/60 will-change-transform hover:bg-muted/50 text-base',
|
||||
trailingIcon: 'group-data-[state=closed]:rotate-0 group-data-[state=open]:rotate-135 text-base text-muted'
|
||||
}"
|
||||
>
|
||||
<template #body="{ item: _item }">
|
||||
<MDC
|
||||
:value="_item.content"
|
||||
unwrap="p"
|
||||
class="px-4"
|
||||
/>
|
||||
</template>
|
||||
</UAccordion>
|
||||
</template>
|
||||
</UTabs>
|
||||
</UPageSection>
|
||||
</template>
|
||||
@@ -0,0 +1,191 @@
|
||||
<script setup lang="ts">
|
||||
import type { IndexCollectionItem } from '@nuxt/content'
|
||||
|
||||
const { footer, global } = useAppConfig()
|
||||
|
||||
defineProps<{
|
||||
page: IndexCollectionItem
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageHero
|
||||
:ui="{
|
||||
headline: 'flex items-center justify-center',
|
||||
title: 'text-shadow-md max-w-lg mx-auto',
|
||||
links: 'mt-4 flex-col justify-center items-center'
|
||||
}"
|
||||
>
|
||||
<template #headline>
|
||||
<Motion
|
||||
:initial="{
|
||||
scale: 1.1,
|
||||
opacity: 0,
|
||||
filter: 'blur(20px)'
|
||||
}"
|
||||
:animate="{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)'
|
||||
}"
|
||||
:transition="{
|
||||
duration: 0.6,
|
||||
delay: 0.1
|
||||
}"
|
||||
>
|
||||
<UColorModeAvatar
|
||||
class="size-18 ring ring-default ring-offset-3 ring-offset-(--ui-bg)"
|
||||
:light="global.picture?.light!"
|
||||
:dark="global.picture?.dark!"
|
||||
:alt="global.picture?.alt!"
|
||||
/>
|
||||
</Motion>
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<Motion
|
||||
:initial="{
|
||||
scale: 1.1,
|
||||
opacity: 0,
|
||||
filter: 'blur(20px)'
|
||||
}"
|
||||
:animate="{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)'
|
||||
}"
|
||||
:transition="{
|
||||
duration: 0.6,
|
||||
delay: 0.1
|
||||
}"
|
||||
>
|
||||
{{ page.title }}
|
||||
</Motion>
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
<Motion
|
||||
:initial="{
|
||||
scale: 1.1,
|
||||
opacity: 0,
|
||||
filter: 'blur(20px)'
|
||||
}"
|
||||
:animate="{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)'
|
||||
}"
|
||||
:transition="{
|
||||
duration: 0.6,
|
||||
delay: 0.3
|
||||
}"
|
||||
>
|
||||
{{ page.description }}
|
||||
</Motion>
|
||||
</template>
|
||||
|
||||
<template #links>
|
||||
<Motion
|
||||
:initial="{
|
||||
scale: 1.1,
|
||||
opacity: 0,
|
||||
filter: 'blur(20px)'
|
||||
}"
|
||||
:animate="{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)'
|
||||
}"
|
||||
:transition="{
|
||||
duration: 0.6,
|
||||
delay: 0.5
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="page.hero.links"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<!-- <UButton v-bind="page.hero.links[0]" /> -->
|
||||
<UButton
|
||||
:color="global.available ? 'success' : 'error'"
|
||||
variant="ghost"
|
||||
class="gap-2"
|
||||
:to="global.available ? global.meetingLink : ''"
|
||||
:label="global.available ? 'Available for new projects' : 'Not available at the moment'"
|
||||
>
|
||||
<template #leading>
|
||||
<span class="relative flex size-2">
|
||||
<span
|
||||
class="absolute inline-flex size-full rounded-full opacity-75"
|
||||
:class="global.available ? 'bg-success animate-ping' : 'bg-error'"
|
||||
/>
|
||||
<span
|
||||
class="relative inline-flex size-2 scale-90 rounded-full"
|
||||
:class="global.available ? 'bg-success' : 'bg-error'"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</UButton>
|
||||
</div>
|
||||
</Motion>
|
||||
|
||||
<div class="gap-x-4 inline-flex mt-4">
|
||||
<Motion
|
||||
v-for="(link, index) of footer?.links"
|
||||
:key="index"
|
||||
|
||||
:initial="{
|
||||
scale: 1.1,
|
||||
opacity: 0,
|
||||
filter: 'blur(20px)'
|
||||
}"
|
||||
:animate="{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)'
|
||||
}"
|
||||
:transition="{
|
||||
duration: 0.6,
|
||||
delay: 0.5 + index * 0.1
|
||||
}"
|
||||
>
|
||||
<UButton
|
||||
v-bind="{ size: 'md', color: 'neutral', variant: 'ghost', ...link }"
|
||||
/>
|
||||
</Motion>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <UMarquee
|
||||
pause-on-hover
|
||||
class="py-2 -mx-8 sm:-mx-12 lg:-mx-16 [--duration:40s]"
|
||||
>
|
||||
<Motion
|
||||
v-for="(img, index) in page.hero.images"
|
||||
:key="index"
|
||||
:initial="{
|
||||
scale: 1.1,
|
||||
opacity: 0,
|
||||
filter: 'blur(20px)'
|
||||
}"
|
||||
:animate="{
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
filter: 'blur(0px)'
|
||||
}"
|
||||
:transition="{
|
||||
duration: 0.6,
|
||||
delay: index * 0.1
|
||||
}"
|
||||
>
|
||||
<NuxtImg
|
||||
width="234"
|
||||
height="234"
|
||||
class="rounded-lg aspect-square object-cover"
|
||||
:class="index % 2 === 0 ? '-rotate-2' : 'rotate-2'"
|
||||
v-bind="img"
|
||||
/>
|
||||
</Motion>
|
||||
</UMarquee> -->
|
||||
</UPageHero>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { IndexCollectionItem } from '@nuxt/content'
|
||||
|
||||
defineProps<{
|
||||
page: IndexCollectionItem
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageSection
|
||||
:ui="{
|
||||
container: 'px-0 !pt-0'
|
||||
}"
|
||||
>
|
||||
<UCarousel
|
||||
v-slot="{ item }"
|
||||
:items="page.testimonials"
|
||||
:autoplay="{ delay: 4000 }"
|
||||
loop
|
||||
dots
|
||||
:ui="{
|
||||
viewport: '-mx-4 sm:-mx-12 lg:-mx-16 bg-elevated/50 max-w-(--ui-container)'
|
||||
}"
|
||||
>
|
||||
<UPageCTA
|
||||
:description="item.quote"
|
||||
variant="naked"
|
||||
class="rounded-none"
|
||||
:ui="{
|
||||
container: 'sm:py-12 lg:py-12 sm:gap-8',
|
||||
description: '!text-base text-balance before:content-[open-quote] before:text-5xl lg:before:text-7xl before:inline-block before:text-dimmed before:absolute before:-ml-6 lg:before:-ml-10 before:-mt-2 lg:before:-mt-4 after:content-[close-quote] after:text-5xl lg:after:text-7xl after:inline-block after:text-dimmed after:absolute after:mt-1 lg:after:mt-0 after:ml-1 lg:after:ml-2'
|
||||
}"
|
||||
>
|
||||
<UUser
|
||||
v-bind="item.author"
|
||||
size="xl"
|
||||
class="justify-center"
|
||||
/>
|
||||
</UPageCTA>
|
||||
</UCarousel>
|
||||
</UPageSection>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import type { IndexCollectionItem } from '@nuxt/content'
|
||||
|
||||
defineProps<{
|
||||
page: IndexCollectionItem
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPageSection
|
||||
:title="page.experience.title"
|
||||
:ui="{
|
||||
container: '!p-0 gap-4 sm:gap-4',
|
||||
title: 'text-left text-xl sm:text-xl lg:text-2xl font-medium',
|
||||
description: 'mt-2'
|
||||
}"
|
||||
>
|
||||
<template #description>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Motion
|
||||
v-for="(experience, index) in page.experience.items"
|
||||
:key="index"
|
||||
:initial="{ opacity: 0, transform: 'translateY(20px)' }"
|
||||
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
|
||||
:transition="{ delay: 0.4 + 0.2 * index }"
|
||||
:in-view-options="{ once: true }"
|
||||
class="text-muted flex items-center text-nowrap gap-2"
|
||||
>
|
||||
<p class="text-sm">
|
||||
{{ experience.date }}
|
||||
</p>
|
||||
<USeparator />
|
||||
<ULink
|
||||
class="flex items-center gap-1"
|
||||
:to="experience.company.url"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="text-sm">
|
||||
{{ experience.position }}
|
||||
</span>
|
||||
<div
|
||||
class="inline-flex items-center gap-1"
|
||||
:style="{ color: experience.company.color }"
|
||||
>
|
||||
<span class="font-medium">{{ experience.company.name }}</span>
|
||||
<img
|
||||
:src="experience.company.logo"
|
||||
:alt="experience.company.name"
|
||||
/>
|
||||
</div>
|
||||
</ULink>
|
||||
</Motion>
|
||||
</div>
|
||||
</template>
|
||||
</UPageSection>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from '#app'
|
||||
|
||||
defineProps({
|
||||
error: {
|
||||
type: Object as PropType<NuxtError>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
}
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
title: 'Page not found',
|
||||
description: 'We are sorry but this page could not be found.'
|
||||
})
|
||||
|
||||
const [{ data: navigation }, { data: files }] = await Promise.all([
|
||||
useAsyncData('navigation', () => {
|
||||
return Promise.all([
|
||||
queryCollectionNavigation('blog')
|
||||
])
|
||||
}, {
|
||||
transform: data => data.flat()
|
||||
}),
|
||||
useLazyAsyncData('search', () => {
|
||||
return Promise.all([
|
||||
queryCollectionSearchSections('blog')
|
||||
])
|
||||
}, {
|
||||
server: false,
|
||||
transform: data => data.flat()
|
||||
})
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader :links="navLinks" />
|
||||
|
||||
<UMain>
|
||||
<UContainer>
|
||||
<UPage>
|
||||
<UError :error="error" />
|
||||
</UPage>
|
||||
</UContainer>
|
||||
</UMain>
|
||||
|
||||
<AppFooter />
|
||||
|
||||
<ClientOnly>
|
||||
<LazyUContentSearch
|
||||
:files="files"
|
||||
shortcut="meta_k"
|
||||
:navigation="navigation"
|
||||
:links="navLinks"
|
||||
:fuse="{ resultLimit: 42 }"
|
||||
/>
|
||||
</ClientOnly>
|
||||
|
||||
<UToaster />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UContainer class="sm:border-x border-default pt-10">
|
||||
<AppHeader :links="navLinks" />
|
||||
<slot />
|
||||
<AppFooter />
|
||||
</UContainer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
const { data: page } = await useAsyncData('about', () => {
|
||||
return queryCollection('about').first()
|
||||
})
|
||||
if (!page.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Page not found',
|
||||
fatal: true
|
||||
})
|
||||
}
|
||||
|
||||
const { global } = useAppConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.seo?.title || page.value?.title,
|
||||
ogTitle: page.value?.seo?.title || page.value?.title,
|
||||
description: page.value?.seo?.description || page.value?.description,
|
||||
ogDescription: page.value?.seo?.description || page.value?.description
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage v-if="page">
|
||||
<UPageHero
|
||||
:title="page.title"
|
||||
:description="page.description"
|
||||
orientation="horizontal"
|
||||
:ui="{
|
||||
container: 'lg:flex sm:flex-row items-center',
|
||||
title: '!mx-0 text-left',
|
||||
description: '!mx-0 text-left',
|
||||
links: 'justify-start'
|
||||
}"
|
||||
>
|
||||
<UColorModeAvatar
|
||||
class="sm:rotate-4 size-36 rounded-lg ring ring-default ring-offset-3 ring-offset-(--ui-bg)"
|
||||
:light="global.picture?.light!"
|
||||
:dark="global.picture?.dark!"
|
||||
:alt="global.picture?.alt!"
|
||||
/>
|
||||
</UPageHero>
|
||||
<UPageSection
|
||||
:ui="{
|
||||
container: '!pt-0'
|
||||
}"
|
||||
>
|
||||
<MDC
|
||||
:value="page.content"
|
||||
unwrap="p"
|
||||
/>
|
||||
<div class="flex flex-row justify-center items-center py-10 space-x-[-2rem]">
|
||||
<PolaroidItem
|
||||
v-for="(image, index) in page.images"
|
||||
:key="index"
|
||||
:image="image"
|
||||
:index
|
||||
/>
|
||||
</div>
|
||||
</UPageSection>
|
||||
</UPage>
|
||||
</template>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
import { mapContentNavigation } from '@nuxt/ui/utils/content'
|
||||
import { findPageBreadcrumb } from '@nuxt/content/utils'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { data: page } = await useAsyncData(route.path, () =>
|
||||
queryCollection('blog').path(route.path).first()
|
||||
)
|
||||
if (!page.value) throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
const { data: surround } = await useAsyncData(`${route.path}-surround`, () =>
|
||||
queryCollectionItemSurroundings('blog', route.path, {
|
||||
fields: ['description']
|
||||
})
|
||||
)
|
||||
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation', ref([]))
|
||||
const blogNavigation = computed(() => navigation.value.find(item => item.path === '/blog')?.children || [])
|
||||
|
||||
const breadcrumb = computed(() => mapContentNavigation(findPageBreadcrumb(blogNavigation?.value, page.value?.path)).map(({ icon, ...link }) => link))
|
||||
|
||||
if (page.value.image) {
|
||||
defineOgImage({ url: page.value.image })
|
||||
} else {
|
||||
defineOgImageComponent('Blog', {
|
||||
headline: breadcrumb.value.map(item => item.label).join(' > ')
|
||||
}, {
|
||||
fonts: ['Geist:400', 'Geist:600']
|
||||
})
|
||||
}
|
||||
|
||||
const title = page.value?.seo?.title || page.value?.title
|
||||
const description = page.value?.seo?.description || page.value?.description
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogDescription: description,
|
||||
ogTitle: title
|
||||
})
|
||||
|
||||
const articleLink = computed(() => `${window?.location}`)
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UMain class="mt-20 px-2">
|
||||
<UContainer class="relative min-h-screen">
|
||||
<UPage v-if="page">
|
||||
<ULink
|
||||
to="/blog"
|
||||
class="text-sm flex items-center gap-1"
|
||||
>
|
||||
<UIcon name="lucide:chevron-left" />
|
||||
Blog
|
||||
</ULink>
|
||||
<div class="flex flex-col gap-3 mt-8">
|
||||
<div class="flex text-xs text-muted items-center justify-center gap-2">
|
||||
<span v-if="page.date">
|
||||
{{ formatDate(page.date) }}
|
||||
</span>
|
||||
<span v-if="page.date && page.minRead">
|
||||
-
|
||||
</span>
|
||||
<span v-if="page.minRead">
|
||||
{{ page.minRead }} MIN READ
|
||||
</span>
|
||||
</div>
|
||||
<NuxtImg
|
||||
:src="page.image"
|
||||
:alt="page.title"
|
||||
class="rounded-lg w-full h-[300px] object-cover object-center"
|
||||
/>
|
||||
<h1 class="text-4xl text-center font-medium max-w-3xl mx-auto mt-4">
|
||||
{{ page.title }}
|
||||
</h1>
|
||||
<p class="text-muted text-center max-w-2xl mx-auto">
|
||||
{{ page.description }}
|
||||
</p>
|
||||
<div class="flex items-center justify-center gap-2 mt-2">
|
||||
<UUser
|
||||
orientation="vertical"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
class="justify-center items-center text-center"
|
||||
v-bind="page.author"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<UPageBody class="max-w-3xl mx-auto">
|
||||
<ContentRenderer
|
||||
v-if="page.body"
|
||||
:value="page"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 text-sm text-muted">
|
||||
<UButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
color="neutral"
|
||||
label="Copy link"
|
||||
@click="copyToClipboard(articleLink, 'Article link copied to clipboard')"
|
||||
/>
|
||||
</div>
|
||||
<UContentSurround :surround />
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</UContainer>
|
||||
</UMain>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
const { data: page } = await useAsyncData('blog-page', () => {
|
||||
return queryCollection('pages').path('/blog').first()
|
||||
})
|
||||
if (!page.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Page not found',
|
||||
fatal: true
|
||||
})
|
||||
}
|
||||
const { data: posts } = await useAsyncData('blogs', () =>
|
||||
queryCollection('blog').order('date', 'DESC').all()
|
||||
)
|
||||
if (!posts.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'blogs posts not found',
|
||||
fatal: true
|
||||
})
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.seo?.title || page.value?.title,
|
||||
ogTitle: page.value?.seo?.title || page.value?.title,
|
||||
description: page.value?.seo?.description || page.value?.description,
|
||||
ogDescription: page.value?.seo?.description || page.value?.description
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage v-if="page">
|
||||
<UPageHero
|
||||
:title="page.title"
|
||||
:description="page.description"
|
||||
:links="page.links"
|
||||
:ui="{
|
||||
title: '!mx-0 text-left',
|
||||
description: '!mx-0 text-left',
|
||||
links: 'justify-start'
|
||||
}"
|
||||
/>
|
||||
<UPageSection
|
||||
:ui="{
|
||||
container: '!pt-0'
|
||||
}"
|
||||
>
|
||||
<UBlogPosts orientation="vertical">
|
||||
<Motion
|
||||
v-for="(post, index) in posts"
|
||||
:key="index"
|
||||
:initial="{ opacity: 0, transform: 'translateY(10px)' }"
|
||||
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
|
||||
:transition="{ delay: 0.2 * index }"
|
||||
:in-view-options="{ once: true }"
|
||||
>
|
||||
<UBlogPost
|
||||
variant="naked"
|
||||
orientation="horizontal"
|
||||
:to="post.path"
|
||||
v-bind="post"
|
||||
:ui="{
|
||||
root: 'md:grid md:grid-cols-2 group overflow-visible transition-all duration-300',
|
||||
image:
|
||||
'group-hover/blog-post:scale-105 rounded-lg shadow-lg border-4 border-muted ring-2 ring-default',
|
||||
header:
|
||||
index % 2 === 0
|
||||
? 'sm:-rotate-1 overflow-visible'
|
||||
: 'sm:rotate-1 overflow-visible'
|
||||
}"
|
||||
/>
|
||||
</Motion>
|
||||
</UBlogPosts>
|
||||
</UPageSection>
|
||||
</UPage>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
const { data: page } = await useAsyncData('index', () => {
|
||||
return queryCollection('index').first()
|
||||
})
|
||||
if (!page.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Page not found',
|
||||
fatal: true
|
||||
})
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.seo.title || page.value?.title,
|
||||
ogTitle: page.value?.seo.title || page.value?.title,
|
||||
description: page.value?.seo.description || page.value?.description,
|
||||
ogDescription: page.value?.seo.description || page.value?.description
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage v-if="page">
|
||||
<LandingHero :page />
|
||||
<UPageSection
|
||||
:ui="{
|
||||
container: '!pt-0 lg:grid lg:grid-cols-2 lg:gap-8'
|
||||
}"
|
||||
>
|
||||
<LandingAbout :page />
|
||||
<LandingWorkExperience :page />
|
||||
<LandingEducation :page />
|
||||
</UPageSection>
|
||||
<!-- <LandingBlog :page /> -->
|
||||
<!-- <LandingTestimonials :page /> -->
|
||||
<!-- <LandingFAQ :page /> -->
|
||||
</UPage>
|
||||
</template>
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
const { data: page } = await useAsyncData('projects-page', () => {
|
||||
return queryCollection('pages').path('/projects').first()
|
||||
})
|
||||
if (!page.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Page not found',
|
||||
fatal: true
|
||||
})
|
||||
}
|
||||
|
||||
const { data: projects } = await useAsyncData('projects', () => {
|
||||
return queryCollection('projects').all()
|
||||
})
|
||||
|
||||
const { global } = useAppConfig()
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.seo?.title || page.value?.title,
|
||||
ogTitle: page.value?.seo?.title || page.value?.title,
|
||||
description: page.value?.seo?.description || page.value?.description,
|
||||
ogDescription: page.value?.seo?.description || page.value?.description
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage v-if="page">
|
||||
<UPageHero
|
||||
:title="page.title"
|
||||
:description="page.description"
|
||||
:links="page.links"
|
||||
:ui="{
|
||||
title: '!mx-0 text-left',
|
||||
description: '!mx-0 text-left',
|
||||
links: 'justify-start'
|
||||
}"
|
||||
>
|
||||
<template #links>
|
||||
<div
|
||||
v-if="page.links"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<UButton
|
||||
:label="page.links[0]?.label"
|
||||
:to="global.meetingLink"
|
||||
v-bind="page.links[0]"
|
||||
/>
|
||||
<UButton
|
||||
:to="`mailto:${global.email}`"
|
||||
v-bind="page.links[1]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UPageHero>
|
||||
<UPageSection
|
||||
:ui="{
|
||||
container: '!pt-0'
|
||||
}"
|
||||
>
|
||||
<Motion
|
||||
v-for="(project, index) in projects"
|
||||
:key="project.title"
|
||||
:initial="{ opacity: 0, transform: 'translateY(10px)' }"
|
||||
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
|
||||
:transition="{ delay: 0.2 * index }"
|
||||
:in-view-options="{ once: true }"
|
||||
>
|
||||
<UPageCard
|
||||
:title="project.title"
|
||||
:description="project.description"
|
||||
:to="project.url"
|
||||
orientation="horizontal"
|
||||
variant="naked"
|
||||
:reverse="index % 2 === 1"
|
||||
class="group"
|
||||
:ui="{
|
||||
wrapper: 'max-sm:order-last'
|
||||
}"
|
||||
>
|
||||
<template #leading>
|
||||
<span class="text-sm text-muted">
|
||||
{{ new Date(project.date).getFullYear() }}
|
||||
</span>
|
||||
</template>
|
||||
<template #footer>
|
||||
<ULink
|
||||
:to="project.url"
|
||||
class="text-sm text-primary flex items-center"
|
||||
>
|
||||
View Project
|
||||
<UIcon
|
||||
name="i-lucide-arrow-right"
|
||||
class="size-4 text-primary transition-all opacity-0 group-hover:translate-x-1 group-hover:opacity-100"
|
||||
/>
|
||||
</ULink>
|
||||
</template>
|
||||
<img
|
||||
:src="project.image"
|
||||
:alt="project.title"
|
||||
class="object-cover w-full h-48 rounded-lg"
|
||||
>
|
||||
</UPageCard>
|
||||
</Motion>
|
||||
</UPageSection>
|
||||
</UPage>
|
||||
</template>
|
||||
@@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
type Event = {
|
||||
title: string
|
||||
date: string
|
||||
location: string
|
||||
url?: string
|
||||
category: 'Conference' | 'Live talk' | 'Podcast'
|
||||
}
|
||||
|
||||
const { data: page } = await useAsyncData('speaking', () => {
|
||||
return queryCollection('speaking').first()
|
||||
})
|
||||
if (!page.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Page not found',
|
||||
fatal: true
|
||||
})
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.seo?.title || page.value?.title,
|
||||
ogTitle: page.value?.seo?.title || page.value?.title,
|
||||
description: page.value?.seo?.description || page.value?.description,
|
||||
ogDescription: page.value?.seo?.description || page.value?.description
|
||||
})
|
||||
|
||||
const { global } = useAppConfig()
|
||||
|
||||
const groupedEvents = computed((): Record<Event['category'], Event[]> => {
|
||||
const events = page.value?.events || []
|
||||
const grouped: Record<Event['category'], Event[]> = {
|
||||
'Conference': [],
|
||||
'Live talk': [],
|
||||
'Podcast': []
|
||||
}
|
||||
for (const event of events) {
|
||||
if (grouped[event.category]) grouped[event.category].push(event)
|
||||
}
|
||||
return grouped
|
||||
})
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage v-if="page">
|
||||
<UPageHero
|
||||
:title="page.title"
|
||||
:description="page.description"
|
||||
:ui="{
|
||||
title: '!mx-0 text-left',
|
||||
description: '!mx-0 text-left',
|
||||
links: 'justify-start'
|
||||
}"
|
||||
>
|
||||
<template #links>
|
||||
<UButton
|
||||
v-if="page.links"
|
||||
:to="`mailto:${global.email}`"
|
||||
v-bind="page.links[0]"
|
||||
/>
|
||||
</template>
|
||||
</UPageHero>
|
||||
<UPageSection
|
||||
:ui="{
|
||||
container: '!pt-0'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(eventsInCategory, category) in groupedEvents"
|
||||
:key="category"
|
||||
class="grid grid-cols-1 lg:grid-cols-3 lg:gap-8 mb-16 last:mb-0"
|
||||
>
|
||||
<div class="lg:col-span-1 mb-4 lg:mb-0">
|
||||
<h2
|
||||
class="lg:sticky lg:top-16 text-xl font-semibold tracking-tight text-highlighted"
|
||||
>
|
||||
{{ category.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase()) }}s
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
<div
|
||||
v-for="(event, index) in eventsInCategory"
|
||||
:key="`${category}-${index}`"
|
||||
class="group relative pl-6 border-l border-default"
|
||||
>
|
||||
<NuxtLink
|
||||
v-if="event.url"
|
||||
:to="event.url"
|
||||
class="absolute inset-0"
|
||||
/>
|
||||
<div class="mb-1 text-sm font-medium text-muted">
|
||||
<span>{{ event.location }}</span>
|
||||
<span
|
||||
v-if="event.location && event.date"
|
||||
class="mx-1"
|
||||
>·</span>
|
||||
<span v-if="event.date">{{ formatDate(event.date) }}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
|
||||
<UButton
|
||||
v-if="event.url"
|
||||
target="_blank"
|
||||
:label="event.category === 'Podcast' ? 'Listen' : 'Watch'"
|
||||
variant="link"
|
||||
class="p-0 pt-2 gap-0"
|
||||
>
|
||||
<template #trailing>
|
||||
<UIcon
|
||||
name="i-lucide-arrow-right"
|
||||
class="size-4 transition-all opacity-0 group-hover:translate-x-1 group-hover:opacity-100"
|
||||
/>
|
||||
</template>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UPageSection>
|
||||
</UPage>
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
export function copyToClipboard(toCopy: string, message: string = 'Copied to clipboard') {
|
||||
const toast = useToast()
|
||||
navigator.clipboard.writeText(toCopy).then(() => {
|
||||
toast.add({ title: message, color: 'success', icon: 'i-lucide-check-circle' })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
export const navLinks: NavigationMenuItem[] = [{
|
||||
label: 'Home',
|
||||
icon: 'i-lucide-home',
|
||||
to: '/'
|
||||
}, {
|
||||
label: 'Projects',
|
||||
icon: 'i-lucide-folder',
|
||||
to: '/projects'
|
||||
// }, {
|
||||
// label: 'Blog',
|
||||
// icon: 'i-lucide-file-text',
|
||||
// to: '/blog'
|
||||
// }, {
|
||||
// label: 'Speaking',
|
||||
// icon: 'i-lucide-mic',
|
||||
// to: '/speaking'
|
||||
}, {
|
||||
label: 'About',
|
||||
icon: 'i-lucide-user',
|
||||
to: '/about'
|
||||
}, {
|
||||
label: 'Git',
|
||||
icon: 'i-simple-icons-gitea',
|
||||
to: 'https://git.kalar.codes/NickKalar',
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
}, {
|
||||
label: 'Resume',
|
||||
icon: 'i-lucide-file',
|
||||
onSelect: () => { const fileUrl = '/2025_Kalar_Resume.pdf';
|
||||
const fileName = '2025_Kalar_Resume.pdf';
|
||||
const link = document.createElement('a');
|
||||
link.href = fileUrl;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
}]
|
||||
@@ -0,0 +1,143 @@
|
||||
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
|
||||
|
||||
const createBaseSchema = () => z.object({
|
||||
title: z.string(),
|
||||
description: z.string()
|
||||
})
|
||||
|
||||
const createButtonSchema = () => z.object({
|
||||
label: z.string(),
|
||||
icon: z.string().optional(),
|
||||
to: z.string().optional(),
|
||||
color: z.enum(['primary', 'neutral', 'success', 'warning', 'error', 'info']).optional(),
|
||||
size: z.enum(['xs', 'sm', 'md', 'lg', 'xl']).optional(),
|
||||
variant: z.enum(['solid', 'outline', 'subtle', 'soft', 'ghost', 'link']).optional(),
|
||||
target: z.enum(['_blank', '_self']).optional()
|
||||
})
|
||||
|
||||
const createImageSchema = () => z.object({
|
||||
src: z.string().editor({ input: 'media' }),
|
||||
alt: z.string()
|
||||
})
|
||||
|
||||
const createAuthorSchema = () => z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
twitter: z.string().optional(),
|
||||
to: z.string().optional(),
|
||||
avatar: createImageSchema().optional()
|
||||
})
|
||||
|
||||
const createTestimonialSchema = () => z.object({
|
||||
quote: z.string(),
|
||||
author: createAuthorSchema()
|
||||
})
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
index: defineCollection({
|
||||
type: 'page',
|
||||
source: 'index.yml',
|
||||
schema: z.object({
|
||||
hero: z.object({
|
||||
links: z.array(createButtonSchema()),
|
||||
images: z.array(createImageSchema())
|
||||
}),
|
||||
about: createBaseSchema(),
|
||||
experience: createBaseSchema().extend({
|
||||
items: z.array(z.object({
|
||||
date: z.date(),
|
||||
position: z.string(),
|
||||
company: z.object({
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
logo: z.string().editor({ input: 'icon' }),
|
||||
color: z.string()
|
||||
})
|
||||
}))
|
||||
}),
|
||||
education: createBaseSchema().extend({
|
||||
items: z.array(z.object({
|
||||
date: z.date(),
|
||||
degree: z.string(),
|
||||
institution: z.object({
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
logo: z.string().editor({ input: 'icon' }),
|
||||
color: z.string()
|
||||
})
|
||||
}))
|
||||
}),
|
||||
testimonials: z.array(createTestimonialSchema()),
|
||||
blog: createBaseSchema(),
|
||||
faq: createBaseSchema().extend({
|
||||
categories: z.array(
|
||||
z.object({
|
||||
title: z.string().nonempty(),
|
||||
questions: z.array(
|
||||
z.object({
|
||||
label: z.string().nonempty(),
|
||||
content: z.string().nonempty()
|
||||
})
|
||||
)
|
||||
}))
|
||||
})
|
||||
})
|
||||
}),
|
||||
projects: defineCollection({
|
||||
type: 'data',
|
||||
source: 'projects/*.yml',
|
||||
schema: z.object({
|
||||
title: z.string().nonempty(),
|
||||
description: z.string().nonempty(),
|
||||
image: z.string().nonempty().editor({ input: 'media' }),
|
||||
url: z.string().nonempty(),
|
||||
tags: z.array(z.string()),
|
||||
date: z.date()
|
||||
})
|
||||
}),
|
||||
blog: defineCollection({
|
||||
type: 'page',
|
||||
source: 'blog/*.md',
|
||||
schema: z.object({
|
||||
minRead: z.number(),
|
||||
date: z.date(),
|
||||
image: z.string().nonempty().editor({ input: 'media' }),
|
||||
author: createAuthorSchema()
|
||||
})
|
||||
}),
|
||||
pages: defineCollection({
|
||||
type: 'page',
|
||||
source: [
|
||||
{ include: 'projects.yml' },
|
||||
{ include: 'blog.yml' }
|
||||
],
|
||||
schema: z.object({
|
||||
links: z.array(createButtonSchema())
|
||||
})
|
||||
}),
|
||||
speaking: defineCollection({
|
||||
type: 'page',
|
||||
source: 'speaking.yml',
|
||||
schema: z.object({
|
||||
links: z.array(createButtonSchema()),
|
||||
events: z.array(z.object({
|
||||
category: z.enum(['Live talk', 'Podcast', 'Conference']),
|
||||
title: z.string(),
|
||||
date: z.date(),
|
||||
location: z.string(),
|
||||
url: z.string().optional()
|
||||
}))
|
||||
})
|
||||
}),
|
||||
about: defineCollection({
|
||||
type: 'page',
|
||||
source: 'about.yml',
|
||||
schema: z.object({
|
||||
content: z.object({}),
|
||||
images: z.array(createImageSchema())
|
||||
})
|
||||
}),
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
title: About Me
|
||||
description: Learn more about my journey as a Florida-based Back-End Developer, my engineering philosophy, and my passion for crafting robust, performant systems.
|
||||
content: |
|
||||
Hi, I'm **Nick Kalar**, a back-end software engineering from California, currently living in Florida. For the past seven years, I've been immersed in the world of digital product creation, focusing on building experiences that are not just functional, but genuinely intuitive and engaging for users.
|
||||
My path started when I started my first IT job as a Mobile Device Administrator for Aera Energy LLC. Aera was transitioning to a more mobile forward approach. The expansion of tablet computing led Aera to investigate using tablets to handle monitoring, maintenance, and asset management in the field. As the Mobile
|
||||
Device Administrator, I was at the forefront of this transition, building the processes and systems of support, training field engineers and workers, and managing the small army of mobile devices deployed across the company's operations. This role ignited my passion for technology and problem-solving, leading me
|
||||
to pursue further my education. After leaving Aera, I decided to go back to college at **California State University, Bakersfield** (Go Roadrunners), where I earned a Bachelor's of Science in Computer Science - Information Systems. It was there I discovered my love for coding and development. I spent countless
|
||||
hours honing my skills in various programming languages and frameworks, collaborating with other students to learn and grow with.
|
||||
|
||||
### My Engineering Philosophy
|
||||
My engineering philosophy is rooted in **collaboration**, **humanity**, and **curiosity**. I believe great software starts with people coming together and understanding the 'why' - the problem(s) at hand, the advantages and drawbacks of the available technology, and the systems needed to solve the issues.
|
||||
I use research, iterative prototyping (often starting with a language like Python and moving to something faster later), and Test Driven Development to ensure the solutions I build are truly effective. My goal is to create systems that can grow and be maintained easily; a fusion of function and simplicity.
|
||||
Whether I'm building microservice APIs in the cloud or writing designing and coding a fullstack system, my target is always on creating value for the end-user.
|
||||
|
||||
### What Drives Me
|
||||
The thing that keeps me going is the joy of solving complex problems and seeing ideas come to life. I love collaborating with people who are passionate about making a difference through technology. Every project is an opportunity to learn something new, push my boundaries, and contribute to something meaningful.
|
||||
|
||||
### Beyond the Screen
|
||||
When I'm not coding, I like to explore bookshops for interesting reads, drink coffee at local cafes, and spend quality time with my pets and my partner, Michelle.
|
||||
Thanks for stopping by. Feel free to browse my [projects](/projects) or [get in touch](/#contact) if you'd like to collaborate!
|
||||
# images:
|
||||
# - src: https://images.unsplash.com/photo-1744877478622-a78c7a3336f6?q=80&w=1587&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D
|
||||
# alt: My coffee workspace
|
||||
@@ -0,0 +1,2 @@
|
||||
title: Latest Articles
|
||||
description: Some of my recent thoughts on design, development, and the tech industry.
|
||||
@@ -0,0 +1,101 @@
|
||||
seo:
|
||||
title: "Nick Kalar - Software Engineer & Data Enthusiast"
|
||||
description: "Welcome to my portfolio! I'm Nick Kalar, a Software Engineer and back-end developer based in Florida. I specialize in building performant services to help companies deliver to their customers faster and more reliably."
|
||||
title: "Hey, I'm Nick Kalar Software Engineer"
|
||||
description: "I craft efficient, thoughtful systems. Based in Florida, bringing ideas to life through code and creativity."
|
||||
hero:
|
||||
links:
|
||||
- label: ""
|
||||
to:
|
||||
color: "neutral"
|
||||
images:
|
||||
# - src: /hero/random-1.avif
|
||||
# alt: Random Image 1
|
||||
about:
|
||||
title: "About Me"
|
||||
description: |
|
||||
As a back-end developer with 7 years of experience, I leverage my curiosity and subject matter expertise to craft robust, scalable systems that drive business success.
|
||||
My approach blends collaboration and strategy with technical expertise, transforming concepts into functional, purposeful applications that seamlessly integrate into larger systems.
|
||||
experience:
|
||||
title: Work Experience
|
||||
items:
|
||||
- position: "Senior Back-end Software Engineer at"
|
||||
date: "2021 - 2025"
|
||||
company:
|
||||
name: EY
|
||||
logo: "https://cdn.brandfetch.io/idB8IjfqRq/w/32/h/32/theme/dark/icon?c=1bxid64Mup7aczewSAYMX"
|
||||
url: "https://ey.com"
|
||||
color: "#FFE600"
|
||||
education:
|
||||
title: Education
|
||||
items:
|
||||
- degree: "B.S. in Computer Science from"
|
||||
date: "2018 - 2020"
|
||||
institution:
|
||||
name: CSUB
|
||||
logo: "https://www.csub.edu/_resources/images/csub-logo-footer.png"
|
||||
url: "https://www.csub.edu"
|
||||
color: "#003594"
|
||||
- degree: "A.A. from"
|
||||
date: "2015 - 2018"
|
||||
institution:
|
||||
name: Bakersfield College
|
||||
logo: "https://upload.wikimedia.org/wikipedia/en/7/70/Bakersfield_college_seal.png"
|
||||
url: "https://www.bakersfieldcollege.edu/index.html"
|
||||
color: "#b10b2d"
|
||||
testimonials:
|
||||
- quote: |
|
||||
Nick has been a wonderful mentor, peer, and friend. I will always
|
||||
remember the patience and leadership he demonstrated when I joined our
|
||||
team, and he taught me how to be an engineer in a client facing role
|
||||
from the ground up. His technical agility has consistently inspired me
|
||||
to grow and challenge myself.
|
||||
author:
|
||||
name: 'Elizabeth Drebin'
|
||||
description: 'Biomedical Engineering Student at Carnegie Mellon University'
|
||||
avatar:
|
||||
src: 'https://media.licdn.com/dms/image/v2/D4E03AQE83RWEEpmimQ/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1700970362718?e=1766016000&v=beta&t=hUMKcwhTJaPnZksBi1-yBuSuoaH83C8YjIFMCqtZFiQ'
|
||||
srcset: 'https://media.licdn.com/dms/image/v2/D4E03AQE83RWEEpmimQ/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1700970362718?e=1766016000&v=beta&t=hUMKcwhTJaPnZksBi1-yBuSuoaH83C8YjIFMCqtZFiQ 2x'
|
||||
- quote: |
|
||||
Nick has engaged with [Client] team members across development, product
|
||||
management, and quality assurance. He's fostered stronger relationships
|
||||
and open communication with these teams, gaining a deeper understanding
|
||||
of the broader context of the issues he is addressing and contributing
|
||||
to more holistic solutions.
|
||||
author:
|
||||
name: 'Kevin Von Storch'
|
||||
description: 'Digital Solution Delivery Lead at Ernst & Young'
|
||||
avatar:
|
||||
src: 'https://media.licdn.com/dms/image/v2/C5603AQHoTzQwnBe7HQ/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1591756110221?e=1766016000&v=beta&t=qrXnoLQktukfddE6Yv0Zc2VuUUmB8hufq95xLv9vlQg'
|
||||
srcset: 'https://media.licdn.com/dms/image/v2/C5603AQHoTzQwnBe7HQ/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1591756110221?e=1766016000&v=beta&t=qrXnoLQktukfddE6Yv0Zc2VuUUmB8hufq95xLv9vlQg 2x'
|
||||
- quote: |
|
||||
Nick is the definition of a utility player. I am so lucky that his
|
||||
interests are in mobile development, allowing me the opportunity to see
|
||||
his work first hand. Nick has come in and immediately added immense value
|
||||
through detailed code reviews, feature creation, service level blueprint
|
||||
creation, architecture design, backlog refinement and more. Nick's ability
|
||||
to manage and lead team members, both more senior and below his rank, is
|
||||
a testament to his leadership skills.
|
||||
author:
|
||||
name: 'Janaeé Wallace'
|
||||
description: 'Technical Product Manager at Ernst & Young'
|
||||
avatar:
|
||||
src: 'https://media.licdn.com/dms/image/v2/C5603AQHD9pDwfCgKJw/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1517606283329?e=1766016000&v=beta&t=sl-Uhox5hXrTEndOB3ZQ94qdg3uolF3cOzTZI3BO02E'
|
||||
srcset: 'https://media.licdn.com/dms/image/v2/C5603AQHD9pDwfCgKJw/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1517606283329?e=1766016000&v=beta&t=sl-Uhox5hXrTEndOB3ZQ94qdg3uolF3cOzTZI3BO02E 2x'
|
||||
# blog:
|
||||
# title: "Latest Articles"
|
||||
# description: "Some of my recent thoughts"
|
||||
# faq:
|
||||
# title: Frequently Asked Questions
|
||||
# description: Answers to common questions about my process and services.
|
||||
# categories:
|
||||
# - title:
|
||||
# questions:
|
||||
# - label:
|
||||
# content: |
|
||||
|
||||
# - label:
|
||||
# content: |
|
||||
|
||||
# - label:
|
||||
# content: |
|
||||
@@ -0,0 +1,6 @@
|
||||
title: Solving Problems, Building Experiences.
|
||||
description: In my spare time, I like to build things to help me learn new skills or solve problems I encounter. Here are some of those projects.
|
||||
links:
|
||||
- label: "Let's talk"
|
||||
color: "neutral"
|
||||
- label: 'Email me'
|
||||
@@ -0,0 +1,6 @@
|
||||
title: Incident Report Generator
|
||||
description: A Python program built using the Pandas library to transform a comma-delimited CSV file to an Excel spreadsheet with charts and tables.
|
||||
image: https://images.unsplash.com/photo-1573068111653-f18bef611c8a?q=80&w=2874&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D
|
||||
url: "https://git.kalar.codes/NickKalar/Incident_Report_Generator"
|
||||
tags: ["Python", "Pandas", "Data Analysis", "Excel", "CSV"]
|
||||
date: "02-14-2025"
|
||||
@@ -0,0 +1,6 @@
|
||||
title: Library Management ETL App
|
||||
description: An Extract, Transform, Load (ETL) app to gather book information from public APIs for a Proof of Concept Library Management System project.
|
||||
image: https://images.unsplash.com/photo-1679055324332-189b4a7bc99e?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D
|
||||
url: "https://git.kalar.codes/NickKalar/LMS-DB-ETL"
|
||||
tags: ["Python", "PostgreSQL", "ETL", "APIs", "Data Engineering"]
|
||||
date: "06-01-2025"
|
||||
@@ -0,0 +1,6 @@
|
||||
title: Meal Picker
|
||||
description: An app made to help keep track of ingredients for meals, and to build markdown checklists for shopping.
|
||||
image: https://images.unsplash.com/photo-1623265300797-4a3541e29a60?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D
|
||||
url: "https://git.kalar.codes/NickKalar/Meal-Picker"
|
||||
tags: ["Python", "Flet", "UI"]
|
||||
date: "11-15-2025"
|
||||
@@ -0,0 +1,21 @@
|
||||
# title: Talks & Workshops
|
||||
# description:
|
||||
# links:
|
||||
# - label: Invite me to speak
|
||||
# size: md
|
||||
# events:
|
||||
# - category: Conference
|
||||
# title: ""
|
||||
# date: "2024-10-26"
|
||||
# location: ""
|
||||
# url: "#"
|
||||
# - category: Live talk
|
||||
# title: ""
|
||||
# date: "2024-08-20"
|
||||
# location: ""
|
||||
# url: "#"
|
||||
# - category: Podcast
|
||||
# title: ""
|
||||
# date: "2024-01-30"
|
||||
# location: ""
|
||||
# url: "#"
|
||||
@@ -0,0 +1,8 @@
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
|
||||
export default withNuxt({
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@nuxt/eslint',
|
||||
'@nuxt/image',
|
||||
'@nuxt/ui',
|
||||
'@nuxt/content',
|
||||
'@vueuse/nuxt',
|
||||
'nuxt-og-image',
|
||||
'motion-v/nuxt'
|
||||
],
|
||||
|
||||
devtools: {
|
||||
enabled: true
|
||||
},
|
||||
|
||||
css: ['~/assets/css/main.css'],
|
||||
|
||||
compatibilityDate: '2024-11-01',
|
||||
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: [
|
||||
'/'
|
||||
],
|
||||
crawlLinks: true
|
||||
}
|
||||
},
|
||||
|
||||
eslint: {
|
||||
config: {
|
||||
stylistic: {
|
||||
commaDangle: 'never',
|
||||
braceStyle: '1tbs'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"typecheck": "nuxt typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.75",
|
||||
"@iconify-json/simple-icons": "^1.2.60",
|
||||
"@nuxt/content": "^3.8.2",
|
||||
"@nuxt/image": "^2.0.0",
|
||||
"@nuxt/ui": "^4.2.1",
|
||||
"@vueuse/nuxt": "^13.9.0",
|
||||
"better-sqlite3": "^12.4.6",
|
||||
"motion-v": "^1.7.3",
|
||||
"nuxt": "^4.2.1",
|
||||
"nuxt-og-image": "^5.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "^1.10.0",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vue-tsc": "^3.1.5"
|
||||
},
|
||||
"packageManager": "pnpm@10.23.0"
|
||||
}
|
||||
Generated
+12263
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
ignoredBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- '@tailwindcss/oxide'
|
||||
- esbuild
|
||||
- unrs-resolver
|
||||
- vue-demi
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- better-sqlite3
|
||||
- sharp
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": [
|
||||
"github>nuxt/renovate-config-nuxt"
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [{
|
||||
"matchDepTypes": ["resolutions"],
|
||||
"enabled": false
|
||||
}],
|
||||
"postUpdateOptions": ["pnpmDedupe"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./.nuxt/tsconfig.app.json" },
|
||||
{ "path": "./.nuxt/tsconfig.server.json" },
|
||||
{ "path": "./.nuxt/tsconfig.shared.json" },
|
||||
{ "path": "./.nuxt/tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user