Feat/ui update #5
No files matched your search
+19
-19
@@ -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'
|
||||||
@@ -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
-4
@@ -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
|
||||||
|
|||||||
+14
-14
@@ -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
@@ -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)
|
||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+72
@@ -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
@@ -1,5 +1,5 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: ["@tailwindcss/postcss"],
|
plugins: ['@tailwindcss/postcss'],
|
||||||
};
|
}
|
||||||
|
|
||||||
export default config;
|
export default config
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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',
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
+145
-2
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
+25
-25
@@ -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')
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+23
-23
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -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',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user