initial commit

This commit is contained in:
2025-08-29 12:03:04 +10:00
commit 672960840e
50 changed files with 10067 additions and 0 deletions

34
src/app/(app)/layout.tsx Normal file
View File

@@ -0,0 +1,34 @@
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"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Liam Pietralla",
description: "Enthusiastic Software Developer & DevOps Engineer",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

40
src/app/(app)/page.tsx Normal file
View File

@@ -0,0 +1,40 @@
export const dynamic = 'force-dynamic'
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";
const IndexPage = async () => {
const home = await getHome();
return (
<div className="flex flex-col gap-4 justify-center items-center h-screen">
<Image className="rounded-full" src="/images/liam_pietralla.jpg" width={200} height={200} alt="Liam Pietralla" />
<h1 className="text-5xl font-bold">Liam Pietralla</h1>
<h2 className="text-xl">Enthusiastic Software Developer & DevOps Engineer</h2>
<Rule className="w-[300px]" />
<div className="flex flex-row gap-4">
{home.mainLinks.map(link => (
<IndexLink key={link.id} {...link} />
))}
</div>
<div className="flex flex-row gap-4">
{home.popoverLinks.map(link => (
<IndexLink key={link.id} {...link} isPopover={true} />
))}
</div>
<Rule className="w-[300px]" />
<a href="mailto:me@liampietralla.com" className="group leading-relaxed">
<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-white"></span>
</a>
</div>
);
}
export default IndexPage;

View File

@@ -0,0 +1,35 @@
export const dynamic = 'force-dynamic'
import ProjectCard from "@/components/project-card";
import Rule from "@/components/horizontal-rule";
import { getProjects } from "@/services/projects-service";
import Image from "next/image";
import Link from "next/link";
import { Fragment } from "react";
const ProjectsPage = async () => {
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">
<Image src="/images/liam_pietralla.jpg" width={50} height={50} alt="Liam Pietralla" className="rounded-full" />
<Link href="/" className="group leading-relaxed font-semi-bold">
Liam Pietralla
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span>
</Link>
</div>
<h1 className="text-5xl font-bold">Projects</h1>
<h2>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">
{projects.docs.map((project, index) => (
<Fragment key={index}>
<ProjectCard {...project} />
{index < projects.docs.length - 1 && <Rule />}
</Fragment>
))}
</div>
</div>
)
}
export default ProjectsPage;

View File

@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, params, searchParams, importMap })
export default NotFound

View File

@@ -0,0 +1,24 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, params, searchParams, importMap })
export default Page

View File

@@ -0,0 +1,5 @@
export const importMap = {
}

View File

@@ -0,0 +1,43 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import {
REST_DELETE,
REST_GET,
REST_OPTIONS,
REST_PATCH,
REST_POST,
REST_PUT,
} from '@payloadcms/next/routes'
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 ?? [] })),
})
export function GET(req: Request, ctx: NextCtx) {
return REST_GET(config)(req, coerceCtx(ctx))
}
export function POST(req: Request, ctx: NextCtx) {
return REST_POST(config)(req, coerceCtx(ctx))
}
export function DELETE(req: Request, ctx: NextCtx) {
return REST_DELETE(config)(req, coerceCtx(ctx))
}
export function PATCH(req: Request, ctx: NextCtx) {
return REST_PATCH(config)(req, coerceCtx(ctx))
}
export function PUT(req: Request, ctx: NextCtx) {
return REST_PUT(config)(req, coerceCtx(ctx))
}
export function OPTIONS(req: Request, ctx: NextCtx) {
return REST_OPTIONS(config)(req, coerceCtx(ctx))
}

View File

@@ -0,0 +1,9 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export async function GET(req: Request): Promise<Response> {
return GRAPHQL_PLAYGROUND_GET(config)(req)
}

View File

@@ -0,0 +1,19 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
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 ?? [] })),
})
export function POST(req: Request) {
return GRAPHQL_POST(config)(req)
}
export function OPTIONS(req: Request, ctx: NextCtx) {
return REST_OPTIONS(config)(req, coerceCtx(ctx))
}

View File

View File

@@ -0,0 +1,31 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import '@payloadcms/next/css'
import type { ServerFunctionClient } from 'payload'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -0,0 +1,13 @@
export async function GET() {
const json = require("../../../../package.json");
const response = {
"status": "Healthy",
"version": json.version
}
return new Response(JSON.stringify(response), {
status: 200,
headers: {
"Content-Type": "application/json"
}
});
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

19
src/app/globals.css Normal file
View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

27
src/collections/Media.ts Normal file
View File

@@ -0,0 +1,27 @@
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
access: {
read: () => true,
},
fields: [
{
name: 'alt',
type: 'text',
required: true,
},
],
upload: {
staticDir: 'portfolio-data/media',
imageSizes: [
{
name: "thumbnail",
width: 150,
height: 150,
position: 'centre',
}
],
adminThumbnail: 'thumbnail',
}
}

View File

@@ -0,0 +1,48 @@
import { CollectionConfig } from "payload";
export const Projects: CollectionConfig = {
slug: "project",
access: {
read: () => true,
},
fields: [
{
name: "title",
type: "text",
required: true,
},
{
name: "description",
type: "textarea",
required: true,
},
{
name: "featuredImage",
type: "relationship",
relationTo: "media",
required: false,
},
{
name: "tags",
type: "array",
required: false,
fields: [
{
name: "tag",
type: "text",
required: true,
},
],
},
{
name: "viewLink",
type: "text",
required: false,
},
{
name: "repositoryLink",
type: "text",
required: false,
}
]
}

13
src/collections/Users.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
// Email added by default
// Add more fields as needed
],
}

View File

@@ -0,0 +1,60 @@
import Link from "next/link";
import { DynamicIcon, IconName } from 'lucide-react/dynamic';
interface HomePageLinkProps {
title: string;
url: string;
icon: string;
id?: string | null;
isPopover?: boolean;
}
const HomePageLink = ({ title, icon, url, isPopover }: HomePageLinkProps) => {
const isRelative = !url.startsWith("http");
const dynIcon = <DynamicIcon name={icon as IconName} />
if (isPopover) {
if (isRelative) {
return (
<div className="relative group">
<Link href={url} className="flex items-center space-x-2">
{dynIcon}
</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>
)
} else {
return (
<div className="relative group">
<a href={url} className="flex items-center space-x-2">
{dynIcon}
</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>
)
}
} else {
if (isRelative) {
return (
<Link href={url} className="group leading-relaxed">
<span className="flex flex-row gap-2">{dynIcon} {title}</span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span>
</Link>
)
} else {
return (
<a href={url} className="group leading-relaxed">
<span className="flex flex-row gap-2">{dynIcon} {title}</span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span>
</a>
)
}
}
}
export default HomePageLink;

View File

@@ -0,0 +1,13 @@
import { cn } from "@/lib/utils";
interface RuleProps {
className?: string;
}
const HorizontalRule = ({ className }: RuleProps) => {
return (
<div className={cn("border-t-1 border-accent", className)} />
)
}
export default HorizontalRule;

View File

@@ -0,0 +1,48 @@
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="border border-white rounded-md p-4 flex flex-col gap-2 max-w-[700px] mx-auto">
<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-gray-600" />
)}
<p className="whitespace-pre-wrap">{project.description}</p>
<div className="flex flex-row gap-2 my-2">
{(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">
{tag.tag}
</span>
))}
</div>
</div>
<HorizontalRule />
<div className="flex flex-row gap-4">
{project.viewLink && (
<a href={project.viewLink} target="_blank" className="group">
<span className="flex flex-row gap-2">
<ExternalLink />
View Project
</span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span>
</a>
)}
{project.repositoryLink && (
<a href={project.repositoryLink} target="_blank" className="group">
<span className="flex flex-row gap-2">
<Code />
View Source
</span>
<span className="block max-w-0 group-hover:max-w-full transition-all duration-500 h-0.5 bg-white"></span>
</a>
)}
</div>
</div>
)
}
export default ProjectCard;

57
src/globals/home.ts Normal file
View File

@@ -0,0 +1,57 @@
import { lucideOptions } from "@/lib/lucid-options";
import { GlobalConfig } from "payload";
export const Home: GlobalConfig = {
slug: "home",
access: {
read: () => true,
},
fields: [
{
name: "mainLinks",
type: "array",
required: true,
fields: [
{
name: "title",
type: "text",
required: true,
},
{
name: "url",
type: "text",
required: true,
},
{
name: "icon",
type: "select",
options: lucideOptions,
required: true,
},
],
},
{
name: "popoverLinks",
type: "array",
required: true,
fields: [
{
name: "title",
type: "text",
required: true,
},
{
name: "url",
type: "text",
required: true,
},
{
name: "icon",
type: "select",
options: lucideOptions,
required: true,
},
],
},
],
}

6
src/lib/lucid-options.ts Normal file
View File

@@ -0,0 +1,6 @@
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" },
]

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,193 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-sqlite'
export async function up({ db }: MigrateUpArgs): Promise<void> {
await db.run(sql`CREATE TABLE \`users_sessions\` (
\`_order\` integer NOT NULL,
\`_parent_id\` integer NOT NULL,
\`id\` text PRIMARY KEY NOT NULL,
\`created_at\` text,
\`expires_at\` text NOT NULL,
FOREIGN KEY (\`_parent_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`)
await db.run(sql`CREATE INDEX \`users_sessions_order_idx\` ON \`users_sessions\` (\`_order\`);`)
await db.run(sql`CREATE INDEX \`users_sessions_parent_id_idx\` ON \`users_sessions\` (\`_parent_id\`);`)
await db.run(sql`CREATE TABLE \`users\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
\`email\` text NOT NULL,
\`reset_password_token\` text,
\`reset_password_expiration\` text,
\`salt\` text,
\`hash\` text,
\`login_attempts\` numeric DEFAULT 0,
\`lock_until\` text
);
`)
await db.run(sql`CREATE INDEX \`users_updated_at_idx\` ON \`users\` (\`updated_at\`);`)
await db.run(sql`CREATE INDEX \`users_created_at_idx\` ON \`users\` (\`created_at\`);`)
await db.run(sql`CREATE UNIQUE INDEX \`users_email_idx\` ON \`users\` (\`email\`);`)
await db.run(sql`CREATE TABLE \`media\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`alt\` text NOT NULL,
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
\`url\` text,
\`thumbnail_u_r_l\` text,
\`filename\` text,
\`mime_type\` text,
\`filesize\` numeric,
\`width\` numeric,
\`height\` numeric,
\`focal_x\` numeric,
\`focal_y\` numeric,
\`sizes_thumbnail_url\` text,
\`sizes_thumbnail_width\` numeric,
\`sizes_thumbnail_height\` numeric,
\`sizes_thumbnail_mime_type\` text,
\`sizes_thumbnail_filesize\` numeric,
\`sizes_thumbnail_filename\` text
);
`)
await db.run(sql`CREATE INDEX \`media_updated_at_idx\` ON \`media\` (\`updated_at\`);`)
await db.run(sql`CREATE INDEX \`media_created_at_idx\` ON \`media\` (\`created_at\`);`)
await db.run(sql`CREATE UNIQUE INDEX \`media_filename_idx\` ON \`media\` (\`filename\`);`)
await db.run(sql`CREATE INDEX \`media_sizes_thumbnail_sizes_thumbnail_filename_idx\` ON \`media\` (\`sizes_thumbnail_filename\`);`)
await db.run(sql`CREATE TABLE \`project_tags\` (
\`_order\` integer NOT NULL,
\`_parent_id\` integer NOT NULL,
\`id\` text PRIMARY KEY NOT NULL,
\`tag\` text NOT NULL,
FOREIGN KEY (\`_parent_id\`) REFERENCES \`project\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`)
await db.run(sql`CREATE INDEX \`project_tags_order_idx\` ON \`project_tags\` (\`_order\`);`)
await db.run(sql`CREATE INDEX \`project_tags_parent_id_idx\` ON \`project_tags\` (\`_parent_id\`);`)
await db.run(sql`CREATE TABLE \`project\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`title\` text NOT NULL,
\`description\` text NOT NULL,
\`featured_image_id\` integer,
\`view_link\` text,
\`repository_link\` text,
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
FOREIGN KEY (\`featured_image_id\`) REFERENCES \`media\`(\`id\`) ON UPDATE no action ON DELETE set null
);
`)
await db.run(sql`CREATE INDEX \`project_featured_image_idx\` ON \`project\` (\`featured_image_id\`);`)
await db.run(sql`CREATE INDEX \`project_updated_at_idx\` ON \`project\` (\`updated_at\`);`)
await db.run(sql`CREATE INDEX \`project_created_at_idx\` ON \`project\` (\`created_at\`);`)
await db.run(sql`CREATE TABLE \`payload_locked_documents\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`global_slug\` text,
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL
);
`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_global_slug_idx\` ON \`payload_locked_documents\` (\`global_slug\`);`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_updated_at_idx\` ON \`payload_locked_documents\` (\`updated_at\`);`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_created_at_idx\` ON \`payload_locked_documents\` (\`created_at\`);`)
await db.run(sql`CREATE TABLE \`payload_locked_documents_rels\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`order\` integer,
\`parent_id\` integer NOT NULL,
\`path\` text NOT NULL,
\`users_id\` integer,
\`media_id\` integer,
\`project_id\` integer,
FOREIGN KEY (\`parent_id\`) REFERENCES \`payload_locked_documents\`(\`id\`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (\`users_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (\`media_id\`) REFERENCES \`media\`(\`id\`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_rels_order_idx\` ON \`payload_locked_documents_rels\` (\`order\`);`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_rels_parent_idx\` ON \`payload_locked_documents_rels\` (\`parent_id\`);`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_rels_path_idx\` ON \`payload_locked_documents_rels\` (\`path\`);`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_rels_users_id_idx\` ON \`payload_locked_documents_rels\` (\`users_id\`);`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_rels_media_id_idx\` ON \`payload_locked_documents_rels\` (\`media_id\`);`)
await db.run(sql`CREATE INDEX \`payload_locked_documents_rels_project_id_idx\` ON \`payload_locked_documents_rels\` (\`project_id\`);`)
await db.run(sql`CREATE TABLE \`payload_preferences\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`key\` text,
\`value\` text,
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL
);
`)
await db.run(sql`CREATE INDEX \`payload_preferences_key_idx\` ON \`payload_preferences\` (\`key\`);`)
await db.run(sql`CREATE INDEX \`payload_preferences_updated_at_idx\` ON \`payload_preferences\` (\`updated_at\`);`)
await db.run(sql`CREATE INDEX \`payload_preferences_created_at_idx\` ON \`payload_preferences\` (\`created_at\`);`)
await db.run(sql`CREATE TABLE \`payload_preferences_rels\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`order\` integer,
\`parent_id\` integer NOT NULL,
\`path\` text NOT NULL,
\`users_id\` integer,
FOREIGN KEY (\`parent_id\`) REFERENCES \`payload_preferences\`(\`id\`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (\`users_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`)
await db.run(sql`CREATE INDEX \`payload_preferences_rels_order_idx\` ON \`payload_preferences_rels\` (\`order\`);`)
await db.run(sql`CREATE INDEX \`payload_preferences_rels_parent_idx\` ON \`payload_preferences_rels\` (\`parent_id\`);`)
await db.run(sql`CREATE INDEX \`payload_preferences_rels_path_idx\` ON \`payload_preferences_rels\` (\`path\`);`)
await db.run(sql`CREATE INDEX \`payload_preferences_rels_users_id_idx\` ON \`payload_preferences_rels\` (\`users_id\`);`)
await db.run(sql`CREATE TABLE \`payload_migrations\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`name\` text,
\`batch\` numeric,
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL
);
`)
await db.run(sql`CREATE INDEX \`payload_migrations_updated_at_idx\` ON \`payload_migrations\` (\`updated_at\`);`)
await db.run(sql`CREATE INDEX \`payload_migrations_created_at_idx\` ON \`payload_migrations\` (\`created_at\`);`)
await db.run(sql`CREATE TABLE \`home_main_links\` (
\`_order\` integer NOT NULL,
\`_parent_id\` integer NOT NULL,
\`id\` text PRIMARY KEY NOT NULL,
\`title\` text NOT NULL,
\`url\` text NOT NULL,
\`icon\` text NOT NULL,
FOREIGN KEY (\`_parent_id\`) REFERENCES \`home\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`)
await db.run(sql`CREATE INDEX \`home_main_links_order_idx\` ON \`home_main_links\` (\`_order\`);`)
await db.run(sql`CREATE INDEX \`home_main_links_parent_id_idx\` ON \`home_main_links\` (\`_parent_id\`);`)
await db.run(sql`CREATE TABLE \`home_popover_links\` (
\`_order\` integer NOT NULL,
\`_parent_id\` integer NOT NULL,
\`id\` text PRIMARY KEY NOT NULL,
\`title\` text NOT NULL,
\`url\` text NOT NULL,
\`icon\` text NOT NULL,
FOREIGN KEY (\`_parent_id\`) REFERENCES \`home\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
`)
await db.run(sql`CREATE INDEX \`home_popover_links_order_idx\` ON \`home_popover_links\` (\`_order\`);`)
await db.run(sql`CREATE INDEX \`home_popover_links_parent_id_idx\` ON \`home_popover_links\` (\`_parent_id\`);`)
await db.run(sql`CREATE TABLE \`home\` (
\`id\` integer PRIMARY KEY NOT NULL,
\`updated_at\` text,
\`created_at\` text
);
`)
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
await db.run(sql`DROP TABLE \`users_sessions\`;`)
await db.run(sql`DROP TABLE \`users\`;`)
await db.run(sql`DROP TABLE \`media\`;`)
await db.run(sql`DROP TABLE \`project_tags\`;`)
await db.run(sql`DROP TABLE \`project\`;`)
await db.run(sql`DROP TABLE \`payload_locked_documents\`;`)
await db.run(sql`DROP TABLE \`payload_locked_documents_rels\`;`)
await db.run(sql`DROP TABLE \`payload_preferences\`;`)
await db.run(sql`DROP TABLE \`payload_preferences_rels\`;`)
await db.run(sql`DROP TABLE \`payload_migrations\`;`)
await db.run(sql`DROP TABLE \`home_main_links\`;`)
await db.run(sql`DROP TABLE \`home_popover_links\`;`)
await db.run(sql`DROP TABLE \`home\`;`)
}

9
src/migrations/index.ts Normal file
View File

@@ -0,0 +1,9 @@
import * as migration_20250828_224637 from './20250828_224637';
export const migrations = [
{
up: migration_20250828_224637.up,
down: migration_20250828_224637.down,
name: '20250828_224637'
},
];

418
src/payload-types.ts Normal file
View File

@@ -0,0 +1,418 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Brisbane'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
};
blocks: {};
collections: {
users: User;
media: Media;
project: Project;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
project: ProjectSelect<false> | ProjectSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
};
globals: {
home: Home;
};
globalsSelect: {
home: HomeSelect<false> | HomeSelect<true>;
};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: number;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
| {
id: string;
createdAt?: string | null;
expiresAt: string;
}[]
| null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media".
*/
export interface Media {
id: number;
alt: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
sizes?: {
thumbnail?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "project".
*/
export interface Project {
id: number;
title: string;
description: string;
featuredImage?: (number | null) | Media;
tags?:
| {
tag: string;
id?: string | null;
}[]
| null;
viewLink?: string | null;
repositoryLink?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
document?:
| ({
relationTo: 'users';
value: number | User;
} | null)
| ({
relationTo: 'media';
value: number | Media;
} | null)
| ({
relationTo: 'project';
value: number | Project;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
user: {
relationTo: 'users';
value: number | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
| T
| {
id?: T;
createdAt?: T;
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media_select".
*/
export interface MediaSelect<T extends boolean = true> {
alt?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
sizes?:
| T
| {
thumbnail?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "project_select".
*/
export interface ProjectSelect<T extends boolean = true> {
title?: T;
description?: T;
featuredImage?: T;
tags?:
| T
| {
tag?: T;
id?: T;
};
viewLink?: T;
repositoryLink?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "home".
*/
export interface Home {
id: number;
mainLinks: {
title: string;
url: string;
icon: 'code-2' | 'notebook' | 'github' | 'linkedin';
id?: string | null;
}[];
popoverLinks: {
title: string;
url: string;
icon: 'code-2' | 'notebook' | 'github' | 'linkedin';
id?: string | null;
}[];
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "home_select".
*/
export interface HomeSelect<T extends boolean = true> {
mainLinks?:
| T
| {
title?: T;
url?: T;
icon?: T;
id?: T;
};
popoverLinks?:
| T
| {
title?: T;
url?: T;
icon?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}

39
src/payload.config.ts Normal file
View File

@@ -0,0 +1,39 @@
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'
import sharp from 'sharp'
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { Users } from './collections/Users'
import { Media } from './collections/Media'
import { Projects } from "./collections/Projects"
import { Home } from './globals/home'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: Users.slug,
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [Users, Media, Projects],
globals: [Home],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
/**
* Both our media and db will reside in the 'portfolio-data' directory
* We can use a docker volume to persist this data
*/
db: sqliteAdapter({
client: { url: "file:./portfolio-data/data.db" }
}),
sharp,
plugins: [],
})

View File

@@ -0,0 +1,9 @@
import { getPayload } from 'payload'
import config from '@payload-config'
export const getHome = async () => {
const payload = await getPayload({ config })
return await payload.findGlobal({
slug: "home"
})
}

View File

@@ -0,0 +1,9 @@
import { getPayload } from 'payload'
import config from '@payload-config'
export const getProjects = async () => {
const payload = await getPayload({ config })
return await payload.find({
collection: "project"
})
}