feat: add @radix-ui/react-dialog dependency and implement dialog component
- Added @radix-ui/react-dialog to dependencies in package.json and bun.lock. - Updated page.tsx to replace placeholder text with "We are in MAGICALWORLD!". - Refactored SUPERCOMMAND component to handle key sequences and show dialog on match. - Implemented Dialog component with header, footer, and description using Radix UI primitives.
This commit is contained in:
parent
08f4fad646
commit
a3a53a68ec
5 changed files with 311 additions and 100 deletions
|
|
@ -1,114 +1,145 @@
|
|||
import React from "react";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
export default function SUPERCOMMAND() {
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const [tabCount, setTabCount] = React.useState(() => {
|
||||
const saved = localStorage.getItem('tabCount');
|
||||
return saved ? parseInt(saved, 10) : 0;
|
||||
});
|
||||
const [showPressSpace, setShowPressSpace] = React.useState(false);
|
||||
const audioContextRef = React.useRef<AudioContext | null>(null);
|
||||
|
||||
// tabCount가 변경될 때마다 localStorage에 저장
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem('tabCount', tabCount.toString());
|
||||
}, [tabCount]);
|
||||
|
||||
// Beep 소리를 생성하는 함수 (Web Audio API 사용)
|
||||
const playBeep = (count: number) => {
|
||||
if (!audioContextRef.current) {
|
||||
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
}
|
||||
const context = audioContextRef.current;
|
||||
const oscillator = context.createOscillator();
|
||||
const gainNode = context.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(context.destination);
|
||||
|
||||
const frequency = 440 + (count - 1); // 콤보 수에 따라 주파수 증가 (440Hz부터 10Hz씩 상승)
|
||||
oscillator.frequency.setValueAtTime(frequency, context.currentTime);
|
||||
oscillator.type = 'sine';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.1, context.currentTime); // 볼륨
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, context.currentTime + 0.5); // 0.5초 후 페이드 아웃
|
||||
|
||||
oscillator.start(context.currentTime);
|
||||
oscillator.stop(context.currentTime + 0.5); // 0.5초로 길게 설정
|
||||
};
|
||||
const [keySequence, setKeySequence] = React.useState("");
|
||||
const [showDialog, setShowDialog] = React.useState(false);
|
||||
const targetSequence = "MAGICALWORLD";
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
console.log(event.key)
|
||||
// Tab 누르면 보이게
|
||||
if (event.key === "Tab") {
|
||||
// event.preventDefault();
|
||||
setVisible(true);
|
||||
playBeep(tabCount); // Tab 누를 때마다 beep 소리 재생
|
||||
} else if (event.key !== "Tab" && event.key !== "Control" && event.key !== "Alt" && event.key !== "Meta" && event.key !== "Shift") {
|
||||
setVisible(false);
|
||||
setTabCount(0);
|
||||
setShowPressSpace(false);
|
||||
}
|
||||
// 영문자만 처리
|
||||
if (event.key.length === 1 && /[a-zA-Z]/.test(event.key)) {
|
||||
console.log(keySequence);
|
||||
console.log(targetSequence);
|
||||
console.log("Key pressed:", event.key);
|
||||
setKeySequence(prev => {
|
||||
const newSequence = prev + event.key;
|
||||
console.log("New sequence:", newSequence);
|
||||
|
||||
// 'Tab' 키를 누를 때마다 카운트(연속 또는 누적)
|
||||
if (event.key === "Tab") {
|
||||
setTabCount((prev) => {
|
||||
const next = prev + 1;
|
||||
if (next === 8 || next === 1108) {
|
||||
setShowPressSpace(true);
|
||||
} else if (next > 8) {
|
||||
setShowPressSpace(false);
|
||||
// 목표 시퀀스와 정확히 일치하는지 확인 (대문자만)
|
||||
if (newSequence === targetSequence) {
|
||||
console.log("Sequence matched!");
|
||||
setShowDialog(true);
|
||||
return "";
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
// 스페이스바 눌렀을 때, 메시지가 보이는 상태이면 페이지 이동
|
||||
if ((event.code === "Space" || event.key === " ") && showPressSpace) {
|
||||
// 원하는 이동 경로로 변경하세요:
|
||||
if (tabCount === 8) {
|
||||
localStorage.setItem('visited8', 'true'); // 방문 플래그 저장
|
||||
const redirectList = [
|
||||
"https://www.youtube.com/watch?v=DjGxGMxvg4M",
|
||||
"https://www.youtube.com/watch?v=oQcaPVGUtuA",
|
||||
"https://www.youtube.com/watch?v=E6RQgBwcmG8",
|
||||
"https://www.youtube.com/watch?v=xUNFDn2my68",
|
||||
"https://www.youtube.com/watch?v=W-_90c08AIY",
|
||||
"https://www.youtube.com/watch?v=GyEIHPyIQQg",
|
||||
"https://www.youtube.com/watch?v=xZi12DkWkHA"
|
||||
]
|
||||
window.location.href = redirectList[Math.floor(Math.random() * redirectList.length)];
|
||||
} else if (tabCount === 1108) {
|
||||
window.location.href = "https://imnya.ng/whoamiandwhoareyou ";
|
||||
}
|
||||
// 목표 시퀀스의 시작 부분과 일치하는지 확인 (대문자만)
|
||||
if (targetSequence.startsWith(newSequence)) {
|
||||
return newSequence;
|
||||
}
|
||||
|
||||
// 일치하지 않으면 현재 키부터 다시 시작
|
||||
const restartSequence = event.key;
|
||||
if (targetSequence.startsWith(restartSequence)) {
|
||||
return restartSequence;
|
||||
}
|
||||
|
||||
// 완전히 초기화
|
||||
return "";
|
||||
});
|
||||
} else {
|
||||
// 특수키나 숫자가 입력되면 시퀀스 초기화
|
||||
setKeySequence("");
|
||||
}
|
||||
};
|
||||
|
||||
// const handleKeyUp = (event: KeyboardEvent) => {
|
||||
// // Tab에서 손을 떼면 숨김
|
||||
// if (event.key === "Tab") {
|
||||
// setVisible(false);
|
||||
// // 필요하면 카운트/메시지 리셋
|
||||
// // setEightCount(0);
|
||||
// // setShowPressSpace(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
// window.addEventListener("keyup", handleKeyUp);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
// window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, [showPressSpace]);
|
||||
|
||||
if (!visible) return null;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="supercommand" className="fixed bottom-5 left-5">
|
||||
<h1 className="text-2xl"><span className="text-3xl font-bold">{tabCount}</span> Combo</h1>
|
||||
{showPressSpace ? <p>스페이스바를 눌러주세요.{tabCount === 8 && localStorage.getItem('visited8') ? null : null}</p> : null}
|
||||
</div>
|
||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||
<DialogContent className="flex flex-col gap-0 p-0 sm:max-h-[min(640px,80vh)] sm:max-w-lg [&>button:last-child]:top-3.5">
|
||||
<DialogHeader className="contents space-y-0 text-left">
|
||||
<DialogTitle className="border-b px-6 py-4 text-base">
|
||||
???
|
||||
</DialogTitle>
|
||||
<div className="overflow-y-auto">
|
||||
<DialogDescription asChild>
|
||||
<div className="px-6 py-4">
|
||||
<div className="[&_strong]:text-foreground space-y-4 [&_strong]:font-semibold pb-32">
|
||||
<p style={{ whiteSpace: 'pre-line' }}>{`마법소녀는 세상을 구했는데
|
||||
어째서 나는 구원 받을 수 없는 거야?
|
||||
누군가 도와주러 와줘
|
||||
|
||||
마법소녀는 세상을 구했는데
|
||||
어째서 나는 구원 받을 수 없는 거야?
|
||||
바라던 평화가 이루어진 순간에
|
||||
저는 필요 없어진 아이인가요?
|
||||
결국 편할대로 이용당해서
|
||||
심정도 신체도 상처투성이고
|
||||
이런 세상이라면 이젠 사라져버려
|
||||
|
||||
장점조차 없어서 자신을 사랑하지 못하고
|
||||
무엇을 해봐도 실패뿐인 열등생이네
|
||||
그럼에도 다른 누군가에게 필요해지길 원해서
|
||||
나 마법소녀가 되어 다시 태어날 거야
|
||||
모두의 감사에 채워져 가
|
||||
|
||||
하지만 언제부턴가 사람들에게 익숙해져
|
||||
당연하게 되었어
|
||||
마법소녀는 죽을 각오를 했는데
|
||||
어째서 나는 보답받지 못하는 거야?
|
||||
쌓인 불안이 해소된 순간에
|
||||
저는 쓸모 없어지는 건가요?
|
||||
|
||||
타인의 불행으로 성립되는 평화에
|
||||
의심조차 품지 않고 살아가다니
|
||||
어이없는 놈들이네
|
||||
이런 세상은 전부 전부 싫어져서
|
||||
믿을 수 있는 것조차 사라지고
|
||||
변하지 못하고 돌아오지 못하고
|
||||
이젠 어떡할 수 없어 괴로워
|
||||
|
||||
마법소녀는 세상을 구했는데
|
||||
어째서 나는 구원 받을 수 없는 거야?
|
||||
누군가 누군가 가르쳐줘 부탁이야
|
||||
모두의 감사가 힘이 된다
|
||||
하지만 언제부턴가 그 수는 줄고
|
||||
어라 나 바보 같아
|
||||
마법소녀는 희망을 잃어버리고
|
||||
눈물과 같이 흘러내려
|
||||
|
||||
마법소녀는 세상을 구했는데
|
||||
어째서 나는 구원 받을 수 없는 거야?
|
||||
바라던 평화가 이루어진 순간에
|
||||
저는 필요없어진 아이인가요?
|
||||
결국 편할대로 이용당해서
|
||||
심정도 신체도 상처투성이고
|
||||
마법이 풀려가
|
||||
|
||||
이런 세상 전부 전부 부숴버리고
|
||||
행복한 결말을 찾아
|
||||
이걸로 괜찮아 이걸로 괜찮아
|
||||
근데 어째서 마음이 답답한 거야?
|
||||
|
||||
마법소녀는 세상을 부쉈는데
|
||||
어째서 나는 구원 받을 수 없는 거야?
|
||||
누군가 누군가 빨리 빨리
|
||||
나를 데려가 줘
|
||||
절망의 늪에서 작별을 고했어`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
<DialogFooter className="px-6 py-6 sm:justify-start fixed bottom-0 left-0 w-full bg-background/80 backdrop-blur-sm">
|
||||
<Button type="button">Okay</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
141
src/components/ui/dialog.tsx
Normal file
141
src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue