Better Github Sponsors && HotKey && Scroll Hash

This commit is contained in:
암냥 2025-03-02 01:13:52 +09:00
commit c4ed0b3e94
No known key found for this signature in database
GPG key ID: C96C0327210DD61A
3 changed files with 170 additions and 131 deletions

View file

@ -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 (

View file

@ -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>
) );
} }

View file

@ -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>
);
} }