format fix
Build and Push Container / Build App (pull_request) Successful in 1m54s
Build and Push Container / Build App (push) Successful in 2m0s
Build and Push Container / Publish App (pull_request) Skipped
Build and Push Container / Publish App (push) Skipped

This commit is contained in:
LiamPietralla committed 2026-10-15 17:28:40 +11:00
1 parent f061457d7f
commit d34a8474fc
35 files changed
+466 -294

No files matched your search

+3 -3
View File
@@ -14,7 +14,7 @@ jobs:
name: Build App
runs-on: ubuntu-latest
steps:
steps:
- name: Checkout Repo
uses: actions/checkout@v4
@@ -96,6 +96,6 @@ jobs:
run: |
export DEPLOYMENT_VERSION="${GITHUB_SHA:0:7}-$(date +%s)"
nomad job run -var="deployment_version=$DEPLOYMENT_VERSION" infra/nomad/portfolio.nomad.hcl
env:
env:
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
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup PNPM and Node
uses: pnpm/setup@v3
with:
runtime: node@24
- name: Setup PNPM and Node
uses: pnpm/setup@v3
with:
runtime: node@24
- name: Generate SBOM
run: pnpm sbom --sbom-format cyclonedx --sbom-type application --out sbom.cyclonedx.json
- name: Generate SBOM
run: pnpm sbom --sbom-format cyclonedx --sbom-type application --out sbom.cyclonedx.json
- name: Upload SBOM
uses: DependencyTrack/gh-upload-sbom@v4
with:
protocol: ${{ vars.DT_API_PROTOCOL }}
server-hostname: ${{ vars.DT_API_HOSTNAME }}
port: ${{ vars.DT_API_PORT }}
api-key: ${{ secrets.DT_API_KEY }}
project-name: 'Liam Portfolio'
project-version: 'main'
auto-create: true
bom-filename: 'sbom.cyclonedx.json'
- name: Upload SBOM
uses: DependencyTrack/gh-upload-sbom@v4
with:
protocol: ${{ vars.DT_API_PROTOCOL }}
server-hostname: ${{ vars.DT_API_HOSTNAME }}
port: ${{ vars.DT_API_PORT }}
api-key: ${{ secrets.DT_API_KEY }}
project-name: 'Liam Portfolio'
project-version: 'main'
auto-create: true
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
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
@@ -18,7 +18,7 @@ pnpm dev
### 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
pnpm run payload:generate:types
@@ -46,4 +46,4 @@ Once the container is built, you can run it with the following command:
```bash
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:
db:
image: postgres:18
restart: unless-stopped
@@ -8,7 +7,7 @@ services:
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=portfolio-dev
ports:
- "5432:5432"
- '5432:5432'
volumes:
- portfolio_db_data:/var/lib/postgresql
@@ -16,7 +15,7 @@ services:
image: ghcr.io/achtungsoftware/alarik:latest
restart: unless-stopped
ports:
- "9000:8080"
- '9000:8080'
environment:
- API_BASE_URL=http://localhost:9000
- CONSOLE_BASE_URL=http://localhost:9001
@@ -33,7 +32,7 @@ services:
image: ghcr.io/achtungsoftware/alarik-console:latest
restart: unless-stopped
ports:
- "9001:3000"
- '9001:3000'
environment:
- NUXT_PUBLIC_API_BASE_URL=http://localhost:9000
- NUXT_PUBLIC_CONSOLE_BASE_URL=http://localhost:9001
@@ -55,8 +54,8 @@ services:
mc mb alarik/payload-media &&
echo 'S3 bucket created';
fi
"
"
volumes:
portfolio_s3_data:
portfolio_db_data:
portfolio_db_data:
+14 -14
View File
@@ -1,25 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
});
})
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
'node_modules/**',
'.next/**',
'out/**',
'build/**',
'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 type { NextConfig } from "next";
import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'
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",
"start": "payload migrate && next start",
"lint": "eslint",
"format": "prettier --write .",
"payload:generate-types": "payload generate:types",
"payload:migrate:create": "payload migrate:create"
},
@@ -35,6 +36,8 @@
"@types/react-dom": "19.3.0",
"eslint": "^9.39.5",
"eslint-config-next": "16.1.2",
"prettier": "^3.9.10",
"prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^4.3.3",
"typescript": "^5.9.3"
},
+72
View File
@@ -230,6 +230,12 @@ importers:
eslint-config-next:
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)
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:
specifier: ^4.3.3
version: 4.3.3
@@ -3905,11 +3911,71 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
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:
resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==}
engines: {node: '>=14'}
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:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
@@ -8915,8 +8981,14 @@ snapshots:
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.9.10: {}
process-warning@5.0.0: {}
prompts@2.4.2:
+3 -3
View File
@@ -1,5 +1,5 @@
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
+15 -15
View File
@@ -1,35 +1,35 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "../globals.css";
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import '../globals.css'
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
variable: '--font-geist-sans',
subsets: ['latin'],
})
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
variable: '--font-geist-mono',
subsets: ['latin'],
})
export const metadata: Metadata = {
title: "Liam Pietralla",
description: "Enthusiastic Software Developer & DevOps Engineer",
};
title: 'Liam Pietralla',
description: 'Enthusiastic Software Developer & DevOps Engineer',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased px-4`}
className={`${geistSans.variable} ${geistMono.variable} px-4 antialiased`}
>
<div className="aurora" />
{children}
</body>
</html>
);
)
}
+52 -25
View File
@@ -1,51 +1,78 @@
import IndexLink from "@/components/home-page-link";
import Rule from "@/components/horizontal-rule";
import { getHome } from "@/services/home-service";
import { Mail } from "lucide-react";
import Image from "next/image";
import IndexLink from '@/components/home-page-link'
import Rule from '@/components/horizontal-rule'
import { getHome } from '@/services/home-service'
import { Mail } from 'lucide-react'
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 home = await getHome();
const home = await getHome()
return (
<div className="flex flex-col gap-5 justify-center items-center h-screen">
<div className="glow-ring animate-fade-in" style={{ animationDelay: "0ms" }}>
<Image className="rounded-full max-w-[200px]" src={ProfileImage} alt="Liam Pietralla" />
<div className="flex h-screen flex-col items-center justify-center gap-5">
<div
className="glow-ring animate-fade-in"
style={{ animationDelay: '0ms' }}
>
<Image
className="max-w-[200px] rounded-full"
src={ProfileImage}
alt="Liam Pietralla"
/>
</div>
<h1 className="text-5xl font-bold animate-fade-in-up" style={{ animationDelay: "120ms" }}>
<h1
className="animate-fade-in-up text-5xl font-bold"
style={{ animationDelay: '120ms' }}
>
Liam Pietralla
</h1>
<div className="flex flex-col md:flex-row gap-0 md:gap-[7px] animate-fade-in-up" style={{ animationDelay: "240ms" }}>
<h2 className="text-xl text-center">Enthusiastic Software Developer</h2>
<h2 className="text-xl text-center mt-[-5px] md:mt-0">& DevOps Engineer</h2>
<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="w-[300px] animate-fade-in" style={{ animationDelay: "320ms" }} />
<div className="flex flex-row gap-4 animate-fade-in-up" style={{ animationDelay: "400ms" }}>
{home.mainLinks.map(link => (
<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} />
))}
</div>
<div className="flex flex-row gap-4 animate-fade-in-up relative z-20" style={{ animationDelay: "480ms" }}>
{home.popoverLinks.map(link => (
<div
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} />
))}
</div>
<Rule className="w-[300px] animate-fade-in" style={{ animationDelay: "560ms" }} />
<Rule
className="animate-fade-in w-[300px]"
style={{ animationDelay: '560ms' }}
/>
<a
href="mailto:me@liampietralla.com"
className="group leading-relaxed animate-fade-in-up"
style={{ animationDelay: "640ms" }}
className="group animate-fade-in-up leading-relaxed"
style={{ animationDelay: '640ms' }}
>
<span className="flex flex-row gap-2">
<Mail />
me@liampietralla.com
</span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-accent"></span>
<span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</a>
</div>
);
)
}
export default IndexPage;
export default IndexPage
+34 -17
View File
@@ -1,28 +1,45 @@
import ProjectCard from "@/components/project-card";
import { getProjects } from "@/services/projects-service";
import Image from "next/image";
import Link from "next/link";
import ProjectCard from '@/components/project-card'
import { getProjects } from '@/services/projects-service'
import Image from 'next/image'
import Link from 'next/link'
import ProfileImage from "../../../../public/images/liam_pietralla.jpg";
import ProfileImage from '../../../../public/images/liam_pietralla.jpg'
const ProjectsPage = async () => {
const projects = await getProjects();
const projects = await getProjects()
return (
<div className="flex flex-col gap-4 justify-center items-center my-15">
<div className="flex flex-row items-center gap-2 my-2 animate-fade-in-up">
<Image src={ProfileImage} alt="Liam Pietralla" className="rounded-full max-w-[50px]" />
<Link href="/" className="group leading-relaxed font-semi-bold">
<div className="my-15 flex flex-col items-center justify-center gap-4">
<div className="animate-fade-in-up my-2 flex flex-row items-center gap-2">
<Image
src={ProfileImage}
alt="Liam Pietralla"
className="max-w-[50px] rounded-full"
/>
<Link href="/" className="group font-semi-bold leading-relaxed">
Liam Pietralla
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-accent"></span>
<span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</Link>
</div>
<h1 className="text-5xl font-bold animate-fade-in-up" style={{ animationDelay: "120ms" }}>Projects</h1>
<h2 className="animate-fade-in-up text-center max-w-2xl" style={{ animationDelay: "240ms" }}>
A collection of interesting projects that I am working on currently or have worked on in the past.
<h1
className="animate-fade-in-up text-5xl font-bold"
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 flex-col gap-4 w-full">
<div className="flex w-full flex-col gap-4">
{projects.docs.map((project, index) => (
<div key={index} className="animate-fade-in-up" style={{ animationDelay: `${320 + index * 120}ms` }}>
<div
key={index}
className="animate-fade-in-up"
style={{ animationDelay: `${320 + index * 120}ms` }}
>
<ProjectCard {...project} />
</div>
))}
@@ -31,4 +48,4 @@ const ProjectsPage = async () => {
)
}
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 })
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 })
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[] }> }
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) {
@@ -40,4 +40,4 @@ export function PUT(req: Request, ctx: NextCtx) {
export function OPTIONS(req: Request, ctx: NextCtx) {
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> {
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[] }> }
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) {
@@ -16,4 +16,4 @@ export function POST(req: Request) {
export function OPTIONS(req: Request, ctx: NextCtx) {
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) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
<RootLayout
config={config}
importMap={importMap}
serverFunction={serverFunction}
>
{children}
</RootLayout>
)
+8 -8
View File
@@ -1,14 +1,14 @@
import pkg from "../../../../package.json";
import pkg from '../../../../package.json'
export async function GET() {
const response = {
"status": "Healthy",
"version": pkg.version
};
status: 'Healthy',
version: pkg.version,
}
return new Response(JSON.stringify(response), {
status: 200,
headers: {
"Content-Type": "application/json"
}
});
}
'Content-Type': 'application/json',
},
})
}
+40 -14
View File
@@ -1,4 +1,4 @@
@import "tailwindcss";
@import 'tailwindcss';
:root {
--background: #0a0a0a;
@@ -31,7 +31,7 @@ body {
.aurora::before,
.aurora::after {
content: "";
content: '';
position: absolute;
width: 60vw;
height: 60vw;
@@ -56,18 +56,32 @@ body {
}
@keyframes drift-1 {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(15vw, 10vh) scale(1.15); }
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); }
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); }
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
@@ -76,8 +90,12 @@ body {
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-fade-in {
@@ -90,7 +108,7 @@ body {
}
.glow-ring::before {
content: "";
content: '';
position: absolute;
inset: -6px;
border-radius: 9999px;
@@ -102,8 +120,12 @@ body {
}
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.glass {
@@ -111,7 +133,11 @@ body {
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;
transition:
background 0.3s ease,
border-color 0.3s ease,
transform 0.3s ease,
box-shadow 0.3s ease;
}
.glass-hover:hover {
+9 -9
View File
@@ -1,26 +1,26 @@
import type { CollectionConfig } from "payload"
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: "media",
slug: 'media',
access: {
read: () => true,
},
fields: [
{
name: "alt",
type: "text",
name: 'alt',
type: 'text',
required: true,
},
],
upload: {
imageSizes: [
{
name: "thumbnail",
name: 'thumbnail',
width: 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 { CollectionConfig } from "payload";
import { revalidatePath } from 'next/cache'
import { CollectionConfig } from 'payload'
export const Projects: CollectionConfig = {
slug: "project",
slug: 'project',
access: {
read: () => true,
},
fields: [
{
name: "title",
type: "text",
name: 'title',
type: 'text',
required: true,
},
{
name: "description",
type: "textarea",
name: 'description',
type: 'textarea',
required: true,
},
{
name: "featuredImage",
type: "relationship",
relationTo: "media",
name: 'featuredImage',
type: 'relationship',
relationTo: 'media',
required: false,
},
{
name: "tags",
type: "array",
name: 'tags',
type: 'array',
required: false,
fields: [
{
name: "tag",
type: "text",
name: 'tag',
type: 'text',
required: true,
},
],
},
{
name: "viewLink",
type: "text",
name: 'viewLink',
type: 'text',
required: false,
},
{
name: "repositoryLink",
type: "text",
name: 'repositoryLink',
type: 'text',
required: false,
}
},
],
hooks: {
afterChange: [
async () => {
revalidatePath("/projects");
}
revalidatePath('/projects')
},
],
afterDelete: [
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 = {
slug: "users",
slug: 'users',
admin: {
useAsTitle: "email",
useAsTitle: 'email',
},
auth: true,
fields: [
+20 -21
View File
@@ -1,42 +1,42 @@
import Link from "next/link";
import { DynamicIcon, IconName } from "lucide-react/dynamic";
import { lucidIconMap } from "@/lib/lucid-options";
import Link from 'next/link'
import { DynamicIcon, IconName } from 'lucide-react/dynamic'
import { lucidIconMap } from '@/lib/lucid-options'
interface HomePageLinkProps {
title: string;
url: string;
icon: string;
id?: string | null;
isPopover?: boolean;
title: string
url: string
icon: string
id?: string | null
isPopover?: boolean
}
const HomePageLink = ({ title, icon, url, isPopover }: HomePageLinkProps) => {
const isRelative = !url.startsWith("http");
const IconComponent = lucidIconMap[icon as IconName];
const isRelative = !url.startsWith('http')
const IconComponent = lucidIconMap[icon as IconName]
if (isPopover) {
const LinkContent = (
<span className="flex items-center space-x-2 glass glass-hover rounded-lg px-3 py-2 transition-all duration-300">
<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 left-1/2 -translate-x-1/2 top-full mt-2 z-10 hidden p-2 px-4 text-sm text-black bg-white rounded-md shadow-lg group-hover:block text-center whitespace-nowrap">
<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) {
return (
<div className="relative group">
<div className="group relative">
<Link href={url}>{LinkContent}</Link>
{Popover}
</div>
)
} else {
return (
<div className="relative group">
<div className="group relative">
<a href={url}>{LinkContent}</a>
{Popover}
</div>
@@ -45,12 +45,12 @@ const HomePageLink = ({ title, icon, url, isPopover }: HomePageLinkProps) => {
} else {
const Content = (
<>
<span className="flex flex-row gap-2 group-hover:text-accent transition-colors duration-300">
<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 max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-accent"></span>
<span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</>
);
)
if (isRelative) {
return (
@@ -68,5 +68,4 @@ const HomePageLink = ({ title, icon, url, isPopover }: HomePageLinkProps) => {
}
}
export default HomePageLink;
export default HomePageLink
+6 -6
View File
@@ -1,15 +1,15 @@
import { cn } from "@/lib/utils";
import { CSSProperties } from "react";
import { cn } from '@/lib/utils'
import { CSSProperties } from 'react'
interface RuleProps {
className?: string;
style?: CSSProperties;
className?: string
style?: CSSProperties
}
const HorizontalRule = ({ className, style }: RuleProps) => {
return (
<div style={style} 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
+23 -14
View File
@@ -1,20 +1,29 @@
import Image from "next/image";
import HorizontalRule from "./horizontal-rule";
import { Code, ExternalLink } from "lucide-react";
import { Project } from "@/payload-types";
import Image from 'next/image'
import HorizontalRule from './horizontal-rule'
import { Code, ExternalLink } from 'lucide-react'
import { Project } from '@/payload-types'
const ProjectCard = (project: Project) => {
return (
<div className="glass glass-hover rounded-lg p-5 flex flex-col gap-3 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>
<div className="flex flex-col gap-2">
{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-white/10" />
{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-white/10"
/>
)}
<p className="whitespace-pre-wrap">{project.description}</p>
<div className="flex flex-row flex-wrap gap-2 my-2">
<div className="my-2 flex flex-row flex-wrap gap-2">
{(project?.tags || []).map((tag) => (
<span key={tag.id} className="bg-white/10 text-foreground rounded-md px-2 py-0.5 text-sm border border-white/10 transition-colors hover:bg-white/20">
<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}
</span>
))}
@@ -24,20 +33,20 @@ const ProjectCard = (project: Project) => {
<div className="flex flex-row gap-4">
{project.viewLink && (
<a href={project.viewLink} target="_blank" className="group">
<span className="flex flex-row gap-2 group-hover:text-accent transition-colors duration-300">
<span className="flex flex-row gap-2 transition-colors duration-300 group-hover:text-accent">
<ExternalLink className="size-4" />
View Project
</span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-accent"></span>
<span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</a>
)}
{project.repositoryLink && (
<a href={project.repositoryLink} target="_blank" className="group">
<span className="flex flex-row gap-2 group-hover:text-accent transition-colors duration-300">
<span className="flex flex-row gap-2 transition-colors duration-300 group-hover:text-accent">
<Code className="size-4" />
View Source
</span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-accent"></span>
<span className="block h-0.5 max-w-0 bg-accent transition-all duration-500 group-hover:max-w-full"></span>
</a>
)}
</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 { revalidatePath } from "next/cache";
import { GlobalConfig } from "payload";
import { lucideOptions } from '@/lib/lucid-options'
import { revalidatePath } from 'next/cache'
import { GlobalConfig } from 'payload'
export const Home: GlobalConfig = {
slug: "home",
slug: 'home',
access: {
read: () => true,
},
fields: [
{
name: "mainLinks",
type: "array",
name: 'mainLinks',
type: 'array',
required: true,
fields: [
{
name: "title",
type: "text",
name: 'title',
type: 'text',
required: true,
},
{
name: "url",
type: "text",
name: 'url',
type: 'text',
required: true,
},
{
name: "icon",
type: "select",
name: 'icon',
type: 'select',
options: lucideOptions,
required: true,
},
],
},
{
name: "popoverLinks",
type: "array",
name: 'popoverLinks',
type: 'array',
required: true,
fields: [
{
name: "title",
type: "text",
name: 'title',
type: 'text',
required: true,
},
{
name: "url",
type: "text",
name: 'url',
type: 'text',
required: true,
},
{
name: "icon",
type: "select",
name: 'icon',
type: 'select',
options: lucideOptions,
required: true,
},
@@ -58,8 +58,8 @@ export const Home: GlobalConfig = {
hooks: {
afterChange: [
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 }[] = [
{ label: "Code 2", value: "code-2" },
{ label: "Notebook", value: "notebook" },
{ label: "Github", value: "github" },
{ label: "Linkedin", value: "linkedin" },
];
export const lucideOptions: { label: string; value: string }[] = [
{ label: 'Code 2', value: 'code-2' },
{ label: 'Notebook', value: 'notebook' },
{ label: 'Github', value: 'github' },
{ label: 'Linkedin', value: 'linkedin' },
]
export const lucidIconMap: Record<string, LucideIcon> = {
"code-2": Code2,
"notebook": Notebook,
"github": Github,
"linkedin": Linkedin,
};
'code-2': Code2,
notebook: Notebook,
github: Github,
linkedin: Linkedin,
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
+19 -19
View File
@@ -1,15 +1,15 @@
import { postgresAdapter } from "@payloadcms/db-postgres"
import { lexicalEditor } from "@payloadcms/richtext-lexical"
import path from "path"
import { buildConfig } from "payload"
import { s3Storage } from "@payloadcms/storage-s3"
import sharp from "sharp"
import { fileURLToPath } from "url"
import { postgresAdapter } from '@payloadcms/db-postgres'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
import { s3Storage } from '@payloadcms/storage-s3'
import sharp from 'sharp'
import { fileURLToPath } from 'url'
import { Media } from "./collections/Media"
import { Projects } from "./collections/Projects"
import { Users } from "./collections/Users"
import { Home } from "./globals/home"
import { Media } from './collections/Media'
import { Projects } from './collections/Projects'
import { Users } from './collections/Users'
import { Home } from './globals/home'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -24,13 +24,13 @@ export default buildConfig({
collections: [Users, Media, Projects],
globals: [Home],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || "",
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"),
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URL || "",
connectionString: process.env.DATABASE_URL || '',
},
}),
sharp,
@@ -39,16 +39,16 @@ export default buildConfig({
collections: {
media: true,
},
bucket: process.env.S3_BUCKET || "",
bucket: process.env.S3_BUCKET || '',
config: {
forcePathStyle: true,
region: process.env.S3_REGION || "",
region: process.env.S3_REGION || '',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID || "",
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "",
accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
},
endpoint: process.env.S3_ENDPOINT || undefined,
}
},
}),
],
})
+2 -2
View File
@@ -4,6 +4,6 @@ import config from '@payload-config'
export const getHome = async () => {
const payload = await getPayload({ config })
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 () => {
const payload = await getPayload({ config })
return await payload.find({
collection: "project"
collection: 'project',
})
}
}
+4 -14
View File
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,12 +19,8 @@
}
],
"paths": {
"@/*": [
"./src/*"
],
"@payload-config": [
"./src/payload.config.ts"
]
"@/*": ["./src/*"],
"@payload-config": ["./src/payload.config.ts"]
}
},
"include": [
@@ -38,7 +30,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}