Compare commits

..

No commits in common. "main" and "staging" have entirely different histories.

30 changed files with 1587 additions and 8990 deletions

View File

@ -1,4 +1,4 @@
FROM node:20-alpine AS builder
FROM node:18-alpine AS builder
RUN apk add --no-cache yarn
@ -13,17 +13,14 @@ COPY ./src/app/favicon.ico ./public/favicon.ico
RUN yarn build
FROM node:20-alpine AS runner
FROM node:18-alpine AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 --ingroup nodejs nextjs
RUN adduser --system --uid 1001 nextjs
RUN mkdir -p ./.next/cache
RUN chown nextjs:nodejs ./.next/cache
ENV NODE_ENV=production
ENV NODE_ENV production
USER nextjs

View File

@ -1,28 +0,0 @@
# 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,7 +6,8 @@ const nextConfig = {
remotePatterns: [
{
protocol: 'https',
hostname: 'media.tenor.com'
hostname: 'media.tenor.com',
pathname: '/1BCeG1aTiBAAAAAd/temptation-stairway-ena.gif'
}
]
},

6216
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,29 +13,20 @@
"@mdx-js/react": "^3.1.0",
"@next/mdx": "^15.3.4",
"@types/mdx": "^2.0.13",
"axios": "^1.13.2",
"clsx": "^2.1.1",
"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",
"dayjs": "^1.11.13",
"next": "14.2.24",
"react": "^18",
"react-dom": "^18",
"sharp": "^0.34.5",
"validator": "^13.15.26"
"react-dom": "^18"
},
"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-config-next": "^16.0.10",
"eslint": "^8",
"eslint-config-next": "14.2.24",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

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

View File

@ -1,7 +1,6 @@
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']
@ -23,7 +22,6 @@ 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>
);

View File

@ -5,41 +5,38 @@ import HomeText from "@/components/texts/home.mdx"
import Link from "@/components/link";
import { FakeWindow, HomeWindows } from "@/components/windows";
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">
<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>&copy; <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>
<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>
<section className="my-8">
<p> Powered with <Link href="https://www.cloudflare.com/" target="_blank">Cloudflare</Link> </p>
</section>
<footer className="mt-20 text-center">
<p>&copy; <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>
</main>
</>);
}

View File

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

View File

@ -1,204 +0,0 @@
'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

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

View File

@ -1,114 +0,0 @@
'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

@ -1,49 +0,0 @@
'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

@ -2,11 +2,11 @@ import Image from "next/image"
import { FloatingLabel } from "./floating-label"
export const LandingImage = () => (
<FloatingLabel placeholder="ENA ❤️. Alert: NOT MY WORK!">
<FloatingLabel placeholder="Coral Glasses ❤️. Alert: NOT MY WORK! See the end of the page">
<Image
className="mb-8 mx-auto h-auto"
className="mb-8 mx-auto"
alt="Coral <3"
src="https://media.tenor.com/1BCeG1aTiBAAAAAd/temptation-stairway-ena.gif"
src="https://media1.tenor.com/m/RIP2rxKM_FgAAAAC/ena-ena-dream-bbq.gif"
width={280}
height={280}
unoptimized

View File

@ -1,7 +1,6 @@
import Image from "next/image"
import Link from "@/components/link";
import { FloatingLabel } from "@/components/floating-label";
import ContactForm from "@/components/form/contact-form";
Welcome!
@ -9,8 +8,6 @@ This is our cozy little corner of the internet where we run services and website
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).
@ -18,9 +15,9 @@ 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.
I only play indie games, and cooking up cursed mods just for fun.
Multifandom with Psychopomp, Deltarune, and ENA! I'm currently dedicated to the ENA SERIES!!
Multifandom with Psychopomp, Deltarune, and ENA! I'm currently dedicated to the ENA SERIES!! Especially <Link href="https://enajoelg.fandom.com/wiki/Coral_Glasses" target="_blank">Coral Glasses</Link> my beloved wife ❤️
<Image
className="mb-12 mx-auto"
@ -90,7 +87,6 @@ and published under the ENA Team. She speaks Korean in the game and is voiced by
The game is part of the surreal and artistically distinct ENA universe, which expands upon his earlier animated web series of the same name.
She's supposed to handle business stuff, but she's sweating, faxing out of her hairline, and spiraling into mild panic every five minutes
## Tell me something
<ContactForm />
She's cute, anxious, awkward, weird, beautiful and i swear, she's literally me at work sweating through every conversation ashdjakwoiqhkaslchmaujqk

View File

@ -1,9 +0,0 @@
'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,7 +3,6 @@ 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
@ -11,13 +10,7 @@ export const FakeWindow = ({
windowText: string
children: React.ReactNode
}) => (
<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="mx-auto w-[480px] md:w-[520px] md:border border-primary">
<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">
@ -52,16 +45,13 @@ export const HomeWindows = () => (
draggable
>
<FloatingLabel placeholder="This is Nola, my OC :3">
<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>
<Image
className=""
alt="Nola"
src="/images/nola.png"
width={200}
height={200}
/>
</FloatingLabel>
</FakeRelativeWindow>
<FakeRelativeWindow
@ -84,7 +74,7 @@ export const HomeWindows = () => (
</Link>
</FakeRelativeWindow>
<FakeRelativeWindow
windowText="cube_coral.exe"
windowText="coral-1.exe"
className='-left-[75%] top-[1980px] z-10'
draggable
>
@ -98,13 +88,13 @@ export const HomeWindows = () => (
/>
</FakeRelativeWindow>
<FakeRelativeWindow
windowText="ena_spin.exe"
windowText="coral_cupcake.mkv"
className="-right-[85%] top-[440px] z-10"
draggable
>
<Image
alt="Coral Cupcake"
src="https://media.tenor.com/Uv-PLe5GIe0AAAAi/gyaruface.gif"
src="https://media1.tenor.com/m/N5K-4AWj8QcAAAAC/coral-glasses-cupcake.gif"
width={240}
height={200}
quality={10}

View File

@ -13,26 +13,6 @@ interface WobblingImageInterface {
}
}
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) {
@ -148,23 +128,21 @@ function WobblingImage ({
onMouseDown={(e) => e.preventDefault()}
onPointerDown={(e) => e.preventDefault()}
>
<ImageClip
name="idle"
src={images.idle}
size={size}
visible={!isPoked && !isAware}
<Image
className={clsx("absolute left-1/2 -translate-x-1/2 top-0 pointer-events-none", isPoked ? "opacity-0" : "opacity-100")}
alt="clip1"
src={isAware ? (images.aware || images.idle) : images.idle}
width={size}
height={size}
draggable={false}
/>
<ImageClip
name="aware"
src={images.aware || images.idle}
size={size}
visible={!isPoked && isAware}
/>
<ImageClip
name="poked"
<Image
className={clsx("absolute left-1/2 -translate-x-1/2 top-0 pointer-events-none", isPoked ? "opacity-100" : "opacity-0")}
alt="clip2"
src={images.poked || images.idle}
size={size}
visible={isPoked}
width={size}
height={size}
draggable={false}
/>
</div>
</div>

View File

@ -1,21 +0,0 @@
'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);

View File

@ -1,12 +0,0 @@
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,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});

View File

@ -1,5 +0,0 @@
import Redis from "ioredis";
export const redis = new Redis(process.env.REDIS_URL!, {
maxRetriesPerRequest: 3,
});

View File

@ -1,56 +0,0 @@
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));
console.log(process.env.SMTP_USER);
console.log(process.env.SMTP_REPLY);
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);
}

View File

@ -1,3 +0,0 @@
export const trimTooLong = (text: string, limit: number, endsWith?: string) => {
return text.length > limit ? text.slice(0, limit) + (endsWith ?? "...") : text;
}

View File

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

View File

@ -1,29 +1,8 @@
'use client'
import { format, isWithinInterval } from "date-fns";
import type { EventsDate } from "./types";
const events: EventsDate[] = [
{
name: 'christmas',
start: [12, 20],
end: [12, 32]
},
]
import dayjs from 'dayjs';
export function getExpirationDate () {
const expirationDate = new Date("2026-02-15");
return format(expirationDate, "dddd, D MMMM YYYY");
return dayjs(expirationDate).format("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,7 +12,6 @@ const config: Config = {
background: "var(--background)",
foreground: "var(--foreground)",
primary: "var(--primary)",
'primary-darker': "var(--primary-darker)",
},
},
screens: {

3523
yarn.lock

File diff suppressed because it is too large Load Diff