mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 20:26:28 -06:00
refactor: large refactor around monorepo
Just a commit point while I'm testing stuff. Already decided at this point to simplify and revert away from PayloadCMS.
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// Fontsource imports
|
||||
import "@fontsource-variable/inter";
|
||||
import "@fontsource-variable/roboto";
|
||||
import "@fontsource-variable/roboto-mono";
|
||||
import "@fontsource/hanken-grotesk/900.css";
|
||||
import "@fontsource/schibsted-grotesk/400.css";
|
||||
import "@fontsource/schibsted-grotesk/500.css";
|
||||
import "@fontsource/schibsted-grotesk/600.css";
|
||||
|
||||
import "@radix-ui/themes/styles.css";
|
||||
import "@/styles/globals.css";
|
||||
import type { Metadata } from "next";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Xevion.dev",
|
||||
description:
|
||||
"The personal website of Xevion, a full-stack software developer.",
|
||||
applicationName: "xevion.dev",
|
||||
};
|
||||
|
||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Providers>
|
||||
<main>{children}</main>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import AppWrapper from "@/components/AppWrapper";
|
||||
import { Flex, Button, Text, Container, Box } from "@radix-ui/themes";
|
||||
import Link from "next/link";
|
||||
import { SiGithub, IconType } from "@icons-pack/react-simple-icons";
|
||||
import { SiLinkedin } from "react-icons/si";
|
||||
import { Rss } from "lucide-react";
|
||||
|
||||
function NavLink({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<Text size="3" className="text-(--gray-11) hover:text-(--gray-12)">
|
||||
{children}
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function IconLink({
|
||||
href,
|
||||
icon: Icon,
|
||||
}: {
|
||||
href: string;
|
||||
icon: React.ElementType;
|
||||
}) {
|
||||
return (
|
||||
<Link href={href} className="text-(--gray-11) hover:text-(--gray-12)">
|
||||
<Icon className="size-5" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function SocialLink({
|
||||
href,
|
||||
icon: IconComponent,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
icon: React.ElementType;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<Flex
|
||||
align="center"
|
||||
className="gap-x-1.5 px-1.5 py-1 rounded-xs bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
<IconComponent className="size-4 text-zinc-300" />
|
||||
<Text size="2" className="text-zinc-100">
|
||||
{children}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
return (
|
||||
<AppWrapper
|
||||
className="overflow-x-hidden font-schibsted"
|
||||
dotsClassName="animate-bg"
|
||||
>
|
||||
{/* Top Navigation Bar */}
|
||||
<Flex justify="end" align="center" width="100%" pt="5" px="6" pb="9">
|
||||
<Flex gap="4" align="center">
|
||||
<NavLink href="/projects">Projects</NavLink>
|
||||
<NavLink href="/blog">Blog</NavLink>
|
||||
<IconLink href="https://github.com/Xevion" icon={SiGithub} />
|
||||
<IconLink href="/rss" icon={Rss} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* Main Content */}
|
||||
<Flex align="center" direction="column">
|
||||
<Box className="max-w-2xl mx-6 border-b border-(--gray-7) divide-y divide-(--gray-7)">
|
||||
{/* Name & Job Title */}
|
||||
<Flex direction="column" pb="4">
|
||||
<Text size="6" weight="bold" highContrast>
|
||||
Ryan Walters,
|
||||
</Text>
|
||||
<Text
|
||||
size="6"
|
||||
weight="regular"
|
||||
style={{
|
||||
color: "var(--gray-11)",
|
||||
}}
|
||||
>
|
||||
Software Engineer
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box py="4" className="text-(--gray-12)">
|
||||
<Text style={{ fontSize: "0.95em" }}>
|
||||
A fanatical software engineer with expertise and passion for
|
||||
sound, scalable and high-performance applications. I'm always
|
||||
working on something new. <br />
|
||||
Sometimes innovative — sometimes crazy.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box py="3">
|
||||
<Text>Find me on</Text>
|
||||
<Flex gapX="2" pl="3" pt="3" pb="2">
|
||||
<SocialLink href="https://github.com/Xevion" icon={SiGithub}>
|
||||
GitHub
|
||||
</SocialLink>
|
||||
<SocialLink
|
||||
href="https://linkedin.com/in/ryancwalters"
|
||||
icon={SiLinkedin}
|
||||
>
|
||||
LinkedIn
|
||||
</SocialLink>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
</AppWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import AppWrapper from "@/components/AppWrapper";
|
||||
import { cn } from "@/utils/helpers";
|
||||
import Link from "next/link";
|
||||
import Balancer from "react-wrap-balancer";
|
||||
import { getPayload } from "payload";
|
||||
import config from "../../../payload.config";
|
||||
import type { Link as PayloadLink } from "@/payload-types";
|
||||
|
||||
export const dynamic = "force-dynamic"; // Don't prerender at build time
|
||||
|
||||
export default async function ProjectsPage() {
|
||||
const payloadConfig = await config;
|
||||
const payload = await getPayload({ config: payloadConfig });
|
||||
|
||||
// Fetch all projects
|
||||
const { docs: projects } = await payload.find({
|
||||
collection: "projects",
|
||||
where: {
|
||||
status: {
|
||||
equals: "published",
|
||||
},
|
||||
},
|
||||
sort: "-updatedAt",
|
||||
});
|
||||
|
||||
// Fetch all links in one query (fixes N+1 problem)
|
||||
const { docs: allLinks } = await payload.find({
|
||||
collection: "links",
|
||||
});
|
||||
|
||||
// Group links by project ID
|
||||
const linksByProject = new Map<number, PayloadLink[]>();
|
||||
for (const link of allLinks) {
|
||||
const projectId =
|
||||
typeof link.project === "number" ? link.project : link.project.id;
|
||||
if (!linksByProject.has(projectId)) {
|
||||
linksByProject.set(projectId, []);
|
||||
}
|
||||
linksByProject.get(projectId)!.push(link);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppWrapper dotsClassName="animate-bg-fast">
|
||||
<div className="relative z-10 mx-auto grid grid-cols-1 justify-center gap-y-4 px-4 py-20 align-middle sm:grid-cols-2 md:max-w-[50rem] lg:max-w-[75rem] lg:grid-cols-3 lg:gap-y-9">
|
||||
<div className="mb-3 text-center sm:col-span-2 md:mb-5 lg:col-span-3 lg:mb-7">
|
||||
<h1 className="pb-3 font-hanken text-4xl text-zinc-200 opacity-100 md:text-5xl">
|
||||
Projects
|
||||
</h1>
|
||||
<Balancer className="text-lg text-zinc-400">
|
||||
created, maintained, or contributed to by me...
|
||||
</Balancer>
|
||||
</div>
|
||||
{projects.map(({ id, name, shortDescription: description, icon }) => {
|
||||
const links = linksByProject.get(id) ?? [];
|
||||
const useAnchor = links.length > 0;
|
||||
const DynamicLink = useAnchor ? Link : "div";
|
||||
const linkProps = useAnchor
|
||||
? { href: links[0]!.url, target: "_blank", rel: "noreferrer" }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<div className="max-w-fit" key={id}>
|
||||
{/* @ts-expect-error because div can't accept href */}
|
||||
<DynamicLink
|
||||
key={name}
|
||||
title={name}
|
||||
className="flex items-center justify-start overflow-hidden rounded bg-black/10 pb-2.5 pl-3 pr-5 pt-1 text-zinc-400 transition-colors hover:bg-zinc-500/10 hover:text-zinc-50"
|
||||
{...linkProps}
|
||||
>
|
||||
<div className="flex h-full w-14 items-center justify-center pr-5">
|
||||
<i
|
||||
className={cn(
|
||||
icon ?? "fa-heart",
|
||||
"fa-solid text-3xl text-opacity-80 saturate-0",
|
||||
)}
|
||||
></i>
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<span className="text-sm md:text-base lg:text-lg">
|
||||
{name}
|
||||
</span>
|
||||
<p
|
||||
className="truncate text-xs opacity-70 md:text-sm lg:text-base"
|
||||
title={description}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</DynamicLink>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AppWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { Provider as BalancerProvider } from "react-wrap-balancer";
|
||||
import { Theme } from "@radix-ui/themes";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
// @ts-expect-error - Radix UI Themes has React 19 type compatibility issues
|
||||
<Theme appearance="dark">
|
||||
<BalancerProvider>
|
||||
{children}
|
||||
<Analytics />
|
||||
</BalancerProvider>
|
||||
</Theme>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { getPayload } from "payload";
|
||||
import config from "../../../payload.config";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const dynamic = "force-dynamic"; // Don't prerender at build time
|
||||
|
||||
type Metadata = {
|
||||
tagline: string;
|
||||
resume: {
|
||||
id: string;
|
||||
url: string;
|
||||
filename: string;
|
||||
};
|
||||
resumeFilename?: string;
|
||||
};
|
||||
|
||||
export default async function ResumePage() {
|
||||
try {
|
||||
const payloadConfig = await config;
|
||||
const payload = await getPayload({ config: payloadConfig });
|
||||
|
||||
// @ts-ignore - Globals will be typed after first database connection
|
||||
const metadata = (await payload.findGlobal({
|
||||
slug: "metadata",
|
||||
})) as Metadata;
|
||||
|
||||
if (!metadata.resume?.url) {
|
||||
throw new Error("Resume URL not found");
|
||||
}
|
||||
|
||||
redirect(metadata.resume.url);
|
||||
} catch (error) {
|
||||
console.error("Failed to acquire resume asset URL", error);
|
||||
throw new Error(`Failed to acquire resume (${error})`);
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
:root {
|
||||
--font-mono: "Roboto Mono", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
|
||||
background: rgb(0, 0, 0);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui;
|
||||
font-size: 18px;
|
||||
line-height: 32px;
|
||||
|
||||
margin: 0;
|
||||
color: rgb(1000, 1000, 1000);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 40px 0;
|
||||
font-size: 64px;
|
||||
line-height: 70px;
|
||||
font-weight: bold;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin: 24px 0;
|
||||
font-size: 42px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 38px;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 24px 0;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin: calc(var(--base) * 0.75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
padding: 45px;
|
||||
max-width: 1024px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 400px) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.admin {
|
||||
color: rgb(0, 0, 0);
|
||||
background: rgb(1000, 1000, 1000);
|
||||
border: 1px solid rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.docs {
|
||||
color: rgb(1000, 1000, 1000);
|
||||
background: rgb(0, 0, 0);
|
||||
border: 1px solid rgb(1000, 1000, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.codeLink {
|
||||
text-decoration: none;
|
||||
padding: 0 0.5rem;
|
||||
background: rgb(60, 60, 60);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/* 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;
|
||||
@@ -1,27 +0,0 @@
|
||||
/* 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;
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
export const importMap = {
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/* 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";
|
||||
|
||||
export const GET = REST_GET(config);
|
||||
export const POST = REST_POST(config);
|
||||
export const DELETE = REST_DELETE(config);
|
||||
export const PATCH = REST_PATCH(config);
|
||||
export const PUT = REST_PUT(config);
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
@@ -1,7 +0,0 @@
|
||||
/* 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 const GET = GRAPHQL_PLAYGROUND_GET(config);
|
||||
@@ -1,8 +0,0 @@
|
||||
/* 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";
|
||||
|
||||
export const POST = GRAPHQL_POST(config);
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
@@ -1,34 +0,0 @@
|
||||
/* 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";
|
||||
|
||||
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;
|
||||
@@ -1,299 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import config from "../../../../payload.config";
|
||||
import { Octokit } from "@octokit/core";
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: process.env.GITHUB_API_TOKEN,
|
||||
request: {
|
||||
fetch: (url: string | URL, options: RequestInit) => {
|
||||
console.log(`${options.method} ${url}`);
|
||||
return fetch(url, options);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type ProjectResult = {
|
||||
id: number;
|
||||
previousUpdated: Date | null;
|
||||
latestUpdated: Date | null;
|
||||
};
|
||||
|
||||
function getRepository(url: string): [string, string] | null {
|
||||
const pattern = /github\.com\/([^/]+)\/([^/]+)/;
|
||||
const match = url.match(pattern);
|
||||
|
||||
if (match === null) return null;
|
||||
return [match[1]!, match[2]!];
|
||||
}
|
||||
|
||||
function isFulfilled<T>(
|
||||
result: PromiseSettledResult<T>,
|
||||
): result is PromiseFulfilledResult<T> {
|
||||
return result.status === "fulfilled";
|
||||
}
|
||||
|
||||
function isRejected<T>(
|
||||
result: PromiseSettledResult<T>,
|
||||
): result is PromiseRejectedResult {
|
||||
return result.status === "rejected";
|
||||
}
|
||||
|
||||
async function handleProject({
|
||||
id: project_id,
|
||||
urls,
|
||||
date_updated: previousUpdated,
|
||||
}: {
|
||||
id: number;
|
||||
urls: string[];
|
||||
date_updated: Date | null;
|
||||
}): Promise<ProjectResult> {
|
||||
const allBranches = await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
const details = getRepository(url);
|
||||
if (!details) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [owner, repo] = details;
|
||||
const branches = await octokit.request(
|
||||
"GET /repos/{owner}/{repo}/branches",
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
headers: {
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return branches.data.map((branch) => ({
|
||||
branch: branch.name,
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
const latestCommits = allBranches
|
||||
.flat()
|
||||
.map(async ({ owner, repo, branch }) => {
|
||||
const commits = await octokit.request(
|
||||
"GET /repos/{owner}/{repo}/commits",
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
sha: branch,
|
||||
per_page: 1,
|
||||
headers: {
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
},
|
||||
);
|
||||
const latestCommit = commits.data[0];
|
||||
|
||||
if (latestCommit == null) {
|
||||
console.warn({
|
||||
target: `${owner}/${repo}@${branch}`,
|
||||
message: "No commits available",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (latestCommit.commit.author == null) {
|
||||
console.warn({
|
||||
target: `${owner}/${repo}@${branch}`,
|
||||
sha: latestCommit.sha,
|
||||
commit: latestCommit.commit.message,
|
||||
url: latestCommit.html_url,
|
||||
message: "No author available",
|
||||
});
|
||||
return null;
|
||||
} else if (latestCommit.commit.author.date == null) {
|
||||
console.warn({
|
||||
target: `${owner}/${repo}@${branch}`,
|
||||
sha: latestCommit.sha,
|
||||
commit: latestCommit.commit.message,
|
||||
url: latestCommit.html_url,
|
||||
message: "No date available",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(latestCommit.commit.author.date);
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(latestCommits);
|
||||
|
||||
results.filter(isRejected).forEach((result) => {
|
||||
console.error("Failed to fetch latest commit date", result.reason);
|
||||
});
|
||||
|
||||
const latestUpdated = results
|
||||
.filter(isFulfilled)
|
||||
.map((v) => v.value)
|
||||
.filter((v) => v != null)
|
||||
.reduce((previous: Date | null, current: Date) => {
|
||||
if (previous == null) return current;
|
||||
return current > previous ? current : previous;
|
||||
}, null);
|
||||
|
||||
if (latestUpdated == null) {
|
||||
console.error("Unable to acquire the latest commit date for project");
|
||||
return {
|
||||
id: project_id,
|
||||
previousUpdated,
|
||||
latestUpdated: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (latestUpdated != null && latestUpdated < new Date("2015-01-01")) {
|
||||
console.error("Invalid commit date acquired", latestUpdated);
|
||||
return {
|
||||
id: project_id,
|
||||
previousUpdated,
|
||||
latestUpdated: null,
|
||||
};
|
||||
}
|
||||
|
||||
const result = { id: project_id, previousUpdated, latestUpdated: null };
|
||||
|
||||
if (previousUpdated == null || latestUpdated > previousUpdated) {
|
||||
const payloadConfig = await config;
|
||||
const payload = await getPayload({ config: payloadConfig });
|
||||
|
||||
await payload.update({
|
||||
collection: "projects",
|
||||
id: project_id,
|
||||
data: {
|
||||
lastUpdated: latestUpdated.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
latestUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { CRON_SECRET, GITHUB_API_TOKEN } = process.env;
|
||||
|
||||
if (!GITHUB_API_TOKEN) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Service unavailable",
|
||||
message: "GITHUB_API_TOKEN not configured",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (!CRON_SECRET) {
|
||||
return NextResponse.json(
|
||||
{ error: "Server misconfiguration" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const authHeader = req.headers.get("authorization");
|
||||
const url = new URL(req.url);
|
||||
const secretQueryParam = url.searchParams.get("secret");
|
||||
if (
|
||||
authHeader !== `Bearer ${CRON_SECRET}` &&
|
||||
secretQueryParam !== CRON_SECRET
|
||||
) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
let request_count = 0;
|
||||
octokit.hook.before("request", async () => {
|
||||
request_count++;
|
||||
});
|
||||
|
||||
try {
|
||||
const payloadConfig = await config;
|
||||
const payload = await getPayload({ config: payloadConfig });
|
||||
|
||||
const { docs: projects } = await payload.find({
|
||||
collection: "projects",
|
||||
});
|
||||
|
||||
const { docs: allLinks } = await payload.find({
|
||||
collection: "links",
|
||||
});
|
||||
|
||||
const eligibleProjects = projects
|
||||
.map((project) => {
|
||||
if (!project.autocheckUpdated) return null;
|
||||
|
||||
const urls = allLinks
|
||||
.filter((link) => {
|
||||
const projectId =
|
||||
typeof link.project === "number" ? link.project : link.project.id;
|
||||
return projectId === project.id;
|
||||
})
|
||||
.map((link) => link.url)
|
||||
.filter((url) => url.includes("github.com"));
|
||||
|
||||
if (urls.length === 0) return null;
|
||||
|
||||
return {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
date_updated:
|
||||
project.lastUpdated != null ? new Date(project.lastUpdated) : null,
|
||||
urls,
|
||||
};
|
||||
})
|
||||
.filter((project) => project !== null);
|
||||
|
||||
const projectPromises = eligibleProjects.map((project) =>
|
||||
handleProject({
|
||||
id: project.id,
|
||||
urls: project.urls,
|
||||
date_updated: project.date_updated,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await Promise.allSettled(projectPromises);
|
||||
|
||||
const isFailed = results.filter(isRejected).length > results.length * 0.1;
|
||||
|
||||
type Response = {
|
||||
request_count: number;
|
||||
errors: { project_name: string; reason: string }[];
|
||||
ignored: number[];
|
||||
changed: { project_name: number; previous: Date | null; latest: Date }[];
|
||||
};
|
||||
|
||||
const fulfilled = results.filter(isFulfilled);
|
||||
|
||||
const response: Response = {
|
||||
request_count,
|
||||
errors: results.filter(isRejected).map((r) => ({
|
||||
project_name: "unknown",
|
||||
reason: r.reason,
|
||||
})),
|
||||
ignored: fulfilled
|
||||
.filter((r) => r.value.latestUpdated == null)
|
||||
.map((r) => r.value.id),
|
||||
changed: fulfilled
|
||||
.filter((r) => r.value.latestUpdated != null)
|
||||
.map((r) => ({
|
||||
project_name: r.value.id,
|
||||
previous: r.value.previousUpdated,
|
||||
latest: r.value.latestUpdated!,
|
||||
})),
|
||||
};
|
||||
|
||||
return NextResponse.json(response, { status: !isFailed ? 200 : 500 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { getPayload } from "payload";
|
||||
import config from "../../../payload.config";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const healthcheckSecret = process.env.HEALTHCHECK_SECRET;
|
||||
|
||||
if (!healthcheckSecret) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Service unavailable",
|
||||
message: "HEALTHCHECK_SECRET not configured",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
const secret = req.headers.get("authorization");
|
||||
if (secret !== healthcheckSecret) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Try a simple Payload API call (fetch one project)
|
||||
const payloadConfig = await config;
|
||||
const payload = await getPayload({ config: payloadConfig });
|
||||
|
||||
await payload.find({
|
||||
collection: "projects",
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
return NextResponse.json({ status: "ok" }, { status: 200 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Payload CMS unhealthy", details: String(error) },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
|
||||
const requestSchema = z.object({
|
||||
collection: z.string(),
|
||||
doc: z.object({
|
||||
id: z.string().or(z.number()),
|
||||
}),
|
||||
});
|
||||
|
||||
function getPathsToRevalidate(collection: string): string[] {
|
||||
switch (collection) {
|
||||
case "projects":
|
||||
return ["/projects"];
|
||||
case "metadata":
|
||||
return ["/"];
|
||||
case "technologies":
|
||||
return ["/projects"];
|
||||
case "links":
|
||||
return ["/projects"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const revalidateKey = process.env.PAYLOAD_REVALIDATE_KEY;
|
||||
const authHeader = req.headers.get("authorization");
|
||||
|
||||
if (!authHeader || authHeader !== `Bearer ${revalidateKey}`) {
|
||||
return NextResponse.json({ message: "Invalid token" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { success, data, error } = requestSchema.safeParse(body);
|
||||
|
||||
if (!success) {
|
||||
console.error({ message: "Invalid JSON body", error });
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid JSON body", error },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const paths = getPathsToRevalidate(data.collection);
|
||||
|
||||
if (paths.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ revalidated: false, message: "No paths to revalidate" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Revalidate all paths
|
||||
try {
|
||||
for (const path of paths) {
|
||||
revalidatePath(path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error({ message: "Error while revalidating", error });
|
||||
return NextResponse.json(
|
||||
{
|
||||
revalidated: false,
|
||||
message: "Error while revalidating",
|
||||
paths,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ revalidated: true, paths }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error({
|
||||
message: "Error while preparing to revalidate",
|
||||
error,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ message: "Error revalidating" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
export const Links: CollectionConfig = {
|
||||
slug: "links",
|
||||
admin: {
|
||||
useAsTitle: "url",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "url",
|
||||
type: "text",
|
||||
required: true,
|
||||
label: "URL",
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
type: "text",
|
||||
label: "Icon (FontAwesome class)",
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "project",
|
||||
type: "relationship",
|
||||
relationTo: "projects",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: "media",
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "alt",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
upload: true,
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
export const Projects: CollectionConfig = {
|
||||
slug: "projects",
|
||||
admin: {
|
||||
useAsTitle: "name",
|
||||
defaultColumns: ["name", "featured", "status", "updatedAt"],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
type: "textarea",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "shortDescription",
|
||||
type: "text",
|
||||
label: "Short Description",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
type: "text",
|
||||
label: "Icon (FontAwesome class)",
|
||||
},
|
||||
{
|
||||
name: "status",
|
||||
type: "select",
|
||||
options: [
|
||||
{ label: "Draft", value: "draft" },
|
||||
{ label: "Published", value: "published" },
|
||||
{ label: "Archived", value: "archived" },
|
||||
],
|
||||
defaultValue: "draft",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "featured",
|
||||
type: "checkbox",
|
||||
label: "Featured Project",
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: "autocheckUpdated",
|
||||
type: "checkbox",
|
||||
label: "Auto-check for GitHub updates",
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description:
|
||||
"Automatically check GitHub for latest commits and update lastUpdated field",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lastUpdated",
|
||||
type: "date",
|
||||
label: "Last Updated",
|
||||
admin: {
|
||||
description:
|
||||
"Automatically updated by cron job based on GitHub commits",
|
||||
date: {
|
||||
displayFormat: "yyyy-MM-dd HH:mm:ss",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wakatimeOffset",
|
||||
type: "number",
|
||||
label: "WakaTime Offset",
|
||||
admin: {
|
||||
description: "Offset for WakaTime fetched data (optional)",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bannerImage",
|
||||
type: "upload",
|
||||
relationTo: "media",
|
||||
label: "Banner Image",
|
||||
},
|
||||
{
|
||||
name: "technologies",
|
||||
type: "relationship",
|
||||
relationTo: "technologies",
|
||||
hasMany: true,
|
||||
label: "Technologies Used",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { CollectionConfig } from "payload";
|
||||
|
||||
export const Technologies: CollectionConfig = {
|
||||
slug: "technologies",
|
||||
admin: {
|
||||
useAsTitle: "name",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
type: "text",
|
||||
label: "URL",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
],
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/utils/helpers";
|
||||
import dynamic from "next/dynamic";
|
||||
import type { FunctionComponent, ReactNode } from "react";
|
||||
|
||||
type WrapperProps = {
|
||||
className?: string;
|
||||
dotsClassName?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const DotsDynamic = dynamic(() => import("@/components/Dots"), { ssr: false });
|
||||
|
||||
const AppWrapper: FunctionComponent<WrapperProps> = ({
|
||||
children,
|
||||
className,
|
||||
dotsClassName,
|
||||
}: WrapperProps) => {
|
||||
return (
|
||||
<main
|
||||
className={cn("relative min-h-screen bg-black text-zinc-50", className)}
|
||||
>
|
||||
<DotsDynamic className={dotsClassName} />
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppWrapper;
|
||||
@@ -1,31 +0,0 @@
|
||||
import Image, { ImageProps } from "next/image";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
type DependentProps = {
|
||||
className?: string | ((loaded: boolean) => string);
|
||||
};
|
||||
|
||||
type DependentImageProps = Omit<ImageProps, "className"> & DependentProps;
|
||||
|
||||
const DependentImage = (props: DependentImageProps) => {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const { className } = props;
|
||||
const renderedClassName = useMemo(() => {
|
||||
if (className === undefined) return "";
|
||||
if (typeof className === "function") return className(loaded);
|
||||
return className;
|
||||
}, [loaded, className]);
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...props}
|
||||
className={renderedClassName}
|
||||
alt="no"
|
||||
onLoad={() => {
|
||||
setLoaded(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DependentImage;
|
||||
@@ -1,139 +0,0 @@
|
||||
import { cn } from "@/utils/helpers";
|
||||
import { p5i, P5I } from "p5i";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
interface DotsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Dots = ({ className }: DotsProps) => {
|
||||
const canvasRef = useRef<HTMLDivElement | null>(null);
|
||||
const w = useRef(window.innerWidth);
|
||||
const h = useRef(window.innerHeight);
|
||||
|
||||
const {
|
||||
mount,
|
||||
unmount,
|
||||
background,
|
||||
stroke,
|
||||
noise,
|
||||
resizeCanvas,
|
||||
cos,
|
||||
sin,
|
||||
TWO_PI,
|
||||
} = p5i();
|
||||
|
||||
const offsetY = window.scrollY;
|
||||
|
||||
const SCALE = 400;
|
||||
const LENGTH = 3;
|
||||
const SPACING = 20;
|
||||
const TARGET_FRAMERATE = 5;
|
||||
const TIMESCALE = 2;
|
||||
const OPACITY = 0.7;
|
||||
|
||||
function getForceOnPoint(x: number, y: number, z: number) {
|
||||
return (noise(x / SCALE, y / SCALE, z) - 0.5) * 2 * TWO_PI;
|
||||
}
|
||||
|
||||
const pointIds = new Set<string>();
|
||||
const points: { x: number; y: number; opacity: number }[] = [];
|
||||
|
||||
function addPoints() {
|
||||
for (let x = -SPACING / 2; x < w.current + SPACING; x += SPACING) {
|
||||
for (
|
||||
let y = -SPACING / 2;
|
||||
y < h.current + offsetY + SPACING;
|
||||
y += SPACING
|
||||
) {
|
||||
const id = `${x}-${y}`;
|
||||
if (pointIds.has(id)) continue;
|
||||
pointIds.add(id);
|
||||
|
||||
points.push({ x, y, opacity: Math.random() * 0.5 + 0.5 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setup({
|
||||
createCanvas,
|
||||
stroke,
|
||||
frameRate,
|
||||
background,
|
||||
noFill,
|
||||
noiseSeed,
|
||||
}: P5I) {
|
||||
createCanvas(w.current, h.current);
|
||||
background("#000000");
|
||||
stroke("rgba(170, 170, 170, 0.05)");
|
||||
noFill();
|
||||
|
||||
frameRate(TARGET_FRAMERATE);
|
||||
noiseSeed(Date.now());
|
||||
|
||||
addPoints();
|
||||
}
|
||||
|
||||
function draw({ circle, frameCount }: P5I) {
|
||||
background("#000000");
|
||||
const t = (frameCount / 80) * TIMESCALE;
|
||||
|
||||
// if (frameCount % 10000) console.log(frameRate());
|
||||
|
||||
for (const p of points) {
|
||||
const rad = getForceOnPoint(p.x, p.y, t);
|
||||
const length = (noise(p.x / SCALE, p.y / SCALE, t * 2) + 0.5) * LENGTH;
|
||||
const nx = p.x + cos(rad) * length;
|
||||
const ny = p.y + sin(rad) * length;
|
||||
|
||||
// const center_distance = Math.sqrt((x - w / 2) ** 2 + (y - h / 2) ** 2);
|
||||
// if (center_distance < 350)
|
||||
// opacity = 0;
|
||||
// opacity =
|
||||
stroke(
|
||||
200,
|
||||
200,
|
||||
200,
|
||||
(Math.abs(cos(rad)) * 0.8 + 0.1) * p.opacity * 255 * OPACITY,
|
||||
);
|
||||
circle(nx, ny - offsetY, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function restart() {
|
||||
if (canvasRef.current) {
|
||||
mount(canvasRef.current, { setup, draw });
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
restart();
|
||||
|
||||
const handleResize = () => {
|
||||
w.current = window.innerWidth;
|
||||
h.current = window.innerHeight;
|
||||
resizeCanvas(w.current, h.current);
|
||||
addPoints();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
unmount();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className={cn(
|
||||
"pointer-events-none fixed bottom-0 left-0 right-0 top-0 z-0 opacity-0",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dots;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
type SteppedSpanProps = {
|
||||
children: string;
|
||||
};
|
||||
|
||||
const SteppedSpan: FunctionComponent<SteppedSpanProps> = ({
|
||||
children,
|
||||
}: SteppedSpanProps) => {
|
||||
return (
|
||||
<div className="stepped">
|
||||
{children.split("").map((char: string, index) => {
|
||||
return <span key={index}>{char}</span>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SteppedSpan;
|
||||
Vendored
-35
@@ -1,35 +0,0 @@
|
||||
// @ts-check
|
||||
import { clientEnv, clientSchema } from "./schema.mjs";
|
||||
|
||||
const _clientEnv = clientSchema.safeParse(clientEnv);
|
||||
|
||||
export const formatErrors = (
|
||||
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
|
||||
errors,
|
||||
) =>
|
||||
Object.entries(errors)
|
||||
.map(([name, value]) => {
|
||||
if (value && "_errors" in value)
|
||||
return `${name}: ${value._errors.join(", ")}\n`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (!_clientEnv.success) {
|
||||
console.error(
|
||||
"❌ Invalid environment variables:\n",
|
||||
...formatErrors(_clientEnv.error.format()),
|
||||
);
|
||||
throw new Error("Invalid environment variables");
|
||||
}
|
||||
|
||||
for (let key of Object.keys(_clientEnv.data)) {
|
||||
if (!key.startsWith("NEXT_PUBLIC_")) {
|
||||
console.warn(
|
||||
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`,
|
||||
);
|
||||
|
||||
throw new Error("Invalid public environment variable name");
|
||||
}
|
||||
}
|
||||
|
||||
export const env = _clientEnv.data;
|
||||
Vendored
-45
@@ -1,45 +0,0 @@
|
||||
// @ts-check
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Specify your server-side environment variables schema here.
|
||||
* This way you can ensure the app isn't built with invalid env vars.
|
||||
*/
|
||||
export const serverSchema = z.object({
|
||||
CRON_SECRET: z.string().optional(),
|
||||
GITHUB_API_TOKEN: z.string().optional(),
|
||||
PAYLOAD_SECRET: z
|
||||
.string()
|
||||
.default("dev-secret-change-in-production-immediately"),
|
||||
DATABASE_URI: z
|
||||
.string()
|
||||
.default(
|
||||
"postgresql://xevion:xevion_dev_password@localhost:5432/xevion_dev",
|
||||
),
|
||||
PAYLOAD_REVALIDATE_KEY: z.string().optional(),
|
||||
HEALTHCHECK_SECRET: z.string().optional(),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||
TITLE: z.preprocess((value) => {
|
||||
if (value === undefined || value === "") return null;
|
||||
return value;
|
||||
}, z.string().nullable()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Specify your client-side environment variables schema here.
|
||||
* This way you can ensure the app isn't built with invalid env vars.
|
||||
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
|
||||
*/
|
||||
export const clientSchema = z.object({
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* You can't destruct `process.env` as a regular object, so you have to do
|
||||
* it manually here. This is because Next.js evaluates this at build time,
|
||||
* and only used environment variables are included in the build.
|
||||
* @type {{ [k in keyof z.infer<typeof clientSchema>]: z.infer<typeof clientSchema>[k] | undefined }}
|
||||
*/
|
||||
export const clientEnv = {
|
||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||
};
|
||||
Vendored
-52
@@ -1,52 +0,0 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars.
|
||||
* It has to be a `.mjs`-file to be imported there.
|
||||
*/
|
||||
import { serverSchema } from "./schema.mjs";
|
||||
import { env as clientEnv, formatErrors } from "./client.mjs";
|
||||
|
||||
const _serverEnv = serverSchema.safeParse(process.env);
|
||||
|
||||
if (!_serverEnv.success) {
|
||||
console.error(
|
||||
"❌ Invalid environment variables:\n",
|
||||
...formatErrors(_serverEnv.error.format()),
|
||||
);
|
||||
throw new Error("Invalid environment variables");
|
||||
}
|
||||
|
||||
for (let key of Object.keys(_serverEnv.data)) {
|
||||
if (key.startsWith("NEXT_PUBLIC_")) {
|
||||
console.warn("❌ You are exposing a server-side env-variable:", key);
|
||||
|
||||
throw new Error("You are exposing a server-side env-variable");
|
||||
}
|
||||
}
|
||||
|
||||
// Production safety checks
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (
|
||||
_serverEnv.data.PAYLOAD_SECRET ===
|
||||
"dev-secret-change-in-production-immediately"
|
||||
) {
|
||||
throw new Error("PAYLOAD_SECRET must be explicitly set in production");
|
||||
}
|
||||
if (_serverEnv.data.DATABASE_URI?.includes("xevion_dev_password")) {
|
||||
throw new Error("DATABASE_URI must be explicitly set in production");
|
||||
}
|
||||
}
|
||||
|
||||
// Development warnings for missing optional secrets
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const missing = [];
|
||||
if (!_serverEnv.data.GITHUB_API_TOKEN) missing.push("GITHUB_API_TOKEN");
|
||||
if (!_serverEnv.data.HEALTHCHECK_SECRET) missing.push("HEALTHCHECK_SECRET");
|
||||
if (!_serverEnv.data.CRON_SECRET) missing.push("CRON_SECRET");
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.warn(`Environment variables missing: [${missing.join(", ")}]`);
|
||||
}
|
||||
}
|
||||
|
||||
export const env = { ..._serverEnv.data, ...clientEnv };
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { GlobalConfig } from "payload";
|
||||
|
||||
export const Metadata: GlobalConfig = {
|
||||
slug: "metadata",
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "tagline",
|
||||
type: "textarea",
|
||||
required: true,
|
||||
label: "Site Tagline",
|
||||
},
|
||||
{
|
||||
name: "resume",
|
||||
type: "upload",
|
||||
relationTo: "media",
|
||||
required: true,
|
||||
label: "Resume File",
|
||||
},
|
||||
{
|
||||
name: "resumeFilename",
|
||||
type: "text",
|
||||
label: "Resume Filename Override",
|
||||
admin: {
|
||||
description:
|
||||
'Optional: Override the filename for the resume (e.g., "resume.pdf")',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,617 +0,0 @@
|
||||
/* tslint: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:db-schema` to regenerate this file.
|
||||
*/
|
||||
|
||||
import type {} from "@payloadcms/db-postgres";
|
||||
import {
|
||||
pgTable,
|
||||
index,
|
||||
uniqueIndex,
|
||||
foreignKey,
|
||||
integer,
|
||||
varchar,
|
||||
timestamp,
|
||||
serial,
|
||||
numeric,
|
||||
boolean,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
} from "@payloadcms/db-postgres/drizzle/pg-core";
|
||||
import { sql, relations } from "@payloadcms/db-postgres/drizzle";
|
||||
export const enum_projects_status = pgEnum("enum_projects_status", [
|
||||
"draft",
|
||||
"published",
|
||||
"archived",
|
||||
]);
|
||||
|
||||
export const users_sessions = pgTable(
|
||||
"users_sessions",
|
||||
{
|
||||
_order: integer("_order").notNull(),
|
||||
_parentID: integer("_parent_id").notNull(),
|
||||
id: varchar("id").primaryKey(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}),
|
||||
expiresAt: timestamp("expires_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}).notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
index("users_sessions_order_idx").on(columns._order),
|
||||
index("users_sessions_parent_id_idx").on(columns._parentID),
|
||||
foreignKey({
|
||||
columns: [columns["_parentID"]],
|
||||
foreignColumns: [users.id],
|
||||
name: "users_sessions_parent_id_fk",
|
||||
}).onDelete("cascade"),
|
||||
],
|
||||
);
|
||||
|
||||
export const users = pgTable(
|
||||
"users",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
email: varchar("email").notNull(),
|
||||
resetPasswordToken: varchar("reset_password_token"),
|
||||
resetPasswordExpiration: timestamp("reset_password_expiration", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}),
|
||||
salt: varchar("salt"),
|
||||
hash: varchar("hash"),
|
||||
loginAttempts: numeric("login_attempts", { mode: "number" }).default(0),
|
||||
lockUntil: timestamp("lock_until", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}),
|
||||
},
|
||||
(columns) => [
|
||||
index("users_updated_at_idx").on(columns.updatedAt),
|
||||
index("users_created_at_idx").on(columns.createdAt),
|
||||
uniqueIndex("users_email_idx").on(columns.email),
|
||||
],
|
||||
);
|
||||
|
||||
export const media = pgTable(
|
||||
"media",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
alt: varchar("alt").notNull(),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
url: varchar("url"),
|
||||
thumbnailURL: varchar("thumbnail_u_r_l"),
|
||||
filename: varchar("filename"),
|
||||
mimeType: varchar("mime_type"),
|
||||
filesize: numeric("filesize", { mode: "number" }),
|
||||
width: numeric("width", { mode: "number" }),
|
||||
height: numeric("height", { mode: "number" }),
|
||||
focalX: numeric("focal_x", { mode: "number" }),
|
||||
focalY: numeric("focal_y", { mode: "number" }),
|
||||
},
|
||||
(columns) => [
|
||||
index("media_updated_at_idx").on(columns.updatedAt),
|
||||
index("media_created_at_idx").on(columns.createdAt),
|
||||
uniqueIndex("media_filename_idx").on(columns.filename),
|
||||
],
|
||||
);
|
||||
|
||||
export const projects = pgTable(
|
||||
"projects",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
name: varchar("name").notNull(),
|
||||
description: varchar("description").notNull(),
|
||||
shortDescription: varchar("short_description").notNull(),
|
||||
icon: varchar("icon"),
|
||||
status: enum_projects_status("status").notNull().default("draft"),
|
||||
featured: boolean("featured").default(false),
|
||||
autocheckUpdated: boolean("autocheck_updated").default(false),
|
||||
lastUpdated: timestamp("last_updated", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}),
|
||||
wakatimeOffset: numeric("wakatime_offset", { mode: "number" }),
|
||||
bannerImage: integer("banner_image_id").references(() => media.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
index("projects_banner_image_idx").on(columns.bannerImage),
|
||||
index("projects_updated_at_idx").on(columns.updatedAt),
|
||||
index("projects_created_at_idx").on(columns.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const projects_rels = pgTable(
|
||||
"projects_rels",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
order: integer("order"),
|
||||
parent: integer("parent_id").notNull(),
|
||||
path: varchar("path").notNull(),
|
||||
technologiesID: integer("technologies_id"),
|
||||
},
|
||||
(columns) => [
|
||||
index("projects_rels_order_idx").on(columns.order),
|
||||
index("projects_rels_parent_idx").on(columns.parent),
|
||||
index("projects_rels_path_idx").on(columns.path),
|
||||
index("projects_rels_technologies_id_idx").on(columns.technologiesID),
|
||||
foreignKey({
|
||||
columns: [columns["parent"]],
|
||||
foreignColumns: [projects.id],
|
||||
name: "projects_rels_parent_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["technologiesID"]],
|
||||
foreignColumns: [technologies.id],
|
||||
name: "projects_rels_technologies_fk",
|
||||
}).onDelete("cascade"),
|
||||
],
|
||||
);
|
||||
|
||||
export const technologies = pgTable(
|
||||
"technologies",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
name: varchar("name").notNull(),
|
||||
url: varchar("url"),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
index("technologies_updated_at_idx").on(columns.updatedAt),
|
||||
index("technologies_created_at_idx").on(columns.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const links = pgTable(
|
||||
"links",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
url: varchar("url").notNull(),
|
||||
icon: varchar("icon"),
|
||||
description: varchar("description"),
|
||||
project: integer("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
index("links_project_idx").on(columns.project),
|
||||
index("links_updated_at_idx").on(columns.updatedAt),
|
||||
index("links_created_at_idx").on(columns.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const payload_locked_documents = pgTable(
|
||||
"payload_locked_documents",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
globalSlug: varchar("global_slug"),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
index("payload_locked_documents_global_slug_idx").on(columns.globalSlug),
|
||||
index("payload_locked_documents_updated_at_idx").on(columns.updatedAt),
|
||||
index("payload_locked_documents_created_at_idx").on(columns.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const payload_locked_documents_rels = pgTable(
|
||||
"payload_locked_documents_rels",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
order: integer("order"),
|
||||
parent: integer("parent_id").notNull(),
|
||||
path: varchar("path").notNull(),
|
||||
usersID: integer("users_id"),
|
||||
mediaID: integer("media_id"),
|
||||
projectsID: integer("projects_id"),
|
||||
technologiesID: integer("technologies_id"),
|
||||
linksID: integer("links_id"),
|
||||
},
|
||||
(columns) => [
|
||||
index("payload_locked_documents_rels_order_idx").on(columns.order),
|
||||
index("payload_locked_documents_rels_parent_idx").on(columns.parent),
|
||||
index("payload_locked_documents_rels_path_idx").on(columns.path),
|
||||
index("payload_locked_documents_rels_users_id_idx").on(columns.usersID),
|
||||
index("payload_locked_documents_rels_media_id_idx").on(columns.mediaID),
|
||||
index("payload_locked_documents_rels_projects_id_idx").on(
|
||||
columns.projectsID,
|
||||
),
|
||||
index("payload_locked_documents_rels_technologies_id_idx").on(
|
||||
columns.technologiesID,
|
||||
),
|
||||
index("payload_locked_documents_rels_links_id_idx").on(columns.linksID),
|
||||
foreignKey({
|
||||
columns: [columns["parent"]],
|
||||
foreignColumns: [payload_locked_documents.id],
|
||||
name: "payload_locked_documents_rels_parent_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["usersID"]],
|
||||
foreignColumns: [users.id],
|
||||
name: "payload_locked_documents_rels_users_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["mediaID"]],
|
||||
foreignColumns: [media.id],
|
||||
name: "payload_locked_documents_rels_media_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["projectsID"]],
|
||||
foreignColumns: [projects.id],
|
||||
name: "payload_locked_documents_rels_projects_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["technologiesID"]],
|
||||
foreignColumns: [technologies.id],
|
||||
name: "payload_locked_documents_rels_technologies_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["linksID"]],
|
||||
foreignColumns: [links.id],
|
||||
name: "payload_locked_documents_rels_links_fk",
|
||||
}).onDelete("cascade"),
|
||||
],
|
||||
);
|
||||
|
||||
export const payload_preferences = pgTable(
|
||||
"payload_preferences",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
key: varchar("key"),
|
||||
value: jsonb("value"),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
index("payload_preferences_key_idx").on(columns.key),
|
||||
index("payload_preferences_updated_at_idx").on(columns.updatedAt),
|
||||
index("payload_preferences_created_at_idx").on(columns.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const payload_preferences_rels = pgTable(
|
||||
"payload_preferences_rels",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
order: integer("order"),
|
||||
parent: integer("parent_id").notNull(),
|
||||
path: varchar("path").notNull(),
|
||||
usersID: integer("users_id"),
|
||||
},
|
||||
(columns) => [
|
||||
index("payload_preferences_rels_order_idx").on(columns.order),
|
||||
index("payload_preferences_rels_parent_idx").on(columns.parent),
|
||||
index("payload_preferences_rels_path_idx").on(columns.path),
|
||||
index("payload_preferences_rels_users_id_idx").on(columns.usersID),
|
||||
foreignKey({
|
||||
columns: [columns["parent"]],
|
||||
foreignColumns: [payload_preferences.id],
|
||||
name: "payload_preferences_rels_parent_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["usersID"]],
|
||||
foreignColumns: [users.id],
|
||||
name: "payload_preferences_rels_users_fk",
|
||||
}).onDelete("cascade"),
|
||||
],
|
||||
);
|
||||
|
||||
export const payload_migrations = pgTable(
|
||||
"payload_migrations",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
name: varchar("name"),
|
||||
batch: numeric("batch", { mode: "number" }),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
index("payload_migrations_updated_at_idx").on(columns.updatedAt),
|
||||
index("payload_migrations_created_at_idx").on(columns.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const metadata = pgTable(
|
||||
"metadata",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
tagline: varchar("tagline").notNull(),
|
||||
resume: integer("resume_id")
|
||||
.notNull()
|
||||
.references(() => media.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
resumeFilename: varchar("resume_filename"),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}),
|
||||
createdAt: timestamp("created_at", {
|
||||
mode: "string",
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}),
|
||||
},
|
||||
(columns) => [index("metadata_resume_idx").on(columns.resume)],
|
||||
);
|
||||
|
||||
export const relations_users_sessions = relations(
|
||||
users_sessions,
|
||||
({ one }) => ({
|
||||
_parentID: one(users, {
|
||||
fields: [users_sessions._parentID],
|
||||
references: [users.id],
|
||||
relationName: "sessions",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export const relations_users = relations(users, ({ many }) => ({
|
||||
sessions: many(users_sessions, {
|
||||
relationName: "sessions",
|
||||
}),
|
||||
}));
|
||||
export const relations_media = relations(media, () => ({}));
|
||||
export const relations_projects_rels = relations(projects_rels, ({ one }) => ({
|
||||
parent: one(projects, {
|
||||
fields: [projects_rels.parent],
|
||||
references: [projects.id],
|
||||
relationName: "_rels",
|
||||
}),
|
||||
technologiesID: one(technologies, {
|
||||
fields: [projects_rels.technologiesID],
|
||||
references: [technologies.id],
|
||||
relationName: "technologies",
|
||||
}),
|
||||
}));
|
||||
export const relations_projects = relations(projects, ({ one, many }) => ({
|
||||
bannerImage: one(media, {
|
||||
fields: [projects.bannerImage],
|
||||
references: [media.id],
|
||||
relationName: "bannerImage",
|
||||
}),
|
||||
_rels: many(projects_rels, {
|
||||
relationName: "_rels",
|
||||
}),
|
||||
}));
|
||||
export const relations_technologies = relations(technologies, () => ({}));
|
||||
export const relations_links = relations(links, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [links.project],
|
||||
references: [projects.id],
|
||||
relationName: "project",
|
||||
}),
|
||||
}));
|
||||
export const relations_payload_locked_documents_rels = relations(
|
||||
payload_locked_documents_rels,
|
||||
({ one }) => ({
|
||||
parent: one(payload_locked_documents, {
|
||||
fields: [payload_locked_documents_rels.parent],
|
||||
references: [payload_locked_documents.id],
|
||||
relationName: "_rels",
|
||||
}),
|
||||
usersID: one(users, {
|
||||
fields: [payload_locked_documents_rels.usersID],
|
||||
references: [users.id],
|
||||
relationName: "users",
|
||||
}),
|
||||
mediaID: one(media, {
|
||||
fields: [payload_locked_documents_rels.mediaID],
|
||||
references: [media.id],
|
||||
relationName: "media",
|
||||
}),
|
||||
projectsID: one(projects, {
|
||||
fields: [payload_locked_documents_rels.projectsID],
|
||||
references: [projects.id],
|
||||
relationName: "projects",
|
||||
}),
|
||||
technologiesID: one(technologies, {
|
||||
fields: [payload_locked_documents_rels.technologiesID],
|
||||
references: [technologies.id],
|
||||
relationName: "technologies",
|
||||
}),
|
||||
linksID: one(links, {
|
||||
fields: [payload_locked_documents_rels.linksID],
|
||||
references: [links.id],
|
||||
relationName: "links",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export const relations_payload_locked_documents = relations(
|
||||
payload_locked_documents,
|
||||
({ many }) => ({
|
||||
_rels: many(payload_locked_documents_rels, {
|
||||
relationName: "_rels",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export const relations_payload_preferences_rels = relations(
|
||||
payload_preferences_rels,
|
||||
({ one }) => ({
|
||||
parent: one(payload_preferences, {
|
||||
fields: [payload_preferences_rels.parent],
|
||||
references: [payload_preferences.id],
|
||||
relationName: "_rels",
|
||||
}),
|
||||
usersID: one(users, {
|
||||
fields: [payload_preferences_rels.usersID],
|
||||
references: [users.id],
|
||||
relationName: "users",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export const relations_payload_preferences = relations(
|
||||
payload_preferences,
|
||||
({ many }) => ({
|
||||
_rels: many(payload_preferences_rels, {
|
||||
relationName: "_rels",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export const relations_payload_migrations = relations(
|
||||
payload_migrations,
|
||||
() => ({}),
|
||||
);
|
||||
export const relations_metadata = relations(metadata, ({ one }) => ({
|
||||
resume: one(media, {
|
||||
fields: [metadata.resume],
|
||||
references: [media.id],
|
||||
relationName: "resume",
|
||||
}),
|
||||
}));
|
||||
|
||||
type DatabaseSchema = {
|
||||
enum_projects_status: typeof enum_projects_status;
|
||||
users_sessions: typeof users_sessions;
|
||||
users: typeof users;
|
||||
media: typeof media;
|
||||
projects: typeof projects;
|
||||
projects_rels: typeof projects_rels;
|
||||
technologies: typeof technologies;
|
||||
links: typeof links;
|
||||
payload_locked_documents: typeof payload_locked_documents;
|
||||
payload_locked_documents_rels: typeof payload_locked_documents_rels;
|
||||
payload_preferences: typeof payload_preferences;
|
||||
payload_preferences_rels: typeof payload_preferences_rels;
|
||||
payload_migrations: typeof payload_migrations;
|
||||
metadata: typeof metadata;
|
||||
relations_users_sessions: typeof relations_users_sessions;
|
||||
relations_users: typeof relations_users;
|
||||
relations_media: typeof relations_media;
|
||||
relations_projects_rels: typeof relations_projects_rels;
|
||||
relations_projects: typeof relations_projects;
|
||||
relations_technologies: typeof relations_technologies;
|
||||
relations_links: typeof relations_links;
|
||||
relations_payload_locked_documents_rels: typeof relations_payload_locked_documents_rels;
|
||||
relations_payload_locked_documents: typeof relations_payload_locked_documents;
|
||||
relations_payload_preferences_rels: typeof relations_payload_preferences_rels;
|
||||
relations_payload_preferences: typeof relations_payload_preferences;
|
||||
relations_payload_migrations: typeof relations_payload_migrations;
|
||||
relations_metadata: typeof relations_metadata;
|
||||
};
|
||||
|
||||
declare module "@payloadcms/db-postgres" {
|
||||
export interface GeneratedDatabaseSchema {
|
||||
schema: DatabaseSchema;
|
||||
}
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
/* 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;
|
||||
projects: Project;
|
||||
technologies: Technology;
|
||||
links: Link;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
projects: ProjectsSelect<false> | ProjectsSelect<true>;
|
||||
technologies: TechnologiesSelect<false> | TechnologiesSelect<true>;
|
||||
links: LinksSelect<false> | LinksSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {
|
||||
metadata: Metadatum;
|
||||
};
|
||||
globalsSelect: {
|
||||
metadata: MetadataSelect<false> | MetadataSelect<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;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "projects".
|
||||
*/
|
||||
export interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
shortDescription: string;
|
||||
icon?: string | null;
|
||||
status: 'draft' | 'published' | 'archived';
|
||||
featured?: boolean | null;
|
||||
/**
|
||||
* Automatically check GitHub for latest commits and update lastUpdated field
|
||||
*/
|
||||
autocheckUpdated?: boolean | null;
|
||||
/**
|
||||
* Automatically updated by cron job based on GitHub commits
|
||||
*/
|
||||
lastUpdated?: string | null;
|
||||
/**
|
||||
* Offset for WakaTime fetched data (optional)
|
||||
*/
|
||||
wakatimeOffset?: number | null;
|
||||
bannerImage?: (number | null) | Media;
|
||||
technologies?: (number | Technology)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "technologies".
|
||||
*/
|
||||
export interface Technology {
|
||||
id: number;
|
||||
name: string;
|
||||
url?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "links".
|
||||
*/
|
||||
export interface Link {
|
||||
id: number;
|
||||
url: string;
|
||||
icon?: string | null;
|
||||
description?: string | null;
|
||||
project: number | Project;
|
||||
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: 'projects';
|
||||
value: number | Project;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'technologies';
|
||||
value: number | Technology;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'links';
|
||||
value: number | Link;
|
||||
} | 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;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "projects_select".
|
||||
*/
|
||||
export interface ProjectsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
description?: T;
|
||||
shortDescription?: T;
|
||||
icon?: T;
|
||||
status?: T;
|
||||
featured?: T;
|
||||
autocheckUpdated?: T;
|
||||
lastUpdated?: T;
|
||||
wakatimeOffset?: T;
|
||||
bannerImage?: T;
|
||||
technologies?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "technologies_select".
|
||||
*/
|
||||
export interface TechnologiesSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
url?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "links_select".
|
||||
*/
|
||||
export interface LinksSelect<T extends boolean = true> {
|
||||
url?: T;
|
||||
icon?: T;
|
||||
description?: T;
|
||||
project?: 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` "metadata".
|
||||
*/
|
||||
export interface Metadatum {
|
||||
id: number;
|
||||
tagline: string;
|
||||
resume: number | Media;
|
||||
/**
|
||||
* Optional: Override the filename for the resume (e.g., "resume.pdf")
|
||||
*/
|
||||
resumeFilename?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "metadata_select".
|
||||
*/
|
||||
export interface MetadataSelect<T extends boolean = true> {
|
||||
tagline?: T;
|
||||
resume?: T;
|
||||
resumeFilename?: 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 {}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// storage-adapter-import-placeholder
|
||||
import { postgresAdapter } from "@payloadcms/db-postgres";
|
||||
import { payloadCloudPlugin } from "@payloadcms/payload-cloud";
|
||||
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
||||
import path from "path";
|
||||
import { buildConfig } from "payload";
|
||||
import { fileURLToPath } from "url";
|
||||
import sharp from "sharp";
|
||||
|
||||
import { Users } from "./collections/Users";
|
||||
import { Media } from "./collections/Media";
|
||||
import { Projects } from "./collections/Projects";
|
||||
import { Technologies } from "./collections/Technologies";
|
||||
import { Links } from "./collections/Links";
|
||||
import { Metadata } from "./globals/Metadata";
|
||||
import { env } from "./env/server.mjs";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
},
|
||||
collections: [Users, Media, Projects, Technologies, Links],
|
||||
globals: [Metadata],
|
||||
editor: lexicalEditor(),
|
||||
secret: env.PAYLOAD_SECRET,
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, "payload-types.ts"),
|
||||
},
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString: env.DATABASE_URI,
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
plugins: [
|
||||
payloadCloudPlugin(),
|
||||
// storage-adapter-placeholder
|
||||
],
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Custom colors */
|
||||
--color-zinc-850: #1d1d20;
|
||||
|
||||
/* Custom font sizes */
|
||||
--font-size-10xl: 10rem;
|
||||
|
||||
/* Drop shadows */
|
||||
--drop-shadow-extreme: 0 0 50px black;
|
||||
|
||||
/* Font families */
|
||||
--font-inter: "Inter", sans-serif;
|
||||
--font-roboto: "Roboto", sans-serif;
|
||||
--font-mono: "Roboto Mono", monospace;
|
||||
--font-hanken: "Hanken Grotesk", sans-serif;
|
||||
--font-schibsted: "Schibsted Grotesk", sans-serif;
|
||||
|
||||
/* Background images */
|
||||
--background-image-gradient-radial: radial-gradient(
|
||||
50% 50% at 50% 50%,
|
||||
var(--tw-gradient-stops)
|
||||
);
|
||||
|
||||
/* Animations */
|
||||
--animate-bg-fast: fade 0.5s ease-in-out 0.5s forwards;
|
||||
--animate-bg: fade 1.2s ease-in-out 1.1s forwards;
|
||||
--animate-fade-in: fade-in 2.5s ease-in-out forwards;
|
||||
--animate-title: title 3s ease-out forwards;
|
||||
--animate-fade-left: fade-left 3s ease-in-out forwards;
|
||||
--animate-fade-right: fade-right 3s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
opacity: 0%;
|
||||
}
|
||||
100% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0%;
|
||||
}
|
||||
75% {
|
||||
opacity: 0%;
|
||||
}
|
||||
100% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-left {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
opacity: 0%;
|
||||
}
|
||||
30% {
|
||||
transform: translateX(0%);
|
||||
opacity: 100%;
|
||||
}
|
||||
100% {
|
||||
opacity: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-right {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0%;
|
||||
}
|
||||
30% {
|
||||
transform: translateX(0%);
|
||||
opacity: 100%;
|
||||
}
|
||||
100% {
|
||||
opacity: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes title {
|
||||
0% {
|
||||
line-height: 0%;
|
||||
letter-spacing: 0.25em;
|
||||
opacity: 0;
|
||||
}
|
||||
25% {
|
||||
line-height: 0%;
|
||||
opacity: 0%;
|
||||
}
|
||||
80% {
|
||||
opacity: 100%;
|
||||
}
|
||||
100% {
|
||||
line-height: 100%;
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply font-inter overflow-x-hidden text-white;
|
||||
}
|
||||
|
||||
.description {
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.description {
|
||||
hyphens: none;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
@apply h-full;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
const isClient = (): boolean => {
|
||||
return typeof window !== "undefined";
|
||||
};
|
||||
|
||||
const isServer = (): boolean => {
|
||||
return !isClient();
|
||||
};
|
||||
|
||||
const hoverableQuery: MediaQueryList | null = isClient()
|
||||
? window.matchMedia("(hover: hover) and (pointer: fine)")
|
||||
: null;
|
||||
|
||||
export function isHoverable() {
|
||||
return hoverableQuery?.matches;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Github, ExternalLink, Link } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
// Promise.allSettled type guards
|
||||
export const isFulfilled = <T>(
|
||||
p: PromiseSettledResult<T>,
|
||||
): p is PromiseFulfilledResult<T> => p.status === "fulfilled";
|
||||
export const isRejected = <T>(
|
||||
p: PromiseSettledResult<T>,
|
||||
): p is PromiseRejectedResult => p.status === "rejected";
|
||||
|
||||
export const LinkIcons: Record<string, LucideIcon> = {
|
||||
github: Github,
|
||||
external: ExternalLink,
|
||||
link: Link,
|
||||
};
|
||||
export type LinkIcon = {
|
||||
icon: keyof typeof LinkIcons;
|
||||
location: string;
|
||||
newTab?: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user