Feat/ui update #5

Merged
LiamPietralla merged 2 commits from feat/ui-update into main 2026-10-15 17:28:50 +11:00
35 changed files with 604 additions and 294 deletions

No files matched your search

+3 -3
View File
@@ -14,7 +14,7 @@ jobs:
name: Build App name: Build App
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -96,6 +96,6 @@ jobs:
run: | run: |
export DEPLOYMENT_VERSION="${GITHUB_SHA:0:7}-$(date +%s)" export DEPLOYMENT_VERSION="${GITHUB_SHA:0:7}-$(date +%s)"
nomad job run -var="deployment_version=$DEPLOYMENT_VERSION" infra/nomad/portfolio.nomad.hcl nomad job run -var="deployment_version=$DEPLOYMENT_VERSION" infra/nomad/portfolio.nomad.hcl
env: env:
NOMAD_ADDR: ${{ vars.NOMAD_ADDR }} NOMAD_ADDR: ${{ vars.NOMAD_ADDR }}
NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }} NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}
+19 -19
View File
@@ -10,25 +10,25 @@ jobs:
name: SBOM Generation and Upload name: SBOM Generation and Upload
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup PNPM and Node - name: Setup PNPM and Node
uses: pnpm/setup@v3 uses: pnpm/setup@v3
with: with:
runtime: node@24 runtime: node@24
- name: Generate SBOM - name: Generate SBOM
run: pnpm sbom --sbom-format cyclonedx --sbom-type application --out sbom.cyclonedx.json run: pnpm sbom --sbom-format cyclonedx --sbom-type application --out sbom.cyclonedx.json
- name: Upload SBOM - name: Upload SBOM
uses: DependencyTrack/gh-upload-sbom@v4 uses: DependencyTrack/gh-upload-sbom@v4
with: with:
protocol: ${{ vars.DT_API_PROTOCOL }} protocol: ${{ vars.DT_API_PROTOCOL }}
server-hostname: ${{ vars.DT_API_HOSTNAME }} server-hostname: ${{ vars.DT_API_HOSTNAME }}
port: ${{ vars.DT_API_PORT }} port: ${{ vars.DT_API_PORT }}
api-key: ${{ secrets.DT_API_KEY }} api-key: ${{ secrets.DT_API_KEY }}
project-name: 'Liam Portfolio' project-name: 'Liam Portfolio'
project-version: 'main' project-version: 'main'
auto-create: true auto-create: true
bom-filename: 'sbom.cyclonedx.json' bom-filename: 'sbom.cyclonedx.json'
+7
View File
@@ -0,0 +1,7 @@
package-lock.json
pnpm-lock.yaml
yarn.lock
src/app/(payload)/admin/importMap.js
src/payload-types.ts
src/migrations
infra/**
+3 -3
View File
@@ -1,6 +1,6 @@
# Liam Pietralla Porfolio # Liam Pietralla Porfolio
The portfolio is built using Next.JS and Payload CMS. Payload is running directly in the Next app, and can be accessed by appending /admin to the route. The portfolio is built using Next.JS and Payload CMS. Payload is running directly in the Next app, and can be accessed by appending /admin to the route.
## Development ## Development
@@ -18,7 +18,7 @@ pnpm dev
### Payload CMS ### Payload CMS
Payload will require rebuilding on the types file once any changes are made: Payload will require rebuilding on the types file once any changes are made:
```bash ```bash
pnpm run payload:generate:types pnpm run payload:generate:types
@@ -46,4 +46,4 @@ Once the container is built, you can run it with the following command:
```bash ```bash
docker run -p 3000:3000 -v liam-portfolio-data:/app/portfolio-data -e PAYLOAD_SECRET=your_secret liam-portfolio docker run -p 3000:3000 -v liam-portfolio-data:/app/portfolio-data -e PAYLOAD_SECRET=your_secret liam-portfolio
``` ```
+5 -6
View File
@@ -1,5 +1,4 @@
services: services:
db: db:
image: postgres:18 image: postgres:18
restart: unless-stopped restart: unless-stopped
@@ -8,7 +7,7 @@ services:
- POSTGRES_PASSWORD=postgres - POSTGRES_PASSWORD=postgres
- POSTGRES_DB=portfolio-dev - POSTGRES_DB=portfolio-dev
ports: ports:
- "5432:5432" - '5432:5432'
volumes: volumes:
- portfolio_db_data:/var/lib/postgresql - portfolio_db_data:/var/lib/postgresql
@@ -16,7 +15,7 @@ services:
image: ghcr.io/achtungsoftware/alarik:latest image: ghcr.io/achtungsoftware/alarik:latest
restart: unless-stopped restart: unless-stopped
ports: ports:
- "9000:8080" - '9000:8080'
environment: environment:
- API_BASE_URL=http://localhost:9000 - API_BASE_URL=http://localhost:9000
- CONSOLE_BASE_URL=http://localhost:9001 - CONSOLE_BASE_URL=http://localhost:9001
@@ -33,7 +32,7 @@ services:
image: ghcr.io/achtungsoftware/alarik-console:latest image: ghcr.io/achtungsoftware/alarik-console:latest
restart: unless-stopped restart: unless-stopped
ports: ports:
- "9001:3000" - '9001:3000'
environment: environment:
- NUXT_PUBLIC_API_BASE_URL=http://localhost:9000 - NUXT_PUBLIC_API_BASE_URL=http://localhost:9000
- NUXT_PUBLIC_CONSOLE_BASE_URL=http://localhost:9001 - NUXT_PUBLIC_CONSOLE_BASE_URL=http://localhost:9001
@@ -55,8 +54,8 @@ services:
mc mb alarik/payload-media && mc mb alarik/payload-media &&
echo 'S3 bucket created'; echo 'S3 bucket created';
fi fi
" "
volumes: volumes:
portfolio_s3_data: portfolio_s3_data:
portfolio_db_data: portfolio_db_data:
+14 -14
View File
@@ -1,25 +1,25 @@
import { dirname } from "path"; import { dirname } from 'path'
import { fileURLToPath } from "url"; import { fileURLToPath } from 'url'
import { FlatCompat } from "@eslint/eslintrc"; import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename); const __dirname = dirname(__filename)
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
}); })
const eslintConfig = [ const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"), ...compat.extends('next/core-web-vitals', 'next/typescript'),
{ {
ignores: [ ignores: [
"node_modules/**", 'node_modules/**',
".next/**", '.next/**',
"out/**", 'out/**',
"build/**", 'build/**',
"next-env.d.ts", 'next-env.d.ts',
], ],
}, },
]; ]
export default eslintConfig; export default eslintConfig
+5 -5
View File
@@ -1,8 +1,8 @@
import { withPayload } from "@payloadcms/next/withPayload"; import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from "next"; import type { NextConfig } from 'next'
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: 'standalone',
}; }
export default withPayload(nextConfig); export default withPayload(nextConfig)
+3
View File
@@ -8,6 +8,7 @@
"build": "next build", "build": "next build",
"start": "payload migrate && next start", "start": "payload migrate && next start",
"lint": "eslint", "lint": "eslint",
"format": "prettier --write .",
"payload:generate-types": "payload generate:types", "payload:generate-types": "payload generate:types",
"payload:migrate:create": "payload migrate:create" "payload:migrate:create": "payload migrate:create"
}, },
@@ -35,6 +36,8 @@
"@types/react-dom": "19.3.0", "@types/react-dom": "19.3.0",
"eslint": "^9.39.5", "eslint": "^9.39.5",
"eslint-config-next": "16.1.2", "eslint-config-next": "16.1.2",
"prettier": "^3.9.10",
"prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
+72
View File
@@ -230,6 +230,12 @@ importers:
eslint-config-next: eslint-config-next:
specifier: 16.1.2 specifier: 16.1.2
version: 16.1.2(@typescript-eslint/parser@8.53.0(eslint@9.39.5(jiti@2.5.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.5.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3) version: 16.1.2(@typescript-eslint/parser@8.53.0(eslint@9.39.5(jiti@2.5.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.5(jiti@2.5.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
prettier:
specifier: ^3.9.10
version: 3.9.10
prettier-plugin-tailwindcss:
specifier: ^0.8.1
version: 0.8.1(prettier@3.9.10)
tailwindcss: tailwindcss:
specifier: ^4.3.3 specifier: ^4.3.3
version: 4.3.3 version: 4.3.3
@@ -3905,11 +3911,71 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier-plugin-tailwindcss@0.8.1:
resolution: {integrity: sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==}
engines: {node: '>=20.19'}
peerDependencies:
'@ianvs/prettier-plugin-sort-imports': '*'
'@prettier/plugin-hermes': '*'
'@prettier/plugin-oxc': '*'
'@prettier/plugin-pug': '*'
'@shopify/prettier-plugin-liquid': '*'
'@trivago/prettier-plugin-sort-imports': '*'
'@zackad/prettier-plugin-twig': '*'
prettier: ^3.0
prettier-plugin-astro: '*'
prettier-plugin-css-order: '*'
prettier-plugin-jsdoc: '*'
prettier-plugin-marko: '*'
prettier-plugin-multiline-arrays: '*'
prettier-plugin-organize-attributes: '*'
prettier-plugin-organize-imports: '*'
prettier-plugin-sort-imports: '*'
prettier-plugin-svelte: '*'
peerDependenciesMeta:
'@ianvs/prettier-plugin-sort-imports':
optional: true
'@prettier/plugin-hermes':
optional: true
'@prettier/plugin-oxc':
optional: true
'@prettier/plugin-pug':
optional: true
'@shopify/prettier-plugin-liquid':
optional: true
'@trivago/prettier-plugin-sort-imports':
optional: true
'@zackad/prettier-plugin-twig':
optional: true
prettier-plugin-astro:
optional: true
prettier-plugin-css-order:
optional: true
prettier-plugin-jsdoc:
optional: true
prettier-plugin-marko:
optional: true
prettier-plugin-multiline-arrays:
optional: true
prettier-plugin-organize-attributes:
optional: true
prettier-plugin-organize-imports:
optional: true
prettier-plugin-sort-imports:
optional: true
prettier-plugin-svelte:
optional: true
prettier@3.8.0: prettier@3.8.0:
resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
prettier@3.9.10:
resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==}
engines: {node: '>=14'}
hasBin: true
process-warning@5.0.0: process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
@@ -8915,8 +8981,14 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier-plugin-tailwindcss@0.8.1(prettier@3.9.10):
dependencies:
prettier: 3.9.10
prettier@3.8.0: {} prettier@3.8.0: {}
prettier@3.9.10: {}
process-warning@5.0.0: {} process-warning@5.0.0: {}
prompts@2.4.2: prompts@2.4.2:
+3 -3
View File
@@ -1,5 +1,5 @@
const config = { const config = {
plugins: ["@tailwindcss/postcss"], plugins: ['@tailwindcss/postcss'],
}; }
export default config; export default config
+13
View File
@@ -0,0 +1,13 @@
// @ts-check
/** @type {import('prettier').Config} */
const config = {
semi: false,
singleQuote: true,
trailingComma: 'all',
plugins: ['prettier-plugin-tailwindcss'],
tailwindStylesheet: 'src/app/globals.css',
tailwindFunctions: ['cn', 'cva'],
}
export default config
+16 -15
View File
@@ -1,34 +1,35 @@
import type { Metadata } from "next"; import type { Metadata } from 'next'
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from 'next/font/google'
import "../globals.css"; import '../globals.css'
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: '--font-geist-sans',
subsets: ["latin"], subsets: ['latin'],
}); })
const geistMono = Geist_Mono({ const geistMono = Geist_Mono({
variable: "--font-geist-mono", variable: '--font-geist-mono',
subsets: ["latin"], subsets: ['latin'],
}); })
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Liam Pietralla", title: 'Liam Pietralla',
description: "Enthusiastic Software Developer & DevOps Engineer", description: 'Enthusiastic Software Developer & DevOps Engineer',
}; }
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased px-4`} className={`${geistSans.variable} ${geistMono.variable} px-4 antialiased`}
> >
<div className="aurora" />
{children} {children}
</body> </body>
</html> </html>
); )
} }
+58 -23
View File
@@ -1,43 +1,78 @@
import IndexLink from "@/components/home-page-link"; import IndexLink from '@/components/home-page-link'
import Rule from "@/components/horizontal-rule"; import Rule from '@/components/horizontal-rule'
import { getHome } from "@/services/home-service"; import { getHome } from '@/services/home-service'
import { Mail } from "lucide-react"; import { Mail } from 'lucide-react'
import Image from "next/image"; import Image from 'next/image'
import ProfileImage from "../../../public/images/liam_pietralla.jpg"; import ProfileImage from '../../../public/images/liam_pietralla.jpg'
const IndexPage = async () => { const IndexPage = async () => {
const home = await getHome(); const home = await getHome()
return ( return (
<div className="flex flex-col gap-4 justify-center items-center h-screen"> <div className="flex h-screen flex-col items-center justify-center gap-5">
<Image className="rounded-full max-w-[200px]" src={ProfileImage} alt="Liam Pietralla" /> <div
<h1 className="text-5xl font-bold">Liam Pietralla</h1> className="glow-ring animate-fade-in"
<div className="flex flex-col md:flex-row gap-0 md:gap-[7px]"> style={{ animationDelay: '0ms' }}
<h2 className="text-xl text-center">Enthusiastic Software Developer</h2> >
<h2 className="text-xl text-center mt-[-5px] md:mt-0">& DevOps Engineer</h2> <Image
className="max-w-[200px] rounded-full"
src={ProfileImage}
alt="Liam Pietralla"
/>
</div> </div>
<Rule className="w-[300px]" /> <h1
<div className="flex flex-row gap-4"> className="animate-fade-in-up text-5xl font-bold"
{home.mainLinks.map(link => ( style={{ animationDelay: '120ms' }}
>
Liam Pietralla
</h1>
<div
className="animate-fade-in-up flex flex-col gap-0 md:flex-row md:gap-[7px]"
style={{ animationDelay: '240ms' }}
>
<h2 className="text-center text-xl">Enthusiastic Software Developer</h2>
<h2 className="mt-[-5px] text-center text-xl md:mt-0">
& DevOps Engineer
</h2>
</div>
<Rule
className="animate-fade-in w-[300px]"
style={{ animationDelay: '320ms' }}
/>
<div
className="animate-fade-in-up flex flex-row gap-4"
style={{ animationDelay: '400ms' }}
>
{home.mainLinks.map((link) => (
<IndexLink key={link.id} {...link} /> <IndexLink key={link.id} {...link} />
))} ))}
</div> </div>
<div className="flex flex-row gap-4"> <div
{home.popoverLinks.map(link => ( className="animate-fade-in-up relative z-20 flex flex-row gap-4"
style={{ animationDelay: '480ms' }}
>
{home.popoverLinks.map((link) => (
<IndexLink key={link.id} {...link} isPopover={true} /> <IndexLink key={link.id} {...link} isPopover={true} />
))} ))}
</div> </div>
<Rule className="w-[300px]" /> <Rule
<a href="mailto:me@liampietralla.com" className="group leading-relaxed"> className="animate-fade-in w-[300px]"
style={{ animationDelay: '560ms' }}
/>
<a
href="mailto:me@liampietralla.com"
className="group animate-fade-in-up leading-relaxed"
style={{ animationDelay: '640ms' }}
>
<span className="flex flex-row gap-2"> <span className="flex flex-row gap-2">
<Mail /> <Mail />
me@liampietralla.com me@liampietralla.com
</span> </span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span> <span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</a> </a>
</div> </div>
); )
} }
export default IndexPage; export default IndexPage
+36 -20
View File
@@ -1,35 +1,51 @@
import ProjectCard from "@/components/project-card"; import ProjectCard from '@/components/project-card'
import Rule from "@/components/horizontal-rule"; import { getProjects } from '@/services/projects-service'
import { getProjects } from "@/services/projects-service"; import Image from 'next/image'
import Image from "next/image"; import Link from 'next/link'
import Link from "next/link";
import { Fragment } from "react";
import ProfileImage from "../../../../public/images/liam_pietralla.jpg"; import ProfileImage from '../../../../public/images/liam_pietralla.jpg'
const ProjectsPage = async () => { const ProjectsPage = async () => {
const projects = await getProjects(); const projects = await getProjects()
return ( return (
<div className="flex flex-col gap-4 justify-center items-center my-15"> <div className="my-15 flex flex-col items-center justify-center gap-4">
<div className="flex flex-row items-center gap-2 my-2"> <div className="animate-fade-in-up my-2 flex flex-row items-center gap-2">
<Image src={ProfileImage} alt="Liam Pietralla" className="rounded-full max-w-[50px]" /> <Image
<Link href="/" className="group leading-relaxed font-semi-bold"> src={ProfileImage}
alt="Liam Pietralla"
className="max-w-[50px] rounded-full"
/>
<Link href="/" className="group font-semi-bold leading-relaxed">
Liam Pietralla Liam Pietralla
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span> <span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</Link> </Link>
</div> </div>
<h1 className="text-5xl font-bold">Projects</h1> <h1
<h2>A collection of interesting projects that I am working on currently or have worked on in the past.</h2> className="animate-fade-in-up text-5xl font-bold"
<div className="flex flex-col gap-4"> style={{ animationDelay: '120ms' }}
>
Projects
</h1>
<h2
className="animate-fade-in-up max-w-2xl text-center"
style={{ animationDelay: '240ms' }}
>
A collection of interesting projects that I am working on currently or
have worked on in the past.
</h2>
<div className="flex w-full flex-col gap-4">
{projects.docs.map((project, index) => ( {projects.docs.map((project, index) => (
<Fragment key={index}> <div
key={index}
className="animate-fade-in-up"
style={{ animationDelay: `${320 + index * 120}ms` }}
>
<ProjectCard {...project} /> <ProjectCard {...project} />
{index < projects.docs.length - 1 && <Rule />} </div>
</Fragment>
))} ))}
</div> </div>
</div> </div>
) )
} }
export default ProjectsPage; export default ProjectsPage
@@ -15,7 +15,10 @@ type Args = {
}> }>
} }
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> => export const generateMetadata = ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams }) generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => const NotFound = ({ params, searchParams }: Args) =>
@@ -15,7 +15,10 @@ type Args = {
}> }>
} }
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> => export const generateMetadata = ({
params,
searchParams,
}: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams }) generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) => const Page = ({ params, searchParams }: Args) =>
+2 -2
View File
@@ -15,7 +15,7 @@ type NextCtx = { params: Promise<{ slug?: string[] }> }
type PayloadCtx = { params: Promise<{ slug: string[] }> } type PayloadCtx = { params: Promise<{ slug: string[] }> }
const coerceCtx = (ctx: NextCtx): PayloadCtx => ({ const coerceCtx = (ctx: NextCtx): PayloadCtx => ({
params: ctx.params.then(p => ({ slug: p?.slug ?? [] })), params: ctx.params.then((p) => ({ slug: p?.slug ?? [] })),
}) })
export function GET(req: Request, ctx: NextCtx) { export function GET(req: Request, ctx: NextCtx) {
@@ -40,4 +40,4 @@ export function PUT(req: Request, ctx: NextCtx) {
export function OPTIONS(req: Request, ctx: NextCtx) { export function OPTIONS(req: Request, ctx: NextCtx) {
return REST_OPTIONS(config)(req, coerceCtx(ctx)) return REST_OPTIONS(config)(req, coerceCtx(ctx))
} }
@@ -6,4 +6,4 @@ import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export async function GET(req: Request): Promise<Response> { export async function GET(req: Request): Promise<Response> {
return GRAPHQL_PLAYGROUND_GET(config)(req) return GRAPHQL_PLAYGROUND_GET(config)(req)
} }
+2 -2
View File
@@ -7,7 +7,7 @@ type NextCtx = { params: Promise<{ slug?: string[] }> }
type PayloadCtx = { params: Promise<{ slug: string[] }> } type PayloadCtx = { params: Promise<{ slug: string[] }> }
const coerceCtx = (ctx: NextCtx): PayloadCtx => ({ const coerceCtx = (ctx: NextCtx): PayloadCtx => ({
params: ctx.params.then(p => ({ slug: p?.slug ?? [] })), params: ctx.params.then((p) => ({ slug: p?.slug ?? [] })),
}) })
export function POST(req: Request) { export function POST(req: Request) {
@@ -16,4 +16,4 @@ export function POST(req: Request) {
export function OPTIONS(req: Request, ctx: NextCtx) { export function OPTIONS(req: Request, ctx: NextCtx) {
return REST_OPTIONS(config)(req, coerceCtx(ctx)) return REST_OPTIONS(config)(req, coerceCtx(ctx))
} }
+5 -1
View File
@@ -23,7 +23,11 @@ const serverFunction: ServerFunctionClient = async function (args) {
} }
const Layout = ({ children }: Args) => ( const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}> <RootLayout
config={config}
importMap={importMap}
serverFunction={serverFunction}
>
{children} {children}
</RootLayout> </RootLayout>
) )
+8 -8
View File
@@ -1,14 +1,14 @@
import pkg from "../../../../package.json"; import pkg from '../../../../package.json'
export async function GET() { export async function GET() {
const response = { const response = {
"status": "Healthy", status: 'Healthy',
"version": pkg.version version: pkg.version,
}; }
return new Response(JSON.stringify(response), { return new Response(JSON.stringify(response), {
status: 200, status: 200,
headers: { headers: {
"Content-Type": "application/json" 'Content-Type': 'application/json',
} },
}); })
} }
+146 -3
View File
@@ -1,13 +1,15 @@
@import "tailwindcss"; @import 'tailwindcss';
:root { :root {
--background: #0a0a0a; --background: #0a0a0a;
--foreground: #ededed; --foreground: #ededed;
--accent: #7c9cff;
} }
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--color-accent: var(--accent);
--font-sans: var(--font-geist-sans); --font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
} }
@@ -15,5 +17,146 @@
body { body {
background: var(--background); background: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: Arial, Helvetica, sans-serif; font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
} overflow-x: hidden;
}
.aurora {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
pointer-events: none;
}
.aurora::before,
.aurora::after {
content: '';
position: absolute;
width: 60vw;
height: 60vw;
border-radius: 9999px;
filter: blur(120px);
opacity: 0.22;
will-change: transform;
}
.aurora::before {
top: -15%;
left: -10%;
background: radial-gradient(circle at center, var(--accent), transparent 70%);
animation: drift-1 18s ease-in-out infinite alternate;
}
.aurora::after {
bottom: -20%;
right: -10%;
background: radial-gradient(circle at center, #b07cff, transparent 70%);
animation: drift-2 22s ease-in-out infinite alternate;
}
@keyframes drift-1 {
0% {
transform: translate(0, 0) scale(1);
}
100% {
transform: translate(15vw, 10vh) scale(1.15);
}
}
@keyframes drift-2 {
0% {
transform: translate(0, 0) scale(1);
}
100% {
transform: translate(-12vw, -8vh) scale(1.2);
}
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
opacity: 0;
animation: fade-in-up 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-fade-in {
opacity: 0;
animation: fade-in 1.2s ease forwards;
}
.glow-ring {
position: relative;
}
.glow-ring::before {
content: '';
position: absolute;
inset: -6px;
border-radius: 9999px;
background: conic-gradient(from 0deg, var(--accent), #b07cff, var(--accent));
filter: blur(8px);
opacity: 0.55;
z-index: -1;
animation: spin-slow 8s linear infinite;
}
@keyframes spin-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.glass {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.08);
transition:
background 0.3s ease,
border-color 0.3s ease,
transform 0.3s ease,
box-shadow 0.3s ease;
}
.glass-hover:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(124, 156, 255, 0.4);
transform: translateY(-2px);
box-shadow: 0 8px 30px -10px rgba(124, 156, 255, 0.35);
}
@media (prefers-reduced-motion: reduce) {
*,
.aurora::before,
.aurora::after,
.glow-ring::before {
animation: none !important;
}
.animate-fade-in-up,
.animate-fade-in {
opacity: 1;
animation: none !important;
}
}
+9 -9
View File
@@ -1,26 +1,26 @@
import type { CollectionConfig } from "payload" import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = { export const Media: CollectionConfig = {
slug: "media", slug: 'media',
access: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
{ {
name: "alt", name: 'alt',
type: "text", type: 'text',
required: true, required: true,
}, },
], ],
upload: { upload: {
imageSizes: [ imageSizes: [
{ {
name: "thumbnail", name: 'thumbnail',
width: 150, width: 150,
height: 150, height: 150,
position: "centre", position: 'centre',
} },
], ],
adminThumbnail: "thumbnail", adminThumbnail: 'thumbnail',
} },
} }
+26 -26
View File
@@ -1,61 +1,61 @@
import { revalidatePath } from "next/cache"; import { revalidatePath } from 'next/cache'
import { CollectionConfig } from "payload"; import { CollectionConfig } from 'payload'
export const Projects: CollectionConfig = { export const Projects: CollectionConfig = {
slug: "project", slug: 'project',
access: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
{ {
name: "title", name: 'title',
type: "text", type: 'text',
required: true, required: true,
}, },
{ {
name: "description", name: 'description',
type: "textarea", type: 'textarea',
required: true, required: true,
}, },
{ {
name: "featuredImage", name: 'featuredImage',
type: "relationship", type: 'relationship',
relationTo: "media", relationTo: 'media',
required: false, required: false,
}, },
{ {
name: "tags", name: 'tags',
type: "array", type: 'array',
required: false, required: false,
fields: [ fields: [
{ {
name: "tag", name: 'tag',
type: "text", type: 'text',
required: true, required: true,
}, },
], ],
}, },
{ {
name: "viewLink", name: 'viewLink',
type: "text", type: 'text',
required: false, required: false,
}, },
{ {
name: "repositoryLink", name: 'repositoryLink',
type: "text", type: 'text',
required: false, required: false,
} },
], ],
hooks: { hooks: {
afterChange: [ afterChange: [
async () => { async () => {
revalidatePath("/projects"); revalidatePath('/projects')
} },
], ],
afterDelete: [ afterDelete: [
async () => { async () => {
revalidatePath("/projects"); revalidatePath('/projects')
} },
] ],
} },
} }
+3 -3
View File
@@ -1,9 +1,9 @@
import type { CollectionConfig } from "payload" import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = { export const Users: CollectionConfig = {
slug: "users", slug: 'users',
admin: { admin: {
useAsTitle: "email", useAsTitle: 'email',
}, },
auth: true, auth: true,
fields: [ fields: [
+40 -30
View File
@@ -1,61 +1,71 @@
import Link from "next/link"; import Link from 'next/link'
import { DynamicIcon, IconName } from "lucide-react/dynamic"; import { DynamicIcon, IconName } from 'lucide-react/dynamic'
import { lucidIconMap } from "@/lib/lucid-options"; import { lucidIconMap } from '@/lib/lucid-options'
interface HomePageLinkProps { interface HomePageLinkProps {
title: string; title: string
url: string; url: string
icon: string; icon: string
id?: string | null; id?: string | null
isPopover?: boolean; isPopover?: boolean
} }
const HomePageLink = ({ title, icon, url, isPopover }: HomePageLinkProps) => { const HomePageLink = ({ title, icon, url, isPopover }: HomePageLinkProps) => {
const isRelative = !url.startsWith("http"); const isRelative = !url.startsWith('http')
const IconComponent = lucidIconMap[icon as IconName]; const IconComponent = lucidIconMap[icon as IconName]
if (isPopover) { if (isPopover) {
const LinkContent = (
<span className="glass glass-hover flex items-center space-x-2 rounded-lg px-3 py-2 transition-all duration-300">
{icon && <IconComponent className="size-4" />}
</span>
)
const Popover = (
<div className="absolute top-full left-1/2 z-10 mt-2 hidden -translate-x-1/2 rounded-md bg-white p-2 px-4 text-center text-sm whitespace-nowrap text-black shadow-lg group-hover:block">
{title}
</div>
)
if (isRelative) { if (isRelative) {
return ( return (
<div className="relative group"> <div className="group relative">
<Link href={url} className="flex items-center space-x-2"> <Link href={url}>{LinkContent}</Link>
{icon && <IconComponent />} {Popover}
</Link>
<div className="absolute left-1/2 -translate-x-1/2 z-10 hidden p-2 px-4 text-sm text-black bg-white rounded-md group-hover:block text-center">
{title}
</div>
</div> </div>
) )
} else { } else {
return ( return (
<div className="relative group"> <div className="group relative">
<a href={url} className="flex items-center space-x-2"> <a href={url}>{LinkContent}</a>
{icon && <IconComponent />} {Popover}
</a>
<div className="absolute left-1/2 -translate-x-1/2 z-10 hidden p-2 px-4 text-sm text-black bg-white rounded-md group-hover:block text-center">
{title}
</div>
</div> </div>
) )
} }
} else { } else {
const Content = (
<>
<span className="flex flex-row gap-2 transition-colors duration-300 group-hover:text-accent">
{icon && <IconComponent className="size-4" />} {title}
</span>
<span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</>
)
if (isRelative) { if (isRelative) {
return ( return (
<Link href={url} className="group leading-relaxed"> <Link href={url} className="group leading-relaxed">
<span className="flex flex-row gap-2">{icon && <IconComponent />} {title}</span> {Content}
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span>
</Link> </Link>
) )
} else { } else {
return ( return (
<a href={url} className="group leading-relaxed"> <a href={url} className="group leading-relaxed">
<span className="flex flex-row gap-2">{icon && <IconComponent />} {title}</span> {Content}
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span>
</a> </a>
) )
} }
} }
} }
export default HomePageLink
export default HomePageLink;
+7 -5
View File
@@ -1,13 +1,15 @@
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils'
import { CSSProperties } from 'react'
interface RuleProps { interface RuleProps {
className?: string; className?: string
style?: CSSProperties
} }
const HorizontalRule = ({ className }: RuleProps) => { const HorizontalRule = ({ className, style }: RuleProps) => {
return ( return (
<div className={cn("border-t-1 border-accent", className)} /> <div style={style} className={cn('border-t-1 border-accent', className)} />
) )
} }
export default HorizontalRule; export default HorizontalRule
+25 -16
View File
@@ -1,20 +1,29 @@
import Image from "next/image"; import Image from 'next/image'
import HorizontalRule from "./horizontal-rule"; import HorizontalRule from './horizontal-rule'
import { Code, ExternalLink } from "lucide-react"; import { Code, ExternalLink } from 'lucide-react'
import { Project } from "@/payload-types"; import { Project } from '@/payload-types'
const ProjectCard = (project: Project) => { const ProjectCard = (project: Project) => {
return ( return (
<div className="border border-white rounded-md p-4 flex flex-col gap-2 max-w-[700px] mx-auto"> <div className="glass glass-hover mx-auto flex max-w-[700px] flex-col gap-3 rounded-lg p-5">
<h2 className="text-2xl font-bold">{project.title}</h2> <h2 className="text-2xl font-bold">{project.title}</h2>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{project.featuredImage && typeof project.featuredImage === "object" && ( {project.featuredImage && typeof project.featuredImage === 'object' && (
<Image src={project.featuredImage.url!} alt={project.title} width={project.featuredImage.width!} height={project.featuredImage.height!} className="rounded-md border border-gray-600" /> <Image
src={project.featuredImage.url!}
alt={project.title}
width={project.featuredImage.width!}
height={project.featuredImage.height!}
className="rounded-md border border-white/10"
/>
)} )}
<p className="whitespace-pre-wrap">{project.description}</p> <p className="whitespace-pre-wrap">{project.description}</p>
<div className="flex flex-row gap-2 my-2"> <div className="my-2 flex flex-row flex-wrap gap-2">
{(project?.tags || []).map((tag) => ( {(project?.tags || []).map((tag) => (
<span key={tag.id} className="bg-white text-black rounded-md px-1 py-0.5 text-sm hover:bg-gray-200"> <span
key={tag.id}
className="rounded-md border border-white/10 bg-white/10 px-2 py-0.5 text-sm text-foreground transition-colors hover:bg-white/20"
>
{tag.tag} {tag.tag}
</span> </span>
))} ))}
@@ -24,20 +33,20 @@ const ProjectCard = (project: Project) => {
<div className="flex flex-row gap-4"> <div className="flex flex-row gap-4">
{project.viewLink && ( {project.viewLink && (
<a href={project.viewLink} target="_blank" className="group"> <a href={project.viewLink} target="_blank" className="group">
<span className="flex flex-row gap-2"> <span className="flex flex-row gap-2 transition-colors duration-300 group-hover:text-accent">
<ExternalLink /> <ExternalLink className="size-4" />
View Project View Project
</span> </span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span> <span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</a> </a>
)} )}
{project.repositoryLink && ( {project.repositoryLink && (
<a href={project.repositoryLink} target="_blank" className="group"> <a href={project.repositoryLink} target="_blank" className="group">
<span className="flex flex-row gap-2"> <span className="flex flex-row gap-2 transition-colors duration-300 group-hover:text-accent">
<Code /> <Code className="size-4" />
View Source View Source
</span> </span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span> <span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</a> </a>
)} )}
</div> </div>
@@ -45,4 +54,4 @@ const ProjectCard = (project: Project) => {
) )
} }
export default ProjectCard; export default ProjectCard
+24 -24
View File
@@ -1,54 +1,54 @@
import { lucideOptions } from "@/lib/lucid-options"; import { lucideOptions } from '@/lib/lucid-options'
import { revalidatePath } from "next/cache"; import { revalidatePath } from 'next/cache'
import { GlobalConfig } from "payload"; import { GlobalConfig } from 'payload'
export const Home: GlobalConfig = { export const Home: GlobalConfig = {
slug: "home", slug: 'home',
access: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
{ {
name: "mainLinks", name: 'mainLinks',
type: "array", type: 'array',
required: true, required: true,
fields: [ fields: [
{ {
name: "title", name: 'title',
type: "text", type: 'text',
required: true, required: true,
}, },
{ {
name: "url", name: 'url',
type: "text", type: 'text',
required: true, required: true,
}, },
{ {
name: "icon", name: 'icon',
type: "select", type: 'select',
options: lucideOptions, options: lucideOptions,
required: true, required: true,
}, },
], ],
}, },
{ {
name: "popoverLinks", name: 'popoverLinks',
type: "array", type: 'array',
required: true, required: true,
fields: [ fields: [
{ {
name: "title", name: 'title',
type: "text", type: 'text',
required: true, required: true,
}, },
{ {
name: "url", name: 'url',
type: "text", type: 'text',
required: true, required: true,
}, },
{ {
name: "icon", name: 'icon',
type: "select", type: 'select',
options: lucideOptions, options: lucideOptions,
required: true, required: true,
}, },
@@ -58,8 +58,8 @@ export const Home: GlobalConfig = {
hooks: { hooks: {
afterChange: [ afterChange: [
async () => { async () => {
revalidatePath("/"); revalidatePath('/')
} },
], ],
} },
} }
+12 -12
View File
@@ -1,15 +1,15 @@
import { Code2, Github, Linkedin, LucideIcon, Notebook } from "lucide-react" import { Code2, Github, Linkedin, LucideIcon, Notebook } from 'lucide-react'
export const lucideOptions: { label: string, value: string }[] = [ export const lucideOptions: { label: string; value: string }[] = [
{ label: "Code 2", value: "code-2" }, { label: 'Code 2', value: 'code-2' },
{ label: "Notebook", value: "notebook" }, { label: 'Notebook', value: 'notebook' },
{ label: "Github", value: "github" }, { label: 'Github', value: 'github' },
{ label: "Linkedin", value: "linkedin" }, { label: 'Linkedin', value: 'linkedin' },
]; ]
export const lucidIconMap: Record<string, LucideIcon> = { export const lucidIconMap: Record<string, LucideIcon> = {
"code-2": Code2, 'code-2': Code2,
"notebook": Notebook, notebook: Notebook,
"github": Github, github: Github,
"linkedin": Linkedin, linkedin: Linkedin,
}; }
+2 -2
View File
@@ -1,5 +1,5 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from 'clsx'
import { twMerge } from "tailwind-merge" import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
+19 -19
View File
@@ -1,15 +1,15 @@
import { postgresAdapter } from "@payloadcms/db-postgres" import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from "@payloadcms/richtext-lexical" import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from "path" import path from 'path'
import { buildConfig } from "payload" import { buildConfig } from 'payload'
import { s3Storage } from "@payloadcms/storage-s3" import { s3Storage } from '@payloadcms/storage-s3'
import sharp from "sharp" import sharp from 'sharp'
import { fileURLToPath } from "url" import { fileURLToPath } from 'url'
import { Media } from "./collections/Media" import { Media } from './collections/Media'
import { Projects } from "./collections/Projects" import { Projects } from './collections/Projects'
import { Users } from "./collections/Users" import { Users } from './collections/Users'
import { Home } from "./globals/home" import { Home } from './globals/home'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@@ -24,13 +24,13 @@ export default buildConfig({
collections: [Users, Media, Projects], collections: [Users, Media, Projects],
globals: [Home], globals: [Home],
editor: lexicalEditor(), editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || "", secret: process.env.PAYLOAD_SECRET || '',
typescript: { typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"), outputFile: path.resolve(dirname, 'payload-types.ts'),
}, },
db: postgresAdapter({ db: postgresAdapter({
pool: { pool: {
connectionString: process.env.DATABASE_URL || "", connectionString: process.env.DATABASE_URL || '',
}, },
}), }),
sharp, sharp,
@@ -39,16 +39,16 @@ export default buildConfig({
collections: { collections: {
media: true, media: true,
}, },
bucket: process.env.S3_BUCKET || "", bucket: process.env.S3_BUCKET || '',
config: { config: {
forcePathStyle: true, forcePathStyle: true,
region: process.env.S3_REGION || "", region: process.env.S3_REGION || '',
credentials: { credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID || "", accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "", secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
}, },
endpoint: process.env.S3_ENDPOINT || undefined, endpoint: process.env.S3_ENDPOINT || undefined,
} },
}), }),
], ],
}) })
+2 -2
View File
@@ -4,6 +4,6 @@ import config from '@payload-config'
export const getHome = async () => { export const getHome = async () => {
const payload = await getPayload({ config }) const payload = await getPayload({ config })
return await payload.findGlobal({ return await payload.findGlobal({
slug: "home" slug: 'home',
}) })
} }
+2 -2
View File
@@ -4,6 +4,6 @@ import config from '@payload-config'
export const getProjects = async () => { export const getProjects = async () => {
const payload = await getPayload({ config }) const payload = await getPayload({ config })
return await payload.find({ return await payload.find({
collection: "project" collection: 'project',
}) })
} }
+4 -14
View File
@@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -23,12 +19,8 @@
} }
], ],
"paths": { "paths": {
"@/*": [ "@/*": ["./src/*"],
"./src/*" "@payload-config": ["./src/payload.config.ts"]
],
"@payload-config": [
"./src/payload.config.ts"
]
} }
}, },
"include": [ "include": [
@@ -38,7 +30,5 @@
".next/types/**/*.ts", ".next/types/**/*.ts",
".next/dev/types/**/*.ts" ".next/dev/types/**/*.ts"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }