more work
This commit is contained in:
parent
a7d8fe26b9
commit
16ab4a587a
@ -1,2 +1,7 @@
|
||||
# RainnnyCLUB
|
||||
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} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "img.icons8.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
@ -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
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 "../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>
|
||||
|
@ -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 |
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 { 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>
|
||||
|
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";
|
||||
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")}
|
||||
>
|
||||
|
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 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<
|
||||
|
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