more work

This commit is contained in:
Braydon 2024-08-29 03:43:36 -04:00
parent a7d8fe26b9
commit 16ab4a587a
22 changed files with 628 additions and 22 deletions

View File

@ -1,2 +1,7 @@
# RainnnyCLUB
My personal portfolio website hosted [here](https://rainnny.club)
## TODO
- [ ] Mobile Responsiveness
- [ ] Discord Integration (Status, Activity, etc)
- [ ] Add Configuration

BIN
bun.lockb

Binary file not shown.

View File

@ -1,4 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "img.icons8.com",
},
],
},
};
export default nextConfig;

View File

@ -12,10 +12,12 @@
"@heroicons/react": "^2.1.5",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^11.3.30",
"lucide-react": "^0.436.0",
"moment": "^2.30.1",
"next": "14.2.3",
"next-themes": "^0.3.0",
"react": "^18",

BIN
public/maven.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
public/waving-hand.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

View File

@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
import { ThemeProvider } from "@/components/theme-provider";
import "../globals.css";
import { ReactElement } from "react";
import { TooltipProvider } from "@/components/ui/tooltip";
const inter = Inter({ subsets: ["latin"] });
@ -34,7 +35,7 @@ const RootLayout = ({
enableSystem
disableTransitionOnChange
>
{children}
<TooltipProvider>{children}</TooltipProvider>
</ThemeProvider>
</body>
</html>

View File

@ -1,10 +1,19 @@
import Greeting from "@/components/landing/greeting";
import Navbar from "@/components/landing/navbar";
import { ReactElement } from "react";
const LandingPage = (): ReactElement => (
<main className="flex flex-col">
<main
className="h-screen flex flex-col"
style={{
background:
"linear-gradient(to top, hsla(240, 8%, 8%, 0.5), hsl(var(--background)))",
}}
>
<Navbar />
Page Content
<div className="h-full flex flex-col justify-center">
<Greeting />
</div>
</main>
);
export default LandingPage;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 244 KiB

View File

@ -0,0 +1,67 @@
"use client";
import BlurFade from "@/components/ui/blur-fade";
import { cn } from "@/lib/utils";
import moment, { Moment } from "moment";
import Image from "next/image";
import { ReactElement } from "react";
import { FlipWords } from "@/components/ui/flip-words";
import Navigation from "./navigation";
const Greeting = (): ReactElement => {
const now: Moment = moment(Date.now());
return (
<section className="flex flex-col gap-5 items-center">
<BlurFade delay={0.3} inView>
<Image
className="shadow-2xl shadow-blue-500 rounded-full select-none pointer-events-none"
src="/me.png"
alt="My Selfie (:"
width={174}
height={174}
/>
</BlurFade>
<BlurFade delay={0.6} inView>
<h1
className={cn(
"flex gap-2 justify-center items-center text-4xl font-bold select-none pointer-events-none",
"text-black dark:text-transparent bg-clip-text bg-gradient-to-br from-zinc-300/60 to-white",
)}
>
Hello, I&apos;m
<span className="text-blue-600 dark:text-transparent bg-clip-text bg-gradient-to-br from-blue-600 to-blue-300">
Braydon
</span>
<span>
<Image
src="/waving-hand.gif"
alt="Waving Hand"
width={32}
height={32}
unoptimized
/>
</span>
</h1>
</BlurFade>
<BlurFade delay={0.9} inView>
<FlipWords
className={cn(
"-mt-3 p-0 max-w-[23rem] text-center select-none pointer-events-none",
"text-black dark:!text-transparent bg-clip-text bg-gradient-to-br from-zinc-300/85 to-white",
)}
words={[
`A ${now.diff(moment([2002, 10, 13]), "years")} year old${" "}
passionate software engineer living in Canada with ${moment([2016, 8, 1]).fromNow(true)} of experience!`,
]}
/>
</BlurFade>
<BlurFade className="mt-3.5" delay={1.25} inView>
<Navigation />
</BlurFade>
</section>
);
};
export default Greeting;

View File

@ -0,0 +1,32 @@
import { ReactElement } from "react";
const HomelabContent = (): ReactElement => (
<ul>
<li>
<b>Server Rack:</b> 22U, 32&quot; Depth
</li>
<li>
<b>Router:</b> UDM Pro
</li>
<li>
<b>UPS:</b> 1350VA
</li>
<li className="my-2.5" />
<li>
<b>Proxmox Node-01:</b>
<li>
- <b>Motherboard:</b> Prime B550-PLUS
</li>
<li>
- <b>CPU:</b> Ryzen 5 5600G
</li>
<li>
- <b>RAM:</b> 38GB of DDR4 @ 3200Mhz
</li>
<li>
- <b>Storage:</b> 8TB (x2 4TB, x1 4TB Parity) Unraid Array
</li>
</li>
</ul>
);
export default HomelabContent;

View File

@ -0,0 +1,20 @@
import { ReactElement } from "react";
type Project = {
name: string;
git: string;
content: ReactElement;
};
const projects: Project[] = [
{
name: "This Website!",
git: "https://github.com/Rainnny7/rainnny.club",
content: <div>This website</div>,
},
];
const MyWork = (): ReactElement => (
<div className="flex gap-3 justify-center">MY WORK HELLO</div>
);
export default MyWork;

View File

@ -0,0 +1,145 @@
import Image from "next/image";
import Link from "next/link";
import { ReactElement } from "react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import SimpleTooltip from "@/components/ui/simple-tooltip";
type Skill = {
name: string;
icon: string;
link: string;
};
const skillset: Skill[] = [
// Languages
{
name: "Java",
icon: "https://img.icons8.com/color/2x/java-coffee-cup-logo.png",
link: "https://www.java.com",
},
{
name: "JavaScript",
icon: "https://img.icons8.com/fluent/2x/javascript.png",
link: "https://developer.mozilla.org/en-US/docs/Web/JavaScript",
},
{
name: "CSS",
icon: "https://img.icons8.com/fluent/2x/css3.png",
link: "https://www.w3schools.com/css",
},
// Operating Systems
{
name: "Linux",
icon: "https://img.icons8.com/color/2x/linux.png",
link: "https://www.linux.org",
},
{
name: "Bash",
icon: "https://img.icons8.com/color/2x/bash.png",
link: "https://www.gnu.org/software/bash",
},
// Databases
{
name: "MariaDB",
icon: "https://img.icons8.com/fluent/2x/maria-db.png",
link: "https://mariadb.org",
},
{
name: "MongoDB",
icon: "https://img.icons8.com/color/2x/mongodb.png",
link: "https://www.mongodb.com",
},
{
name: "Redis",
icon: "https://img.icons8.com/color/2x/redis.png",
link: "https://redis.io",
},
// Software
{
name: "Git",
icon: "https://img.icons8.com/color/2x/git.png",
link: "https://git-scm.com",
},
{
name: "Docker",
icon: "https://img.icons8.com/fluent/2x/docker.png",
link: "https://www.docker.com",
},
{
name: "Jenkins",
icon: "https://img.icons8.com/color/2x/jenkins.png",
link: "https://www.jenkins.io",
},
{
name: "Figma",
icon: "https://img.icons8.com/fluent/2x/figma.png",
link: "https://www.figma.com",
},
{
name: "Postman",
icon: "https://img.icons8.com/dusk/2x/postman-api.png",
link: "https://www.postman.com",
},
// Frameworks & Libraries
{
name: "Maven",
icon: "/maven.png",
link: "https://maven.apache.org",
},
{
name: "NPM",
icon: "https://img.icons8.com/color/2x/npm.png",
link: "https://www.npmjs.com",
},
{
name: "React",
icon: "https://img.icons8.com/dusk/2x/react.png",
link: "https://reactjs.org/",
},
{
name: "NextJS",
icon: "https://img.icons8.com/color/2x/nextjs.png",
link: "https://nextjs.org/",
},
{
name: "TailwindCSS",
icon: "https://img.icons8.com/color/2x/tailwindcss.png",
link: "https://tailwindcss.com",
},
{
name: "Redux",
icon: "https://img.icons8.com/color/2x/redux.png",
link: "https://redux.js.org",
},
{
name: "Nginx",
icon: "https://img.icons8.com/color/2x/nginx.png",
link: "https://www.nginx.com",
},
];
const Skills = (): ReactElement => (
<div className="max-w-[30rem] flex flex-wrap gap-3 justify-center">
{skillset.map((skill, index) => (
<Link key={index} className="cursor-default" href={skill.link}>
<SimpleTooltip content={skill.name}>
<Image
src={skill.icon}
alt={`${skill.name} Skill Logo`}
width={36}
height={36}
/>
</SimpleTooltip>
</Link>
))}
</div>
);
export default Skills;

View File

@ -15,20 +15,24 @@ import { navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
import { HeartIcon } from "@heroicons/react/24/solid";
import { cn } from "@/lib/utils";
import ThemeSwitcher from "./theme-switcher";
import { Button } from "../ui/button";
import { Button } from "@/components/ui/button";
import { BookOpenIcon } from "@heroicons/react/24/outline";
import { SignalIcon } from "@heroicons/react/24/outline";
import BlurFade from "@/components/ui/blur-fade";
import { CodeBracketIcon } from "@heroicons/react/24/outline";
const Navbar = (): ReactElement => (
<nav className="py-4 flex gap-14 justify-center items-center border-b">
<Branding />
<Links />
</nav>
<BlurFade className="pt-1" delay={1.35} inView>
<nav className="py-4 flex gap-14 justify-center items-center border-b">
<Branding />
<Links />
</nav>
</BlurFade>
);
const Branding = (): ReactElement => (
<Link
className="flex gap-4 items-center hover:opacity-75 transition-all transform-gpu"
className="flex gap-4 items-center hover:opacity-75 cursor-default transition-all transform-gpu"
href="/"
>
<Image
@ -62,7 +66,7 @@ const Links = (): ReactElement => (
className={cn(navigationMenuTriggerStyle(), "gap-2")}
target="_blank"
>
<span>Donate</span>
<span>Buy me a Coffee</span>
<HeartIcon
className="text-red-500 animate-pulse"
width={20}
@ -82,9 +86,17 @@ const Links = (): ReactElement => (
const UsefulLinksContent = (): ReactElement => (
<div className="p-3 flex gap-5">
{/* Git */}
<Link href="https://git.rainnny.club" target="_blank">
<Button className="gap-3 cursor-default" variant="ghost">
<CodeBracketIcon width={24} height={24} />
<span>Gitea</span>
</Button>
</Link>
{/* Wiki */}
<Link href="https://docs.rainnny.club" target="_blank">
<Button className="gap-3" variant="ghost">
<Button className="gap-3 cursor-default" variant="ghost">
<BookOpenIcon width={24} height={24} />
<span>Wiki</span>
</Button>
@ -92,7 +104,7 @@ const UsefulLinksContent = (): ReactElement => (
{/* Status Page */}
<Link href="https://status.rainnny.club" target="_blank">
<Button className="gap-3" variant="ghost">
<Button className="gap-3 cursor-default" variant="ghost">
<SignalIcon width={24} height={24} />
<span>Service Status</span>
</Button>

View File

@ -0,0 +1,75 @@
"use client";
import { ReactElement, useState } from "react";
import { Button } from "@/components/ui/button";
import { BriefcaseIcon } from "@heroicons/react/24/outline";
import { WrenchIcon } from "@heroicons/react/24/outline";
import { ServerStackIcon } from "@heroicons/react/24/outline";
import { cn } from "@/lib/utils";
import BlurFade from "@/components/ui/blur-fade";
import HomelabContent from "./nav-content/homelab";
import Skills from "./nav-content/skills";
import MyWork from "./nav-content/my-work";
type Item = {
name: string;
icon: ReactElement;
content: ReactElement;
};
const items: Item[] = [
{
name: "My Work",
icon: <BriefcaseIcon width={22} height={22} />,
content: <MyWork />,
},
{
name: "Skills",
icon: <WrenchIcon width={22} height={22} />,
content: <Skills />,
},
{
name: "Homelab",
icon: <ServerStackIcon width={22} height={22} />,
content: <HomelabContent />,
},
];
const Navigation = (): ReactElement => {
const [selected, setSelected] = useState<Item | undefined>(undefined);
return (
<div className="flex flex-col">
{/* Selection Buttons */}
<div className="flex gap-5 justify-center">
{items.map((item, index) => {
const active: boolean = selected === item;
return (
<BlurFade key={index} delay={0.9 + 0.3 * index} inView>
<Button
className={cn(
"py-6 gap-2 bg-white/75 dark:bg-zinc-800/75 cursor-default hover:opacity-75 transition-all transform-gpu",
active && "opacity-70",
)}
variant="ghost"
onClick={() =>
active ? setSelected(undefined) : setSelected(item)
}
>
{item.icon}
{item.name}
</Button>
</BlurFade>
);
})}
</div>
{/* Selected Content */}
{selected && (
<BlurFade key={selected.name} delay={0.05} inView>
<div className="mt-4 p-4 border rounded-xl">{selected.content}</div>
</BlurFade>
)}
</div>
);
};
export default Navigation;

View File

@ -12,7 +12,7 @@ const ThemeSwitcher = (): ReactElement => {
const isLight = theme === "light";
return (
<Button
className="mx-7 px-5 py-1.5 flex items-center relative hover:opacity-85"
className="relative mx-7 px-5 py-1.5 flex items-center cursor-default hover:opacity-85"
variant="ghost"
onClick={() => setTheme(isLight ? "dark" : "light")}
>

View File

@ -0,0 +1,62 @@
"use client";
import { useRef } from "react";
import { AnimatePresence, motion, useInView, Variants } from "framer-motion";
interface BlurFadeProps {
children: React.ReactNode;
className?: string;
variant?: {
hidden: { y: number };
visible: { y: number };
};
duration?: number;
delay?: number;
yOffset?: number;
inView?: boolean;
inViewMargin?: string;
blur?: string;
}
export default function BlurFade({
children,
className,
variant,
duration = 0.4,
delay = 0,
yOffset = 6,
inView = false,
inViewMargin = "-50px",
blur = "6px",
}: BlurFadeProps) {
const ref = useRef(null);
const inViewResult = useInView(ref, {
once: true,
margin: inViewMargin as any,
});
const isInView = !inView || inViewResult;
const defaultVariants: Variants = {
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
};
const combinedVariants = variant || defaultVariants;
return (
<AnimatePresence>
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
exit="hidden"
variants={combinedVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: "easeOut",
}}
className={className}
>
{children}
</motion.div>
</AnimatePresence>
);
}

View File

@ -0,0 +1,99 @@
"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, LayoutGroup } from "framer-motion";
import { cn } from "@/lib/utils";
export const FlipWords = ({
words,
duration = 3000,
className,
}: {
words: string[];
duration?: number;
className?: string;
}) => {
const [currentWord, setCurrentWord] = useState(words[0]);
const [isAnimating, setIsAnimating] = useState<boolean>(false);
// thanks for the fix Julian - https://github.com/Julian-AT
const startAnimation = useCallback(() => {
const word = words[words.indexOf(currentWord) + 1] || words[0];
setCurrentWord(word);
setIsAnimating(true);
}, [currentWord, words]);
useEffect(() => {
if (!isAnimating)
setTimeout(() => {
startAnimation();
}, duration);
}, [isAnimating, duration, startAnimation]);
return (
<AnimatePresence
onExitComplete={() => {
setIsAnimating(false);
}}
>
<motion.div
initial={{
opacity: 0,
y: 10,
}}
animate={{
opacity: 1,
y: 0,
}}
transition={{
type: "spring",
stiffness: 100,
damping: 10,
}}
exit={{
opacity: 0,
y: -40,
x: 40,
filter: "blur(8px)",
scale: 2,
position: "absolute",
}}
className={cn(
"z-10 inline-block relative text-left text-neutral-900 dark:text-neutral-100 px-2",
className,
)}
key={currentWord}
>
{/* edit suggested by Sajal: https://x.com/DewanganSajal */}
{currentWord.split(" ").map((word, wordIndex) => (
<motion.span
key={word + wordIndex}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
delay: wordIndex * 0.3,
duration: 0.3,
}}
className="inline-block whitespace-nowrap"
>
{word.split("").map((letter, letterIndex) => (
<motion.span
key={word + letterIndex}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
delay: wordIndex * 0.3 + letterIndex * 0.05,
duration: 0.2,
}}
className="inline-block"
>
{letter}
</motion.span>
))}
<span className="inline-block">&nbsp;</span>
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
};

View File

@ -41,7 +41,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium cursor-default transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<

View File

@ -0,0 +1,45 @@
import { ReactElement, ReactNode } from "react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { SIDE_OPTIONS } from "@radix-ui/react-popper";
/**
* The props for a simple tooltip.
*/
type SimpleTooltipProps = {
/**
* The content to display in the tooltip.
*/
content: string | ReactElement;
/**
* The side to display the tooltip on.
*/
side?: (typeof SIDE_OPTIONS)[number];
/**
* The children to render in this tooltip.
*/
children: ReactNode;
};
/**
* A simple tooltip, this is wrapping the
* shadcn tooltip to make it easier to use.
*
* @return the tooltip jsx
*/
const SimpleTooltip = ({
content,
side,
children,
}: SimpleTooltipProps): ReactElement => (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent side={side}>{content}</TooltipContent>
</Tooltip>
);
export default SimpleTooltip;

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -1,7 +0,0 @@
interface Config {
/**
* The name of this app.
*/
name: string;
description: string;
}