more work
This commit is contained in:
parent
a7d8fe26b9
commit
16ab4a587a
@ -1,2 +1,7 @@
|
|||||||
# RainnnyCLUB
|
# RainnnyCLUB
|
||||||
My personal portfolio website hosted [here](https://rainnny.club)
|
My personal portfolio website hosted [here](https://rainnny.club)
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- [ ] Mobile Responsiveness
|
||||||
|
- [ ] Discord Integration (Status, Activity, etc)
|
||||||
|
- [ ] Add Configuration
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "img.icons8.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
@ -12,10 +12,12 @@
|
|||||||
"@heroicons/react": "^2.1.5",
|
"@heroicons/react": "^2.1.5",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.3.30",
|
"framer-motion": "^11.3.30",
|
||||||
"lucide-react": "^0.436.0",
|
"lucide-react": "^0.436.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"next": "14.2.3",
|
"next": "14.2.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
BIN
public/maven.png
Normal file
BIN
public/maven.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
BIN
public/waving-hand.gif
Normal file
BIN
public/waving-hand.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 238 KiB |
@ -3,6 +3,7 @@ import { Inter } from "next/font/google";
|
|||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ const RootLayout = ({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
{children}
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
|
import Greeting from "@/components/landing/greeting";
|
||||||
import Navbar from "@/components/landing/navbar";
|
import Navbar from "@/components/landing/navbar";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
const LandingPage = (): ReactElement => (
|
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 />
|
<Navbar />
|
||||||
Page Content
|
<div className="h-full flex flex-col justify-center">
|
||||||
|
<Greeting />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
export default LandingPage;
|
export default LandingPage;
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 244 KiB |
67
src/components/landing/greeting.tsx
Normal file
67
src/components/landing/greeting.tsx
Normal 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'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;
|
32
src/components/landing/nav-content/homelab.tsx
Normal file
32
src/components/landing/nav-content/homelab.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
|
||||||
|
const HomelabContent = (): ReactElement => (
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Server Rack:</b> 22U, 32" 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;
|
20
src/components/landing/nav-content/my-work.tsx
Normal file
20
src/components/landing/nav-content/my-work.tsx
Normal 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;
|
145
src/components/landing/nav-content/skills.tsx
Normal file
145
src/components/landing/nav-content/skills.tsx
Normal 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;
|
@ -15,20 +15,24 @@ 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 "../ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { BookOpenIcon } from "@heroicons/react/24/outline";
|
import { BookOpenIcon } from "@heroicons/react/24/outline";
|
||||||
import { SignalIcon } 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 => (
|
const Navbar = (): ReactElement => (
|
||||||
<nav className="py-4 flex gap-14 justify-center items-center border-b">
|
<BlurFade className="pt-1" delay={1.35} inView>
|
||||||
<Branding />
|
<nav className="py-4 flex gap-14 justify-center items-center border-b">
|
||||||
<Links />
|
<Branding />
|
||||||
</nav>
|
<Links />
|
||||||
|
</nav>
|
||||||
|
</BlurFade>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Branding = (): ReactElement => (
|
const Branding = (): ReactElement => (
|
||||||
<Link
|
<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="/"
|
href="/"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@ -62,7 +66,7 @@ const Links = (): ReactElement => (
|
|||||||
className={cn(navigationMenuTriggerStyle(), "gap-2")}
|
className={cn(navigationMenuTriggerStyle(), "gap-2")}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span>Donate</span>
|
<span>Buy me a Coffee</span>
|
||||||
<HeartIcon
|
<HeartIcon
|
||||||
className="text-red-500 animate-pulse"
|
className="text-red-500 animate-pulse"
|
||||||
width={20}
|
width={20}
|
||||||
@ -82,9 +86,17 @@ const Links = (): ReactElement => (
|
|||||||
|
|
||||||
const UsefulLinksContent = (): ReactElement => (
|
const UsefulLinksContent = (): ReactElement => (
|
||||||
<div className="p-3 flex gap-5">
|
<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 */}
|
{/* Wiki */}
|
||||||
<Link href="https://docs.rainnny.club" target="_blank">
|
<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} />
|
<BookOpenIcon width={24} height={24} />
|
||||||
<span>Wiki</span>
|
<span>Wiki</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -92,7 +104,7 @@ const UsefulLinksContent = (): ReactElement => (
|
|||||||
|
|
||||||
{/* Status Page */}
|
{/* Status Page */}
|
||||||
<Link href="https://status.rainnny.club" target="_blank">
|
<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} />
|
<SignalIcon width={24} height={24} />
|
||||||
<span>Service Status</span>
|
<span>Service Status</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
75
src/components/landing/navigation.tsx
Normal file
75
src/components/landing/navigation.tsx
Normal 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;
|
@ -12,7 +12,7 @@ const ThemeSwitcher = (): ReactElement => {
|
|||||||
const isLight = theme === "light";
|
const isLight = theme === "light";
|
||||||
return (
|
return (
|
||||||
<Button
|
<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"
|
variant="ghost"
|
||||||
onClick={() => setTheme(isLight ? "dark" : "light")}
|
onClick={() => setTheme(isLight ? "dark" : "light")}
|
||||||
>
|
>
|
||||||
|
62
src/components/ui/blur-fade.tsx
Normal file
62
src/components/ui/blur-fade.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
99
src/components/ui/flip-words.tsx
Normal file
99
src/components/ui/flip-words.tsx
Normal 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"> </span>
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
@ -41,7 +41,7 @@ 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 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<
|
||||||
|
45
src/components/ui/simple-tooltip.tsx
Normal file
45
src/components/ui/simple-tooltip.tsx
Normal 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;
|
30
src/components/ui/tooltip.tsx
Normal file
30
src/components/ui/tooltip.tsx
Normal 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 }
|
@ -1,7 +0,0 @@
|
|||||||
interface Config {
|
|
||||||
/**
|
|
||||||
* The name of this app.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user