Improve mobile responsiveness
All checks were successful
Deploy Site / docker (ubuntu-latest, 2.44.0) (push) Successful in 1m33s

This commit is contained in:
Braydon 2024-08-29 17:05:41 -04:00
parent d6ad12a6e2
commit 1b4e893bfa
23 changed files with 980 additions and 938 deletions

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"trailingComma": "es5",
"tabWidth": 4
}

View File

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

BIN
bun.lockb

Binary file not shown.

View File

@ -22,6 +22,7 @@
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"sharp": "^0.33.5",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
}, },

View File

@ -11,33 +11,33 @@ const inter = Inter({ subsets: ["latin"] });
* The metadata for this app. * The metadata for this app.
*/ */
export const metadata: Metadata = { export const metadata: Metadata = {
title: "RainnnyCLUB", title: "RainnnyCLUB",
description: description:
"My name is Braydon and I am a self-taught software engineer living in Canada.", "My name is Braydon and I am a self-taught software engineer living in Canada.",
}; };
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: "#5555FF", themeColor: "#5555FF",
}; };
/** /**
* The primary layout for this app. * The primary layout for this app.
*/ */
const RootLayout = ({ const RootLayout = ({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>): ReactElement => ( }>): ReactElement => (
<html lang="en"> <html lang="en">
<body className={inter.className}> <body className={inter.className}>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="dark" defaultTheme="dark"
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >
<TooltipProvider>{children}</TooltipProvider> <TooltipProvider>{children}</TooltipProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>
); );
export default RootLayout; export default RootLayout;

View File

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

View File

@ -3,69 +3,70 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 240 10% 3.9%; --foreground: 240 10% 3.9%;
--card: 0 0% 100%; --card: 0 0% 100%;
--card-foreground: 240 10% 3.9%; --card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%; --primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%; --secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%; --muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%; --accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%; --accent-foreground: 240 5.9% 10%;
--destructive: 0 72.22% 50.59%; --destructive: 0 72.22% 50.59%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%; --border: 240 5.9% 90%;
--input: 240 5.9% 90%; --input: 240 5.9% 90%;
--ring: 240 5% 64.9%; --ring: 240 5% 64.9%;
--radius: 0.5rem; --radius: 0.5rem;
--chart-1: 12 76% 61%; --chart-1: 12 76% 61%;
--chart-2: 173 58% 39%; --chart-2: 173 58% 39%;
--chart-3: 197 37% 24%; --chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67%; --chart-5: 27 87% 67%;
} }
.dark { .dark {
--background: 0 0% 0%; --background: 0 0% 0%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
--card: 240 10% 3.9%; --card: 240 10% 3.9%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%; --popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 98%;
--primary: 0 0% 98%; --primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%; --primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%; --secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%; --muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%; --accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%; --destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%; --input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%; --ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55%; --chart-5: 340 75% 55%;
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
} }
body {
@apply bg-background text-foreground; body {
} @apply bg-background text-foreground;
}
} }

View File

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

View File

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

View File

@ -1,7 +1,6 @@
"use client"; "use client";
import { MagicCard } from "@/components/ui/magic-card"; import { MagicCard } from "@/components/ui/magic-card";
import { cn } from "@/lib/utils";
import moment, { Moment } from "moment"; import moment, { Moment } from "moment";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { UseThemeProps } from "next-themes/dist/types"; import { UseThemeProps } from "next-themes/dist/types";
@ -9,102 +8,122 @@ import Link from "next/link";
import { ReactElement } from "react"; import { ReactElement } from "react";
type Project = { type Project = {
name: string; name: string;
link?: string | undefined; link?: string | undefined;
previewContent: ReactElement; previewContent: ReactElement;
startDate: Moment; startDate: Moment;
endDate?: Moment | undefined; endDate?: Moment | undefined;
}; };
const projects: Project[] = [ const projects: Project[] = [
{ {
name: "This Website!", name: "This Website!",
link: "https://github.com/Rainnny7/rainnny.club", link: "https://github.com/Rainnny7/rainnny.club",
previewContent: <div>This website!</div>, previewContent: <div>This website!</div>,
startDate: moment([2024, 7, 29]), startDate: moment([2024, 7, 29]),
}, },
{ {
name: "WildNetwork", name: "WildNetwork",
link: "https://discord.gg/WildPrison", link: "https://discord.gg/WildPrison",
previewContent: ( previewContent: (
<p> <p>
WildNetwork is a Minecraft server that contains multiple gamemodes, one WildNetwork is a Minecraft server that contains multiple
of which is Prison, which is the most popular. I first joined the server gamemodes, one of which is Prison, which is the most popular. I
as a Developer where I would work behind the scenes to create new first joined the server as a Developer where I would work behind
features, now I&apos;m currently working as a System Administrator. the scenes to create new features, now I&apos;m currently
</p> working as a System Administrator.
), </p>
startDate: moment([2020, 7, 1]), ),
}, startDate: moment([2020, 7, 1]),
{ },
name: "Lucity", {
link: "https://youtube.com/@iamLucid", name: "Bonfire",
previewContent: ( link: "https://bonfire.wtf",
<p> previewContent: (
Lucity was a minigame network for the game Minecraft, and was owned by <p>
the YouTuber iamLucid. When I worked at Lucity, I was the development Bonfire is a platform similar to Discord that a friend and I are
lead, I focused mainly on infrastructure, databases, and monitoring working on together in our free time. Bonfire is perfect for
systems. A few things that I have made - a dynamically managed server connecting with friends or building a global community.
system, proxy rotation via the TCPShield API, and an API that can Personalize your space to chat, and hang out.
interact with the entire network from a normal Java app. </p>
</p> ),
), startDate: moment([2024, 3, 30]),
startDate: moment([2020, 7, 1]), },
endDate: moment([2022, 10, 30]), {
}, name: "Lucity",
{ link: "https://youtube.com/@iamLucid",
name: "Rainplex", previewContent: (
previewContent: ( <p>
<p> Lucity was a minigame network for the game Minecraft, and was
Rainplex was a remake of the once popular Minecraft server, Mineplex. owned by the YouTuber iamLucid. When I worked at Lucity, I was
Rainplex initially came to light using the plugin, Skript where it just the development lead, I focused mainly on infrastructure,
contained a Hub. After some time, the entirety of the network was databases, and monitoring systems. A few things that I have made
re-coded in the Java programming from the ground up. Rainplex went - a dynamically managed server system, proxy rotation via the
through numerous re-codes over the time it was active, however I have TCPShield API, and an API that can interact with the entire
since abandoned development due to lack of free time. network from a normal Java app.
</p> </p>
), ),
startDate: moment([2018, 8, 1]), startDate: moment([2020, 7, 1]),
endDate: moment([2021, 6, 11]), endDate: moment([2022, 10, 30]),
}, },
{ {
name: "Arcane Client", name: "Rainplex",
link: "https://github.com/ArcaneClientNET", previewContent: (
previewContent: ( <p>
<p> Rainplex was a remake of the once popular Minecraft server,
Arcane is the all-in-one Minecraft mod pack. This client was built to be Mineplex. Rainplex initially came to light using the plugin,
similar to LunarClient for portfolio and experience sake. I have since Skript where it just contained a Hub. After some time, the
abandoned development due to lack of free time. entirety of the network was re-coded in the Java programming
</p> from the ground up. Rainplex went through numerous re-codes over
), the time it was active, however I have since abandoned
startDate: moment([2021, 6, 1]), development due to lack of free time.
endDate: moment([2021, 10, 1]), </p>
}, ),
startDate: moment([2018, 8, 1]),
endDate: moment([2021, 6, 11]),
},
{
name: "Arcane Client",
link: "https://github.com/ArcaneClientNET",
previewContent: (
<p>
Arcane is the all-in-one Minecraft mod pack. This client was
built to be similar to LunarClient for portfolio and experience
sake. I have since abandoned development due to lack of free
time.
</p>
),
startDate: moment([2021, 6, 1]),
endDate: moment([2021, 10, 1]),
},
]; ];
const MyWork = (): ReactElement => { const MyWork = (): ReactElement => {
const { theme }: UseThemeProps = useTheme(); const { theme }: UseThemeProps = useTheme();
return ( return (
<div className="max-w-[50rem] flex flex-wrap gap-3 justify-center"> <div className="max-w-[50rem] flex flex-wrap gap-3 justify-center">
{projects.map((project, index) => ( {projects.map((project, index) => (
<Link key={index} href={project.link || "#"} target="_blank"> <Link key={index} href={project.link || "#"} target="_blank">
<MagicCard <MagicCard
className="w-[15rem] p-3.5" className="w-[15rem] p-3.5"
gradientColor={theme === "dark" ? "#262626" : "#D9D9D955"} gradientColor={
> theme === "dark" ? "#262626" : "#D9D9D955"
<h1 className="font-bold select-none pointer-events-none"> }
{project.name} >
</h1> <h1 className="font-bold select-none pointer-events-none">
{project.name}
</h1>
{/* Years Active */} {/* Years Active */}
<h2> <h2>
{project.startDate.format("MMM YYYY")} {project.startDate.format("MMM YYYY")}
{project.endDate && ` - ${project.endDate.format("MMM YYYY")}`} {project.endDate &&
</h2> ` - ${project.endDate.format("MMM YYYY")}`}
</MagicCard> </h2>
</Link> </MagicCard>
))} </Link>
</div> ))}
); </div>
);
}; };
export default MyWork; export default MyWork;

View File

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

View File

@ -2,114 +2,115 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ReactElement } from "react"; import { ReactElement } from "react";
import { import {
NavigationMenu, NavigationMenu,
NavigationMenuContent, NavigationMenuContent,
NavigationMenuIndicator, NavigationMenuItem,
NavigationMenuItem, NavigationMenuLink,
NavigationMenuLink, NavigationMenuList,
NavigationMenuList, NavigationMenuTrigger,
NavigationMenuTrigger, navigationMenuTriggerStyle,
NavigationMenuViewport,
} from "@/components/ui/navigation-menu"; } from "@/components/ui/navigation-menu";
import { navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
import { HeartIcon } from "@heroicons/react/24/solid"; import { HeartIcon } from "@heroicons/react/24/solid";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import ThemeSwitcher from "./theme-switcher"; import ThemeSwitcher from "./theme-switcher";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { BookOpenIcon } from "@heroicons/react/24/outline"; import {
import { SignalIcon } from "@heroicons/react/24/outline"; BookOpenIcon,
CodeBracketIcon,
SignalIcon,
} from "@heroicons/react/24/outline";
import BlurFade from "@/components/ui/blur-fade"; import BlurFade from "@/components/ui/blur-fade";
import { CodeBracketIcon } from "@heroicons/react/24/outline";
const Navbar = (): ReactElement => ( const Navbar = (): ReactElement => (
<BlurFade className="pt-1" delay={1.35} inView> <BlurFade className="pt-1" delay={1.35} inView>
<nav className="py-4 flex gap-14 justify-center items-center border-b"> <nav className="px-3 xs:px-7 py-4 flex gap-3 xs:gap-10 sm:gap-14 justify-center items-center border-b transition-all transform-gpu">
<Branding /> <Branding />
<Links /> <Links />
</nav> <ThemeSwitcher />
</BlurFade> </nav>
</BlurFade>
); );
const Branding = (): ReactElement => ( const Branding = (): ReactElement => (
<Link <Link
className="flex gap-4 items-center hover:opacity-75 cursor-default transition-all transform-gpu" className="flex gap-3 items-center hover:opacity-75 cursor-default transition-all transform-gpu"
href="/" href="/"
> >
<Image <Image
className="rounded-full" className="rounded-full"
src="/me.png" src="/me.png"
alt="My Selfie (:" alt="My Selfie (:"
width={40} width={40}
height={40} height={40}
/> />
<h1 className="text-xl font-bold">RainnnyCLUB</h1> <h1 className="hidden sm:flex text-xl font-bold">RainnnyCLUB</h1>
</Link> </Link>
); );
const Links = (): ReactElement => ( const Links = (): ReactElement => (
<NavigationMenu> <NavigationMenu>
<NavigationMenuList> <NavigationMenuList>
{/* Useful Links */} {/* Useful Links */}
<NavigationMenuItem> <NavigationMenuItem>
<NavigationMenuTrigger>Useful Links</NavigationMenuTrigger> <NavigationMenuTrigger>Useful Links</NavigationMenuTrigger>
<NavigationMenuContent> <NavigationMenuContent>
<NavigationMenuLink> <NavigationMenuLink>
<UsefulLinksContent /> <UsefulLinksContent />
</NavigationMenuLink> </NavigationMenuLink>
</NavigationMenuContent> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>
{/* Donate */} {/* Donate */}
<NavigationMenuItem> <NavigationMenuItem>
<Link href="https://buymeacoffee.com/Rainnny7" legacyBehavior passHref> <Link
<NavigationMenuLink href="https://buymeacoffee.com/Rainnny7"
className={cn(navigationMenuTriggerStyle(), "gap-2")} legacyBehavior
target="_blank" passHref
> >
<span>Buy me a Coffee</span> <NavigationMenuLink
<HeartIcon className={cn(navigationMenuTriggerStyle(), "gap-2")}
className="text-red-500 animate-pulse" target="_blank"
width={20} >
height={20} <span className="hidden sm:flex">Buy me a Coffee</span>
/> <span className="sm:hidden">Donate</span>
</NavigationMenuLink> <HeartIcon
</Link> className="text-red-500 animate-pulse"
</NavigationMenuItem> width={20}
height={20}
{/* Theme Switcher */} />
<NavigationMenuItem> </NavigationMenuLink>
<ThemeSwitcher /> </Link>
</NavigationMenuItem> </NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
</NavigationMenu> </NavigationMenu>
); );
const UsefulLinksContent = (): ReactElement => ( const UsefulLinksContent = (): ReactElement => (
<div className="p-3 flex gap-5"> <div className="p-3 flex gap-5">
{/* Git */} {/* Git */}
<Link href="https://git.rainnny.club" target="_blank"> <Link href="https://git.rainnny.club" target="_blank">
<Button className="gap-3 cursor-default" variant="ghost"> <Button className="gap-3 cursor-default" variant="ghost">
<CodeBracketIcon width={24} height={24} /> <CodeBracketIcon width={24} height={24} />
<span>Gitea</span> <span>Gitea</span>
</Button> </Button>
</Link> </Link>
{/* Wiki */} {/* Wiki */}
<Link href="https://docs.rainnny.club" target="_blank"> <Link href="https://docs.rainnny.club" target="_blank">
<Button className="gap-3 cursor-default" variant="ghost"> <Button className="gap-3 cursor-default" variant="ghost">
<BookOpenIcon width={24} height={24} /> <BookOpenIcon width={24} height={24} />
<span>Wiki</span> <span>Wiki</span>
</Button> </Button>
</Link> </Link>
{/* Status Page */} {/* Status Page */}
<Link href="https://status.rainnny.club" target="_blank"> <Link href="https://status.rainnny.club" target="_blank">
<Button className="gap-3 cursor-default" variant="ghost"> <Button className="gap-3 cursor-default" variant="ghost">
<SignalIcon width={24} height={24} /> <SignalIcon width={24} height={24} />
<span>Service Status</span> <span>Service Status</span>
</Button> </Button>
</Link> </Link>
</div> </div>
); );
export default Navbar; export default Navbar;

View File

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

View File

@ -8,31 +8,31 @@ import { motion } from "framer-motion";
import { UseThemeProps } from "next-themes/dist/types"; import { UseThemeProps } from "next-themes/dist/types";
const ThemeSwitcher = (): ReactElement => { const ThemeSwitcher = (): ReactElement => {
const { theme, setTheme }: UseThemeProps = useTheme(); const { theme, setTheme }: UseThemeProps = useTheme();
const isLight = theme === "light"; const isLight = theme === "light";
return ( return (
<Button <Button
className="relative mx-7 px-5 py-1.5 flex items-center cursor-default hover:opacity-85" className="relative px-5 py-1.5 flex items-center cursor-default hover:opacity-85"
variant="ghost" variant="ghost"
onClick={() => setTheme(isLight ? "dark" : "light")} onClick={() => setTheme(isLight ? "dark" : "light")}
> >
<motion.div <motion.div
initial={{ rotate: 0, scale: 1 }} initial={{ rotate: 0, scale: 1 }}
animate={{ rotate: isLight ? 0 : -90, scale: isLight ? 1 : 0 }} animate={{ rotate: isLight ? 0 : -90, scale: isLight ? 1 : 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="absolute" className="absolute"
> >
<Sun className="w-[1.2rem] h-[1.2rem]" /> <Sun className="w-[1.2rem] h-[1.2rem]" />
</motion.div> </motion.div>
<motion.div <motion.div
initial={{ rotate: 90, scale: 0 }} initial={{ rotate: 90, scale: 0 }}
animate={{ rotate: isLight ? 90 : 0, scale: isLight ? 0 : 1 }} animate={{ rotate: isLight ? 90 : 0, scale: isLight ? 0 : 1 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="absolute" className="absolute"
> >
<MoonStar className="w-[1.2rem] h-[1.2rem]" /> <MoonStar className="w-[1.2rem] h-[1.2rem]" />
</motion.div> </motion.div>
</Button> </Button>
); );
}; };
export default ThemeSwitcher; export default ThemeSwitcher;

View File

@ -5,5 +5,5 @@ import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types"; import { type ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>; return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
} }

View File

@ -1,56 +1,57 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default:
destructive: "bg-primary text-primary-foreground hover:bg-primary/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/90", destructive:
outline: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", outline:
secondary: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
"bg-secondary text-secondary-foreground hover:bg-secondary/80", secondary:
ghost: "hover:bg-accent hover:text-accent-foreground", "bg-secondary text-secondary-foreground hover:bg-secondary/80",
link: "text-primary underline-offset-4 hover:underline", ghost: "hover:bg-accent hover:text-accent-foreground",
}, link: "text-primary underline-offset-4 hover:underline",
size: { },
default: "h-10 px-4 py-2", size: {
sm: "h-9 rounded-md px-3", default: "h-10 px-4 py-2",
lg: "h-11 rounded-md px-8", sm: "h-9 rounded-md px-3",
icon: "h-10 w-10", lg: "h-11 rounded-md px-8",
}, icon: "h-10 w-10",
}, },
defaultVariants: { },
variant: "default", defaultVariants: {
size: "default", variant: "default",
}, size: "default",
} },
) }
);
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean;
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} }
) );
Button.displayName = "Button" Button.displayName = "Button";
export { Button, buttonVariants } export { Button, buttonVariants };

View File

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

View File

@ -6,59 +6,59 @@ import { motion, useMotionTemplate, useMotionValue } from "framer-motion";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export interface MagicCardProps extends React.HTMLAttributes<HTMLDivElement> { export interface MagicCardProps extends React.HTMLAttributes<HTMLDivElement> {
gradientSize?: number; gradientSize?: number;
gradientColor?: string; gradientColor?: string;
gradientOpacity?: number; gradientOpacity?: number;
} }
export function MagicCard({ export function MagicCard({
children, children,
className, className,
gradientSize = 200, gradientSize = 200,
gradientColor = "#262626", gradientColor = "#262626",
gradientOpacity = 0.8, gradientOpacity = 0.8,
}: MagicCardProps) { }: MagicCardProps) {
const mouseX = useMotionValue(-gradientSize); const mouseX = useMotionValue(-gradientSize);
const mouseY = useMotionValue(-gradientSize); const mouseY = useMotionValue(-gradientSize);
const handleMouseMove = useCallback( const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => { (e: React.MouseEvent<HTMLDivElement>) => {
const { left, top } = e.currentTarget.getBoundingClientRect(); const { left, top } = e.currentTarget.getBoundingClientRect();
mouseX.set(e.clientX - left); mouseX.set(e.clientX - left);
mouseY.set(e.clientY - top); mouseY.set(e.clientY - top);
}, },
[mouseX, mouseY], [mouseX, mouseY]
); );
const handleMouseLeave = useCallback(() => { const handleMouseLeave = useCallback(() => {
mouseX.set(-gradientSize); mouseX.set(-gradientSize);
mouseY.set(-gradientSize); mouseY.set(-gradientSize);
}, [mouseX, mouseY, gradientSize]); }, [mouseX, mouseY, gradientSize]);
useEffect(() => { useEffect(() => {
mouseX.set(-gradientSize); mouseX.set(-gradientSize);
mouseY.set(-gradientSize); mouseY.set(-gradientSize);
}, [mouseX, mouseY, gradientSize]); }, [mouseX, mouseY, gradientSize]);
return ( return (
<div <div
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
className={cn( className={cn(
"group relative flex size-full overflow-hidden rounded-xl bg-neutral-100 dark:bg-neutral-900 border text-black dark:text-white", "group relative flex size-full overflow-hidden rounded-xl bg-neutral-100 dark:bg-neutral-900 border text-black dark:text-white",
className, className
)} )}
> >
<div className="relative z-10">{children}</div> <div className="relative z-10">{children}</div>
<motion.div <motion.div
className="pointer-events-none absolute -inset-px rounded-xl opacity-0 transition-opacity duration-300 group-hover:opacity-100" className="pointer-events-none absolute -inset-px rounded-xl opacity-0 transition-opacity duration-300 group-hover:opacity-100"
style={{ style={{
background: useMotionTemplate` background: useMotionTemplate`
radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientColor}, transparent 100%) radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientColor}, transparent 100%)
`, `,
opacity: gradientOpacity, opacity: gradientOpacity,
}} }}
/> />
</div> </div>
); );
} }

View File

@ -6,123 +6,123 @@ import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef< const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>, React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center", "relative z-10 flex max-w-max flex-1 items-center justify-center",
className, className
)} )}
{...props} {...props}
> >
{children} {children}
<NavigationMenuViewport /> <NavigationMenuViewport />
</NavigationMenuPrimitive.Root> </NavigationMenuPrimitive.Root>
)); ));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef< const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>, React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1", "group flex flex-1 list-none items-center justify-center space-x-1",
className, className
)} )}
{...props} {...props}
/> />
)); ));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item; const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva( 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 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", "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< const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger <NavigationMenuPrimitive.Trigger
ref={ref} ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)} className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props} {...props}
> >
{children}{" "} {children}{" "}
<ChevronDown <ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180" className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true" aria-hidden="true"
/> />
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
)); ));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef< const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>, React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ", "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className, className
)} )}
{...props} {...props}
/> />
)); ));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link; const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef< const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}> <div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
className={cn( className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]", "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className, className
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
</div> </div>
)); ));
NavigationMenuViewport.displayName = NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName; NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef< const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
ref={ref} ref={ref}
className={cn( className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in", "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className, className
)} )}
{...props} {...props}
> >
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" /> <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
)); ));
NavigationMenuIndicator.displayName = NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName; NavigationMenuPrimitive.Indicator.displayName;
export { export {
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
NavigationMenu, NavigationMenu,
NavigationMenuList, NavigationMenuList,
NavigationMenuItem, NavigationMenuItem,
NavigationMenuContent, NavigationMenuContent,
NavigationMenuTrigger, NavigationMenuTrigger,
NavigationMenuLink, NavigationMenuLink,
NavigationMenuIndicator, NavigationMenuIndicator,
NavigationMenuViewport, NavigationMenuViewport,
}; };

View File

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

View File

@ -1,30 +1,30 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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", "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 className
)} )}
{...props} {...props}
/> />
)) ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }

View File

@ -1,80 +1,83 @@
import type { Config } from "tailwindcss" import type {Config} from "tailwindcss"
const defaultTheme = require('tailwindcss/defaultTheme')
const config = { const config = {
darkMode: ["class"], darkMode: ["class"],
content: [ content: [
'./pages/**/*.{ts,tsx}', "./src/**/*.{ts,tsx}",
'./components/**/*.{ts,tsx}', ],
'./app/**/*.{ts,tsx}', prefix: "",
'./src/**/*.{ts,tsx}', theme: {
], screens: {
prefix: "", 'xs': '475px',
theme: { ...defaultTheme.screens,
container: { },
center: true, container: {
padding: "2rem", center: true,
screens: { padding: "2rem",
"2xl": "1400px", screens: {
}, "2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: {height: "0"},
to: {height: "var(--radix-accordion-content-height)"},
},
"accordion-up": {
from: {height: "var(--radix-accordion-content-height)"},
to: {height: "0"},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
}, },
extend: { plugins: [require("tailwindcss-animate")],
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config } satisfies Config
export default config export default config