Compare commits
2 Commits
40d9489395
...
20f0ccb85b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20f0ccb85b | ||
|
|
59cc8ee154 |
@@ -1,25 +1,137 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
|
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||||
|
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
|
||||||
import { prisma } from '@/lib/prisma';
|
import { prisma } from '@/lib/prisma';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// In a real app, you would check for admin session here
|
const { searchParams } = new URL(request.url);
|
||||||
// For now, we trust the 'x-admin-request' header if it's set by the server-side component or middleware
|
const filter = searchParams.get('filter') || 'all';
|
||||||
// but typically you'd verify the session cookie/token
|
const limit = parseInt(searchParams.get('limit') || '50');
|
||||||
|
const offset = parseInt(searchParams.get('offset') || '0');
|
||||||
|
|
||||||
const contacts = await prisma.contact.findMany({
|
let whereClause = {};
|
||||||
orderBy: {
|
|
||||||
createdAt: 'desc',
|
switch (filter) {
|
||||||
},
|
case 'unread':
|
||||||
take: 100,
|
whereClause = { responded: false };
|
||||||
});
|
break;
|
||||||
|
case 'responded':
|
||||||
|
whereClause = { responded: true };
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
whereClause = {};
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ contacts });
|
const [contacts, total] = await Promise.all([
|
||||||
} catch (error) {
|
prisma.contact.findMany({
|
||||||
console.error('Error fetching contacts:', error);
|
where: whereClause,
|
||||||
return NextResponse.json(
|
orderBy: { createdAt: 'desc' },
|
||||||
{ error: 'Failed to fetch contacts' },
|
take: limit,
|
||||||
{ status: 500 }
|
skip: offset,
|
||||||
);
|
}),
|
||||||
}
|
prisma.contact.count({ where: whereClause })
|
||||||
|
]);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
contacts,
|
||||||
|
total,
|
||||||
|
hasMore: offset + contacts.length < total
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Handle missing database table gracefully
|
||||||
|
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn('Contact table does not exist. Returning empty result.');
|
||||||
|
}
|
||||||
|
return NextResponse.json({
|
||||||
|
contacts: [],
|
||||||
|
total: 0,
|
||||||
|
hasMore: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error('Error fetching contacts:', error);
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch contacts' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Rate limiting for POST requests
|
||||||
|
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
|
||||||
|
if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute
|
||||||
|
return new NextResponse(
|
||||||
|
JSON.stringify({ error: 'Rate limit exceeded' }),
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...getRateLimitHeaders(ip, 5, 60000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { name, email, subject, message } = body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!name || !email || !subject || !message) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'All fields are required' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email format
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Invalid email format' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contact = await prisma.contact.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
responded: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Contact created successfully',
|
||||||
|
contact
|
||||||
|
}, { status: 201 });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Handle missing database table gracefully
|
||||||
|
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn('Contact table does not exist.');
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Database table not found. Please run migrations.' },
|
||||||
|
{ status: 503 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.error('Error creating contact:', error);
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to create contact' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from "next/server";
|
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
|
||||||
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
const filter = searchParams.get('filter') || 'all';
|
|
||||||
const limit = parseInt(searchParams.get('limit') || '50');
|
|
||||||
const offset = parseInt(searchParams.get('offset') || '0');
|
|
||||||
|
|
||||||
let whereClause = {};
|
|
||||||
|
|
||||||
switch (filter) {
|
|
||||||
case 'unread':
|
|
||||||
whereClause = { responded: false };
|
|
||||||
break;
|
|
||||||
case 'responded':
|
|
||||||
whereClause = { responded: true };
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
whereClause = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const [contacts, total] = await Promise.all([
|
|
||||||
prisma.contact.findMany({
|
|
||||||
where: whereClause,
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
take: limit,
|
|
||||||
skip: offset,
|
|
||||||
}),
|
|
||||||
prisma.contact.count({ where: whereClause })
|
|
||||||
]);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
contacts,
|
|
||||||
total,
|
|
||||||
hasMore: offset + contacts.length < total
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Handle missing database table gracefully
|
|
||||||
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.warn('Contact table does not exist. Returning empty result.');
|
|
||||||
}
|
|
||||||
return NextResponse.json({
|
|
||||||
contacts: [],
|
|
||||||
total: 0,
|
|
||||||
hasMore: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.error('Error fetching contacts:', error);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch contacts' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Rate limiting for POST requests
|
|
||||||
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
|
|
||||||
if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute
|
|
||||||
return new NextResponse(
|
|
||||||
JSON.stringify({ error: 'Rate limit exceeded' }),
|
|
||||||
{
|
|
||||||
status: 429,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...getRateLimitHeaders(ip, 5, 60000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json();
|
|
||||||
const { name, email, subject, message } = body;
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!name || !email || !subject || !message) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'All fields are required' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate email format
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
if (!emailRegex.test(email)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid email format' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contact = await prisma.contact.create({
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
subject,
|
|
||||||
message,
|
|
||||||
responded: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
message: 'Contact created successfully',
|
|
||||||
contact
|
|
||||||
}, { status: 201 });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Handle missing database table gracefully
|
|
||||||
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.warn('Contact table does not exist.');
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Database table not found. Please run migrations.' },
|
|
||||||
{ status: 503 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.error('Error creating contact:', error);
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to create contact' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,21 +16,27 @@ export default function ClientProviders({
|
|||||||
}) {
|
}) {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [is404Page, setIs404Page] = useState(false);
|
const [is404Page, setIs404Page] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
// Check if we're on a 404 page by looking for the data attribute
|
// Check if we're on a 404 page by looking for the data attribute or pathname
|
||||||
const check404 = () => {
|
const check404 = () => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const has404Component = document.querySelector('[data-404-page]');
|
const has404Component = document.querySelector('[data-404-page]');
|
||||||
setIs404Page(!!has404Component);
|
const is404Path = pathname === '/404' || window.location.pathname === '/404' || window.location.pathname.includes('404');
|
||||||
|
setIs404Page(!!has404Component || is404Path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Check immediately and after a short delay
|
// Check immediately and after a short delay
|
||||||
check404();
|
check404();
|
||||||
const timeout = setTimeout(check404, 100);
|
const timeout = setTimeout(check404, 100);
|
||||||
return () => clearTimeout(timeout);
|
const interval = setInterval(check404, 500);
|
||||||
}, []);
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnalyticsProvider>
|
<AnalyticsProvider>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default function KernelPanic404() {
|
|||||||
const inputContainerRef = useRef<HTMLDivElement>(null);
|
const inputContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
const bodyRef = useRef<HTMLDivElement>(null); // We'll use a wrapper div instead of document.body for some effects if possible, but strict effects might need body.
|
const bodyRef = useRef<HTMLDivElement>(null); // We'll use a wrapper div instead of document.body for some effects if possible, but strict effects might need body.
|
||||||
|
const bootStartedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/* --- SYSTEM CORE --- */
|
/* --- SYSTEM CORE --- */
|
||||||
@@ -20,6 +21,10 @@ export default function KernelPanic404() {
|
|||||||
const body = document.body;
|
const body = document.body;
|
||||||
|
|
||||||
if (!output || !input || !inputContainer || !overlay) return;
|
if (!output || !input || !inputContainer || !overlay) return;
|
||||||
|
|
||||||
|
// Prevent double initialization - check if boot already started or output has content
|
||||||
|
if (bootStartedRef.current || output.children.length > 0) return;
|
||||||
|
bootStartedRef.current = true;
|
||||||
|
|
||||||
let audioCtx: AudioContext | null = null;
|
let audioCtx: AudioContext | null = null;
|
||||||
let systemFrozen = false;
|
let systemFrozen = false;
|
||||||
@@ -554,12 +559,19 @@ export default function KernelPanic404() {
|
|||||||
if (!output) return;
|
if (!output) return;
|
||||||
const d = document.createElement("div");
|
const d = document.createElement("div");
|
||||||
d.innerHTML = text;
|
d.innerHTML = text;
|
||||||
if (type === "log-warn") d.style.color = "#ffb000";
|
// Default color for normal text - use setProperty with important to override globals.css
|
||||||
|
if (!type || (type !== "log-warn" && type !== "log-err" && type !== "alert" && type !== "log-sys" && type !== "log-k" && type !== "log-dim")) {
|
||||||
|
d.style.setProperty("color", "var(--phosphor)", "important");
|
||||||
|
}
|
||||||
|
if (type === "log-warn") d.style.setProperty("color", "#ffb000", "important");
|
||||||
if (type === "log-err" || type === "alert")
|
if (type === "log-err" || type === "alert")
|
||||||
d.style.color = "var(--alert)";
|
d.style.setProperty("color", "var(--alert)", "important");
|
||||||
if (type === "log-dim") d.style.opacity = "0.6";
|
if (type === "log-dim") {
|
||||||
if (type === "log-sys") d.style.color = "cyan";
|
d.style.opacity = "0.6";
|
||||||
if (type === "log-k") d.style.color = "#fff";
|
d.style.setProperty("color", "var(--phosphor)", "important");
|
||||||
|
}
|
||||||
|
if (type === "log-sys") d.style.setProperty("color", "cyan", "important");
|
||||||
|
if (type === "log-k") d.style.setProperty("color", "#fff", "important");
|
||||||
if (type === "pulse-red") d.classList.add("pulse-red");
|
if (type === "pulse-red") d.classList.add("pulse-red");
|
||||||
if (type === "fsociety-mask") d.classList.add("fsociety-mask");
|
if (type === "fsociety-mask") d.classList.add("fsociety-mask");
|
||||||
if (type === "memory-error") d.classList.add("memory-error");
|
if (type === "memory-error") d.classList.add("memory-error");
|
||||||
@@ -1442,18 +1454,40 @@ export default function KernelPanic404() {
|
|||||||
--phosphor: #33ff00;
|
--phosphor: #33ff00;
|
||||||
--phosphor-sec: #008f11;
|
--phosphor-sec: #008f11;
|
||||||
--alert: #ff3333;
|
--alert: #ff3333;
|
||||||
--font: "Courier New", Courier, monospace;
|
--font: 'Courier New', Courier, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--font);
|
||||||
|
color: var(--phosphor);
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
transition: filter 0.5s, transform 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- CRT EFFECTS --- */
|
/* --- CRT EFFECTS --- */
|
||||||
.crt-wrap {
|
.crt-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: radial-gradient(circle at center, #111 0%, #000 100%);
|
background: radial-gradient(circle at center, #111 0%, #000 100%);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
}
|
||||||
|
|
||||||
|
/* Override globals.css div color rule for 404 page - must be very specific */
|
||||||
|
html body:has([data-404-page]) .crt-wrap,
|
||||||
|
html body:has([data-404-page]) .crt-wrap *,
|
||||||
|
html body:has([data-404-page]) #output,
|
||||||
|
html body:has([data-404-page]) #output div,
|
||||||
|
html body:has([data-404-page]) #output * {
|
||||||
|
color: var(--phosphor) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.crt-wrap::before {
|
.crt-wrap::before {
|
||||||
@@ -1463,25 +1497,22 @@ export default function KernelPanic404() {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background:
|
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
|
||||||
linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
|
linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
||||||
linear-gradient(
|
background-size: 100% 4px, 3px 100%;
|
||||||
90deg,
|
|
||||||
rgba(255, 0, 0, 0.06),
|
|
||||||
rgba(0, 255, 0, 0.02),
|
|
||||||
rgba(0, 0, 255, 0.06)
|
|
||||||
);
|
|
||||||
background-size:
|
|
||||||
100% 4px,
|
|
||||||
3px 100%;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 90;
|
z-index: 90;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glow {
|
.glow {
|
||||||
text-shadow:
|
text-shadow: 0 0 2px var(--phosphor-sec), 0 0 8px var(--phosphor);
|
||||||
0 0 2px var(--phosphor-sec),
|
}
|
||||||
0 0 8px var(--phosphor);
|
|
||||||
|
/* Hide chat widget on 404 page */
|
||||||
|
body:has([data-404-page]) [data-chat-widget] {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- TERMINAL --- */
|
/* --- TERMINAL --- */
|
||||||
@@ -1502,7 +1533,6 @@ export default function KernelPanic404() {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: var(--phosphor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#output::-webkit-scrollbar {
|
#output::-webkit-scrollbar {
|
||||||
@@ -1522,7 +1552,6 @@ export default function KernelPanic404() {
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--phosphor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#cmd-input {
|
#cmd-input {
|
||||||
@@ -1847,7 +1876,7 @@ export default function KernelPanic404() {
|
|||||||
|
|
||||||
<div id="flash-overlay" ref={overlayRef}></div>
|
<div id="flash-overlay" ref={overlayRef}></div>
|
||||||
|
|
||||||
<div className="crt-wrap glow" ref={bodyRef}>
|
<div className="crt-wrap glow" ref={bodyRef} data-404-page="true">
|
||||||
<div id="terminal">
|
<div id="terminal">
|
||||||
<div id="output" ref={outputRef}></div>
|
<div id="output" ref={outputRef}></div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
41
app/components/KernelPanic404Wrapper.tsx
Normal file
41
app/components/KernelPanic404Wrapper.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function KernelPanic404Wrapper() {
|
||||||
|
useEffect(() => {
|
||||||
|
// Ensure body and html don't interfere
|
||||||
|
document.body.style.background = "#020202";
|
||||||
|
document.body.style.color = "#33ff00";
|
||||||
|
document.documentElement.style.background = "#020202";
|
||||||
|
document.documentElement.style.color = "#33ff00";
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Cleanup
|
||||||
|
document.body.style.background = "";
|
||||||
|
document.body.style.color = "";
|
||||||
|
document.documentElement.style.background = "";
|
||||||
|
document.documentElement.style.color = "";
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src="/404-terminal.html"
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
border: "none",
|
||||||
|
zIndex: 9999,
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
backgroundColor: "#020202",
|
||||||
|
}}
|
||||||
|
data-404-page="true"
|
||||||
|
allowTransparency={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,30 +1,80 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Suspense } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
// Dynamically import KernelPanic404 to avoid SSR issues
|
// Dynamically import KernelPanic404Wrapper to avoid SSR issues
|
||||||
const KernelPanic404 = dynamic(() => import("./components/KernelPanic404"), {
|
const KernelPanic404 = dynamic(() => import("./components/KernelPanic404Wrapper"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => (
|
loading: () => (
|
||||||
<div className="flex items-center justify-center min-h-screen bg-black text-[#33ff00] font-mono">
|
<div style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: "#020202",
|
||||||
|
color: "#33ff00",
|
||||||
|
fontFamily: "monospace"
|
||||||
|
}}>
|
||||||
<div>Loading terminal...</div>
|
<div>Loading terminal...</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
backgroundColor: "#020202",
|
||||||
|
zIndex: 9998
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
color: "#33ff00",
|
||||||
|
fontFamily: "monospace"
|
||||||
|
}}>
|
||||||
|
Loading terminal...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen w-full bg-black overflow-hidden relative">
|
<div style={{
|
||||||
<Suspense
|
position: "fixed",
|
||||||
fallback={
|
top: 0,
|
||||||
<div className="flex items-center justify-center min-h-screen bg-black text-[#33ff00] font-mono">
|
left: 0,
|
||||||
<div>Loading terminal...</div>
|
width: "100vw",
|
||||||
</div>
|
height: "100vh",
|
||||||
}
|
margin: 0,
|
||||||
>
|
padding: 0,
|
||||||
<KernelPanic404 />
|
overflow: "hidden",
|
||||||
</Suspense>
|
backgroundColor: "#020202",
|
||||||
</main>
|
zIndex: 9998
|
||||||
|
}}>
|
||||||
|
<KernelPanic404 />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const ToastItem = ({ toast, onRemove }: ToastProps) => {
|
|||||||
const getColors = () => {
|
const getColors = () => {
|
||||||
switch (toast.type) {
|
switch (toast.type) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return 'bg-stone-50 border-green-200 text-green-800 shadow-md';
|
return 'bg-stone-50 border-green-300 text-green-900 shadow-md';
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'bg-stone-50 border-red-200 text-red-800 shadow-md';
|
return 'bg-stone-50 border-red-200 text-red-800 shadow-md';
|
||||||
case 'warning':
|
case 'warning':
|
||||||
@@ -291,7 +291,7 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
{children}
|
{children}
|
||||||
|
|
||||||
{/* Toast Container */}
|
{/* Toast Container */}
|
||||||
<div className="fixed top-4 right-4 z-50 space-y-2 max-w-xs">
|
<div className="fixed top-4 right-4 z-50 space-y-3 max-w-sm">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{toasts.map((toast) => (
|
{toasts.map((toast) => (
|
||||||
<ToastItem
|
<ToastItem
|
||||||
|
|||||||
1700
public/404-terminal.html
Normal file
1700
public/404-terminal.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user