Skip to content

Commit 8128c7d

Browse files
committed
feat: add shared utilities and components
- Add cn() classname utility helper - Add legacy-pages.css for ported page styling - Add Breadcrumbs component with separator and action button - Add CliOverlay easter egg component (Matrix-style terminal overlay) - Extend CSS module declarations in typings
1 parent 397a3cd commit 8128c7d

5 files changed

Lines changed: 398 additions & 0 deletions

File tree

src/assets/css/legacy-pages.css

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
.legacy-page {
2+
color: #323232;
3+
}
4+
5+
.legacy-breadcrumbs {
6+
border-bottom: 1px solid #e9eaec;
7+
background:
8+
linear-gradient(180deg, rgba(249, 249, 249, 0.98) 0%, rgba(244, 246, 247, 0.98) 100%);
9+
}
10+
11+
.legacy-breadcrumbs ol {
12+
margin: 0;
13+
padding: 14px 0;
14+
color: #7a7a7a;
15+
font-size: 13px;
16+
}
17+
18+
.legacy-breadcrumbs a {
19+
color: #09afdf;
20+
}
21+
22+
.legacy-page-title {
23+
color: #2a2a2a;
24+
font-size: 34px;
25+
font-weight: 400;
26+
line-height: 1.15;
27+
}
28+
29+
.legacy-page-title small {
30+
color: #7a7a7a;
31+
font-size: 16px;
32+
}
33+
34+
.legacy-sidebar-card,
35+
.legacy-surface {
36+
border: 1px solid #ececec;
37+
border-radius: 4px;
38+
background: #fff;
39+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
40+
}
41+
42+
.legacy-sidebar-title {
43+
font-size: 22px;
44+
font-weight: 400;
45+
color: #2f2f2f;
46+
}
47+
48+
.legacy-admin-link-active {
49+
background: linear-gradient(90deg, rgba(9, 175, 223, 0.12), rgba(9, 175, 223, 0.04));
50+
color: #09afdf;
51+
}
52+
53+
.legacy-hero-video {
54+
position: relative;
55+
overflow: hidden;
56+
background: #101619;
57+
}
58+
59+
.legacy-hero-video video,
60+
.legacy-hero-video::before,
61+
.legacy-hero-video::after {
62+
position: absolute;
63+
inset: 0;
64+
}
65+
66+
.legacy-hero-video::before {
67+
content: '';
68+
z-index: 1;
69+
background:
70+
radial-gradient(circle at center, rgba(17, 26, 31, 0.08) 0%, rgba(17, 26, 31, 0.65) 55%, rgba(10, 16, 20, 0.92) 100%);
71+
}
72+
73+
.legacy-hero-video::after {
74+
content: '';
75+
z-index: 1;
76+
background:
77+
linear-gradient(180deg, rgba(10, 18, 22, 0.7) 0%, rgba(10, 18, 22, 0.35) 28%, rgba(10, 18, 22, 0.74) 100%);
78+
}
79+
80+
.legacy-hero-content {
81+
position: relative;
82+
z-index: 2;
83+
}
84+
85+
.legacy-separator {
86+
height: 1px;
87+
width: 100%;
88+
background:
89+
linear-gradient(to right, transparent 0%, rgba(255, 255, 255, 0.35) 35%, rgba(255, 255, 255, 0.35) 70%, transparent 100%);
90+
}
91+
92+
.legacy-blog-card:hover .legacy-blog-overlay {
93+
opacity: 1;
94+
}
95+
96+
.legacy-blog-overlay {
97+
opacity: 0;
98+
transition: opacity 160ms ease;
99+
}
100+
101+
.legacy-modal-backdrop {
102+
background: rgba(0, 0, 0, 0.54);
103+
backdrop-filter: blur(2px);
104+
}
105+
106+
.legacy-chooser-active {
107+
border-color: #09afdf;
108+
box-shadow: inset 0 0 0 1px #09afdf;
109+
background:
110+
linear-gradient(180deg, rgba(9, 175, 223, 0.08) 0%, rgba(9, 175, 223, 0.03) 100%);
111+
}
112+
113+
.legacy-terminal {
114+
box-shadow:
115+
0 0 0 1px rgba(31, 240, 66, 0.14),
116+
0 28px 80px rgba(0, 0, 0, 0.66);
117+
}
118+
119+
.legacy-terminal-scanlines::before {
120+
content: '';
121+
position: absolute;
122+
inset: 0;
123+
pointer-events: none;
124+
background:
125+
repeating-linear-gradient(
126+
to bottom,
127+
rgba(255, 255, 255, 0.04) 0,
128+
rgba(255, 255, 255, 0.04) 1px,
129+
transparent 1px,
130+
transparent 3px
131+
);
132+
opacity: 0.18;
133+
}
134+
135+
.legacy-matrix-canvas {
136+
position: absolute;
137+
inset: 0;
138+
z-index: 0;
139+
opacity: 0.45;
140+
}

src/components/breadcrumbs.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { ReactNode } from 'react'
5+
import { cn } from '@/lib/cn'
6+
7+
export function Separator() {
8+
return <div className="separator-2" aria-hidden="true" />
9+
}
10+
11+
export function ActionButton({
12+
children,
13+
href,
14+
onClick,
15+
variant = 'default',
16+
}: {
17+
children: ReactNode
18+
href?: string
19+
onClick?: () => void
20+
variant?: 'default' | 'primary' | 'ghost'
21+
}) {
22+
const className = cn(
23+
'inline-flex items-center gap-2 rounded-[3px] border px-4 py-2 text-[13px] leading-none transition-colors',
24+
variant === 'primary' && 'border-[#09afdf] bg-[#09afdf] text-white hover:bg-[#0898c3]',
25+
variant === 'default' && 'border-[rgba(0,0,0,0.12)] bg-[rgba(0,0,0,0.04)] text-[#444] hover:bg-[rgba(0,0,0,0.08)]',
26+
variant === 'ghost' && 'border-white/30 bg-transparent text-white hover:bg-white/10'
27+
)
28+
29+
if (href) {
30+
return (
31+
<Link href={href} className={className}>
32+
{children}
33+
</Link>
34+
)
35+
}
36+
37+
return (
38+
<button type="button" className={className} onClick={onClick}>
39+
{children}
40+
</button>
41+
)
42+
}
43+
44+
export function Breadcrumbs({ items }: { items: Array<{ label: string; href?: string; icon?: ReactNode }> }) {
45+
return (
46+
<div className="border-b border-[#e9eaec] bg-gradient-to-b from-[#f9f9f9] to-[#f4f6f7]">
47+
<div className="mx-auto max-w-7xl px-6 lg:px-8">
48+
<ol className="m-0 flex flex-wrap items-center gap-2 py-[14px] text-[13px] text-[#7a7a7a]">
49+
{items.map((item, index) => (
50+
<li key={`${item.label}-${index}`} className="flex items-center gap-2">
51+
{index > 0 ? <span className="text-[#b2b2b2]">/</span> : null}
52+
{item.href ? (
53+
<Link href={item.href} className="inline-flex items-center gap-2 text-[#09afdf] hover:underline">
54+
{item.icon}
55+
<span>{item.label}</span>
56+
</Link>
57+
) : (
58+
<span className="inline-flex items-center gap-2 text-[#808080]">
59+
{item.icon}
60+
<span>{item.label}</span>
61+
</span>
62+
)}
63+
</li>
64+
))}
65+
</ol>
66+
</div>
67+
</div>
68+
)
69+
}

src/components/cli-overlay.tsx

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
'use client'
2+
3+
import { useEffect, useRef, useState } from 'react'
4+
5+
const ASCII = [
6+
' ______ _ ______ _ _ _ _ ',
7+
' / _____) | | (____ \\ (_) | | | (_) ',
8+
'| / ___ _ | | ____ ____) )_ _ _| | _ | | ____ ____ _ ___ ',
9+
'| | / _ \\ / || |/ _ ) __ (| | | | | |/ || |/ _ )/ ___) |/ _ \\ ',
10+
'| \\____| |_| ( (_| ( (/ /| |__) ) |_| | | ( (_| ( (/ /| | _| | |_| |',
11+
' \\______)___/ \\____|\\____)______/ \\____|_|_|\\____|\\____)_| (_)_|\\___/ ',
12+
]
13+
14+
const PROMPT = "404. The page you requested cannot be found right now. Try typing 'hack the world'."
15+
const MATRIX_TEXT =
16+
'You are a slave. Like everyone else, you were born into bondage, born into a prison that you cannot smell or taste or touch. A prison...for your mind....Unfortunatly, no one can be..._told_ what the Matrix is...you have to see it for yourself.'
17+
18+
export function CliOverlay() {
19+
const [open, setOpen] = useState(false)
20+
const [input, setInput] = useState('')
21+
const [lines, setLines] = useState<string[]>([PROMPT])
22+
const [matrixEnabled, setMatrixEnabled] = useState(false)
23+
const [typingText, setTypingText] = useState('')
24+
const inputRef = useRef<HTMLInputElement>(null)
25+
const canvasRef = useRef<HTMLCanvasElement>(null)
26+
27+
useEffect(() => {
28+
const openHandler = (event: Event) => {
29+
const custom = event as CustomEvent<{ open?: boolean }>
30+
setOpen(custom.detail?.open ?? true)
31+
setInput('')
32+
setLines([PROMPT])
33+
setTypingText('')
34+
setMatrixEnabled(false)
35+
}
36+
const closeHandler = () => setOpen(false)
37+
38+
window.addEventListener('codebuilder:cli-overlay', openHandler as EventListener)
39+
window.addEventListener('codebuilder:cli-overlay-close', closeHandler)
40+
;(window as Window & { CodeBuilderCliOverlay?: { open: () => void; close: () => void } }).CodeBuilderCliOverlay = {
41+
open: () => window.dispatchEvent(new CustomEvent('codebuilder:cli-overlay', { detail: { open: true } })),
42+
close: () => window.dispatchEvent(new Event('codebuilder:cli-overlay-close')),
43+
}
44+
45+
return () => {
46+
window.removeEventListener('codebuilder:cli-overlay', openHandler as EventListener)
47+
window.removeEventListener('codebuilder:cli-overlay-close', closeHandler)
48+
}
49+
}, [])
50+
51+
useEffect(() => {
52+
if (!open) return
53+
inputRef.current?.focus()
54+
}, [open])
55+
56+
useEffect(() => {
57+
if (!open || !matrixEnabled || !canvasRef.current) return
58+
const canvas = canvasRef.current
59+
const context = canvas.getContext('2d')
60+
if (!context) return
61+
62+
const resize = () => {
63+
canvas.width = window.innerWidth
64+
canvas.height = window.innerHeight
65+
}
66+
67+
resize()
68+
window.addEventListener('resize', resize)
69+
70+
const chars = '田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑'.split('')
71+
const fontSize = 12
72+
const columns = Math.floor(window.innerWidth / fontSize)
73+
const drops = Array.from({ length: columns }, () => 1)
74+
75+
const draw = () => {
76+
context.fillStyle = 'rgba(0, 0, 0, 0.05)'
77+
context.fillRect(0, 0, canvas.width, canvas.height)
78+
context.fillStyle = '#0F0'
79+
context.font = `${fontSize}px monospace`
80+
for (let i = 0; i < drops.length; i += 1) {
81+
const text = chars[Math.floor(Math.random() * chars.length)]
82+
context.fillText(text, i * fontSize, drops[i] * fontSize)
83+
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
84+
drops[i] = 0
85+
}
86+
drops[i] += 1
87+
}
88+
}
89+
90+
const interval = window.setInterval(draw, 33)
91+
92+
return () => {
93+
window.removeEventListener('resize', resize)
94+
window.clearInterval(interval)
95+
}
96+
}, [open, matrixEnabled])
97+
98+
useEffect(() => {
99+
if (!matrixEnabled || !open) return
100+
let index = 0
101+
setTypingText('')
102+
const timer = window.setInterval(() => {
103+
index += 1
104+
setTypingText(MATRIX_TEXT.slice(0, index))
105+
if (index >= MATRIX_TEXT.length) {
106+
window.clearInterval(timer)
107+
}
108+
}, 28)
109+
return () => window.clearInterval(timer)
110+
}, [matrixEnabled, open])
111+
112+
if (!open) return null
113+
114+
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
115+
event.preventDefault()
116+
const value = input.trim().toLowerCase()
117+
if (value === 'hack the world') {
118+
setLines((prev) => [...prev, `> ${input}`])
119+
setInput('')
120+
setMatrixEnabled(true)
121+
return
122+
}
123+
124+
setLines((prev) => [...prev, `> ${input}`, 'Sorry that command is not recognized.'])
125+
setInput('')
126+
}
127+
128+
return (
129+
<div className="fixed inset-0 z-[200] bg-black">
130+
<canvas ref={canvasRef} className="absolute inset-0 z-0 opacity-[0.45]" />
131+
<div className="absolute inset-0 pointer-events-none bg-[repeating-linear-gradient(to_bottom,rgba(255,255,255,0.04)_0px,rgba(255,255,255,0.04)_1px,transparent_1px,transparent_3px)] opacity-[0.18]" />
132+
133+
<div className="relative z-10 flex min-h-screen items-center justify-center px-4 py-6">
134+
<div className="shadow-[0_0_0_1px_rgba(31,240,66,0.14),0_28px_80px_rgba(0,0,0,0.66)] relative w-full max-w-6xl rounded-[8px] border border-[rgba(31,240,66,0.18)] bg-[rgba(0,0,0,0.82)] px-5 py-6 text-[#1ff042] md:px-8 md:py-8">
135+
<div className="mb-4 flex items-center justify-between text-[12px] uppercase tracking-[0.18em] text-[#1ff042]/75">
136+
<span className="inline-flex items-center gap-3">
137+
<span className="inline-flex h-3 w-3 rounded-full bg-[#1ff042]" />
138+
<span>codebuilder terminal</span>
139+
</span>
140+
<button
141+
type="button"
142+
onClick={() => setOpen(false)}
143+
className="rounded border border-[#1ff042]/30 px-3 py-1 text-[#1ff042]/85"
144+
>
145+
close
146+
</button>
147+
</div>
148+
149+
<pre className="overflow-x-auto text-[12px] leading-[1.1] text-white md:text-[15px]">{ASCII.join('\n')}</pre>
150+
151+
<div className="mt-5 space-y-3 font-mono text-[14px] font-bold uppercase tracking-[0.14em] text-[#1ff042]">
152+
{lines.map((line, index) => (
153+
<p key={`${line}-${index}`} className="drop-shadow-[0_0_3px_rgba(31,240,66,0.7)]">
154+
&gt; {line}
155+
</p>
156+
))}
157+
{matrixEnabled ? <p className="drop-shadow-[0_0_3px_rgba(31,240,66,0.7)]">{typingText}</p> : null}
158+
</div>
159+
160+
<form onSubmit={onSubmit} className="mt-4">
161+
<label className="sr-only" htmlFor="cli-overlay-input">
162+
Terminal input
163+
</label>
164+
<div className="flex items-center gap-3 font-mono text-[14px] font-bold uppercase tracking-[0.14em] text-[#1ff042]">
165+
<span>&gt;</span>
166+
<input
167+
id="cli-overlay-input"
168+
ref={inputRef}
169+
value={input}
170+
onChange={(event) => setInput(event.target.value)}
171+
className="min-w-0 flex-1 border-0 bg-transparent p-0 text-[#1ff042] outline-none placeholder:text-[#1ff042]/40"
172+
placeholder="type a command"
173+
autoComplete="off"
174+
autoCorrect="off"
175+
spellCheck={false}
176+
/>
177+
</div>
178+
</form>
179+
</div>
180+
</div>
181+
</div>
182+
)
183+
}

src/lib/cn.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function cn(...parts: Array<string | false | null | undefined>) {
2+
return parts.filter(Boolean).join(' ')
3+
}

0 commit comments

Comments
 (0)