Compare commits
35 Commits
0b04eed82b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 88acf47ed5 | |||
| 9211ba9446 | |||
| 95368d156d | |||
| 8b7cf681b4 | |||
| 5b675c21ba | |||
| 281325c9db | |||
| f3e176f6c7 | |||
| 8a9c9517cb | |||
| 90f6e840f3 | |||
| 598fbf9d4a | |||
| 15e4fd556c | |||
| 50461f1644 | |||
| 7cd5113ef0 | |||
| 46fdd77353 | |||
| 3866e8ade0 | |||
| 107ee8794d | |||
| 1215f2f987 | |||
| 32e609ad71 | |||
| 29d251459a | |||
| 4760f51095 | |||
| 6510022716 | |||
| 573b6202be | |||
| 15eb1ea46b | |||
| 76e2455889 | |||
| 6d9e5ae56f | |||
| 0fcefcbbe5 | |||
| b7c69fb781 | |||
| 3a5e1f07cf | |||
| 3c90cbbe2e | |||
| 4065f0cd84 | |||
| a4b3d74af3 | |||
| bbd92bcdda | |||
| f8195aa401 | |||
| b3c03e0c41 | |||
| fe79418bfd |
11
Dockerfile
11
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine AS builder
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache yarn
|
||||
|
||||
@@ -13,14 +13,17 @@ COPY ./src/app/favicon.ico ./public/favicon.ico
|
||||
|
||||
RUN yarn build
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
RUN adduser --system --uid 1001 --ingroup nodejs nextjs
|
||||
|
||||
ENV NODE_ENV production
|
||||
RUN mkdir -p ./.next/cache
|
||||
RUN chown nextjs:nodejs ./.next/cache
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
USER nextjs
|
||||
|
||||
|
||||
28
amp.Dockerfile
Normal file
28
amp.Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
# Install dependencies
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock* ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Build
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN yarn build
|
||||
|
||||
# Production image
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Only copy necessary files
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
@@ -6,8 +6,7 @@ const nextConfig = {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'media.tenor.com',
|
||||
pathname: '/1BCeG1aTiBAAAAAd/temptation-stairway-ena.gif'
|
||||
hostname: 'media.tenor.com'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
6218
package-lock.json
generated
6218
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -13,20 +13,29 @@
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@next/mdx": "^15.3.4",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"axios": "^1.13.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "14.2.24",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"ioredis": "^5.8.2",
|
||||
"jsdom": "^27.4.0",
|
||||
"next": "^14.2.35",
|
||||
"nodemailer": "^7.0.12",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
"react-dom": "^18",
|
||||
"sharp": "^0.34.5",
|
||||
"validator": "^13.15.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^5.2.0",
|
||||
"@types/jsdom": "^27.0.0",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/validator": "^13.15.10",
|
||||
"add": "^2.0.6",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.24",
|
||||
"eslint-config-next": "^16.0.10",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5",
|
||||
|
||||
BIN
public/images/coral-dialogue.jpg
Normal file
BIN
public/images/coral-dialogue.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
public/images/coralz_idle.png
Normal file
BIN
public/images/coralz_idle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
BIN
public/images/coralz_poked.png
Normal file
BIN
public/images/coralz_poked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 176 KiB |
BIN
public/images/coralz_scared.png
Normal file
BIN
public/images/coralz_scared.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
BIN
public/images/event_santahat1.png
Normal file
BIN
public/images/event_santahat1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
public/images/event_snowflake.png
Normal file
BIN
public/images/event_snowflake.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
public/sound/poke.wav
Normal file
BIN
public/sound/poke.wav
Normal file
Binary file not shown.
BIN
public/sound/squeak.wav
Normal file
BIN
public/sound/squeak.wav
Normal file
Binary file not shown.
102
src/app/api/contact/route.ts
Normal file
102
src/app/api/contact/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { rateLimited, sendEmail, validateTurnstile } from "@/lib/server-utils";
|
||||
import { trimTooLong } from "@/lib/strings";
|
||||
|
||||
import validator from "validator";
|
||||
|
||||
const validateInput = (data: any) => {
|
||||
return (
|
||||
typeof data !== "object" ||
|
||||
(
|
||||
!data.anon &&
|
||||
(
|
||||
typeof data.name !== "string" ||
|
||||
typeof data.email !== "string"
|
||||
) ||
|
||||
typeof data.message !== "string" ||
|
||||
typeof data["cf-turnstile-response"] !== "string"
|
||||
) ||
|
||||
!data.anon &&
|
||||
(
|
||||
!data.name.trim() ||
|
||||
!data.email.trim() ||
|
||||
!validator.isEmail(data.email) ||
|
||||
data.email.length > 50
|
||||
) ||
|
||||
!data.message.trim(),
|
||||
!data["cf-turnstile-response"].trim()
|
||||
)
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const agent = req.headers.get("cf-connecting-ip") ?? req.headers.get("x-forwarded-for") ?? "damn";
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid input" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const cf_token = data["cf-turnstile-response"];
|
||||
|
||||
console.log(`[${agent}] Verify captcha`);
|
||||
|
||||
if (typeof cf_token !== 'string' || !cf_token.trim()) {
|
||||
return NextResponse.json(
|
||||
{ error: "Captcha Failed" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const captchaVerified = await validateTurnstile(cf_token, agent);
|
||||
|
||||
if (!captchaVerified) {
|
||||
return NextResponse.json(
|
||||
{ error: "Captcha Failed" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[${agent}] Verify rate limit`);
|
||||
|
||||
const isRateLimited = await rateLimited(agent);
|
||||
|
||||
if (isRateLimited) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests" },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
if (validateInput(data)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid input" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[${agent}] Sending email...`);
|
||||
|
||||
try {
|
||||
const email = data.anon || !data.email ? process.env.SMTP_USER : data.email;
|
||||
const name = trimTooLong(data.anon || !data.name ? 'Anonymous' : data.name, 20);
|
||||
|
||||
await sendEmail(name, email, data.message);
|
||||
|
||||
console.log(`[${agent}] Email sended...`);
|
||||
|
||||
return NextResponse.json({ status: "ok" });
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
return NextResponse.json({
|
||||
status: "failed",
|
||||
message: err instanceof Error ? err.message : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,16 @@
|
||||
:root {
|
||||
--background: #0F0A1F;
|
||||
--primary: #F48120;
|
||||
--primary-darker: #cc6d1f;
|
||||
}
|
||||
|
||||
@keyframes click-bounce {
|
||||
0%, 100% {
|
||||
transform: translate(0px, 0px) scale(1, 1);
|
||||
}
|
||||
10% {
|
||||
transform: translate(0px, 14px) scale(1.2, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes silly-bounce {
|
||||
@@ -13,7 +23,7 @@
|
||||
animation-timing-function: linear(0.2, 0.8, 1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(0px, -180px) scale(1.18, 0.9);
|
||||
transform: translate(0px, -120px) scale(1.18, 0.9);
|
||||
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Roboto_Mono } from 'next/font/google'
|
||||
import "./globals.css";
|
||||
import Script from "next/script";
|
||||
|
||||
const roboto_mono = Roboto_Mono({
|
||||
subsets: ['latin']
|
||||
@@ -22,6 +23,7 @@ export default function RootLayout({
|
||||
className={`bg-background ${roboto_mono.className} antialiased text-white`}
|
||||
>
|
||||
{children}
|
||||
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import WobblingImage from "@/components/wobbling-image";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "@/components/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Wep wep",
|
||||
@@ -8,11 +10,23 @@ export default function NotFound() {
|
||||
return (
|
||||
<main className="flex items-center h-screen pt-16 md:pt-32 pb-12 px-8 md:px-0">
|
||||
<div className="mx-auto w-[380px]">
|
||||
<header className="mb-8 -ms-5 text-center">
|
||||
<h1 className="font-bold text-8xl">404</h1>
|
||||
</header>
|
||||
<div className="">
|
||||
Oh you lost, you know there's nothing here...
|
||||
<noscript>
|
||||
<header className="mb-8 -ms-5 text-center">
|
||||
<h1 className="font-bold text-8xl">404</h1>
|
||||
</header>
|
||||
<div className="">
|
||||
Oh you lost, you know there's nothing here...
|
||||
</div>
|
||||
</noscript>
|
||||
<WobblingImage
|
||||
images={{
|
||||
idle: "/images/coralz_idle.png",
|
||||
aware: "/images/coralz_scared.png",
|
||||
poked: "/images/coralz_poked.png"
|
||||
}}
|
||||
/>
|
||||
<div className="-mt-2">
|
||||
<Link className="text-xs" target="_blank" href="https://x.com/JoelGuerraC/status/1840178999546319101">{">]"}Image source</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
import { LandingImage } from "@/components/landing-image";
|
||||
import { NolaGlitchClientOnly } from "@/components/nola-glitch";
|
||||
import { Sosmed } from "@/components/sosmed";
|
||||
import HomeText from "@/components/home-text.mdx"
|
||||
import HomeText from "@/components/texts/home.mdx"
|
||||
|
||||
import Link from "next/link";
|
||||
import Link from "@/components/link";
|
||||
import { FakeWindow, HomeWindows } from "@/components/windows";
|
||||
import Taskbar from "@/components/taskbar";
|
||||
import { SnowfallBackground } from "@/components/events/christmas";
|
||||
|
||||
import { WindowManagerProvider } from "@/hooks/window-manager";
|
||||
import { ThemeEventsProvider } from "@/hooks/theme-events";
|
||||
|
||||
export default function Home() {
|
||||
return (<>
|
||||
<main className="flex items-center pt-16 md:pt-24 pb-12 px-8 md:px-0 overflow-x-hidden">
|
||||
<WindowManagerProvider>
|
||||
<FakeWindow windowText="Homepage">
|
||||
<HomeWindows />
|
||||
<header className="text-center mb-8">
|
||||
<h1 className="font-bold text-3xl leading-normal">
|
||||
Nonszy Work<span className="text-primary">space</span>
|
||||
</h1>
|
||||
</header>
|
||||
<noscript>
|
||||
<LandingImage />
|
||||
</noscript>
|
||||
<NolaGlitchClientOnly />
|
||||
<Sosmed />
|
||||
<article className="space-y-6 leading-relaxed relative">
|
||||
<HomeText />
|
||||
</article>
|
||||
<section className="my-8">
|
||||
<p>⚡ Powered with <Link href="https://www.cloudflare.com/" target="_blank" className="text-primary underline">Cloudflare</Link> ☁️</p>
|
||||
</section>
|
||||
<footer className="mt-20 text-center">
|
||||
<p>© <span className="text-sm">2025 Nomi Nonszy</span></p>
|
||||
<p className="text-sm">
|
||||
<Link href={"/terms"} className="text-primary underline">Terms</Link> and <Link href={"/privacy"} className="text-primary underline">Privacy</Link>
|
||||
</p>
|
||||
</footer>
|
||||
</FakeWindow>
|
||||
</WindowManagerProvider>
|
||||
<ThemeEventsProvider>
|
||||
<SnowfallBackground />
|
||||
<WindowManagerProvider>
|
||||
<FakeWindow windowText="Homepage">
|
||||
<HomeWindows />
|
||||
<header className="text-center mb-8">
|
||||
<h1 className="font-bold text-3xl leading-normal">
|
||||
Nonszy Work<span className="text-primary">space</span>
|
||||
</h1>
|
||||
</header>
|
||||
<noscript>
|
||||
<LandingImage />
|
||||
</noscript>
|
||||
<NolaGlitchClientOnly />
|
||||
<Sosmed />
|
||||
<article className="space-y-5 leading-relaxed relative text-sm">
|
||||
<HomeText />
|
||||
</article>
|
||||
<footer className="mt-20 text-center">
|
||||
<p>© <span className="text-sm">2025 Nomi Nonszy</span></p>
|
||||
<p className="text-sm">
|
||||
<Link href={"/terms"}>Terms</Link> and <Link href={"/privacy"}>Privacy</Link>
|
||||
</p>
|
||||
</footer>
|
||||
</FakeWindow>
|
||||
</WindowManagerProvider>
|
||||
</ThemeEventsProvider>
|
||||
</main>
|
||||
</>);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ export const FakeRelativeWindow = ({
|
||||
content: "animate-[fade-in-half_1s]"
|
||||
});
|
||||
|
||||
const offsetRef = useRef({ x: 0, y: 0 });
|
||||
const [isDragging, setDragging] = useState(false);
|
||||
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
const pos = useRef({
|
||||
dragging: false,
|
||||
@@ -64,22 +67,12 @@ export const FakeRelativeWindow = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!windowManager.isLocalDataExists) {
|
||||
if (!currentWindow) populateWindow();
|
||||
return () => {
|
||||
windowManager.remove(windowName);
|
||||
}
|
||||
populateWindow();
|
||||
return () => {
|
||||
windowManager.remove(windowName);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(windowManager.windows)
|
||||
}, [windowManager.windows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (popupRef.current) popupRef.current.style.transform = `translate(${currentWindow?.offset.x || 0}px, ${currentWindow?.offset.y || 0}px)`;
|
||||
}, [currentWindow?.offset])
|
||||
|
||||
useEffect(() => {
|
||||
if (!withAnim) return;
|
||||
const node = popupRef.current;
|
||||
@@ -104,6 +97,18 @@ export const FakeRelativeWindow = ({
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (popupRef.current && currentWindow) {
|
||||
const initPos = {
|
||||
x: currentWindow.offset.x,
|
||||
y: currentWindow.offset.y
|
||||
}
|
||||
offsetRef.current.x = initPos.x;
|
||||
offsetRef.current.y = initPos.y;
|
||||
popupRef.current.style.transform = `translate(${initPos.x}px, ${initPos.y}px)`;
|
||||
}
|
||||
}, [popupRef.current, currentWindow]);
|
||||
|
||||
const toggleMinimize = () => windowManager.toggleMinimize(windowName);
|
||||
const handleClose = () => windowManager.close(windowName);
|
||||
|
||||
@@ -113,19 +118,26 @@ export const FakeRelativeWindow = ({
|
||||
pos.current.dragging = true;
|
||||
pos.current.mouseX = e.clientX;
|
||||
pos.current.mouseY = e.clientY;
|
||||
pos.current.x = currentWindow.offset.x;
|
||||
pos.current.y = currentWindow.offset.y;
|
||||
pos.current.x = offsetRef.current.x;
|
||||
pos.current.y = offsetRef.current.y;
|
||||
document.body.style.userSelect = "none";
|
||||
setDragging(true);
|
||||
}
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
windowManager.move(windowName, {
|
||||
x: offsetRef.current.x,
|
||||
y: offsetRef.current.y
|
||||
});
|
||||
|
||||
pos.current.dragging = false;
|
||||
document.body.style.userSelect = "";
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
setDragging(false);
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
@@ -133,12 +145,10 @@ export const FakeRelativeWindow = ({
|
||||
if (pos.current.dragging && popupRef.current) {
|
||||
const dx = e.clientX - pos.current.mouseX;
|
||||
const dy = e.clientY - pos.current.mouseY;
|
||||
const newX = pos.current.x + dx;
|
||||
const newY = pos.current.y + dy;
|
||||
windowManager.move(windowName, {
|
||||
x: newX,
|
||||
y: newY
|
||||
});
|
||||
const x = pos.current.x + dx;
|
||||
const y = pos.current.y + dy;
|
||||
offsetRef.current = { x, y }
|
||||
popupRef.current.style.transform = `translate(${x}px, ${y}px)`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,28 +156,60 @@ export const FakeRelativeWindow = ({
|
||||
|
||||
return (
|
||||
<div className={clsx("absolute hidden lg:block", className)}>
|
||||
<div className={clsx("mx-auto md:border bg-background border-primary", withAnim && animation.window)} ref={popupRef}>
|
||||
{/* Outline Window */}
|
||||
{draggable && (
|
||||
<div
|
||||
className="hidden md:flex bg-primary p-2 justify-between text-background windowbar"
|
||||
onMouseDown={onMouseDown}
|
||||
className={clsx("absolute border-primary w-full z-10", isDragging && "md:border")}
|
||||
ref={popupRef}
|
||||
>
|
||||
<div className="ms-1 pointer-events-none">
|
||||
{currentWindow ? currentWindow.name : "Error!"}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div
|
||||
className={clsx("hidden md:flex p-2 gap-2 justify-end text-background windowbar", isDragging ? "opacity-0" : "opacity-100")}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
<button
|
||||
className="bg-primary border border-[#FFA826] border-outset p-1"
|
||||
className={clsx("bg-primary border border-[#FFA826] border-outset p-1")}
|
||||
onClick={toggleMinimize}
|
||||
>
|
||||
<Icon icon="lucide:minus"/>
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary border border-[#FFA826] border-outset p-1"
|
||||
className={clsx("bg-primary border border-[#FFA826] border-outset p-1")}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<Icon icon="lucide:x"/>
|
||||
</button>
|
||||
</div>
|
||||
<div className={clsx("m-1", currentWindow?.minimized && "h-0")}>
|
||||
<div className={clsx("md:p-4 opacity-0")}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main window */}
|
||||
<div
|
||||
className={clsx("md:border bg-background bg-opacity-50 border-primary", withAnim && animation.window)}
|
||||
style={{
|
||||
transform: `translate(${currentWindow.offset.x}px, ${currentWindow.offset.y}px)`
|
||||
}}
|
||||
>
|
||||
<div className="hidden md:flex bg-primary p-2 justify-between text-background windowbar">
|
||||
<div className="ms-1 pointer-events-none">
|
||||
{currentWindow ? currentWindow.name : "Error!"}
|
||||
</div>
|
||||
<div className={clsx("flex gap-2", !isDragging ? "opacity-0" : "opacity-100")}>
|
||||
<button
|
||||
className="bg-primary border border-[#FFA826] border-outset p-1 pointer-events-none"
|
||||
>
|
||||
<Icon icon="lucide:minus"/>
|
||||
</button>
|
||||
<button
|
||||
className="bg-primary border border-[#FFA826] border-outset p-1 pointer-events-none"
|
||||
>
|
||||
<Icon icon="lucide:x"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx("m-1 border border-primary", currentWindow?.minimized ? "h-0 overflow-y-clip" : "h-fit")}>
|
||||
<div className={clsx("md:p-4", withAnim && animation.content)}>
|
||||
|
||||
204
src/components/events/christmas.tsx
Normal file
204
src/components/events/christmas.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
'use client'
|
||||
|
||||
import { useThemeEvents } from '@/hooks/theme-events';
|
||||
import { getEvent } from '@/lib/utils';
|
||||
import clsx from 'clsx';
|
||||
import NextImage from 'next/image';
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface Snowflake {
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
speed: number;
|
||||
sway: number;
|
||||
swayOffset: number;
|
||||
opacity: number;
|
||||
rotation: number;
|
||||
rotationSpeed: number;
|
||||
}
|
||||
|
||||
interface ChristmasProps {
|
||||
left: number;
|
||||
top: number;
|
||||
size: number;
|
||||
img: string;
|
||||
className?: string;
|
||||
flip?: boolean;
|
||||
absolute?: boolean;
|
||||
}
|
||||
|
||||
export const ChristmasExclusive = ({ children }: { children: ReactNode }) => {
|
||||
const eventNow = useThemeEvents()?.event;
|
||||
|
||||
if (eventNow?.name == 'christmas') return children;
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ChristmasProperty: React.FC<ChristmasProps> = ({
|
||||
left,
|
||||
top,
|
||||
img,
|
||||
size,
|
||||
className,
|
||||
flip
|
||||
}) => {
|
||||
return (
|
||||
<ChristmasExclusive>
|
||||
<div
|
||||
className={clsx('absolute md:block hidden', flip && '-scale-x-100', className)}
|
||||
style={{
|
||||
left, top
|
||||
}}
|
||||
>
|
||||
<NextImage
|
||||
className='pointer-events-none'
|
||||
src={img}
|
||||
alt='Christmas prop'
|
||||
height={size}
|
||||
width={size}
|
||||
unoptimized={img.slice(img.length - 3, img.length) == 'gif'}
|
||||
/>
|
||||
</div>
|
||||
</ChristmasExclusive>
|
||||
)
|
||||
}
|
||||
|
||||
function SnowfallRawBackground() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const snowflakesRef = useRef<Snowflake[]>([]);
|
||||
const animationFrameRef = useRef<number>(0);
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
setDimensions({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
|
||||
return () => window.removeEventListener('resize', updateDimensions);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
canvas.width = dimensions.width;
|
||||
canvas.height = dimensions.height;
|
||||
|
||||
// Load the snowflake image using a plain Image so we can draw it to canvas reliably
|
||||
const img = new Image();
|
||||
img.src = '/images/event_snowflake.png';
|
||||
let mounted = true;
|
||||
|
||||
const start = () => {
|
||||
if (!mounted) return;
|
||||
imageRef.current = img;
|
||||
|
||||
const createSnowflake = (init?: boolean): Snowflake => {
|
||||
let posy = -10;
|
||||
let radius = Math.random() * 18 + 10;
|
||||
let speed = Math.random() * 1 + 0.5;
|
||||
let sway = Math.random() * 0.5 + 0.2;
|
||||
|
||||
if (init) {
|
||||
const initHeight = (dimensions.height / 1.1);
|
||||
posy = Math.random() * initHeight + posy;
|
||||
radius = (posy / (initHeight + posy) - 1) * -radius;
|
||||
}
|
||||
|
||||
if (window.innerWidth < 768) {
|
||||
radius /= 1.4;
|
||||
}
|
||||
|
||||
return {
|
||||
x: Math.random() * dimensions.width,
|
||||
y: posy,
|
||||
radius,
|
||||
speed,
|
||||
sway,
|
||||
swayOffset: Math.random() * Math.PI * 2,
|
||||
opacity: Math.random() * 0.5 + 0.2,
|
||||
rotation: Math.random() * Math.PI * 2,
|
||||
rotationSpeed: (Math.random() - 0.5) * 0.02,
|
||||
}
|
||||
};
|
||||
|
||||
const snowflakes: Snowflake[] = [];
|
||||
for (let i = 0; i < 80; i++) {
|
||||
snowflakes.push(createSnowflake(true));
|
||||
}
|
||||
snowflakesRef.current = snowflakes;
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
snowflakesRef.current.forEach((flake, index) => {
|
||||
flake.y += flake.speed;
|
||||
flake.x += Math.sin(flake.swayOffset + flake.y * 0.02) * flake.sway;
|
||||
flake.rotation += flake.rotationSpeed;
|
||||
|
||||
const shrink = dimensions.height < 768 ? 0.005 : 0.01;
|
||||
flake.radius = Math.max(2, flake.radius - shrink);
|
||||
|
||||
if (flake.y > dimensions.height || flake.radius <= 0.5) {
|
||||
snowflakesRef.current[index] = createSnowflake();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = flake.opacity;
|
||||
ctx.translate(flake.x, flake.y);
|
||||
ctx.rotate(flake.rotation);
|
||||
|
||||
ctx.drawImage(
|
||||
img,
|
||||
-flake.radius / 2,
|
||||
-flake.radius / 2,
|
||||
flake.radius,
|
||||
flake.radius
|
||||
);
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
if (img.complete) start();
|
||||
else img.onload = start;
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
img.onload = null;
|
||||
imageRef.current = null;
|
||||
};
|
||||
}, [dimensions]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="fixed top-0 left-0 w-full h-full pointer-events-none"
|
||||
style={{ zIndex: 0 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const SnowfallBackground = () => (
|
||||
<ChristmasExclusive>
|
||||
<SnowfallRawBackground />
|
||||
</ChristmasExclusive>
|
||||
)
|
||||
1
src/components/events/eventprops.tsx
Normal file
1
src/components/events/eventprops.tsx
Normal file
@@ -0,0 +1 @@
|
||||
'use client'
|
||||
114
src/components/form/contact-form.tsx
Normal file
114
src/components/form/contact-form.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import axios, { AxiosError } from "axios";
|
||||
|
||||
import { CheckboxInput, Input, Submit } from "./form";
|
||||
import { FloatingLabel } from "../floating-label";
|
||||
import { CloudflareTurnstile } from "../turnstile";
|
||||
|
||||
export default function ContactForm() {
|
||||
const [anon, setAnon] = useState(false);
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
const [status, setStatus] = useState<'success' | 'loading' | 'failed' | 'idle'>('idle');
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
|
||||
const statusMsg = {
|
||||
success: 'Sended!',
|
||||
loading: 'Sending...',
|
||||
failed: 'Failed :(',
|
||||
idle: 'Send!'
|
||||
}
|
||||
|
||||
const send = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setStatus('idle');
|
||||
setErrorMsg('');
|
||||
|
||||
if (status == 'loading' || !formRef.current) return;
|
||||
|
||||
const formData = new FormData(formRef.current);
|
||||
|
||||
const data = {
|
||||
anon,
|
||||
name: formData.get('name')?.toString(),
|
||||
email: formData.get('email')?.toString(),
|
||||
message: formData.get('message')!.toString(),
|
||||
"cf-turnstile-response": formData.get('cf-turnstile-response')?.toString()
|
||||
}
|
||||
|
||||
if ((!data.name || !data.email) && !data.anon) {
|
||||
setErrorMsg("Name and email are required. Unless you check the box to send anonymously");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.message.length < 1) {
|
||||
setErrorMsg("What do you want to tell me???");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data["cf-turnstile-response"]) {
|
||||
setErrorMsg("Please fill in the captcha");
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('loading');
|
||||
|
||||
try {
|
||||
await axios.post('/api/contact', data);
|
||||
setStatus('success');
|
||||
}
|
||||
catch (err) {
|
||||
setStatus('failed');
|
||||
setErrorMsg("Something went wrong, Sorry...");
|
||||
|
||||
if (err instanceof AxiosError) {
|
||||
if (err.status == 429) setErrorMsg("Limit reached, please try again later...");
|
||||
if (err.status == 403) setErrorMsg("Captcha failed or expired, please try again later");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action="POST" className="space-y-4" ref={formRef} onSubmit={send}>
|
||||
<FloatingLabel placeholder="Leave an anonymous message, but I can't reply to you">
|
||||
<CheckboxInput
|
||||
name="anon"
|
||||
placeholder="Send anonymously🤫"
|
||||
onChange={setAnon}
|
||||
/>
|
||||
</FloatingLabel>
|
||||
<label className={anon ? "hidden" : "block"} htmlFor="contact-name">
|
||||
<div className="mb-2">Name</div>
|
||||
<Input
|
||||
id="contact-name"
|
||||
className="w-full"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Any name you want me to know"
|
||||
/>
|
||||
</label>
|
||||
<label className={anon ? "hidden" : "block"} htmlFor="contact-email">
|
||||
<div className="mb-2">Email</div>
|
||||
<Input
|
||||
id="contact-email"
|
||||
className="w-full"
|
||||
name="email"
|
||||
type="email"
|
||||
/>
|
||||
</label>
|
||||
<label className="block" htmlFor="contact-msg">
|
||||
<div className="mb-2">Message</div>
|
||||
<textarea
|
||||
name="message"
|
||||
id="contact-msg"
|
||||
className="max-h-96 h-32 w-full p-3 bg-background border border-primary"
|
||||
placeholder="Tell me something cool, or ask question"
|
||||
/>
|
||||
</label>
|
||||
<CloudflareTurnstile />
|
||||
<div className="text-primary">{errorMsg}</div>
|
||||
<Submit disabled={status === "loading"} className="w-full">{statusMsg[status]}</Submit>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
49
src/components/form/form.tsx
Normal file
49
src/components/form/form.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
'use client'
|
||||
|
||||
import clsx from "clsx";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function Input ({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={clsx("block py-3 px-4 bg-background text-white border border-primary", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Submit ({ className, type, ...props }: React.ComponentProps<'button'>) {
|
||||
return (
|
||||
<button
|
||||
type={'submit'}
|
||||
className={clsx("block py-3 px-4 bg-primary disabled:bg-primary-darker text-background", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface CheckboxInputProps {
|
||||
name: string,
|
||||
placeholder: string,
|
||||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
export function CheckboxInput ({ placeholder, name, onChange }: CheckboxInputProps) {
|
||||
const [checked, setCheck] = useState(false);
|
||||
|
||||
const onClick = () => {
|
||||
setCheck(c => !c);
|
||||
onChange(!checked);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<button type="button" id={`checkbox-${name}`} className="inline-block bg-none border border-primary w-4 h-4" onClick={onClick}>
|
||||
{checked && <Icon icon="lucide:check" className="text-primary" />}
|
||||
</button>
|
||||
<label htmlFor={`checkbox-${name}`} className="ps-2 inline-block">{placeholder}</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import Image from "next/image"
|
||||
import Link from "next/link";
|
||||
import { FloatingLabel } from "@/components/floating-label";
|
||||
|
||||
Welcome!
|
||||
|
||||
This is our cozy little corner of the internet where we run a bunch of services and websites on my own servers.
|
||||
|
||||
We've got tools and resources for us but some we provide it for you, stuff that just works and doesn't burn our wallet.
|
||||
|
||||
## About Me
|
||||
|
||||
Nomi Nonszy (or also known as Nonszy, Nomi Nonsense, whatever, some ppl know my real name).
|
||||
I write code and make sure the server doesn't catch on fire lol.
|
||||
|
||||
I build sick web apps with modern stacks (and trash).
|
||||
Big fan of open-source stuff, CLI life, Arch 💅.
|
||||
I only play indie games, and cooking up cursed mods just for fun.
|
||||
|
||||
<Image
|
||||
className="mb-12 mx-auto"
|
||||
alt="Coral"
|
||||
src="https://media1.tenor.com/m/CpCvknTOMMwAAAAC/ena-ena-joel-g.gif"
|
||||
width={250}
|
||||
height={250}
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
{new Date(Date.now()) < new Date("2026-02-16") && (
|
||||
<p>
|
||||
Pls add my <Link href="https://steamcommunity.com/id/nomi_nonsz" target="_blank" className="text-[#F48120] underline">Steam</Link>, I am so lonely 💔.
|
||||
</p>
|
||||
)}
|
||||
|
||||
## Vision
|
||||
|
||||
Developers who brings together web technology, AI, and game development to create innovative, meaningful, and impactful digital experiences.
|
||||
|
||||
We aspire to blend creativity and technical mastery to shape products that empower others and reflect our passion for independent, modern technology.
|
||||
|
||||
## Mission
|
||||
|
||||
- Gain a solid understanding of IT infrastructure, including how systems operate behind the scenes, how to independently manage servers, and how to ensure stability, security, and scalability throughout the development and deployment lifecycle.
|
||||
- Develop self-hosted systems and applications, whether on local machines or remote environments, promoting digital independence, data ownership, and efficient resource control.
|
||||
- Build the ability to design and develop web applications that are responsive, intuitive, fast, and visually compelling, with a strong focus on user experience and modern interface principles.
|
||||
- Integrate artificial intelligence into digital products, creating smart, helpful, and engaging features that elevate the overall value and interactivity of applications.
|
||||
|
||||
## Why Nonsense
|
||||
|
||||
Nonszy is an absurd abbreviation of Nonsense, it's not that we don't understand the each other or its uses, it's just a word you've chosen to dismiss our existence.
|
||||
|
||||
Our work on web technologies has improved game development and art for anyone, but you still choose to see us as nothing more than senseless non-existent entities.
|
||||
|
||||
But you, who's life is centered around nothing but a shallow sense of being superior to others through your knowledge or skill?
|
||||
|
||||
We love what we do because it helps those in need, whether they realize it or not. It's a sense-less world you live in, but it's just us or someone who know how to keep things better.
|
||||
|
||||
Let's continue pushing ahead towards a brighter tomorrow where we can use technology for the greater good, including the ones who are on your side. We love you as much as we do for who you are, and we're just here to bring about positive change for all.
|
||||
|
||||
<FloatingLabel placeholder="Pat pat pat">
|
||||
<Image
|
||||
className="mb-12 mx-auto"
|
||||
alt="Pat coral"
|
||||
src="https://media.tenor.com/jZkB6qUnOQUAAAAi/coral-glasses-dream-bbq-ena.gif"
|
||||
width={350}
|
||||
height={350}
|
||||
unoptimized
|
||||
/>
|
||||
</FloatingLabel>
|
||||
@@ -2,11 +2,11 @@ import Image from "next/image"
|
||||
import { FloatingLabel } from "./floating-label"
|
||||
|
||||
export const LandingImage = () => (
|
||||
<FloatingLabel placeholder="Coral Glasses ❤️. Alert: NOT MY WORK! character by JoelG, See terms and conditions">
|
||||
<FloatingLabel placeholder="ENA ❤️. Alert: NOT MY WORK!">
|
||||
<Image
|
||||
className="mb-8 mx-auto"
|
||||
className="mb-8 mx-auto h-auto"
|
||||
alt="Coral <3"
|
||||
src="https://media1.tenor.com/m/RIP2rxKM_FgAAAAC/ena-ena-dream-bbq.gif"
|
||||
src="https://media.tenor.com/1BCeG1aTiBAAAAAd/temptation-stairway-ena.gif"
|
||||
width={280}
|
||||
height={280}
|
||||
unoptimized
|
||||
|
||||
14
src/components/link.tsx
Normal file
14
src/components/link.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import clsx from "clsx";
|
||||
import NextLink from "next/link"
|
||||
|
||||
function Link({ className, ...props }: React.ComponentProps<typeof NextLink>) {
|
||||
return (
|
||||
<NextLink
|
||||
className={clsx("text-primary underline", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Link;
|
||||
@@ -15,7 +15,7 @@ export const SimpleArticle = ({
|
||||
Back
|
||||
</ButtonPrimary>
|
||||
</Link>
|
||||
<article className="space-y-6 [&_p]:leading-relaxed relative [&_h1]:text-2xl [&_h2]:text-xl">
|
||||
<article className="space-y-5 [&_p]:leading-relaxed [&_p]:text-sm [&_li]:text-sm relative [&_h1]:text-2xl [&_h2]:text-xl">
|
||||
{children}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
96
src/components/texts/home.mdx
Normal file
96
src/components/texts/home.mdx
Normal file
@@ -0,0 +1,96 @@
|
||||
import Image from "next/image"
|
||||
import Link from "@/components/link";
|
||||
import { FloatingLabel } from "@/components/floating-label";
|
||||
import ContactForm from "@/components/form/contact-form";
|
||||
|
||||
Welcome!
|
||||
|
||||
This is our cozy little corner of the internet where we run services and websites on my own servers.
|
||||
|
||||
We've got tools, resources, game server and other stuff that just works and doesn't burn our wallet, maybe.
|
||||
|
||||
I love putting silly favorite characters on this site
|
||||
|
||||
## About Me
|
||||
|
||||
Nomi Nonszy (also known as Nonszy, Nomi Nonsense, whatever).
|
||||
I write code, do some art, and make sure our server doesn't catch on fire lol.
|
||||
|
||||
I build sick web apps with modern stacks. Big fan of open-source stuff.
|
||||
Hate something that make things overengineered and boilerplate, but still use them anyway.
|
||||
Love with indie games, and cooking up cursed mods just for fun.
|
||||
|
||||
Multifandom with Psychopomp, Deltarune, and ENA! I'm currently dedicated to the ENA SERIES!!
|
||||
|
||||
<Image
|
||||
className="mb-12 mx-auto"
|
||||
alt="Coral"
|
||||
src="https://media1.tenor.com/m/CpCvknTOMMwAAAAC/ena-ena-joel-g.gif"
|
||||
width={250}
|
||||
height={250}
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
{new Date(Date.now()) < new Date("2026-02-16") && (
|
||||
<p>
|
||||
Pls add my <Link href="https://steamcommunity.com/id/nomi_nonsz" target="_blank">Steam</Link>, I am so lonely 💔.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* ## Vision
|
||||
|
||||
To contribute to the global developer ecosystem by sharing knowledge, conducting research, and crafting tools and systems that make technology more efficient, accessible, and affordable for everyone.
|
||||
|
||||
## Mission
|
||||
|
||||
Our mission is to explore, learn, and innovate across all layers of technology from app development to infrastructure management
|
||||
|
||||
- Learn the art of server management, containerization, cloud deployment, and resource monitoring to understand how platforms operate at scale.
|
||||
|
||||
- Explore how automation can simplify complex processes like provisioning, monitoring, scaling, and maintaining servers, allowing us to focus more on development and innovation.
|
||||
|
||||
- Study and implement security best practices to protect our systems from threats, while also ensuring high availability and fault tolerance across distributed environments.
|
||||
|
||||
- Conduct experiments and share findings, build open-source tools, and support fellow developers by creating systems and free platform that make development easier, safer, and more efficient.
|
||||
|
||||
- Delve into low-level programming and computer system fundamentals to understand how software interacts with hardware, empowering us to optimize systems from the ground up. */}
|
||||
|
||||
## Why Nonsense
|
||||
|
||||
Nonszy is an absurd abbreviation of Nonsense, it's not that we don't understand the each other or its uses, it's just a word you've chosen to dismiss our existence.
|
||||
|
||||
We love what we do because it helps those in need, whether they realize it or not. It's a sense-less world you live in, but it's just us or someone who know how to keep things better.
|
||||
|
||||
<FloatingLabel placeholder="Pat pat pat">
|
||||
<Image
|
||||
className="mb-12 mx-auto"
|
||||
alt="Pat coral"
|
||||
src="https://media.tenor.com/jZkB6qUnOQUAAAAi/coral-glasses-dream-bbq-ena.gif"
|
||||
width={350}
|
||||
height={350}
|
||||
unoptimized
|
||||
/>
|
||||
</FloatingLabel>
|
||||
|
||||
## Coral Glasses
|
||||
|
||||
<Image
|
||||
className="float-left mr-5 pb-1"
|
||||
alt="Coral Dialogue"
|
||||
src="/images/coral-dialogue.jpg"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
Coral Glasses is a supporting character from the video game
|
||||
<Link href="https://store.steampowered.com/app/2134320/ENA_Dream_BBQ/" target="_blank">ENA: Dream BBQ</Link>,
|
||||
developed by
|
||||
<Link href="https://joelgc.com/" target="_blank">JoelG</Link>,
|
||||
and published under the ENA Team. She speaks Korean in the game and is voiced by Kim Minsol
|
||||
|
||||
The game is part of the surreal and artistically distinct ENA universe, which expands upon his earlier animated web series of the same name.
|
||||
|
||||
|
||||
## Tell me something
|
||||
|
||||
<ContactForm />
|
||||
@@ -1,5 +1,5 @@
|
||||
# Privacy Policy
|
||||
|
||||
This website ("nonszy.space") fully respects the privacy of its visitors. This document explains how we handle visitor information. The short answer is: **we do not collect, track, or store your personal information**.
|
||||
This website ("nonszy.space") fully respects the privacy of its visitors. This page explains how we handle visitor information. The short answer is: **we do not collect, track, or store your personal information**.
|
||||
|
||||
However, future feature additions will update the privacy policy.
|
||||
9
src/components/turnstile.tsx
Normal file
9
src/components/turnstile.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
export const CloudflareTurnstile = () => (
|
||||
<div
|
||||
className="cf-turnstile [&_#content]:bg-background"
|
||||
data-sitekey={process.env.NEXT_PUBLIC_CF_SITE_KEY}
|
||||
data-theme="dark"
|
||||
/>
|
||||
)
|
||||
@@ -3,6 +3,7 @@ import { FloatingLabel } from "@/components/floating-label";
|
||||
import { FakeRelativeWindow, RestoreWindowsButton } from "./client-windows";
|
||||
import Image from "next/image"
|
||||
import Link from "next/link";
|
||||
import { ChristmasProperty } from "./events/christmas";
|
||||
|
||||
export const FakeWindow = ({
|
||||
windowText, children
|
||||
@@ -10,7 +11,13 @@ export const FakeWindow = ({
|
||||
windowText: string
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<div className="mx-auto w-[480px] md:w-[520px] md:border border-primary">
|
||||
<div className="relative md:bg-background mx-auto w-[480px] md:w-[520px] md:border border-primary z-10">
|
||||
<ChristmasProperty
|
||||
img="/images/event_santahat1.png"
|
||||
size={180}
|
||||
left={-60}
|
||||
top={-80}
|
||||
/>
|
||||
<div className="p-1 pb-0">
|
||||
<div className="hidden md:flex bg-primary p-2 justify-between text-background">
|
||||
<div className="ms-1 pointer-events-none">
|
||||
@@ -45,13 +52,16 @@ export const HomeWindows = () => (
|
||||
draggable
|
||||
>
|
||||
<FloatingLabel placeholder="This is Nola, my OC :3">
|
||||
<Image
|
||||
className=""
|
||||
alt="Nola"
|
||||
src="/images/nola.png"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<div className="relative">
|
||||
<ChristmasProperty img="/images/event_santahat1.png" size={150} top={-40} left={70} flip />
|
||||
<Image
|
||||
className=""
|
||||
alt="Nola"
|
||||
src="/images/nola.png"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
</FloatingLabel>
|
||||
</FakeRelativeWindow>
|
||||
<FakeRelativeWindow
|
||||
@@ -74,7 +84,7 @@ export const HomeWindows = () => (
|
||||
</Link>
|
||||
</FakeRelativeWindow>
|
||||
<FakeRelativeWindow
|
||||
windowText="coral-1.exe"
|
||||
windowText="cube_coral.exe"
|
||||
className='-left-[75%] top-[1980px] z-10'
|
||||
draggable
|
||||
>
|
||||
@@ -88,16 +98,17 @@ export const HomeWindows = () => (
|
||||
/>
|
||||
</FakeRelativeWindow>
|
||||
<FakeRelativeWindow
|
||||
windowText="coral_cupcake.mkv"
|
||||
windowText="ena_spin.exe"
|
||||
className="-right-[85%] top-[440px] z-10"
|
||||
draggable
|
||||
>
|
||||
<Image
|
||||
alt="Coral Cupcake"
|
||||
src="https://media1.tenor.com/m/N5K-4AWj8QcAAAAC/coral-glasses-cupcake.gif"
|
||||
src="https://media.tenor.com/Uv-PLe5GIe0AAAAi/gyaruface.gif"
|
||||
width={240}
|
||||
height={200}
|
||||
quality={10}
|
||||
priority={false}
|
||||
unoptimized
|
||||
/>
|
||||
</FakeRelativeWindow>
|
||||
|
||||
174
src/components/wobbling-image.tsx
Normal file
174
src/components/wobbling-image.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
'use client'
|
||||
|
||||
import clsx from "clsx";
|
||||
import Image from "next/image";
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
|
||||
interface WobblingImageInterface {
|
||||
images: {
|
||||
idle: string;
|
||||
aware?: string;
|
||||
poked?: string;
|
||||
weird?: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface ImageClipInterface {
|
||||
name: string;
|
||||
src: string;
|
||||
size: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
const ImageClip: React.FC<ImageClipInterface> = ({
|
||||
name, src, size, visible
|
||||
}) => (
|
||||
<Image
|
||||
className={clsx("absolute left-1/2 -translate-x-1/2 top-0 pointer-events-none", visible ? "opacity-100" : "opacity-0")}
|
||||
alt={name}
|
||||
src={src}
|
||||
width={size}
|
||||
height={size}
|
||||
draggable={false}
|
||||
/>
|
||||
)
|
||||
|
||||
function WobblingImage ({
|
||||
images
|
||||
}: WobblingImageInterface) {
|
||||
const size = 400;
|
||||
const [isPoked, setPoked] = useState(false);
|
||||
const [isAware, setAware] = useState(false);
|
||||
const pokeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
const audioPath = "/sound/poke.wav";
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const [audioBuffer, setAudioBuffer] = useState<AudioBuffer | null>(null);
|
||||
const [audioLoaded, setAudioLoaded] = useState(false);
|
||||
|
||||
const [dummyAudio, setDummyAudio] = useState<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof Audio !== undefined) setDummyAudio(new Audio(audioPath))
|
||||
return () => {
|
||||
if (dummyAudio) dummyAudio.pause();
|
||||
}
|
||||
}, [])
|
||||
|
||||
const initializeAudioContext = useCallback((): AudioContext => {
|
||||
if (!audioContextRef.current) {
|
||||
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
}
|
||||
return audioContextRef.current;
|
||||
}, []);
|
||||
|
||||
const loadAudio = useCallback(async (url: string): Promise<AudioBuffer> => {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const context = initializeAudioContext();
|
||||
return await context.decodeAudioData(arrayBuffer);
|
||||
}, [initializeAudioContext]);
|
||||
|
||||
const initializeAudio = useCallback(async (): Promise<void> => {
|
||||
if (audioBuffer) return;
|
||||
|
||||
setAudioLoaded(false);
|
||||
try {
|
||||
const buffer = await loadAudio(audioPath);
|
||||
setAudioBuffer(buffer);
|
||||
} catch (error) {
|
||||
console.error('Failed to load audio:', error);
|
||||
}
|
||||
finally {
|
||||
setAudioLoaded(true);
|
||||
}
|
||||
}, [audioBuffer, loadAudio]);
|
||||
|
||||
const playRandomPitch = useCallback(async (): Promise<void> => {
|
||||
await initializeAudio();
|
||||
|
||||
if (!audioBuffer || !audioContextRef.current) return;
|
||||
|
||||
const context = audioContextRef.current;
|
||||
const changeToLowPitch = Math.floor(Math.random() * 66);
|
||||
const randomPitch = Math.random() * 0.38 + (changeToLowPitch == 1 ? 0.25 : 0.75);
|
||||
|
||||
const source = context.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.playbackRate.value = randomPitch;
|
||||
source.connect(context.destination);
|
||||
source.start(0);
|
||||
|
||||
source.onended = () => {
|
||||
source.disconnect();
|
||||
};
|
||||
}, [audioBuffer, initializeAudio]);
|
||||
|
||||
|
||||
function handleClick() {
|
||||
const pokedDuration = 700 + Math.floor(Math.random() * 100)
|
||||
if (!isAware) setAware(true);
|
||||
|
||||
setIsAnimating(false);
|
||||
requestAnimationFrame(() => setIsAnimating(true));
|
||||
|
||||
playRandomPitch();
|
||||
if (dummyAudio && !audioLoaded) {
|
||||
dummyAudio.currentTime = 0;
|
||||
dummyAudio.play();
|
||||
}
|
||||
|
||||
if (pokeTimeoutRef.current) {
|
||||
clearTimeout(pokeTimeoutRef.current);
|
||||
}
|
||||
setPoked(true);
|
||||
pokeTimeoutRef.current = setTimeout(() => {
|
||||
setPoked(false);
|
||||
}, pokedDuration);
|
||||
}
|
||||
|
||||
function handleAnimationEnd() {
|
||||
setIsAnimating(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div
|
||||
className={clsx(
|
||||
"relative mx-auto cursor-grab active:cursor-grabbing h-[400px] select-none",
|
||||
isAnimating && "animate-[click-bounce_230ms_ease-out]"
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onAnimationEnd={handleAnimationEnd}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(); } }}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onPointerDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<ImageClip
|
||||
name="idle"
|
||||
src={images.idle}
|
||||
size={size}
|
||||
visible={!isPoked && !isAware}
|
||||
/>
|
||||
<ImageClip
|
||||
name="aware"
|
||||
src={images.aware || images.idle}
|
||||
size={size}
|
||||
visible={!isPoked && isAware}
|
||||
/>
|
||||
<ImageClip
|
||||
name="poked"
|
||||
src={images.poked || images.idle}
|
||||
size={size}
|
||||
visible={isPoked}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WobblingImage;
|
||||
21
src/hooks/theme-events.tsx
Normal file
21
src/hooks/theme-events.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { EventsDate } from "@/lib/types";
|
||||
import { getEvent } from "@/lib/utils";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
interface ThemeEventsContextType {
|
||||
event: EventsDate | undefined
|
||||
}
|
||||
|
||||
const ThemeEventsContext = createContext<ThemeEventsContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeEventsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const eventNow = getEvent();
|
||||
|
||||
return <ThemeEventsContext.Provider value={{ event: eventNow }}>
|
||||
{children}
|
||||
</ThemeEventsContext.Provider>
|
||||
}
|
||||
|
||||
export const useThemeEvents = () => useContext(ThemeEventsContext);
|
||||
12
src/lib/mailer.ts
Normal file
12
src/lib/mailer.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export const transporter = nodemailer.createTransport({
|
||||
// @ts-ignore
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
secure: process.env.SMTP_SECURE == "true",
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
});
|
||||
5
src/lib/redis.ts
Normal file
5
src/lib/redis.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
export const redis = new Redis(process.env.REDIS_URL!, {
|
||||
maxRetriesPerRequest: 3,
|
||||
});
|
||||
53
src/lib/server-utils.ts
Normal file
53
src/lib/server-utils.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import axios from "axios";
|
||||
import { JSDOM } from 'jsdom';
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
import { redis } from "./redis";
|
||||
import { transporter } from "./mailer";
|
||||
import { trimTooLong } from "./strings";
|
||||
import { escape } from "validator";
|
||||
|
||||
export async function rateLimited(clientId: string) {
|
||||
const key = `contact:${clientId}`;
|
||||
const count = await redis.incr(key);
|
||||
if (count === 1) {
|
||||
await redis.expire(key, 600);
|
||||
}
|
||||
return count > 3;
|
||||
}
|
||||
|
||||
export async function validateTurnstile(token: string, remoteip: string) {
|
||||
const body = new URLSearchParams({
|
||||
secret: process.env.CF_SECRET_KEY!,
|
||||
response: token,
|
||||
remoteip
|
||||
})
|
||||
|
||||
const res = await axios.post("https://challenges.cloudflare.com/turnstile/v0/siteverify", body, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
});
|
||||
|
||||
const result = await res.data;
|
||||
return result.success === true;
|
||||
}
|
||||
|
||||
export async function sendEmail(name: string, email: string, message: string) {
|
||||
const rawMessage = `From: ${name}\nEmail: ${email}\n\nMessage:\n${trimTooLong(message, 5000)}`;
|
||||
const messageHTML = sanitize(escape(rawMessage));
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `Nonszy Contact Form <${process.env.SMTP_USER}>`,
|
||||
replyTo: email,
|
||||
to: process.env.SMTP_REPLY,
|
||||
subject: `[CONTACT_FORM] from ${name}`,
|
||||
text: rawMessage,
|
||||
// html: messageHTML
|
||||
})
|
||||
}
|
||||
|
||||
export function sanitize(dirty: string) {
|
||||
const window = new JSDOM('').window;
|
||||
return DOMPurify(window).sanitize(dirty);
|
||||
}
|
||||
3
src/lib/strings.ts
Normal file
3
src/lib/strings.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const trimTooLong = (text: string, limit: number, endsWith?: string) => {
|
||||
return text.length > limit ? text.slice(0, limit) + (endsWith ?? "...") : text;
|
||||
}
|
||||
5
src/lib/types.ts
Normal file
5
src/lib/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type EventsDate = {
|
||||
name: string,
|
||||
start: number[],
|
||||
end: number[]
|
||||
}
|
||||
@@ -1,8 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { format, isWithinInterval } from "date-fns";
|
||||
import type { EventsDate } from "./types";
|
||||
|
||||
const events: EventsDate[] = [
|
||||
{
|
||||
name: 'christmas',
|
||||
start: [12, 20],
|
||||
end: [12, 32]
|
||||
},
|
||||
]
|
||||
|
||||
export function getExpirationDate () {
|
||||
const expirationDate = new Date("2026-02-15");
|
||||
return dayjs(expirationDate).format("dddd, D MMMM YYYY");
|
||||
return format(expirationDate, "dddd, D MMMM YYYY");
|
||||
}
|
||||
|
||||
export function getEvent (): EventsDate | undefined {
|
||||
return events.find((e) => {
|
||||
const today = new Date(Date.now());
|
||||
const year = today.getFullYear();
|
||||
|
||||
const start = new Date(year, e.start[0] - 1, e.start[1]);
|
||||
const end = new Date(year, e.end[0] - 1, e.end[1]);
|
||||
|
||||
return isWithinInterval(today, { start, end })
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const config: Config = {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
primary: "var(--primary)",
|
||||
'primary-darker': "var(--primary-darker)",
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
@@ -24,6 +25,7 @@ const config: Config = {
|
||||
},
|
||||
animation: {
|
||||
"silly-bouncing": 'silly-bounce 0.8s infinite',
|
||||
"click-bouncing": 'click-bounce 200ms',
|
||||
'window-popup': 'window-popup 1s',
|
||||
'window-popdown': 'window-popdown 1s'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user