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

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
portfolio-data

1
.env.template Normal file
View File

@@ -0,0 +1 @@
PAYLOAD_SECRET=123

44
.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
!.env.template
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
portfolio-data/

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM node:24-alpine AS base
ENV NODE_ENV=production
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* .npmrc* ./
RUN corepack enable pnpm && pnpm i --frozen-lockfile;
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build
FROM base AS runner
WORKDIR /app
RUN corepack enable pnpm
RUN mkdir -p /app/portfolio-data && chown node:node /app/portfolio-data
COPY --chown=node --from=deps /app/node_modules ./node_modules
COPY --chown=node --from=builder /app/public ./public
COPY --chown=node --from=builder /app/next.config.ts ./next.config.ts
COPY --chown=node --from=builder /app/.next ./.next
COPY --chown=node --from=builder /app/package.json ./package.json
COPY --chown=node --from=builder /app/tsconfig.json ./tsconfig.json
COPY --chown=node --from=builder /app/src ./src
USER node
EXPOSE 3000
CMD ["pnpm", "start"]

49
README.md Normal file
View File

@@ -0,0 +1,49 @@
# 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.
Next is currently using a sqlite database and local file storage. Both are output to a `portfolio-data` directory.
## Development
To develop the application use pnpm to install the dependencies:
```bash
pnpm install
```
Once downloaded you can use pnpm to run the project:
```bash
pnpm dev
```
### Payload CMS
Payload will require rebuilding on the types file once any changes are made:
```bash
pnpm run payload:generate:types
```
In local mode payload will apply any changes to the config to the database automatically. To generate a migration once changes are made, you can use the following command:
```bash
pnpm run payload:migrate:create
```
## Deploying
Deploying the portfolio is done as a docker container. It can be built with the following command:
```bash
docker build -t liam-portfolio .
```
### Running the Container
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
```

25
eslint.config.mjs Normal file
View File

@@ -0,0 +1,25 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
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"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
];
export default eslintConfig;

6
next.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import { withPayload } from "@payloadcms/next/withPayload";
import type { NextConfig } from "next";
const nextConfig: NextConfig = { };
export default withPayload(nextConfig);

40
package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "liam-portfolio",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "payload migrate && next start",
"lint": "eslint",
"payload:generate-types": "payload generate:types",
"payload:migrate:create": "payload migrate:create"
},
"dependencies": {
"@payloadcms/db-sqlite": "^3.53.0",
"@payloadcms/next": "^3.53.0",
"@payloadcms/richtext-lexical": "^3.53.0",
"clsx": "^2.1.1",
"graphql": "^16.11.0",
"lucide-react": "^0.541.0",
"next": "15.5.0",
"payload": "^3.53.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"sharp": "^0.34.3",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.5.0",
"tailwindcss": "^4",
"typescript": "^5"
},
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b"
}

7373
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

5
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,5 @@
onlyBuiltDependencies:
- '@tailwindcss/oxide'
- esbuild
- sharp
- unrs-resolver

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

1
public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

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"
})
}

43
tsconfig.json Normal file
View File

@@ -0,0 +1,43 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
],
"@payload-config": [
"./src/payload.config.ts"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}