Compare commits
No commits in common. "main" and "staging" have entirely different histories.
11
Dockerfile
11
Dockerfile
@ -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
|
||||
|
||||
|
||||
@ -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"]
|
||||
@ -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
6216
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -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 |
@ -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 : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>© <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>© <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>
|
||||
</>);
|
||||
}
|
||||
|
||||
@ -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)`
|
||||
}}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
@ -1 +0,0 @@
|
||||
'use client'
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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"
|
||||
/>
|
||||
)
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
@ -1,5 +0,0 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
export const redis = new Redis(process.env.REDIS_URL!, {
|
||||
maxRetriesPerRequest: 3,
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export const trimTooLong = (text: string, limit: number, endsWith?: string) => {
|
||||
return text.length > limit ? text.slice(0, limit) + (endsWith ?? "...") : text;
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export type EventsDate = {
|
||||
name: string,
|
||||
start: number[],
|
||||
end: number[]
|
||||
}
|
||||
@ -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 })
|
||||
})
|
||||
}
|
||||
@ -12,7 +12,6 @@ const config: Config = {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
primary: "var(--primary)",
|
||||
'primary-darker': "var(--primary-darker)",
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user