Prettier format global

This commit is contained in:
2024-12-18 14:43:32 -06:00
parent 05a6d18858
commit c9b240c6b4
20 changed files with 1300 additions and 997 deletions

View File

@@ -35,7 +35,7 @@ const v2_redirects = [
return { return {
source: url, source: url,
destination: `https://undefined.behavio.rs/posts${url.slice( destination: `https://undefined.behavio.rs/posts${url.slice(
nthIndex(url, "/", 4) nthIndex(url, "/", 4),
)}`, )}`,
permanent: false, permanent: false,
}; };

View File

@@ -23,10 +23,11 @@
"@vercel/analytics": "^0.1.6", "@vercel/analytics": "^0.1.6",
"next": "^15.1.1", "next": "^15.1.1",
"plaiceholder": "^2.5.0", "plaiceholder": "^2.5.0",
"prettier": "^3.4.2",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^4.10.1", "react-icons": "^4.10.1",
"react-markdown": "^8.0.4", "react-markdown": "^9.0.1",
"react-ogp": "^0.0.3", "react-ogp": "^0.0.3",
"react-wrap-balancer": "^0.4.0", "react-wrap-balancer": "^0.4.0",
"sass": "^1.56.2", "sass": "^1.56.2",
@@ -36,17 +37,17 @@
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@eslint/eslintrc": "^3",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"postcss": "^8", "autoprefixer": "^10.4.7",
"tailwindcss": "^3.4.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.1.1", "eslint-config-next": "15.1.1",
"@eslint/eslintrc": "^3", "postcss": "^8",
"autoprefixer": "^10.4.7", "prettier-plugin-tailwindcss": "^0.6.9",
"prettier-plugin-tailwindcss": "^0.1.13" "tailwindcss": "^3.4.1",
"typescript": "^5"
}, },
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "6.11.3" "initVersion": "6.11.3"

View File

@@ -1,14 +1,14 @@
import type { FunctionComponent, ReactNode } from "react"; import type { FunctionComponent, ReactNode } from "react";
import {Disclosure} from '@headlessui/react' import { Disclosure } from "@headlessui/react";
import { HiBars3, HiXMark } from "react-icons/hi2"; import { HiBars3, HiXMark } from "react-icons/hi2";
import { classNames } from "../utils/helpers"; import { classNames } from "../utils/helpers";
import Link from "next/link"; import Link from "next/link";
const navigation: { id: string, name: string, href: string }[] = [ const navigation: { id: string; name: string; href: string }[] = [
{id: 'home', name: 'Home', href: '/',}, { id: "home", name: "Home", href: "/" },
{id: 'projects', name: 'Projects', href: '/projects'}, { id: "projects", name: "Projects", href: "/projects" },
{id: 'contact', name: 'Contact', href: '/contact'}, { id: "contact", name: "Contact", href: "/contact" },
] ];
type WrapperProps = { type WrapperProps = {
className?: string; className?: string;
@@ -17,17 +17,28 @@ type WrapperProps = {
children?: ReactNode | ReactNode[] | null; children?: ReactNode | ReactNode[] | null;
}; };
const AppWrapper: FunctionComponent<WrapperProps> = ({current, children, hideNavigation, className}: WrapperProps) => { const AppWrapper: FunctionComponent<WrapperProps> = ({
return <main className={classNames("body-gradient min-h-screen text-zinc-50", className)}> current,
{!hideNavigation ? <Disclosure as="nav"> children,
hideNavigation,
className,
}: WrapperProps) => {
return (
<main
className={classNames(
"body-gradient min-h-screen text-zinc-50",
className,
)}
>
{!hideNavigation ? (
<Disclosure as="nav">
{({ open }) => ( {({ open }) => (
<> <>
<div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
<div className="relative flex h-16 items-center justify-between"> <div className="relative flex h-16 items-center justify-between">
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden"> <div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
{/* Mobile menu button*/} {/* Mobile menu button*/}
<Disclosure.Button <Disclosure.Button className="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-zinc-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
className="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-zinc-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white">
<span className="sr-only">Open main menu</span> <span className="sr-only">Open main menu</span>
{open ? ( {open ? (
<HiXMark className="block h-6 w-6" aria-hidden="true" /> <HiXMark className="block h-6 w-6" aria-hidden="true" />
@@ -38,16 +49,20 @@ const AppWrapper: FunctionComponent<WrapperProps> = ({current, children, hideNav
</div> </div>
<div className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start"> <div className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div className="hidden sm:ml-6 sm:block"> <div className="hidden sm:ml-6 sm:block">
<div className="flex space-x-4 text-lg font-roboto "> <div className="flex space-x-4 font-roboto text-lg">
{navigation.map((item) => ( {navigation.map((item) => (
<Link <Link
key={item.name} key={item.name}
href={item.href} href={item.href}
className={classNames( className={classNames(
item.id == current ? 'bg-zinc-900 text-white' : 'text-gray-300 hover:bg-zinc-800/60 hover:text-white', item.id == current
'px-2.5 py-1 rounded-md' ? "bg-zinc-900 text-white"
: "text-gray-300 hover:bg-zinc-800/60 hover:text-white",
"rounded-md px-2.5 py-1",
)} )}
aria-current={item.id == current ? 'page' : undefined} aria-current={
item.id == current ? "page" : undefined
}
> >
{item.name} {item.name}
</Link> </Link>
@@ -59,17 +74,18 @@ const AppWrapper: FunctionComponent<WrapperProps> = ({current, children, hideNav
</div> </div>
<Disclosure.Panel className="sm:hidden"> <Disclosure.Panel className="sm:hidden">
<div className="space-y-1 px-2 pt-2 pb-3"> <div className="space-y-1 px-2 pb-3 pt-2">
{navigation.map((item) => ( {navigation.map((item) => (
<Link key={item.name} href={item.href}> <Link key={item.name} href={item.href}>
<Disclosure.Button <Disclosure.Button
as="a" as="a"
className={classNames( className={classNames(
item.id == current ? 'bg-zinc-900 text-white' : 'text-gray-300 hover:bg-zinc-700 hover:text-white', item.id == current
'block px-3 py-2 rounded-md text-base font-medium' ? "bg-zinc-900 text-white"
: "text-gray-300 hover:bg-zinc-700 hover:text-white",
"block rounded-md px-3 py-2 text-base font-medium",
)} )}
aria-current={item.id == current ? 'page' : undefined} aria-current={item.id == current ? "page" : undefined}
> >
{item.name} {item.name}
</Disclosure.Button> </Disclosure.Button>
@@ -79,9 +95,11 @@ const AppWrapper: FunctionComponent<WrapperProps> = ({current, children, hideNav
</Disclosure.Panel> </Disclosure.Panel>
</> </>
)} )}
</Disclosure> : null} </Disclosure>
) : null}
{children} {children}
</main> </main>
} );
};
export default AppWrapper; export default AppWrapper;

View File

@@ -3,24 +3,28 @@ import {useMemo, useState} from "react";
type DependentProps = { type DependentProps = {
className?: string | ((loaded: boolean) => string); className?: string | ((loaded: boolean) => string);
} };
type DependentImageProps = Omit<ImageProps, "className"> & DependentProps; type DependentImageProps = Omit<ImageProps, "className"> & DependentProps;
const DependentImage = (props: DependentImageProps) => { const DependentImage = (props: DependentImageProps) => {
const [loaded, setLoaded] = useState(false) const [loaded, setLoaded] = useState(false);
const { className } = props; const { className } = props;
const renderedClassName = useMemo(() => { const renderedClassName = useMemo(() => {
if (className === undefined) return ""; if (className === undefined) return "";
if (typeof className === "function") return className(loaded); if (typeof className === "function") return className(loaded);
return className; return className;
}, [loaded, className]) }, [loaded, className]);
return <Image {...props} return (
<Image
{...props}
className={renderedClassName} className={renderedClassName}
onLoadingComplete={() => { onLoadingComplete={() => {
setLoaded(true) setLoaded(true);
}}/> }}
} />
);
};
export default DependentImage; export default DependentImage;

View File

@@ -2,7 +2,7 @@ import React, {useRef} from "react";
import { useOnClickOutside, useToggle } from "usehooks-ts"; import { useOnClickOutside, useToggle } from "usehooks-ts";
import { classNames, isHoverable } from "../utils/helpers"; import { classNames, isHoverable } from "../utils/helpers";
import DependentImage from "./DependentImage"; import DependentImage from "./DependentImage";
import ReactMarkdown from 'react-markdown' import ReactMarkdown from "react-markdown";
import Balancer from "react-wrap-balancer"; import Balancer from "react-wrap-balancer";
import Link from "next/link"; import Link from "next/link";
@@ -17,76 +17,110 @@ type ItemCardProps = {
description: string; description: string;
links?: LinkIcon[]; links?: LinkIcon[];
location: string; location: string;
} };
const ItemCard = ({banner, bannerBlur, title, description, links, location, bannerSettings}: ItemCardProps) => { const ItemCard = ({
banner,
bannerBlur,
title,
description,
links,
location,
bannerSettings,
}: ItemCardProps) => {
const itemRef = useRef<HTMLDivElement>(null); const itemRef = useRef<HTMLDivElement>(null);
const mobileButtonRef = useRef<HTMLAnchorElement>(null); const mobileButtonRef = useRef<HTMLAnchorElement>(null);
const [active, toggleActive, setActive] = useToggle() const [active, toggleActive, setActive] = useToggle();
const router = useRouter(); const router = useRouter();
useOnClickOutside(itemRef, (event) => { useOnClickOutside(itemRef, (event) => {
if (mobileButtonRef.current != null && mobileButtonRef.current?.contains(event.target as Node)) if (
mobileButtonRef.current != null &&
mobileButtonRef.current?.contains(event.target as Node)
)
return; return;
else else setActive(false);
setActive(false); });
})
const navigate = () => { const navigate = () => {
if (!isHoverable()) toggleActive(); if (!isHoverable()) toggleActive();
else { else {
router.push(location); router.push(location);
} }
} };
return <> return (
<div ref={itemRef} <>
className={classNames("item [&:not(:first-child)]:mt-3", active ? "active" : null)} <div
onClick={navigate}> ref={itemRef}
<DependentImage fill src={banner} className={classNames(
"item [&:not(:first-child)]:mt-3",
active ? "active" : null,
)}
onClick={navigate}
>
<DependentImage
fill
src={banner}
quality={bannerSettings?.quality ?? 75} quality={bannerSettings?.quality ?? 75}
blurDataURL={bannerBlur} blurDataURL={bannerBlur}
className={(loaded) => classNames("object-cover", loaded ? null : "blur-xl")} className={(loaded) =>
classNames("object-cover", loaded ? null : "blur-xl")
}
placeholder="blur" placeholder="blur"
alt={`Banner for ${title}`} alt={`Banner for ${title}`}
/> />
<div className="elements grid grid-cols-12 h-full m-2 px-1 sm:px-4"> <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 <div
className="col-span-12 sm:col-span-9 lg:col-span-8 max-h-full overflow-hidden drop-shadow-2xl pb-2 md:p-1 pl-2"> className="description mt-0 overflow-hidden text-base font-light sm:text-xl md:mt-1.5 md:text-xl"
<Link href={{pathname: location}}
className="text-lg sm:text-2xl md:text-3xl font-semibold font-roboto">{title}</Link>
<div className="description mt-0 md:mt-1.5 text-base sm:text-xl md:text-xl font-light overflow-hidden"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate(); navigate();
}}> }}
>
<Balancer> <Balancer>
<ReactMarkdown>{description}</ReactMarkdown> <ReactMarkdown>{description}</ReactMarkdown>
</Balancer> </Balancer>
</div> </div>
</div> </div>
{(links?.length ?? 0) > 0 ? {(links?.length ?? 0) > 0 ? (
<div <div className="col-span-3 hidden max-h-full w-full justify-end sm:flex md:py-5 lg:col-span-4">
className="hidden sm:flex col-span-3 lg:col-span-4 w-full justify-end max-h-full md:py-5"> <div className="icon-grid grid aspect-square grid-cols-2 grid-rows-2 p-2 md:gap-3">
<div className="grid grid-cols-2 grid-rows-2 p-2 md:gap-3 aspect-square icon-grid"> {links!.map(({ icon, location, newTab }) => (
{links!.map(({icon, location, newTab}) => <Link
<Link key={location} href={location} target={(newTab ?? true) ? "_blank" : "_self"} key={location}
onClick={e => e.stopPropagation()}> href={location}
target={(newTab ?? true) ? "_blank" : "_self"}
onClick={(e) => e.stopPropagation()}
>
{LinkIcons[icon]?.({})} {LinkIcons[icon]?.({})}
</Link>)} </Link>
))}
</div> </div>
</div> : null}
</div> </div>
) : null}
</div> </div>
<Link aria-disabled={!active} ref={mobileButtonRef} href={active ? {pathname: location} : {}} </div>
<Link
aria-disabled={!active}
ref={mobileButtonRef}
href={active ? { pathname: location } : {}}
className={classNames( className={classNames(
"transition-all bg-zinc-800 rounded border border-zinc-900 shadow w-full flex items-center justify-center", "flex w-full items-center justify-center rounded border border-zinc-900 bg-zinc-800 shadow transition-all",
active ? "opacity-100 h-9 p-2" : "opacity-0 h-0 p-0" active ? "h-9 p-2 opacity-100" : "h-0 p-0 opacity-0",
)}> )}
>
Learn More Learn More
</Link> </Link>
</> </>
} );
};
export default ItemCard; export default ItemCard;

View File

@@ -2,17 +2,18 @@ import {FunctionComponent} from "react";
type SteppedSpanProps = { type SteppedSpanProps = {
children: string; children: string;
} };
const SteppedSpan: FunctionComponent<SteppedSpanProps> = ({children}: SteppedSpanProps) => { const SteppedSpan: FunctionComponent<SteppedSpanProps> = ({
return <div className="stepped"> children,
}: SteppedSpanProps) => {
{children.split('').map((char: string, index) => { return (
return <span key={index}> <div className="stepped">
{char} {children.split("").map((char: string, index) => {
</span> return <span key={index}>{char}</span>;
})} })}
</div> </div>
} );
};
export default SteppedSpan; export default SteppedSpan;

View File

@@ -1,19 +1,21 @@
import type { FunctionComponent } from "react"; import type { FunctionComponent } from "react";
const WorkInProgress: FunctionComponent = () => { const WorkInProgress: FunctionComponent = () => {
return <div className="w-full my-10 flex flex-col items-center"> return (
<div <div className="my-10 flex w-full flex-col items-center">
className="bg-zinc-800/30 border border-zinc-700 rounded-md max-w-[23rem] sm:max-w-[25rem] lg:max-w-[30rem] mx-3 w-full p-5 flex flex-col items-center justify-center"> <div className="mx-3 flex w-full max-w-[23rem] flex-col items-center justify-center rounded-md border border-zinc-700 bg-zinc-800/30 p-5 sm:max-w-[25rem] lg:max-w-[30rem]">
<span className="bg-gradient-to-r from-orange-500 via-fuchsia-600 to-cyan-500 text-transparent font-semibold bg-clip-text text-3xl sm:text-4xl pb-2 text-center"> <span className="bg-gradient-to-r from-orange-500 via-fuchsia-600 to-cyan-500 bg-clip-text pb-2 text-center text-3xl font-semibold text-transparent sm:text-4xl">
Work In Progress Work In Progress
</span> </span>
<p className="text-lg text-center"> <p className="text-center text-lg">
This website is a work in-progress. This website is a work in-progress.
<br /> <br />
Unfortunately, this page hasn&apos;t been finished yet. Check back later. Unfortunately, this page hasn&apos;t been finished yet. Check back
later.
</p> </p>
</div> </div>
</div> </div>
} );
};
export default WorkInProgress; export default WorkInProgress;

View File

@@ -1,12 +1,26 @@
import {Html, Head, Main, NextScript} from 'next/document' import { Html, Head, Main, NextScript } from "next/document";
export default function Document() { export default function Document() {
return ( return (
<Html> <Html>
<Head> <Head>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> rel="apple-touch-icon"
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
</Head> </Head>
@@ -15,5 +29,5 @@ export default function Document() {
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
) );
} }

View File

@@ -5,42 +5,52 @@ import {AiFillMail} from "react-icons/ai";
import Link from "next/link"; import Link from "next/link";
import type { IconType } from "react-icons"; import type { IconType } from "react-icons";
import Tippy from "@tippyjs/react"; import Tippy from "@tippyjs/react";
import 'tippy.js/dist/tippy.css'; import "tippy.js/dist/tippy.css";
const socials: { icon: IconType, href?: string, hint?: string, hideHint?: boolean }[] = [ const socials: {
icon: IconType;
href?: string;
hint?: string;
hideHint?: boolean;
}[] = [
{ {
icon: BsGithub, icon: BsGithub,
href: "https://github.com/Xevion/" href: "https://github.com/Xevion/",
}, },
{ {
icon: AiFillMail, icon: AiFillMail,
href: "mailto:xevion@xevion.dev", href: "mailto:xevion@xevion.dev",
hint: "xevion@xevion.dev" hint: "xevion@xevion.dev",
}, },
{ {
icon: BsDiscord, icon: BsDiscord,
hint: "Xevion#8506" hint: "Xevion#8506",
} },
] ];
const ContactPage: NextPage = () => { const ContactPage: NextPage = () => {
return <AppWrapper current='contact'> return (
<div className="w-full my-10 flex flex-col items-center"> <AppWrapper current="contact">
<div <div className="my-10 flex w-full flex-col items-center">
className="bg-zinc-800/50 border border-zinc-800 rounded-md max-w-[23rem] sm:max-w-[25rem] lg:max-w-[30rem] mx-3 w-full p-5 flex flex-col"> <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"> <div className="flex justify-center gap-x-5 text-center">
{socials.map(({ icon: Icon, href, hint, hideHint }, index) => { {socials.map(({ icon: Icon, href, hint, hideHint }, index) => {
const inner = <Icon className="w-8 h-8"/>; const inner = <Icon className="h-8 w-8" />;
return <Tippy key={index} disabled={hideHint} content={hint ?? href}> return (
{ <Tippy key={index} disabled={hideHint} content={hint ?? href}>
href != undefined ? <Link href={href}>{inner}</Link> : <span>{inner}</span> {href != undefined ? (
} <Link href={href}>{inner}</Link>
) : (
<span>{inner}</span>
)}
</Tippy> </Tippy>
);
})} })}
</div> </div>
</div> </div>
</div> </div>
</AppWrapper> </AppWrapper>
} );
};
export default ContactPage; export default ContactPage;

View File

@@ -11,67 +11,92 @@ type Screenshot = [string, null | string | ReactNode];
type ScreenshotWithQuality = [string, null | string | ReactNode, number]; type ScreenshotWithQuality = [string, null | string | ReactNode, number];
const images: (Screenshot | ScreenshotWithQuality)[] = [ const images: (Screenshot | ScreenshotWithQuality)[] = [
["/grain/index.jpg", null, 100], ["/grain/index.jpg", null, 100],
["/grain/hidden.jpg", null, 100] ["/grain/hidden.jpg", null, 100],
] ];
const GrainPage: NextPage = () => { const GrainPage: NextPage = () => {
return <> return (
<>
<Head> <Head>
<title>Grain | Xevion.dev</title> <title>Grain | Xevion.dev</title>
</Head> </Head>
<AppWrapper> <AppWrapper>
<div className="w-full overflow-auto h-full min-h-screen flex justify-center"> <div className="flex h-full min-h-screen w-full justify-center overflow-auto">
<div className="relative my-10 p-3 px-6 w-full max-w-screen-md"> <div className="relative my-10 w-full max-w-screen-md p-3 px-6">
<div className="pb-2 flex justify-between"> <div className="flex justify-between pb-2">
<div className="text-3xl font-semibold"> <div className="text-3xl font-semibold">Grain</div>
Grain
</div>
<div className="flex items-center justify-end space-x-1.5"> <div className="flex items-center justify-end space-x-1.5">
<Link href="https://grain.xevion.dev" target="_blank"> <Link href="https://grain.xevion.dev" target="_blank">
<RxOpenInNewWindow className="w-6 h-6 hover:text-zinc-200"/> <RxOpenInNewWindow className="h-6 w-6 hover:text-zinc-200" />
</Link> </Link>
<Link href="https://github.com/Xevion/grain" target="_blank"> <Link href="https://github.com/Xevion/grain" target="_blank">
<BsGithub className="w-6 h-6 hover:text-zinc-200"/> <BsGithub className="h-6 w-6 hover:text-zinc-200" />
</Link> </Link>
</div> </div>
</div> </div>
<div className="relative"> <div className="relative">
<Link href="https://grain.xevion.dev/"> <Link href="https://grain.xevion.dev/">
<Image fill quality={100} sizes="100vw" src="/grain/banner.jpeg" alt="" <Image
className="!relative pointer-events-none min-h-[10rem] rounded-md object-cover"/> fill
quality={100}
sizes="100vw"
src="/grain/banner.jpeg"
alt=""
className="pointer-events-none !relative min-h-[10rem] rounded-md object-cover"
/>
</Link> </Link>
</div> </div>
<div className="mt-3 w-full prose prose-invert prose-lg"> <div className="prose prose-lg prose-invert mt-3 w-full">
<p> <p>
After seeing an online post with beautiful noise patterns & gradients, I decided to After seeing an online post with beautiful noise patterns &
try and recreate it. The result was Grain, a simple web app that generates beautiful noise. gradients, I decided to try and recreate it. The result was
Under the hood, this app uses multiple layers of SVGs that automatically rescale with the browsers viewport. Grain, a simple web app that generates beautiful noise. Under
That way, the noise is always crisp and clear, no matter the screen size. the hood, this app uses multiple layers of SVGs that
automatically rescale with the browsers viewport. That way, the
noise is always crisp and clear, no matter the screen size.
</p> </p>
<ul className="md:columns-2"> <ul className="md:columns-2">
<li>Performant - SVG generation and layering is optimized</li> <li>Performant - SVG generation and layering is optimized</li>
<li>Small - Builds in less than 16 seconds</li> <li>Small - Builds in less than 16 seconds</li>
<li>Open Source - Want to use my gradients? Check it out on <Link href="https://github.com/Xevion/grain" target="_blank">GitHub</Link>.</li> <li>
Open Source - Want to use my gradients? Check it out on{" "}
<Link href="https://github.com/Xevion/grain" target="_blank">
GitHub
</Link>
.
</li>
</ul> </ul>
<h3>Screenshots</h3> <h3>Screenshots</h3>
<div className="relative space-y-8"> <div className="relative space-y-8">
{images.map(([src, description, quality]) => { {images.map(([src, description, quality]) => {
return <div key={src} className="flex flex-col justify-center w-full"> return (
<Image fill sizes="100vw" src={src} alt="" quality={quality ?? 75} <div
className="shadow-lg !my-1 !relative pointer-events-none min-h-[10rem] rounded-md object-cover"/> key={src}
{description != null ? className="flex w-full flex-col justify-center"
<span >
className="text-center text-base">{description}</span> : null} <Image
fill
sizes="100vw"
src={src}
alt=""
quality={quality ?? 75}
className="pointer-events-none !relative !my-1 min-h-[10rem] rounded-md object-cover shadow-lg"
/>
{description != null ? (
<span className="text-center text-base">
{description}
</span>
) : null}
</div> </div>
} );
)} })}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</AppWrapper> </AppWrapper>
</> </>
} );
};
export default GrainPage; export default GrainPage;

View File

@@ -104,7 +104,7 @@ export async function getStaticProps() {
...project, ...project,
bannerBlur: base64, bannerBlur: base64,
}; };
}) }),
), ),
}, },
}; };

View File

@@ -3,9 +3,11 @@ import AppWrapper from "../components/AppWrapper";
import WorkInProgress from "../components/WorkInProgress"; import WorkInProgress from "../components/WorkInProgress";
const PathsPage: NextPage = () => { const PathsPage: NextPage = () => {
return <AppWrapper> return (
<AppWrapper>
<WorkInProgress /> <WorkInProgress />
</AppWrapper> </AppWrapper>
} );
};
export default PathsPage; export default PathsPage;

View File

@@ -6,39 +6,45 @@ import Link from "next/link";
import AppWrapper from "../components/AppWrapper"; import AppWrapper from "../components/AppWrapper";
const PhototagPage: NextPage = () => { const PhototagPage: NextPage = () => {
return <> return (
<>
<Head> <Head>
<title>Phototag | Xevion.dev</title> <title>Phototag | Xevion.dev</title>
</Head> </Head>
<AppWrapper> <AppWrapper>
<div className="w-full overflow-auto h-full min-h-screen flex justify-center"> <div className="flex h-full min-h-screen w-full justify-center overflow-auto">
<div className="relative my-10 p-3 px-6 w-full max-w-screen-md"> <div className="relative my-10 w-full max-w-screen-md p-3 px-6">
<div className="pb-2 flex justify-between"> <div className="flex justify-between pb-2">
<div className="text-2xl font-semibold"> <div className="text-2xl font-semibold">Phototag</div>
Phototag
</div>
<div className="flex items-center justify-end space-x-1.5"> <div className="flex items-center justify-end space-x-1.5">
<Link href="https://github.com/Xevion/phototag" target="_blank"> <Link href="https://github.com/Xevion/phototag" target="_blank">
<BsGithub className="w-5 h-5 hover:text-zinc-200"/> <BsGithub className="h-5 w-5 hover:text-zinc-200" />
</Link> </Link>
</div> </div>
</div> </div>
<div className="relative"> <div className="relative">
<Image fill sizes="100vw" src="/phototag.png" alt="" <Image
className="!relative pointer-events-none min-h-[10rem] rounded-md object-cover"/> fill
sizes="100vw"
src="/phototag.png"
alt=""
className="pointer-events-none !relative min-h-[10rem] rounded-md object-cover"
/>
</div> </div>
<div className="mt-3 w-full prose prose-invert prose-lg"> <div className="prose prose-lg prose-invert mt-3 w-full">
<p> <p>
Phototag is a powerful tool that helps you quickly and easily add rich, descriptive tags to Phototag is a powerful tool that helps you quickly and easily
your photos. Using Google&apos;s Vision API, Phototag automatically generates tags based on add rich, descriptive tags to your photos. Using Google&apos;s
the visual content of your photos, making it easier than ever to organize and find your Vision API, Phototag automatically generates tags based on the
photos. visual content of your photos, making it easier than ever to
organize and find your photos.
</p> </p>
<p> <p>
With support for IPTC metadata and Adobe XMP Sidecar files, you can easily integrate With support for IPTC metadata and Adobe XMP Sidecar files, you
Phototag into your existing workflow on Windows. Whether you&apos;re a professional can easily integrate Phototag into your existing workflow on
photographer or a casual snapshot taker, Phototag is the perfect tool for adding clarity and Windows. Whether you&apos;re a professional photographer or a
context to your photos. casual snapshot taker, Phototag is the perfect tool for adding
clarity and context to your photos.
</p> </p>
<ul className="md:columns-2"> <ul className="md:columns-2">
<li>Simple, but configurable</li> <li>Simple, but configurable</li>
@@ -52,8 +58,8 @@ const PhototagPage: NextPage = () => {
</div> </div>
</div> </div>
</AppWrapper> </AppWrapper>
</> </>
} );
};
export default PhototagPage; export default PhototagPage;

View File

@@ -8,48 +8,71 @@ import type {ReactNode} from "react";
const images: [string, string | ReactNode][] = [ const images: [string, string | ReactNode][] = [
["/portal/home.jpeg", "The home page."], ["/portal/home.jpeg", "The home page."],
["/portal/events.png", <> A page listing all current events. <br/> Initial data is cached for performance, but [
becomes "/portal/events.png",
dynamic when filtered.</>], <>
["/portal/admin.png", "A secure admin panel for our officers to view, filter & edit members & events."], {" "}
A page listing all current events. <br /> Initial data is cached for
performance, but becomes dynamic when filtered.
</>,
],
[
"/portal/admin.png",
"A secure admin panel for our officers to view, filter & edit members & events.",
],
["/portal/event.png", "The view of a specific event."], ["/portal/event.png", "The view of a specific event."],
["/portal/checkin.png", "The check-in view."], ["/portal/checkin.png", "The check-in view."],
["/portal/filters.png", "Organization filtering options. Dynamic semester filtering & event sorting is also available."], [
"/portal/filters.png",
"Organization filtering options. Dynamic semester filtering & event sorting is also available.",
],
["/portal/login.png", "The login. Fast form validation, seamless login."], ["/portal/login.png", "The login. Fast form validation, seamless login."],
["/portal/profile.png", <>The member profile view; fully editable on both desktop & mobile. <br/> Seamless editing [
of profiles for users. Full validation available.</>], "/portal/profile.png",
["/portal/status.png", "Members can check their progress towards becoming full members & view what events they attended."], <>
] The member profile view; fully editable on both desktop & mobile. <br />{" "}
Seamless editing of profiles for users. Full validation available.
</>,
],
[
"/portal/status.png",
"Members can check their progress towards becoming full members & view what events they attended.",
],
];
const PortalPage: NextPage = () => { const PortalPage: NextPage = () => {
return <> return (
<>
<Head> <Head>
<title>Portal | Xevion.dev</title> <title>Portal | Xevion.dev</title>
</Head> </Head>
<AppWrapper> <AppWrapper>
<div className="w-full overflow-auto h-full min-h-screen flex justify-center"> <div className="flex h-full min-h-screen w-full justify-center overflow-auto">
<div className="relative my-10 p-3 px-6 w-full max-w-screen-md"> <div className="relative my-10 w-full max-w-screen-md p-3 px-6">
<div className="pb-2 flex justify-between"> <div className="flex justify-between pb-2">
<div className="text-3xl font-semibold"> <div className="text-3xl font-semibold">Portal</div>
Portal
</div>
<div className="flex items-center justify-end space-x-1.5"> <div className="flex items-center justify-end space-x-1.5">
<Link href="https://github.com/acmutsa/Portal" target="_blank"> <Link href="https://github.com/acmutsa/Portal" target="_blank">
<BsGithub className="w-6 h-6 hover:text-zinc-200"/> <BsGithub className="h-6 w-6 hover:text-zinc-200" />
</Link> </Link>
</div> </div>
</div> </div>
<div className="relative"> <div className="relative">
<Link href="https://portal.acmutsa.org/"> <Link href="https://portal.acmutsa.org/">
<Image fill sizes="100vw" src="/portal/banner.jpeg" alt="" <Image
className="!relative pointer-events-none min-h-[10rem] rounded-md object-cover"/> fill
sizes="100vw"
src="/portal/banner.jpeg"
alt=""
className="pointer-events-none !relative min-h-[10rem] rounded-md object-cover"
/>
</Link> </Link>
</div> </div>
<div className="mt-3 w-full prose prose-invert prose-lg"> <div className="prose prose-lg prose-invert mt-3 w-full">
<p> <p>
Created in service of our membership, Portal was designed as a approachable membership Created in service of our membership, Portal was designed as a
portal approachable membership portal for our users so we could{" "}
for our users so we could <b>track membership</b>, <b>advertise events</b> and replace our <b>track membership</b>, <b>advertise events</b> and replace our
existing <b>database solution</b>. existing <b>database solution</b>.
</p> </p>
<ul className="md:columns-2"> <ul className="md:columns-2">
@@ -61,21 +84,31 @@ const PortalPage: NextPage = () => {
<h3>Screenshots</h3> <h3>Screenshots</h3>
<div className="relative space-y-8"> <div className="relative space-y-8">
{images.map(([src, description]) => { {images.map(([src, description]) => {
return <div key={src} className="flex flex-col justify-center w-full"> return (
<Image fill sizes="100vw" src={src} alt="" <div
className="shadow-lg !my-1 !relative pointer-events-none min-h-[10rem] rounded-md object-cover"/> key={src}
<span className="flex w-full flex-col justify-center"
className="text-center text-base">{description}</span> >
<Image
fill
sizes="100vw"
src={src}
alt=""
className="pointer-events-none !relative !my-1 min-h-[10rem] rounded-md object-cover shadow-lg"
/>
<span className="text-center text-base">
{description}
</span>
</div> </div>
} );
)} })}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</AppWrapper> </AppWrapper>
</> </>
} );
};
export default PortalPage; export default PortalPage;

View File

@@ -7,136 +7,152 @@ import {IconType} from "react-icons";
import { PiPlantBold } from "react-icons/pi"; import { PiPlantBold } from "react-icons/pi";
import { HiOutlineRss } from "react-icons/hi"; import { HiOutlineRss } from "react-icons/hi";
import { GiHummingbird, GiPathDistance } from "react-icons/gi"; import { GiHummingbird, GiPathDistance } from "react-icons/gi";
import {MdOutlineGrain, MdOutlineDns, MdOutlineLeaderboard} from "react-icons/md"; import {
MdOutlineGrain,
MdOutlineDns,
MdOutlineLeaderboard,
} from "react-icons/md";
import { FiLayers } from "react-icons/fi"; import { FiLayers } from "react-icons/fi";
import { FaReact } from "react-icons/fa"; import { FaReact } from "react-icons/fa";
import { SiPowershell } from "react-icons/si"; import { SiPowershell } from "react-icons/si";
import { RxTimer } from "react-icons/rx"; import { RxTimer } from "react-icons/rx";
const ProjectsPage: NextPage = () => { const ProjectsPage: NextPage = () => {
const projects: { name: string, description: string, url?: string, icon: IconType }[] = [ const projects: {
name: string;
description: string;
url?: string;
icon: IconType;
}[] = [
{ {
name: "Portal", name: "Portal",
description: "ACM Membership & Event System", description: "ACM Membership & Event System",
url: "https://portal.acmutsa.org", url: "https://portal.acmutsa.org",
icon: RiMagicLine icon: RiMagicLine,
}, },
{ {
name: "Runnerspace", name: "Runnerspace",
description: "Hackathon MySpace Clone", description: "Hackathon MySpace Clone",
url: "https://runnerspace.xevion.dev", url: "https://runnerspace.xevion.dev",
icon: RiMagicLine icon: RiMagicLine,
}, },
{ {
name: "v6 Place", name: "v6 Place",
description: "Paint Images with IPv6 Addresses", description: "Paint Images with IPv6 Addresses",
url: "https://github.com/Xevion/v6-place", url: "https://github.com/Xevion/v6-place",
icon: BiNetworkChart icon: BiNetworkChart,
}, },
{ {
name: "Phototag", name: "Phototag",
description: "Effortlessly Tag Photos", description: "Effortlessly Tag Photos",
url: "/phototag", url: "/phototag",
icon: BiHash icon: BiHash,
}, },
{ {
name: "Paths", name: "Paths",
description: "Graph Traversal Algorithms", description: "Graph Traversal Algorithms",
url: "https://github.com/Xevion/Paths", url: "https://github.com/Xevion/Paths",
icon: GiPathDistance icon: GiPathDistance,
}, },
{ {
name: "undefined behaviors", name: "undefined behaviors",
description: "Astro-based Blog", description: "Astro-based Blog",
url: "https://undefined.behavio.rs", url: "https://undefined.behavio.rs",
icon: HiOutlineRss icon: HiOutlineRss,
}, },
{ {
name: "v2.xevion.dev", name: "v2.xevion.dev",
description: "Jekyll-based Blog", description: "Jekyll-based Blog",
url: "https://v2.xevion.dev", url: "https://v2.xevion.dev",
icon: HiOutlineRss icon: HiOutlineRss,
}, },
{ {
name: "Grain", name: "Grain",
description: "Pretty SVG-based Noise", description: "Pretty SVG-based Noise",
url: "https://grain.xevion.dev", url: "https://grain.xevion.dev",
icon: MdOutlineGrain icon: MdOutlineGrain,
}, },
{ {
name: "Hydroponics Expanded", name: "Hydroponics Expanded",
description: "A RimWorld Mod for Hydroponics", description: "A RimWorld Mod for Hydroponics",
url: "https://github.com/Xevion/RimWorld-Hydroponics-Expanded", url: "https://github.com/Xevion/RimWorld-Hydroponics-Expanded",
icon: PiPlantBold icon: PiPlantBold,
}, },
{ {
name: "Time Banner", name: "Time Banner",
description: "Instant PNG Timestamps in Rust", description: "Instant PNG Timestamps in Rust",
url: "https://github.com/Xevion/time-banner", url: "https://github.com/Xevion/time-banner",
icon: RxTimer icon: RxTimer,
}, },
{ {
name: "The Office", name: "The Office",
description: "View Quotes from The Office", description: "View Quotes from The Office",
url: "https://the-office.xevion.dev", url: "https://the-office.xevion.dev",
icon: FiLayers icon: FiLayers,
}, },
{ {
name: "Boids", name: "Boids",
description: "Flocking Simulation", description: "Flocking Simulation",
url: "https://github.com/Xevion/Boids", url: "https://github.com/Xevion/Boids",
icon: GiHummingbird icon: GiHummingbird,
}, },
{ {
name: "bus-reminder", name: "bus-reminder",
description: "Last Bus Departure Reminder", description: "Last Bus Departure Reminder",
url: "http://github.com/Xevion/bus-reminder", url: "http://github.com/Xevion/bus-reminder",
icon: BiBus icon: BiBus,
}, },
{ {
name: "rdap", name: "rdap",
description: "Javascript RDAP Client", description: "Javascript RDAP Client",
url: "https://rdap.xevion.dev", url: "https://rdap.xevion.dev",
icon: MdOutlineDns icon: MdOutlineDns,
}, },
{ {
name: "icons", name: "icons",
description: "Dynamic React-Icons Loading", description: "Dynamic React-Icons Loading",
url: "https://icons.xevion.dev", url: "https://icons.xevion.dev",
icon: FaReact icon: FaReact,
}, },
{ {
name: "trivia", name: "trivia",
description: "CLI + Flask Leaderboard", description: "CLI + Flask Leaderboard",
url: "http://github.com/Xevion/trivia", url: "http://github.com/Xevion/trivia",
icon: MdOutlineLeaderboard icon: MdOutlineLeaderboard,
}, },
{ {
name: "Powershell", name: "Powershell",
description: "Scripts & Guides", description: "Scripts & Guides",
url: "https://powershell.xevion.dev", url: "https://powershell.xevion.dev",
icon: SiPowershell icon: SiPowershell,
} },
] ];
return <AppWrapper current='projects'> return (
<div className="mt-20 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-20 gap-y-14 h-full py-2 max-w-500 w-max mx-auto flex "> <AppWrapper current="projects">
{projects.map(({name, description, url, icon: Icon}) => <Link <div className="max-w-500 mx-auto mt-20 flex grid h-full w-max grid-cols-1 gap-x-20 gap-y-14 py-2 md:grid-cols-2 lg:grid-cols-3">
{projects.map(({ name, description, url, icon: Icon }) => (
<Link
key={name} key={name}
className="relative flex flex-shrink items-center opacity-75 hover:opacity-100 transition-opacity" className="relative flex flex-shrink items-center opacity-75 transition-opacity hover:opacity-100"
href={url ?? ""} href={url ?? ""}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
title={name}> title={name}
<div className="pt-2 pr-5"> >
<Icon className="text-3xl saturate-0 opacity-80"/> <div className="pr-5 pt-2">
<Icon className="text-3xl opacity-80 saturate-0" />
</div> </div>
<div className="flex-auto"> <div className="flex-auto">
<div className="text-normal">{name}</div> <div className="text-normal">{name}</div>
<div className="text-sm opacity-70 font-normal">{description}</div> <div className="text-sm font-normal opacity-70">
{description}
</div> </div>
</Link>)} </div>
</Link>
))}
</div> </div>
</AppWrapper> </AppWrapper>
);
}; };
export default ProjectsPage; export default ProjectsPage;

View File

@@ -1,4 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@900&display=swap'); @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=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+Mono&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@200;300;400;500;700;900&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@200;300;400;500;700;900&display=swap");
@@ -8,7 +8,12 @@
@tailwind utilities; @tailwind utilities;
.body-gradient { .body-gradient {
background-image: linear-gradient(to top left, black, rgb(161 161 170 / 0.1), black); background-image: linear-gradient(
to top left,
black,
rgb(161 161 170 / 0.1),
black
);
} }
@mixin active { @mixin active {
@@ -21,12 +26,13 @@
} }
} }
html, body { html,
body {
@apply font-inter; @apply font-inter;
} }
.item { .item {
@apply w-full aspect-[7/2] lg:aspect-[5/3] sm:h-[14rem] md:h-[16rem] relative pointer-events-auto cursor-pointer overflow-hidden transition-all rounded; @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 { > img {
@apply rounded transition-all; @apply rounded transition-all;
} }
@@ -52,16 +58,17 @@ html, body {
.icon-grid { .icon-grid {
direction: rtl; direction: rtl;
@apply min-w-0 max-w-full min-h-0 max-h-full; @apply max-h-full min-h-0 min-w-0 max-w-full;
> a > svg { > a > svg {
@apply w-full h-full; @apply h-full w-full;
} }
> svg, a { > svg,
a {
width: 75%; width: 75%;
height: 75%; height: 75%;
@apply transition-transform drop-shadow-md hover:scale-[120%] opacity-80 hover:opacity-100 text-white m-auto aspect-square; @apply m-auto aspect-square text-white opacity-80 drop-shadow-md transition-transform hover:scale-[120%] hover:opacity-100;
} }
} }
@@ -73,5 +80,5 @@ html, body {
} }
body { body {
@apply h-full @apply h-full;
} }

View File

@@ -5,21 +5,20 @@ import {RxOpenInNewWindow} from "react-icons/rx";
export type Project = { export type Project = {
title: string; title: string;
banner: string; banner: string;
bannerSettings?: {quality: number;} bannerSettings?: { quality: number };
longDescription: string; longDescription: string;
shortDescription: string; shortDescription: string;
links?: LinkIcon[]; links?: LinkIcon[];
location: string; location: string;
} };
export const LinkIcons: Record<string, IconType> = { export const LinkIcons: Record<string, IconType> = {
github: AiFillGithub, github: AiFillGithub,
external: RxOpenInNewWindow, external: RxOpenInNewWindow,
link: AiOutlineLink link: AiOutlineLink,
} };
export type LinkIcon = { export type LinkIcon = {
icon: keyof typeof LinkIcons; icon: keyof typeof LinkIcons;
location: string; location: string;
newTab?: boolean; newTab?: boolean;
} };

View File

@@ -5,19 +5,19 @@ module.exports = {
extend: { extend: {
colors: { colors: {
zinc: { zinc: {
850: "#1D1D20" 850: "#1D1D20",
} },
}, },
fontSize: { fontSize: {
'10xl': '10rem', "10xl": "10rem",
}, },
fontFamily: { fontFamily: {
inter: ["\"Inter\"", "sans-serif"], inter: ['"Inter"', "sans-serif"],
roboto: ["\"Roboto\"", "sans-serif"], roboto: ['"Roboto"', "sans-serif"],
mono: ["\"Roboto Mono\"", "monospace"], mono: ['"Roboto Mono"', "monospace"],
hanken: ["\"Hanken Grotesk\"", "sans-serif"], hanken: ['"Hanken Grotesk"', "sans-serif"],
}, },
}, },
}, },
plugins: [require('@tailwindcss/typography')], plugins: [require("@tailwindcss/typography")],
}; };

851
yarn.lock
View File

File diff suppressed because it is too large Load Diff