feat(ui): implement SUPERCOMMAND component and integrate it into the layout; add Projects component for project display
This commit is contained in:
parent
07f65bd76d
commit
506a9bef40
6 changed files with 151 additions and 4 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import "./scrollbar.css";
|
import "./scrollbar.css";
|
||||||
|
import SUPERCOMMAND from "@/components/SUPERCOMMAND";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "남현석 | :two_hearts: imnya.ng",
|
title: "남현석 | :two_hearts: imnya.ng",
|
||||||
|
|
@ -16,6 +17,7 @@ export default function RootLayout({
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<body className="antialiased">
|
<body className="antialiased">
|
||||||
{children}
|
{children}
|
||||||
|
<SUPERCOMMAND />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import NeoFetch from "@/components/NeoFetch";
|
import NeoFetch from "@/components/NeoFetch";
|
||||||
|
import Projects from "@/components/Projects";
|
||||||
import TimelineComponent from "@/components/timeline";
|
import TimelineComponent from "@/components/timeline";
|
||||||
import Top from "@/components/Top";
|
import Top from "@/components/Top";
|
||||||
|
|
||||||
|
|
@ -16,8 +17,7 @@ export default function Home() {
|
||||||
<p>최근에는 정보보안 분야 중 <strong>웹 해킹</strong>에 관심이 많습니다.</p>
|
<p>최근에는 정보보안 분야 중 <strong>웹 해킹</strong>에 관심이 많습니다.</p>
|
||||||
<br />
|
<br />
|
||||||
<p>대표적인 프로젝트들은 아래와 같습니다.</p>
|
<p>대표적인 프로젝트들은 아래와 같습니다.</p>
|
||||||
<a href="https://www.youtube.com/playlist?list=PLZeYZotn5_IOJDek6e35NKzUtJm09yxZD">Effect Playing Contest 2025 Broadcast Develop</a><br />
|
<Projects />
|
||||||
<a href="https://github.com/imnyang/today.isangjeong">today.isangjeong</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TimelineComponent />
|
<TimelineComponent />
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export default function NeoFetch() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<div id="color" className="grid grid-cols-8 w-fit">
|
<div id="color" className="grid grid-cols-8 w-fit pb-8">
|
||||||
<div className="bg-[#191017] min-w-8"> </div>
|
<div className="bg-[#191017] min-w-8"> </div>
|
||||||
<div className="bg-[#f38ba8] min-w-8"></div>
|
<div className="bg-[#f38ba8] min-w-8"></div>
|
||||||
<div className="bg-[#a6e3a1] min-w-8"></div>
|
<div className="bg-[#a6e3a1] min-w-8"></div>
|
||||||
|
|
|
||||||
55
src/components/Projects.tsx
Normal file
55
src/components/Projects.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { SquareArrowOutUpRight } from 'lucide-react';
|
||||||
|
|
||||||
|
{/* <a href="https://www.youtube.com/playlist?list=PLZeYZotn5_IOJDek6e35NKzUtJm09yxZD">Effect Playing Contest 2025 Broadcast Develop</a><br />
|
||||||
|
<a href="https://github.com/imnyang/today.isangjeong">today.isangjeong</a> */}
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
{
|
||||||
|
name: 'EPC 2025 Broadcast Manager',
|
||||||
|
url: 'https://github.com/NY0510/slunchv2',
|
||||||
|
desc: '얼불춤 끼얏호우',
|
||||||
|
detail: '달성이 주관하고 ADOFAI.gg가 공동 주최하는 Effect Playing Contest 2025 방송 화면의 대부분의 기능을 개발하였습니다.',
|
||||||
|
tags: ['React', 'ElysiaJS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '@today.isangjeong',
|
||||||
|
url: 'https://instagram.com/today.isangjeong',
|
||||||
|
desc: '인스타에서 급식 공유를 간편하게',
|
||||||
|
detail: '오늘의 급식을 사진으로 공유하는 인스타그램 계정입니다. 매일 학교 급식을 자동으로 정리하여 제공합니다.',
|
||||||
|
tags: ['TypeScript', '자체 개발 Instagram Library', '@napi-rs/canvas'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Projects() {
|
||||||
|
return (
|
||||||
|
<section className="break-keep break-words w-full md:w-1/2">
|
||||||
|
<div className="space-y-8">
|
||||||
|
{projects.map((project, idx) => (
|
||||||
|
<div className="space-y-2" key={idx}>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex justify-between items-center gap-y-2 gap-x-4">
|
||||||
|
{project.url ? (
|
||||||
|
<a href={project.url} target="_blank" rel="noopener noreferrer" className="text-lg text-nowrap items-center gap-x-2 flex flex-row">
|
||||||
|
{project.name}
|
||||||
|
<SquareArrowOutUpRight size={16} />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-lg font-medium">{project.name}</span>
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-muted-foreground text-nowrap">{project.desc}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">{project.detail}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 *:bg-muted *:px-2 *:py-0.5 *:rounded *:text-xs *:font-medium *:text-muted-foreground">
|
||||||
|
{project.tags.map((tag, i) => (
|
||||||
|
<span key={i}>{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
src/components/SUPERCOMMAND.tsx
Normal file
90
src/components/SUPERCOMMAND.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
"use client";
|
||||||
|
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 [keySequence, setKeySequence] = React.useState("");
|
||||||
|
const [showDialog, setShowDialog] = React.useState(false);
|
||||||
|
const targetSequence = "MAGICALWORLD";
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
// 영문자만 처리
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 목표 시퀀스와 정확히 일치하는지 확인 (대문자만)
|
||||||
|
if (newSequence === targetSequence) {
|
||||||
|
console.log("Sequence matched!");
|
||||||
|
setShowDialog(true);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 목표 시퀀스의 시작 부분과 일치하는지 확인 (대문자만)
|
||||||
|
if (targetSequence.startsWith(newSequence)) {
|
||||||
|
return newSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 일치하지 않으면 현재 키부터 다시 시작
|
||||||
|
const restartSequence = event.key;
|
||||||
|
if (targetSequence.startsWith(restartSequence)) {
|
||||||
|
return restartSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 완전히 초기화
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 특수키나 숫자가 입력되면 시퀀스 초기화
|
||||||
|
setKeySequence("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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" onClick={() => {
|
||||||
|
document.cookie = "MagicalGirl=true; path=/";
|
||||||
|
setShowDialog(false);
|
||||||
|
}}>Okay</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ export default function TimelineComponent() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="timeline"
|
id="timeline"
|
||||||
className="w-full flex flex-col items-center justify-center px-12 mt-8"
|
className="w-full md:w-1/2 flex flex-col items-center justify-center px-12 mt-8"
|
||||||
>
|
>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<h1 className="text-2xl font-bold mb-4 w-full">🌠 수상 및 교육</h1>
|
<h1 className="text-2xl font-bold mb-4 w-full">🌠 수상 및 교육</h1>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue