Better Github Sponsors && HotKey && Scroll Hash
This commit is contained in:
parent
a404865be1
commit
c4ed0b3e94
3 changed files with 170 additions and 131 deletions
37
src/App.tsx
37
src/App.tsx
|
|
@ -9,13 +9,38 @@ import './index.css';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hash = window.location.hash.substring(1);
|
// 초기 로드 시 hash에 맞게 스크롤
|
||||||
if (hash) {
|
const scrollToHash = () => {
|
||||||
const element = document.getElementById(hash);
|
const hash = window.location.hash.substring(1);
|
||||||
if (element) {
|
if (hash) {
|
||||||
element.scrollIntoView({ behavior: 'smooth' });
|
const element = document.getElementById(hash);
|
||||||
|
if (element) {
|
||||||
|
setTimeout(() => {
|
||||||
|
element.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, 100); // 브라우저가 레이아웃을 그릴 시간을 줌
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
scrollToHash();
|
||||||
|
|
||||||
|
// 스크롤 시 hash 업데이트 로직
|
||||||
|
const sections = document.querySelectorAll(".section");
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
window.history.replaceState(null, "", `#${entry.target.id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0.6 } // 60% 보이면 활성화
|
||||||
|
);
|
||||||
|
|
||||||
|
sections.forEach(section => observer.observe(section));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
sections.forEach(section => observer.unobserve(section));
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Send, AlignJustify, BadgeCheck, House, CircleHelp, ChartGantt, PhoneCall } from "lucide-react";
|
import { Send, AlignJustify, BadgeCheck, House, CircleHelp, ChartGantt, PhoneCall } from "lucide-react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -8,27 +7,25 @@ import {
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
|
||||||
export default function BottomBar() {
|
export default function BottomBar() {
|
||||||
const [email, setEmail] = useState<string>('me@imnya.ng');
|
const [email, setEmail] = useState<string>('me@imnya.ng');
|
||||||
const [hash, setHash] = useState<string>(window.location.hash);
|
const [hash, setHash] = useState<string>(window.location.hash);
|
||||||
|
const [accessKeyCombo, setAccessKeyCombo] = useState<string>("Alt");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const emaillist = ['me', 'mail', 'not', 'cat', 'neko', 'meow', 'heart']
|
const emaillist = ['me', 'mail', 'not', 'cat', 'neko', 'meow', 'heart'];
|
||||||
const domainlist = ['imnya.ng', 'al-1s.kr']
|
const domainlist = ['imnya.ng', 'al-1s.kr'];
|
||||||
|
|
||||||
// furry is 0.001%
|
|
||||||
const randomEmail = () => {
|
const randomEmail = () => {
|
||||||
const random = Math.floor(Math.random() * 1000);
|
const random = Math.floor(Math.random() * 1000);
|
||||||
if (random === 0) {
|
if (random === 0) {
|
||||||
setEmail(`furry@${domainlist[Math.floor(Math.random() * domainlist.length)]}`);
|
setEmail(`furry@${domainlist[Math.floor(Math.random() * domainlist.length)]}`);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
setEmail(`${emaillist[Math.floor(Math.random() * emaillist.length)]}@${domainlist[Math.floor(Math.random() * domainlist.length)]}`);
|
setEmail(`${emaillist[Math.floor(Math.random() * emaillist.length)]}@${domainlist[Math.floor(Math.random() * domainlist.length)]}`);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
randomEmail();
|
randomEmail();
|
||||||
|
|
||||||
const handleHashChange = () => {
|
const handleHashChange = () => {
|
||||||
|
|
@ -42,6 +39,25 @@ export default function BottomBar() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
let keyCombo = "Alt";
|
||||||
|
|
||||||
|
if (/Mac/i.test(ua)) {
|
||||||
|
keyCombo = "Control + Option";
|
||||||
|
} else if (/Linux/i.test(ua)) {
|
||||||
|
keyCombo = "Alt";
|
||||||
|
if (/Firefox/i.test(ua)) {
|
||||||
|
keyCombo = "Alt + Shift";
|
||||||
|
}
|
||||||
|
} else if (/Windows/i.test(ua)) {
|
||||||
|
if (/Firefox/i.test(ua)) {
|
||||||
|
keyCombo = "Alt + Shift";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAccessKeyCombo(keyCombo);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex justify-center fixed bottom-0 z-50">
|
<div className="w-full flex justify-center fixed bottom-0 z-50">
|
||||||
|
|
@ -49,11 +65,14 @@ export default function BottomBar() {
|
||||||
<div className="flex items-center justify-between w-full h-full py-4 px-8">
|
<div className="flex items-center justify-between w-full h-full py-4 px-8">
|
||||||
<a href={`mailto:${email}`} className="flex flex-row gap-4"><Send width={16} /> {email}</a>
|
<a href={`mailto:${email}`} className="flex flex-row gap-4"><Send width={16} /> {email}</a>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => window.location.hash = "#top"} accessKey="1" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
{["top", "about", "project", "timeline", "contact"].map((section, index) => (
|
||||||
<button onClick={() => window.location.hash = "#about"} accessKey="2" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
<button
|
||||||
<button onClick={() => window.location.hash = "#project"} accessKey="3" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
key={section}
|
||||||
<button onClick={() => window.location.hash = "#timeline"} accessKey="4" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
onClick={() => window.location.hash = `#${section}`}
|
||||||
<button onClick={() => window.location.hash = "#contact"} accessKey="5" className="w-[0px] h-[0px] text-[0px] text-background"></button>
|
accessKey={(index + 1).toString()}
|
||||||
|
className="w-[0px] h-[0px] text-[0px] text-background"
|
||||||
|
></button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-center">
|
<div className="flex flex-row items-center justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
@ -61,76 +80,25 @@ export default function BottomBar() {
|
||||||
<button><AlignJustify /></button>
|
<button><AlignJustify /></button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem asChild>
|
{["top", "about", "project", "timeline", "contact"].map((section, index) => {
|
||||||
<a href="#top" className="flex flex-row items-center justify-between">
|
const icons = [House, CircleHelp, ChartGantt, ChartGantt, PhoneCall];
|
||||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
const Icon = icons[index];
|
||||||
{hash === "#top" ? (
|
return (
|
||||||
<BadgeCheck width={16} height={16} />
|
<DropdownMenuItem asChild key={section}>
|
||||||
) : (
|
<a href={`#${section}`} className="flex flex-row items-center justify-between">
|
||||||
<House width={16} height={16} />
|
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
||||||
)}
|
{hash === `#${section}` ? (
|
||||||
Home
|
<BadgeCheck width={16} height={16} />
|
||||||
</div>
|
) : (
|
||||||
|
<Icon width={16} height={16} />
|
||||||
<p className="text-muted-foreground">Alt + 1</p>
|
)}
|
||||||
</a>
|
{section.charAt(0).toUpperCase() + section.slice(1)}
|
||||||
</DropdownMenuItem>
|
</div>
|
||||||
<DropdownMenuItem asChild>
|
<p className="text-muted-foreground">{accessKeyCombo} + {index + 1}</p>
|
||||||
<a href="#about" className="flex flex-row items-center justify-between">
|
</a>
|
||||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
</DropdownMenuItem>
|
||||||
{hash === "#about" ? (
|
);
|
||||||
<BadgeCheck width={16} height={16} />
|
})}
|
||||||
) : (
|
|
||||||
<CircleHelp width={16} height={16} />
|
|
||||||
)}
|
|
||||||
About
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground">Alt + 2</p>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a href="#project" className="flex flex-row items-center justify-between">
|
|
||||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
|
||||||
{hash === "#project" ? (
|
|
||||||
<BadgeCheck width={16} height={16} />
|
|
||||||
) : (
|
|
||||||
<ChartGantt width={16} height={16} />
|
|
||||||
)}
|
|
||||||
Project
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground">Alt + 3</p>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a href="#timeline" className="flex flex-row items-center justify-between">
|
|
||||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
|
||||||
{hash === "#timeline" ? (
|
|
||||||
<BadgeCheck width={16} height={16} />
|
|
||||||
) : (
|
|
||||||
<ChartGantt width={16} height={16} />
|
|
||||||
)}
|
|
||||||
Timeline
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground">Alt + 4</p>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem asChild>
|
|
||||||
<a href="#contact" className="flex flex-row items-center justify-between">
|
|
||||||
<div className="flex flex-row gap-2 items-center justify-between h-4">
|
|
||||||
{hash === "#contact" ? (
|
|
||||||
<BadgeCheck width={16} height={16} />
|
|
||||||
) : (
|
|
||||||
<PhoneCall width={16} height={16} />
|
|
||||||
)}
|
|
||||||
Contact
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground">Alt + 5</p>
|
|
||||||
</a>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuLabel>© 2021-2025 imnyang</DropdownMenuLabel>
|
<DropdownMenuLabel>© 2021-2025 imnyang</DropdownMenuLabel>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
@ -139,5 +107,5 @@ export default function BottomBar() {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,91 @@
|
||||||
import { Github, Instagram, Rss } from "lucide-react";
|
import { Github, Instagram, Rss } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
export default function Contact() {
|
export default function Contact() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen flex items-center justify-center">
|
<div className="w-full h-screen flex items-center justify-center">
|
||||||
<div className="w-full md:w-[50%] p-4 flex items-center justify-center flex-col gap-4">
|
<div className="w-full md:w-[50%] p-4 flex items-center justify-center flex-col gap-4">
|
||||||
<div className="flex items-center justify-center gap-4 flex-row">
|
<div className="flex items-center justify-center gap-4 flex-row">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<a href="https://github.com/imnyang" target="_blank" rel="noreferrer" className="flex flex-row gap-4"><Github /></a>
|
<a
|
||||||
</TooltipTrigger>
|
href="https://github.com/imnyang"
|
||||||
<TooltipContent className="px-2 py-1 text-xs">Github</TooltipContent>
|
target="_blank"
|
||||||
</Tooltip>
|
rel="noreferrer"
|
||||||
<Tooltip>
|
className="flex flex-row gap-4"
|
||||||
<TooltipTrigger asChild>
|
>
|
||||||
<a href="https://x.com/imnya_ng" target="_blank" rel="noreferrer" className="flex flex-row gap-4 text-3xl">𝕏</a>
|
<Github />
|
||||||
</TooltipTrigger>
|
</a>
|
||||||
<TooltipContent className="px-2 py-1 text-xs">𝕏</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipContent className="px-2 py-1 text-xs">
|
||||||
<Tooltip>
|
Github
|
||||||
<TooltipTrigger asChild>
|
</TooltipContent>
|
||||||
<a href="https://instagram.com/loopback.ip" target="_blank" rel="noreferrer" className="flex flex-row gap-4"><Instagram /></a>
|
</Tooltip>
|
||||||
</TooltipTrigger>
|
<Tooltip>
|
||||||
<TooltipContent className="px-2 py-1 text-xs">Instagram</TooltipContent>
|
<TooltipTrigger asChild>
|
||||||
</Tooltip>
|
<a
|
||||||
<Tooltip>
|
href="https://x.com/imnya_ng"
|
||||||
<TooltipTrigger asChild>
|
target="_blank"
|
||||||
<a href="https://blog.imnya.ng" target="_blank" rel="noreferrer" className="flex flex-row gap-4"><Rss /></a>
|
rel="noreferrer"
|
||||||
</TooltipTrigger>
|
className="flex flex-row gap-4 text-3xl"
|
||||||
<TooltipContent className="px-2 py-1 text-xs">Blog</TooltipContent>
|
>
|
||||||
</Tooltip>
|
𝕏
|
||||||
</TooltipProvider>
|
</a>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
<iframe src="https://github.com/sponsors/imnyang/card" title="Sponsor imnyang" height="117" width="600" className="rounded-2xl" />
|
<TooltipContent className="px-2 py-1 text-xs">𝕏</TooltipContent>
|
||||||
</div>
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<a
|
||||||
|
href="https://instagram.com/loopback.ip"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex flex-row gap-4"
|
||||||
|
>
|
||||||
|
<Instagram />
|
||||||
|
</a>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="px-2 py-1 text-xs">
|
||||||
|
Instagram
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<a
|
||||||
|
href="https://blog.imnya.ng"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex flex-row gap-4"
|
||||||
|
>
|
||||||
|
<Rss />
|
||||||
|
</a>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="px-2 py-1 text-xs">
|
||||||
|
Blog
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="flex flex-row gap-3 items-center justify-center">
|
||||||
|
<p>Github</p>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<a href="https://github.com/sponsors/imnyang" target="_blank" rel="noreferrer" className="flex items-center justify-center gap-2">
|
||||||
|
<span>💕</span>
|
||||||
|
<span>Sponsor</span>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue