chore: migrate from Tailwind v3 to v4 and upgrade dependencies

Major changes:
- Upgrade Tailwind CSS from v3 to v4 with @tailwindcss/postcss
- Remove deprecated Tailwind v3 configuration and PostCSS plugins
- Upgrade Zod from v3 to v4
- Update React Markdown and other dependencies to latest versions
- Remove @plaiceholder, @headlessui, and other unused dependencies
- Replace SCSS with standard CSS globals
- Add font packages (@fontsource-variable) for better typography
- Consolidate Prettier configuration to .prettierrc
- Remove legacy contact page and custom Payload SCSS
- Add Mantine hooks and Lucide React for improved UI components
This commit is contained in:
2025-10-26 01:32:57 -05:00
parent 0dcf6f93ba
commit da366b9538
22 changed files with 8023 additions and 5160 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}
+1 -6
View File
@@ -12,12 +12,7 @@ const compat = new FlatCompat({
const eslintConfig = [
...compat.extends("next/core-web-vitals"),
{
ignores: [
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
ignores: [".next/**", "out/**", "build/**", "next-env.d.ts"],
},
];
+4 -3
View File
@@ -1,8 +1,6 @@
import { withPayload } from "@payloadcms/next/withPayload";
// @ts-check
import withPlaiceholder from "@plaiceholder/next";
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds.
@@ -70,6 +68,9 @@ const config = {
},
],
},
turbopack: {
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
},
async redirects() {
// Source cannot end with / slash
return [
@@ -92,4 +93,4 @@ const config = {
];
},
};
export default withPayload(withPlaiceholder(config));
export default withPayload(config);
+19 -28
View File
@@ -1,6 +1,5 @@
{
"name": "xevion.dev",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
@@ -11,55 +10,47 @@
"start": "next start"
},
"dependencies": {
"@floating-ui/react": "^0.27.16",
"@headlessui/react": "^2.2.0",
"@kodingdotninja/use-tailwind-breakpoint": "^1.0.0",
"@fontsource-variable/inter": "^5.1.0",
"@fontsource-variable/roboto": "^5.1.0",
"@fontsource-variable/roboto-mono": "^5.1.0",
"@fontsource/hanken-grotesk": "^5.1.0",
"@mantine/hooks": "^8",
"@next/eslint-plugin-next": "^15.1.1",
"@octokit/core": "^6.1.2",
"@octokit/core": "^7.0.5",
"@payloadcms/db-postgres": "^3.61.1",
"@payloadcms/next": "^3.61.1",
"@payloadcms/payload-cloud": "^3.61.1",
"@payloadcms/richtext-lexical": "^3.61.1",
"@plaiceholder/next": "^3.0.0",
"@tailwindcss/typography": "^0.5.8",
"@tanstack/react-query": "^5.90",
"@vercel/analytics": "^1.5.0",
"clsx": "^2.1.1",
"cssnano": "^7.0.6",
"cssnano": "^7.1.1",
"graphql": "^16.11.0",
"lucide-react": "^0.548.0",
"next": "^15.5.6",
"p5i": "^0.6.0",
"payload": "^3.61.1",
"plaiceholder": "^3.0.0",
"prettier": "^3.4.2",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-icons": "^4.10.1",
"react-markdown": "^9.0.1",
"react-markdown": "^10.1.0",
"react-wrap-balancer": "^1",
"sass": "^1.56.2",
"sharp": "^0.34",
"superjson": "^2.2",
"tailwind-merge": "^2.6.0",
"usehooks-ts": "^3.1.1",
"zod": "^3.24.1"
"tailwind-merge": "^3.3.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.38.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.7",
"eslint": "^9.17.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^24.9.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"eslint": "^9.38.0",
"eslint-config-next": "^15.1.1",
"postcss": "^8",
"prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^3.4.1",
"typescript": "^5"
},
"ct3aMetadata": {
"initVersion": "6.11.3"
"prettier": "^3.6.2",
"tailwindcss": "^4",
"typescript": "^5.7.2"
},
"packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf"
}
+7145 -4553
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,7 +1,6 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
},
};
-4
View File
@@ -1,4 +0,0 @@
/** @type {import("prettier").Config} */
module.exports = {
plugins: [require.resolve("prettier-plugin-tailwindcss")],
};
-128
View File
@@ -1,128 +0,0 @@
"use client";
import AppWrapper from "@/components/AppWrapper";
import { BsDiscord, BsGithub } from "react-icons/bs";
import { AiFillMail } from "react-icons/ai";
import Link from "next/link";
import type { IconType } from "react-icons";
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
useHover,
useFocus,
useDismiss,
useRole,
useInteractions,
FloatingPortal,
} from "@floating-ui/react";
import { useState } from "react";
const socials: {
icon: IconType;
href?: string;
hint?: string;
hideHint?: boolean;
}[] = [
{
icon: BsGithub,
href: "https://github.com/Xevion/",
},
{
icon: AiFillMail,
href: "mailto:xevion@xevion.dev",
hint: "xevion@xevion.dev",
},
{
icon: BsDiscord,
hint: "Xevion#8506",
},
];
function SocialTooltip({
icon: Icon,
href,
hint,
hideHint,
}: {
icon: IconType;
href?: string;
hint?: string;
hideHint?: boolean;
}) {
const [isOpen, setIsOpen] = useState(false);
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: "top",
whileElementsMounted: autoUpdate,
middleware: [offset(10), flip(), shift()],
});
const hover = useHover(context);
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: "tooltip" });
const { getReferenceProps, getFloatingProps } = useInteractions([
hover,
focus,
dismiss,
role,
]);
const inner = <Icon className="h-8 w-8" />;
const tooltipContent = hint ?? href;
return (
<>
{href != undefined ? (
<Link
href={href}
ref={refs.setReference}
{...getReferenceProps()}
>
{inner}
</Link>
) : (
<span
ref={refs.setReference}
{...getReferenceProps()}
>
{inner}
</span>
)}
{!hideHint && isOpen && tooltipContent && (
<FloatingPortal>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
className="z-50 rounded bg-zinc-900 px-3 py-2 text-sm text-zinc-100 shadow-lg"
>
{tooltipContent}
</div>
</FloatingPortal>
)}
</>
);
}
export default function ContactPage() {
return (
<AppWrapper>
<div className="my-10 flex w-full flex-col items-center">
<div className="mx-3 flex w-full max-w-[23rem] flex-col rounded-md border border-zinc-800 bg-zinc-800/50 p-5 sm:max-w-[25rem] lg:max-w-[30rem]">
<div className="flex justify-center gap-x-5 text-center">
{socials.map((social, index) => (
<SocialTooltip key={index} {...social} />
))}
</div>
</div>
</div>
</AppWrapper>
);
}
+8 -1
View File
@@ -1,5 +1,12 @@
import React from "react";
import "@/styles/globals.scss";
// Fontsource imports
import "@fontsource-variable/inter";
import "@fontsource-variable/roboto";
import "@fontsource-variable/roboto-mono";
import "@fontsource/hanken-grotesk/900.css";
import "@/styles/globals.css";
import type { Metadata } from "next";
import { Providers } from "./providers";
+2 -1
View File
@@ -31,7 +31,8 @@ export default async function ProjectsPage() {
// 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;
const projectId =
typeof link.project === "number" ? link.project : link.project.id;
if (!linksByProject.has(projectId)) {
linksByProject.set(projectId, []);
}
View File
-1
View File
@@ -7,7 +7,6 @@ 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;
+1 -3
View File
@@ -224,9 +224,7 @@ export async function GET(req: Request) {
const urls = allLinks
.filter((link) => {
const projectId =
typeof link.project === "number"
? link.project
: link.project.id;
typeof link.project === "number" ? link.project : link.project.id;
return projectId === project.id;
})
.map((link) => link.url)
+5 -1
View File
@@ -41,7 +41,11 @@ const Dots = ({ className }: DotsProps) => {
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) {
for (
let y = -SPACING / 2;
y < h.current + offsetY + SPACING;
y += SPACING
) {
const id = `${x}-${y}`;
if (pointIds.has(id)) continue;
pointIds.add(id);
-121
View File
@@ -1,121 +0,0 @@
import React, { useRef } from "react";
import { useOnClickOutside, useToggle } from "usehooks-ts";
import { cn, isHoverable } from "@/utils/helpers";
import ReactMarkdown from "react-markdown";
import Balancer from "react-wrap-balancer";
import Link from "next/link";
import { useRouter } from "next/router";
import { type LinkIcon, LinkIcons } from "@/utils/types";
import DependentImage from "@/components/DependentImage";
type ItemCardProps = {
banner: string;
bannerSettings?: { quality: number };
title: string;
description: string;
links?: LinkIcon[];
location: string;
};
const ItemCard = ({
banner,
title,
description,
links,
location,
bannerSettings,
}: ItemCardProps) => {
const itemRef = useRef<HTMLDivElement>(null);
const mobileButtonRef = useRef<HTMLAnchorElement>(null);
const [active, toggleActive, setActive] = useToggle();
const router = useRouter();
// @ts-expect-error Some kind of regression in usehooks-ts causes the useOnClickOutside hook to not accept 'null' types
useOnClickOutside(itemRef, (event) => {
if (
mobileButtonRef.current != null &&
mobileButtonRef.current?.contains(event.target as Node)
)
return;
else setActive(false);
});
const navigate = () => {
if (!isHoverable()) toggleActive();
else {
router.push(location);
}
};
return (
<>
<div
ref={itemRef}
className={cn(
"item [&:not(:first-child)]:mt-3",
active ? "active" : null,
)}
onClick={navigate}
>
<DependentImage
fill
src={banner}
quality={bannerSettings?.quality ?? 75}
className={(loaded) => cn("object-cover", loaded ? null : "blur-xl")}
alt={`Banner for ${title}`}
/>
<div className="elements m-2 grid h-full grid-cols-12 px-1 sm:px-4">
<div className="col-span-12 max-h-full overflow-hidden pb-2 pl-2 drop-shadow-2xl sm:col-span-9 md:p-1 lg:col-span-8">
<Link
href={{ pathname: location }}
className="font-roboto text-lg font-semibold sm:text-2xl md:text-3xl"
>
{title}
</Link>
<div
className="description mt-0 overflow-hidden text-base font-light sm:text-xl md:mt-1.5 md:text-xl"
onClick={(e) => {
e.stopPropagation();
navigate();
}}
>
<Balancer>
<ReactMarkdown>{description}</ReactMarkdown>
</Balancer>
</div>
</div>
{(links?.length ?? 0) > 0 ? (
<div className="col-span-3 hidden max-h-full w-full justify-end sm:flex md:py-5 lg:col-span-4">
<div className="icon-grid grid aspect-square grid-cols-2 grid-rows-2 p-2 md:gap-3">
{links!.map(({ icon, location, newTab }) => (
<Link
key={location}
href={location}
target={(newTab ?? true) ? "_blank" : "_self"}
onClick={(e) => e.stopPropagation()}
>
{LinkIcons[icon]?.({})}
</Link>
))}
</div>
</div>
) : null}
</div>
</div>
<Link
aria-disabled={!active}
ref={mobileButtonRef}
href={active ? { pathname: location } : {}}
className={cn(
"flex w-full items-center justify-center rounded border border-zinc-900 bg-zinc-800 shadow transition-all",
active ? "h-9 p-2 opacity-100" : "h-0 p-0 opacity-0",
)}
>
Learn More
</Link>
</>
);
};
export default ItemCard;
+617
View File
@@ -0,0 +1,617 @@
/* 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 -1
View File
@@ -1,5 +1,5 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
+119
View File
@@ -0,0 +1,119 @@
@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;
/* 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;
}
-76
View File
@@ -1,76 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@900&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@200;300;400;500;700;900&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
// Used for ItemCard
@mixin active {
.elements {
@apply grid opacity-100;
}
> img {
@apply blur-2xl;
}
}
html,
body {
@apply overflow-x-hidden font-inter text-white;
}
.item {
@apply pointer-events-auto relative aspect-[7/2] w-full cursor-pointer overflow-hidden rounded transition-all sm:h-[14rem] md:h-[16rem] lg:aspect-[5/3];
> img {
@apply rounded transition-all;
}
.elements {
@apply hidden opacity-0 transition-all delay-100;
> * {
// z-index: 30;
min-height: 0;
}
}
@media (hover: hover) and (pointer: fine) {
&:hover {
@include active;
}
}
&.active {
@include active;
}
}
.icon-grid {
direction: rtl;
@apply max-h-full min-h-0 min-w-0 max-w-full;
> a > svg {
@apply h-full w-full;
}
> svg,
a {
width: 75%;
height: 75%;
@apply m-auto aspect-square text-white opacity-80 drop-shadow-md transition-transform hover:scale-[120%] hover:opacity-100;
}
}
.description {
hyphens: auto;
@screen md {
hyphens: none;
}
}
body {
@apply h-full;
}
-8
View File
@@ -1,7 +1,3 @@
import create from "@kodingdotninja/use-tailwind-breakpoint";
import resolveConfig from "tailwindcss/resolveConfig";
import tailwindConfig from "@/../tailwind.config.cjs";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
@@ -24,7 +20,3 @@ const hoverableQuery: MediaQueryList | null = isClient()
export function isHoverable() {
return hoverableQuery?.matches;
}
const config = resolveConfig(tailwindConfig);
export const { useBreakpoint, useBreakpointValue, useBreakpointEffect } =
create(config.theme!.screens);
+6 -7
View File
@@ -1,6 +1,5 @@
import type { IconType } from "react-icons";
import { AiFillGithub, AiOutlineLink } from "react-icons/ai";
import { RxOpenInNewWindow } from "react-icons/rx";
import { Github, ExternalLink, Link } from "lucide-react";
import type { LucideIcon } from "lucide-react";
// Promise.allSettled type guards
export const isFulfilled = <T>(
@@ -10,10 +9,10 @@ export const isRejected = <T>(
p: PromiseSettledResult<T>,
): p is PromiseRejectedResult => p.status === "rejected";
export const LinkIcons: Record<string, IconType> = {
github: AiFillGithub,
external: RxOpenInNewWindow,
link: AiOutlineLink,
export const LinkIcons: Record<string, LucideIcon> = {
github: Github,
external: ExternalLink,
link: Link,
};
export type LinkIcon = {
icon: keyof typeof LinkIcons;
-126
View File
@@ -1,126 +0,0 @@
const defaultTheme = require("tailwindcss/defaultTheme");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
zinc: {
850: "#1D1D20",
},
},
fontSize: {
"10xl": "10rem",
},
typography: {
DEFAULT: {
css: {
"code::before": {
content: '""',
},
"code::after": {
content: '""',
},
},
},
quoteless: {
css: {
"blockquote p:first-of-type::before": { content: "none" },
"blockquote p:first-of-type::after": { content: "none" },
},
},
},
dropShadow: {
extreme: "0 0 50px black",
},
fontFamily: {
inter: ['"Inter"', "sans-serif"],
roboto: ['"Roboto"', "sans-serif"],
mono: ['"Roboto Mono"', "monospace"],
hanken: ['"Hanken Grotesk"', "sans-serif"],
},
backgroundImage: {
"gradient-radial":
"radial-gradient(50% 50% at 50% 50%, var(--tw-gradient-stops))",
},
animation: {
"bg-fast": "fade 0.5s ease-in-out 0.5s forwards",
bg: "fade 1.2s ease-in-out 1.1s forwards",
"fade-in": "fade-in 2.5s ease-in-out forwards",
title: "title 3s ease-out forwards",
"fade-left": "fade-left 3s ease-in-out forwards",
"fade-right": "fade-right 3s ease-in-out forwards",
},
keyframes: {
fade: {
"0%": {
opacity: "0%",
},
"100%": {
opacity: "100%",
},
},
"fade-in": {
"0%": {
opacity: "0%",
},
"75%": {
opacity: "0%",
},
"100%": {
opacity: "100%",
},
},
"fade-left": {
"0%": {
transform: "translateX(100%)",
opacity: "0%",
},
"30%": {
transform: "translateX(0%)",
opacity: "100%",
},
"100%": {
opacity: "0%",
},
},
"fade-right": {
"0%": {
transform: "translateX(-100%)",
opacity: "0%",
},
"30%": {
transform: "translateX(0%)",
opacity: "100%",
},
"100%": {
opacity: "0%",
},
},
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%",
},
},
},
},
},
plugins: [require("@tailwindcss/typography")],
};