feat: update dependencies and add Nix code highlighting feature

This commit is contained in:
암냥 2025-12-08 17:53:48 +09:00
commit a126b84165
No known key found for this signature in database
5 changed files with 163 additions and 32 deletions

128
src/app/flake.nix/page.tsx Normal file
View file

@ -0,0 +1,128 @@
"use client";
import { useEffect, useRef, useCallback } from "react";
import hljs from "highlight.js/lib/core";
import nix from "highlight.js/lib/languages/nix";
import "highlight.js/styles/github-dark.css";
hljs.registerLanguage("nix", nix);
declare global {
interface Window {
showSaveFilePicker?: (options?: {
suggestedName?: string;
types?: Array<{
description?: string;
accept: Record<string, string[]>;
}>;
}) => Promise<FileSystemFileHandle>;
}
}
const code = `{
description = "My personal website";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
}
outputs = { self, nixpkgs }: {
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
// install pakages
nix.settings = {
sandbox = false;
experimental-features = [ "flakes" ];
};
time.timeZone = "Asia/Seoul";
}
}`;
const customTheme = `
.hljs {
background: #191017 !important;
color: #fcf8f9 !important;
}
.hljs-attr { color: #f38ba8; }
.hljs-string { color: #a6e3a1; }
.hljs-number { color: #f9e2af; }
.hljs-literal { color: #89b4fa; }
.hljs-type { color: #f5c2e7; }
.hljs-title { color: #94e2d5; }
`;
export default function FlakeNix() {
const codeRef = useRef<HTMLElement>(null);
const downloadFile = useCallback(async () => {
if (window.showSaveFilePicker) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: "flake.nix",
types: [
{
description: "Nix Files",
accept: { "text/plain": [".nix"] },
},
],
});
const writable = await handle.createWritable();
await writable.write(code);
await writable.close();
} catch (e) {
// 사용자가 취소한 경우
if ((e as Error).name !== "AbortError") {
console.error(e);
}
}
} else {
// fallback: showSaveFilePicker를 지원하지 않는 브라우저
const blob = new Blob([code], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "flake.nix";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}, []);
useEffect(() => {
const style = document.createElement("style");
style.textContent = customTheme;
document.head.appendChild(style);
if (codeRef.current) {
hljs.highlightElement(codeRef.current);
}
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
downloadFile();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [downloadFile]);
return (
<main className="flex h-screen items-center justify-center">
<div className="max-w-3xl w-full mx-auto px-6 rounded-2xl overflow-auto">
<pre>
<code ref={codeRef} className="language-nix">
{code}
</code>
</pre>
</div>
</main>
);
}

View file

@ -7,7 +7,7 @@ import Image from "next/image";
export default async function BlogIndex() {
return (
<main className="min-h-screen w-screen">
<section id="top" className="max-w-3xl w-full mx-auto px-4">
{/* <section id="top" className="max-w-3xl w-full mx-auto px-4">
<div className="flex flex-row items-center w-full h-full gap-6 mt-8">
<Image
src="/Frame.svg"
@ -25,8 +25,9 @@ export default async function BlogIndex() {
<Image src="/1500x500.jpg" alt="test image" className="object-cover object-center transition-transform duration-300 hover:scale-105" width={3168} height={1344} />
</picture>
</figure>
</section>
<section id="about" className="h-screen max-w-3xl mx-auto px-4">
</section> */}
<Top />
<section id="about" className="h-screen max-w-4xl mx-auto px-4">
<div className=" mt-8">
<h1 className="text-2xl font-bold mb-4 w-full">💕 About</h1>
<p> <strong> </strong> <strong>, </strong> .</p>

View file

@ -60,30 +60,30 @@ export default function Top() {
const handleBackgroundTouchClick = () => {
if (!isMobile) return;
setClickCount((prev) => {
const newCount = prev + 1;
// Clear existing timeout
if (clickTimeoutRef.current) {
clearTimeout(clickTimeoutRef.current);
}
// Check if required clicks reached
if (newCount >= REQUIRED_CLICKS) {
const newValue = !isMagicalGirlEnabled;
setIsMagicalGirlEnabled(newValue);
document.cookie = `MagicalGirl=${newValue}; path=/; max-age=${60 * 60 * 24 * 365}`;
window.location.reload();
return 0;
}
// Reset click count after 1 second if not enough clicks
clickTimeoutRef.current = setTimeout(() => {
setClickCount(0);
}, 1000);
return newCount;
});
};
@ -131,17 +131,15 @@ export default function Top() {
}
}}
>
{isMagicalGirlEnabled && (
<Image
src={"/char.avif"}
alt="character"
width={4740}
height={7584}
className="w-[50vh] lg:w-[30vw] translate-y-[10%] transition-transform duration-100 ease-out"
unoptimized
onClick={requestPermission}
/>
)}
<Image
src={"/char.avif"}
alt="character"
width={4740}
height={7584}
className="w-[50vh] lg:w-[30vw] translate-y-[10%] transition-transform duration-100 ease-out"
unoptimized
onClick={requestPermission}
/>
</div>
</section>
);