Compare commits

..

30 Commits

Author SHA1 Message Date
88acf47ed5 fix ssl ambigous 2026-02-09 00:39:22 +07:00
9211ba9446 add another debug 2026-02-09 00:32:30 +07:00
95368d156d standalone sucks, nixpacks sucks 2026-01-01 19:38:10 +07:00
8b7cf681b4 update email message 2026-01-01 19:19:51 +07:00
5b675c21ba more logs i'll be damn 2026-01-01 19:00:10 +07:00
281325c9db well i guess 2026-01-01 18:40:23 +07:00
f3e176f6c7 fix build part 2 2026-01-01 18:32:19 +07:00
8a9c9517cb try to fix build error 2026-01-01 18:29:02 +07:00
90f6e840f3 try to fix runtime error and add runtime logs 2026-01-01 18:26:04 +07:00
598fbf9d4a Remove log 2026-01-01 17:50:21 +07:00
15e4fd556c Add cloudflare turnstile 2026-01-01 17:46:33 +07:00
50461f1644 not ready for html message 2026-01-01 16:31:00 +07:00
7cd5113ef0 add working send email 2026-01-01 15:31:22 +07:00
46fdd77353 add contact api 2026-01-01 13:01:11 +07:00
3866e8ade0 Add contact form 2026-01-01 09:25:02 +07:00
107ee8794d add context hooks and fix new window not appear 2025-12-26 19:00:34 +07:00
1215f2f987 change snowflakes 2025-12-26 17:55:35 +07:00
32e609ad71 fix conflicting packages 2025-12-25 16:58:12 +07:00
29d251459a Add christmas events 2025-12-25 16:53:09 +07:00
4760f51095 patch security omg 2025-12-18 10:50:16 +07:00
6510022716 remove healthcheck 2025-11-22 18:02:21 +08:00
573b6202be Update Dockerfile 2025-11-22 12:49:35 +08:00
15eb1ea46b Update Dockerfile 2025-11-22 11:45:24 +08:00
76e2455889 Add curl 2025-11-22 10:14:41 +07:00
6d9e5ae56f Add health check 2025-11-22 09:37:37 +07:00
0fcefcbbe5 change docker image 2025-10-12 14:06:05 +07:00
b7c69fb781 update 404 page performance 2025-10-12 13:49:19 +07:00
3a5e1f07cf Merge pull request 'Update the 404 Page' (#1) from staging into main
Reviewed-on: #1
2025-10-11 21:52:24 +08:00
3c90cbbe2e update home page 2025-10-11 20:51:05 +07:00
4065f0cd84 change the 404 poke animation again lol 2025-10-11 20:50:20 +07:00
36 changed files with 9093 additions and 1664 deletions

View File

@@ -1,4 +1,4 @@
FROM node:18-alpine AS builder FROM node:20-alpine AS builder
RUN apk add --no-cache yarn RUN apk add --no-cache yarn
@@ -13,14 +13,17 @@ COPY ./src/app/favicon.ico ./public/favicon.ico
RUN yarn build RUN yarn build
FROM node:18-alpine AS runner FROM node:20-alpine AS runner
WORKDIR /app WORKDIR /app
RUN addgroup --system --gid 1001 nodejs 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 USER nextjs

28
amp.Dockerfile Normal file
View 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"]

View File

@@ -6,8 +6,7 @@ const nextConfig = {
remotePatterns: [ remotePatterns: [
{ {
protocol: 'https', protocol: 'https',
hostname: 'media.tenor.com', hostname: 'media.tenor.com'
pathname: '/1BCeG1aTiBAAAAAd/temptation-stairway-ena.gif'
} }
] ]
}, },

6218
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,20 +13,29 @@
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"@next/mdx": "^15.3.4", "@next/mdx": "^15.3.4",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"axios": "^1.13.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "date-fns": "^4.1.0",
"next": "14.2.24", "dompurify": "^3.3.1",
"ioredis": "^5.8.2",
"jsdom": "^27.4.0",
"next": "^14.2.35",
"nodemailer": "^7.0.12",
"react": "^18", "react": "^18",
"react-dom": "^18" "react-dom": "^18",
"sharp": "^0.34.5",
"validator": "^13.15.26"
}, },
"devDependencies": { "devDependencies": {
"@iconify/react": "^5.2.0", "@iconify/react": "^5.2.0",
"@types/jsdom": "^27.0.0",
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^7.0.4",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@types/validator": "^13.15.10",
"add": "^2.0.6", "add": "^2.0.6",
"eslint": "^8", "eslint-config-next": "^16.0.10",
"eslint-config-next": "14.2.24",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5", "typescript": "^5",

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

View File

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

View File

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View 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 : ''
});
}
}

View File

@@ -5,13 +5,14 @@
:root { :root {
--background: #0F0A1F; --background: #0F0A1F;
--primary: #F48120; --primary: #F48120;
--primary-darker: #cc6d1f;
} }
@keyframes click-bounce { @keyframes click-bounce {
0%, 100% { 0%, 100% {
transform: translate(0px, 0px) scale(1, 1); transform: translate(0px, 0px) scale(1, 1);
} }
15% { 10% {
transform: translate(0px, 14px) scale(1.2, 0.9); transform: translate(0px, 14px) scale(1.2, 0.9);
} }
} }
@@ -22,7 +23,7 @@
animation-timing-function: linear(0.2, 0.8, 1); animation-timing-function: linear(0.2, 0.8, 1);
} }
50% { 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); animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
} }
} }

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Roboto_Mono } from 'next/font/google' import { Roboto_Mono } from 'next/font/google'
import "./globals.css"; import "./globals.css";
import Script from "next/script";
const roboto_mono = Roboto_Mono({ const roboto_mono = Roboto_Mono({
subsets: ['latin'] subsets: ['latin']
@@ -22,6 +23,7 @@ export default function RootLayout({
className={`bg-background ${roboto_mono.className} antialiased text-white`} className={`bg-background ${roboto_mono.className} antialiased text-white`}
> >
{children} {children}
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />
</body> </body>
</html> </html>
); );

View File

@@ -20,8 +20,9 @@ export default function NotFound() {
</noscript> </noscript>
<WobblingImage <WobblingImage
images={{ images={{
idle: "/images/coralz_0.png", idle: "/images/coralz_idle.png",
poked: "/images/coralz_1.png" aware: "/images/coralz_scared.png",
poked: "/images/coralz_poked.png"
}} }}
/> />
<div className="-mt-2"> <div className="-mt-2">

View File

@@ -1,42 +1,45 @@
import { LandingImage } from "@/components/landing-image"; import { LandingImage } from "@/components/landing-image";
import { NolaGlitchClientOnly } from "@/components/nola-glitch"; import { NolaGlitchClientOnly } from "@/components/nola-glitch";
import { Sosmed } from "@/components/sosmed"; import { Sosmed } from "@/components/sosmed";
import HomeText from "@/components/home-text.mdx" import HomeText from "@/components/texts/home.mdx"
import Link from "@/components/link"; import Link from "@/components/link";
import { FakeWindow, HomeWindows } from "@/components/windows"; import { FakeWindow, HomeWindows } from "@/components/windows";
import { SnowfallBackground } from "@/components/events/christmas";
import { WindowManagerProvider } from "@/hooks/window-manager"; import { WindowManagerProvider } from "@/hooks/window-manager";
import { ThemeEventsProvider } from "@/hooks/theme-events";
export default function Home() { export default function Home() {
return (<> return (<>
<main className="flex items-center pt-16 md:pt-24 pb-12 px-8 md:px-0 overflow-x-hidden"> <main className="flex items-center pt-16 md:pt-24 pb-12 px-8 md:px-0 overflow-x-hidden">
<WindowManagerProvider> <ThemeEventsProvider>
<FakeWindow windowText="Homepage"> <SnowfallBackground />
<HomeWindows /> <WindowManagerProvider>
<header className="text-center mb-8"> <FakeWindow windowText="Homepage">
<h1 className="font-bold text-3xl leading-normal"> <HomeWindows />
Nonszy Work<span className="text-primary">space</span> <header className="text-center mb-8">
</h1> <h1 className="font-bold text-3xl leading-normal">
</header> Nonszy Work<span className="text-primary">space</span>
<noscript> </h1>
<LandingImage /> </header>
</noscript> <noscript>
<NolaGlitchClientOnly /> <LandingImage />
<Sosmed /> </noscript>
<article className="space-y-5 leading-relaxed relative text-sm"> <NolaGlitchClientOnly />
<HomeText /> <Sosmed />
</article> <article className="space-y-5 leading-relaxed relative text-sm">
<section className="my-8"> <HomeText />
<p> Powered with <Link href="https://www.cloudflare.com/" target="_blank">Cloudflare</Link> </p> </article>
</section> <footer className="mt-20 text-center">
<footer className="mt-20 text-center"> <p>&copy; <span className="text-sm">2025 Nomi Nonszy</span></p>
<p>&copy; <span className="text-sm">2025 Nomi Nonszy</span></p> <p className="text-sm">
<p className="text-sm"> <Link href={"/terms"}>Terms</Link> and <Link href={"/privacy"}>Privacy</Link>
<Link href={"/terms"}>Terms</Link> and <Link href={"/privacy"}>Privacy</Link> </p>
</p> </footer>
</footer> </FakeWindow>
</FakeWindow> </WindowManagerProvider>
</WindowManagerProvider> </ThemeEventsProvider>
</main> </main>
</>); </>);
} }

View File

@@ -67,11 +67,9 @@ export const FakeRelativeWindow = ({
} }
useEffect(() => { useEffect(() => {
if (!windowManager.isLocalDataExists) { populateWindow();
if (!currentWindow) populateWindow(); return () => {
return () => { windowManager.remove(windowName);
windowManager.remove(windowName);
}
} }
}, []); }, []);
@@ -191,7 +189,7 @@ export const FakeRelativeWindow = ({
{/* Main window */} {/* Main window */}
<div <div
className={clsx("md:border bg-background border-primary", withAnim && animation.window)} className={clsx("md:border bg-background bg-opacity-50 border-primary", withAnim && animation.window)}
style={{ style={{
transform: `translate(${currentWindow.offset.x}px, ${currentWindow.offset.y}px)` transform: `translate(${currentWindow.offset.x}px, ${currentWindow.offset.y}px)`
}} }}

View 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>
)

View File

@@ -0,0 +1 @@
'use client'

View 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>
);
}

View 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>
)
}

View File

@@ -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>

View File

@@ -2,11 +2,11 @@ import Image from "next/image"
import { FloatingLabel } from "./floating-label" import { FloatingLabel } from "./floating-label"
export const LandingImage = () => ( 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 <Image
className="mb-8 mx-auto" className="mb-8 mx-auto h-auto"
alt="Coral <3" 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} width={280}
height={280} height={280}
unoptimized unoptimized

View 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 />

View 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"
/>
)

View File

@@ -3,6 +3,7 @@ import { FloatingLabel } from "@/components/floating-label";
import { FakeRelativeWindow, RestoreWindowsButton } from "./client-windows"; import { FakeRelativeWindow, RestoreWindowsButton } from "./client-windows";
import Image from "next/image" import Image from "next/image"
import Link from "next/link"; import Link from "next/link";
import { ChristmasProperty } from "./events/christmas";
export const FakeWindow = ({ export const FakeWindow = ({
windowText, children windowText, children
@@ -10,7 +11,13 @@ export const FakeWindow = ({
windowText: string windowText: string
children: React.ReactNode 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="p-1 pb-0">
<div className="hidden md:flex bg-primary p-2 justify-between text-background"> <div className="hidden md:flex bg-primary p-2 justify-between text-background">
<div className="ms-1 pointer-events-none"> <div className="ms-1 pointer-events-none">
@@ -45,13 +52,16 @@ export const HomeWindows = () => (
draggable draggable
> >
<FloatingLabel placeholder="This is Nola, my OC :3"> <FloatingLabel placeholder="This is Nola, my OC :3">
<Image <div className="relative">
className="" <ChristmasProperty img="/images/event_santahat1.png" size={150} top={-40} left={70} flip />
alt="Nola" <Image
src="/images/nola.png" className=""
width={200} alt="Nola"
height={200} src="/images/nola.png"
/> width={200}
height={200}
/>
</div>
</FloatingLabel> </FloatingLabel>
</FakeRelativeWindow> </FakeRelativeWindow>
<FakeRelativeWindow <FakeRelativeWindow
@@ -74,7 +84,7 @@ export const HomeWindows = () => (
</Link> </Link>
</FakeRelativeWindow> </FakeRelativeWindow>
<FakeRelativeWindow <FakeRelativeWindow
windowText="coral-1.exe" windowText="cube_coral.exe"
className='-left-[75%] top-[1980px] z-10' className='-left-[75%] top-[1980px] z-10'
draggable draggable
> >
@@ -88,13 +98,13 @@ export const HomeWindows = () => (
/> />
</FakeRelativeWindow> </FakeRelativeWindow>
<FakeRelativeWindow <FakeRelativeWindow
windowText="coral_cupcake.mkv" windowText="ena_spin.exe"
className="-right-[85%] top-[440px] z-10" className="-right-[85%] top-[440px] z-10"
draggable draggable
> >
<Image <Image
alt="Coral Cupcake" 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} width={240}
height={200} height={200}
quality={10} quality={10}

View File

@@ -7,16 +7,38 @@ import { useCallback, useEffect, useRef, useState } from "react"
interface WobblingImageInterface { interface WobblingImageInterface {
images: { images: {
idle: string; idle: string;
aware?: string;
poked?: string; poked?: string;
weird?: 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 ({ function WobblingImage ({
images images
}: WobblingImageInterface) { }: WobblingImageInterface) {
const size = 400; const size = 400;
const [isPoked, setPoked] = useState(false); const [isPoked, setPoked] = useState(false);
const [isAware, setAware] = useState(false);
const pokeTimeoutRef = useRef<NodeJS.Timeout | null>(null); const pokeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [isAnimating, setIsAnimating] = useState(false); const [isAnimating, setIsAnimating] = useState(false);
@@ -70,8 +92,8 @@ function WobblingImage ({
if (!audioBuffer || !audioContextRef.current) return; if (!audioBuffer || !audioContextRef.current) return;
const context = audioContextRef.current; const context = audioContextRef.current;
const changeToLowPitch = Math.floor(Math.random() * 10); const changeToLowPitch = Math.floor(Math.random() * 66);
const randomPitch = Math.random() * 0.2 + (changeToLowPitch == 1 ? 0.25 : 0.85); const randomPitch = Math.random() * 0.38 + (changeToLowPitch == 1 ? 0.25 : 0.75);
const source = context.createBufferSource(); const source = context.createBufferSource();
source.buffer = audioBuffer; source.buffer = audioBuffer;
@@ -86,6 +108,9 @@ function WobblingImage ({
function handleClick() { function handleClick() {
const pokedDuration = 700 + Math.floor(Math.random() * 100)
if (!isAware) setAware(true);
setIsAnimating(false); setIsAnimating(false);
requestAnimationFrame(() => setIsAnimating(true)); requestAnimationFrame(() => setIsAnimating(true));
@@ -101,7 +126,7 @@ function WobblingImage ({
setPoked(true); setPoked(true);
pokeTimeoutRef.current = setTimeout(() => { pokeTimeoutRef.current = setTimeout(() => {
setPoked(false); setPoked(false);
}, 600); }, pokedDuration);
} }
function handleAnimationEnd() { function handleAnimationEnd() {
@@ -112,8 +137,8 @@ function WobblingImage ({
<div className=""> <div className="">
<div <div
className={clsx( className={clsx(
"relative mx-auto cursor-grab h-[400px] select-none", "relative mx-auto cursor-grab active:cursor-grabbing h-[400px] select-none",
isAnimating && "animate-[click-bounce_250ms_ease-out]" isAnimating && "animate-[click-bounce_230ms_ease-out]"
)} )}
onClick={handleClick} onClick={handleClick}
onAnimationEnd={handleAnimationEnd} onAnimationEnd={handleAnimationEnd}
@@ -123,21 +148,23 @@ function WobblingImage ({
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onPointerDown={(e) => e.preventDefault()} onPointerDown={(e) => e.preventDefault()}
> >
<Image <ImageClip
className={clsx("absolute left-1/2 -translate-x-1/2 top-0 pointer-events-none", isPoked ? "opacity-0" : "opacity-100")} name="idle"
alt="clip1"
src={images.idle} src={images.idle}
width={size} size={size}
height={size} visible={!isPoked && !isAware}
draggable={false}
/> />
<Image <ImageClip
className={clsx("absolute left-1/2 -translate-x-1/2 top-0 pointer-events-none", isPoked ? "opacity-100" : "opacity-0")} name="aware"
alt="clip2" src={images.aware || images.idle}
size={size}
visible={!isPoked && isAware}
/>
<ImageClip
name="poked"
src={images.poked || images.idle} src={images.poked || images.idle}
width={size} size={size}
height={size} visible={isPoked}
draggable={false}
/> />
</div> </div>
</div> </div>

View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
export type EventsDate = {
name: string,
start: number[],
end: number[]
}

View File

@@ -1,8 +1,29 @@
'use client' '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 () { export function getExpirationDate () {
const expirationDate = new Date("2026-02-15"); 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 })
})
} }

View File

@@ -12,6 +12,7 @@ const config: Config = {
background: "var(--background)", background: "var(--background)",
foreground: "var(--foreground)", foreground: "var(--foreground)",
primary: "var(--primary)", primary: "var(--primary)",
'primary-darker': "var(--primary-darker)",
}, },
}, },
screens: { screens: {

3529
yarn.lock

File diff suppressed because it is too large Load Diff