Skip to content

Commit 4814103

Browse files
committed
feat: add 403 Forbidden error page
1 parent fdabcfc commit 4814103

1 file changed

Lines changed: 271 additions & 0 deletions

File tree

src/app/403/page.tsx

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
'use client'
2+
3+
import { useCallback, 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 default function FourOhThreePage() {
19+
const [input, setInput] = useState('')
20+
const [lines, setLines] = useState<string[]>([])
21+
const [matrixVisible, setMatrixVisible] = useState(false)
22+
const [typingText, setTypingText] = useState('')
23+
const [isTyping, setIsTyping] = useState(false)
24+
const [currentOutput, setCurrentOutput] = useState('')
25+
const inputRef = useRef<HTMLInputElement>(null)
26+
const canvasRef = useRef<HTMLCanvasElement>(null)
27+
const terminalRef = useRef<HTMLDivElement>(null)
28+
29+
// Focus the hidden input on any click
30+
useEffect(() => {
31+
const handleClick = () => inputRef.current?.focus()
32+
document.addEventListener('click', handleClick)
33+
inputRef.current?.focus()
34+
return () => document.removeEventListener('click', handleClick)
35+
}, [])
36+
37+
// Mirror input value to the visible output element
38+
useEffect(() => {
39+
setCurrentOutput(input)
40+
}, [input])
41+
42+
// "hack the world" typewriter effect
43+
useEffect(() => {
44+
if (!isTyping) return
45+
let index = 0
46+
setTypingText('')
47+
const timer = window.setInterval(() => {
48+
index += 1
49+
setTypingText(MATRIX_TEXT.slice(0, index))
50+
if (index >= MATRIX_TEXT.length) {
51+
window.clearInterval(timer)
52+
// Show the matrix canvas after typewriter completes (matching original)
53+
setMatrixVisible(true)
54+
}
55+
}, 50)
56+
return () => window.clearInterval(timer)
57+
}, [isTyping])
58+
59+
// Matrix rain canvas
60+
useEffect(() => {
61+
if (!matrixVisible || !canvasRef.current) return
62+
const canvas = canvasRef.current
63+
const context = canvas.getContext('2d')
64+
if (!context) return
65+
66+
const resize = () => {
67+
canvas.width = window.innerWidth
68+
canvas.height = window.innerHeight
69+
}
70+
resize()
71+
window.addEventListener('resize', resize)
72+
73+
const chars = '田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑'.split('')
74+
const fontSize = 10
75+
const columns = Math.floor(window.innerWidth / fontSize)
76+
const drops = Array.from({ length: columns }, () => 1)
77+
78+
const draw = () => {
79+
context.fillStyle = 'rgba(0, 0, 0, 0.05)'
80+
context.fillRect(0, 0, canvas.width, canvas.height)
81+
context.fillStyle = '#0F0'
82+
context.font = `${fontSize}px arial`
83+
for (let i = 0; i < drops.length; i += 1) {
84+
const text = chars[Math.floor(Math.random() * chars.length)]
85+
context.fillText(text, i * fontSize, drops[i] * fontSize)
86+
if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
87+
drops[i] = 0
88+
}
89+
drops[i] += 1
90+
}
91+
}
92+
93+
const interval = window.setInterval(draw, 33)
94+
return () => {
95+
window.removeEventListener('resize', resize)
96+
window.clearInterval(interval)
97+
}
98+
}, [matrixVisible])
99+
100+
const onSubmit = useCallback(
101+
(e: React.FormEvent) => {
102+
e.preventDefault()
103+
const value = input.trim().toLowerCase()
104+
105+
if (value === 'hack the world') {
106+
setLines((prev) => [...prev])
107+
setInput('')
108+
setCurrentOutput('')
109+
setIsTyping(true)
110+
return
111+
}
112+
113+
setLines((prev) => [...prev, 'Sorry that command is not recognized.'])
114+
setInput('')
115+
setCurrentOutput('')
116+
},
117+
[input]
118+
)
119+
120+
return (
121+
<>
122+
{/* CSS for cursor blink animation */}
123+
<style>{`
124+
@keyframes cursor-blink {
125+
0% { opacity: 0; }
126+
50% { opacity: 1; }
127+
100% { opacity: 0; }
128+
}
129+
.cli-new-output::after {
130+
content: '';
131+
display: inline-block;
132+
vertical-align: -0.15em;
133+
width: 0.75em;
134+
height: 1em;
135+
margin-left: 5px;
136+
background: #1ff042;
137+
box-shadow:
138+
1px 1px 1px rgba(31, 240, 66, 0.65),
139+
-1px -1px 1px rgba(31, 240, 66, 0.65),
140+
1px -1px 1px rgba(31, 240, 66, 0.65),
141+
-1px 1px 1px rgba(31, 240, 66, 0.65);
142+
animation: cursor-blink 1.25s steps(1) infinite;
143+
}
144+
`}</style>
145+
146+
{/* Full-screen black background — covers header/footer from root layout */}
147+
<div className="fixed inset-0 z-[200] bg-black">
148+
{/* Matrix canvas — hidden until typewriter completes */}
149+
<canvas
150+
ref={canvasRef}
151+
className="fixed inset-0"
152+
style={{
153+
display: matrixVisible ? 'block' : 'none',
154+
opacity: 0.7,
155+
zIndex: -1,
156+
}}
157+
/>
158+
159+
{/* Hidden form/input — captures keystrokes (matching original Laravel) */}
160+
<form
161+
onSubmit={onSubmit}
162+
className="fixed top-0 left-0 opacity-0"
163+
style={{ backgroundColor: 'black' }}
164+
>
165+
<input
166+
ref={inputRef}
167+
type="text"
168+
value={input}
169+
onChange={(e) => setInput(e.target.value)}
170+
autoComplete="off"
171+
autoCorrect="off"
172+
spellCheck={false}
173+
className="fixed top-0 left-0 bg-black opacity-0"
174+
/>
175+
</form>
176+
177+
{/* Terminal container */}
178+
<div
179+
ref={terminalRef}
180+
className="relative mx-5 mt-10 rounded-lg p-5 pb-0"
181+
style={{
182+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
183+
height: '95%',
184+
paddingTop: '20px',
185+
paddingBottom: '0px',
186+
}}
187+
>
188+
{/* ASCII Art */}
189+
<pre className="overflow-x-auto text-white" style={{ lineHeight: '1', fontSize: '15px' }}>
190+
{ASCII.join('\n')}
191+
</pre>
192+
193+
{/* Initial prompt */}
194+
<p
195+
className="mt-4 block whitespace-pre-wrap font-bold uppercase"
196+
style={{
197+
color: '#1ff042',
198+
fontFamily: "'AndaleMono', monospace",
199+
fontSize: '0.9em',
200+
letterSpacing: '0.15em',
201+
textShadow: '0 0 2px rgba(31, 240, 66, 0.75)',
202+
lineHeight: '1',
203+
marginBottom: '0.75em',
204+
}}
205+
>
206+
<span className="mr-0">{'> '}</span>
207+
{PROMPT}
208+
</p>
209+
210+
{/* Error messages */}
211+
{lines.map((line, index) => (
212+
<p
213+
key={index}
214+
className="block whitespace-pre-wrap font-bold uppercase"
215+
style={{
216+
color: '#1ff042',
217+
fontFamily: "'AndaleMono', monospace",
218+
fontSize: '0.9em',
219+
letterSpacing: '0.15em',
220+
textShadow: '0 0 2px rgba(31, 240, 66, 0.75)',
221+
lineHeight: '1',
222+
marginBottom: '0.75em',
223+
}}
224+
>
225+
<span className="mr-0">{'> '}</span>
226+
{line}
227+
</p>
228+
))}
229+
230+
{/* Typewriter text (hack the world) */}
231+
{isTyping ? (
232+
<p
233+
className="block whitespace-pre-wrap font-bold uppercase"
234+
style={{
235+
color: '#1ff042',
236+
fontFamily: "'AndaleMono', monospace",
237+
fontSize: '0.9em',
238+
letterSpacing: '0.15em',
239+
textShadow: '0 0 2px rgba(31, 240, 66, 0.75)',
240+
lineHeight: '1',
241+
marginBottom: '0.75em',
242+
}}
243+
>
244+
<span className="mr-0">{'> '}</span>
245+
<span>{typingText}</span>
246+
</p>
247+
) : null}
248+
249+
{/* Current input line with blinking cursor */}
250+
{!isTyping ? (
251+
<p
252+
className="block whitespace-pre-wrap font-bold uppercase"
253+
style={{
254+
color: '#1ff042',
255+
fontFamily: "'AndaleMono', monospace",
256+
fontSize: '0.9em',
257+
letterSpacing: '0.15em',
258+
textShadow: '0 0 2px rgba(31, 240, 66, 0.75)',
259+
lineHeight: '1',
260+
marginBottom: '0.75em',
261+
}}
262+
>
263+
<span className="mr-0">{'> '}</span>
264+
<span className="cli-new-output">{currentOutput}</span>
265+
</p>
266+
) : null}
267+
</div>
268+
</div>
269+
</>
270+
)
271+
}

0 commit comments

Comments
 (0)