Compare commits

..

No commits in common. "c5e0b08b7ed7d0ee759f7c9ba51ba4b29f1bdadb" and "c2c1136c586d445d8d57a7acdb7370691a70aacf" have entirely different histories.

17 changed files with 1178 additions and 1281 deletions

View file

@ -1,9 +1,9 @@
FROM node:26-alpine AS base FROM node:23-alpine AS base
WORKDIR /app WORKDIR /app
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
RUN npm install -g pnpm RUN corepack enable && corepack prepare pnpm@11.1.1 --activate
FROM base AS deps FROM base AS deps
WORKDIR /app WORKDIR /app
@ -28,7 +28,7 @@ ENV HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
# I know all the paths are messed up # I know all the paths are messed up but I don't have time to fix it
COPY --from=builder /app/backend/public ./public COPY --from=builder /app/backend/public ./public
COPY --from=builder /app/backend/.next ./.next COPY --from=builder /app/backend/.next ./.next
COPY --from=builder /app/backend/.next/static ./.next/standalone/backend/.next/static COPY --from=builder /app/backend/.next/static ./.next/standalone/backend/.next/static

View file

@ -2,9 +2,6 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
experimental: {
useTypeScriptCli: true,
},
async headers() { async headers() {
return [ return [
{ {

View file

@ -2,7 +2,7 @@
"name": "@tomodachi-share/backend", "name": "@tomodachi-share/backend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"packageManager": "pnpm@11.18.0", "packageManager": "pnpm@11.8.0",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
@ -12,39 +12,39 @@
}, },
"dependencies": { "dependencies": {
"@2toad/profanity": "^3.3.0", "@2toad/profanity": "^3.3.0",
"@auth/prisma-adapter": "2.11.3", "@auth/prisma-adapter": "2.11.2",
"@prisma/client": "6.19.3", "@prisma/client": "6.19.3",
"@tomodachi-share/shared": "workspace:*", "@tomodachi-share/shared": "workspace:*",
"bit-buffer": "^0.3.0", "bit-buffer": "^0.3.0",
"charinfo-ex": "^0.0.5", "charinfo-ex": "^0.0.5",
"dayjs": "^1.11.21", "dayjs": "^1.11.21",
"downshift": "^9.4.0", "downshift": "^9.3.6",
"file-type": "^22.0.1", "file-type": "^22.0.1",
"next": "16.2.12", "next": "16.2.9",
"next-auth": "5.0.0-beta.32", "next-auth": "5.0.0-beta.30",
"qrcode-generator": "^2.0.4", "qrcode-generator": "^2.0.4",
"react": "^19.2.8", "react": "^19.2.7",
"react-dom": "^19.2.8", "react-dom": "^19.2.7",
"redis": "^6.2.0", "redis": "^6.0.0",
"satori": "^0.29.0", "satori": "^0.26.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"sjcl-with-all": "1.0.8", "sjcl-with-all": "1.0.8",
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.6", "@eslint/eslintrc": "^3.3.5",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.1",
"@types/node": "^26.1.2", "@types/node": "^26.0.0",
"@types/react": "^19.2.18", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.4", "@types/react-dom": "^19.2.3",
"@types/sjcl": "^1.0.34", "@types/sjcl": "^1.0.34",
"eslint": "^10.8.0", "eslint": "^10.5.0",
"eslint-config-next": "16.2.12", "eslint-config-next": "16.2.9",
"prisma": "^6.19.3", "prisma": "^6.19.3",
"schema-dts": "^2.0.0", "schema-dts": "^2.0.0",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.1",
"typescript": "^7.0.2" "typescript": "^6.0.3"
}, },
"exports": { "exports": {
".": "./src/types.d.ts" ".": "./src/types.d.ts"

View file

@ -11,43 +11,43 @@
}, },
"dependencies": { "dependencies": {
"@bprogress/react": "^1.2.7", "@bprogress/react": "^1.2.7",
"@fontsource-variable/lexend": "^5.3.0", "@fontsource-variable/lexend": "^5.2.11",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@nanostores/react": "^1.1.0", "@nanostores/react": "^1.1.0",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.0",
"@tomodachi-share/shared": "workspace:*", "@tomodachi-share/shared": "workspace:*",
"@types/react": "^19.2.18", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.4", "@types/react-dom": "^19.2.3",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"dayjs": "^1.11.21", "dayjs": "^1.11.20",
"downshift": "^9.4.0", "downshift": "^9.3.2",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"nanostores": "^1.4.2", "nanostores": "^1.3.0",
"qrcode-generator": "^2.0.4", "qrcode-generator": "^2.0.4",
"react": "^19.2.8", "react": "^19.2.6",
"react-dom": "^19.2.8", "react-dom": "^19.2.6",
"react-dropzone": "^19.1.1", "react-dropzone": "^15.0.0",
"react-image-crop": "^11.1.2", "react-image-crop": "^11.0.10",
"react-router": "^8.3.0", "react-router": "^7.15.0",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.0",
"vite-plugin-node-polyfills": "^0.26.0", "vite-plugin-node-polyfills": "^0.26.0",
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^9.39.4",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "^26.1.2", "@types/node": "^24.12.4",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.5", "@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.8.0", "eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.8.0", "globals": "^17.6.0",
"typescript": "~7.0.2", "typescript": "~6.0.3",
"typescript-eslint": "^8.65.0", "typescript-eslint": "^8.59.3",
"vite": "^8.2.0" "vite": "^8.0.12"
} }
} }

View file

@ -1,10 +1,10 @@
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { type DropzoneOptions, useDropzone } from "react-dropzone"; import { type DropzoneOptions, type FileWithPath, useDropzone } from "react-dropzone";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
interface Props { interface Props {
type?: "file" | "image"; type?: "file" | "image";
onDrop: (acceptedFiles: File[]) => void; onDrop: (acceptedFiles: FileWithPath[]) => void;
options?: DropzoneOptions; options?: DropzoneOptions;
children?: ReactNode; children?: ReactNode;
} }
@ -12,7 +12,7 @@ interface Props {
export default function Dropzone({ type = "image", onDrop, options, children }: Props) { export default function Dropzone({ type = "image", onDrop, options, children }: Props) {
const [isDraggingOver, setIsDraggingOver] = useState(false); const [isDraggingOver, setIsDraggingOver] = useState(false);
const handleDrop = (acceptedFiles: File[]) => { const handleDrop = (acceptedFiles: FileWithPath[]) => {
setIsDraggingOver(false); setIsDraggingOver(false);
onDrop(acceptedFiles); onDrop(acceptedFiles);
}; };

View file

@ -1,3 +1,4 @@
import { Icon } from "@iconify/react";
import { Link } from "react-router"; import { Link } from "react-router";
export default function Footer() { export default function Footer() {
@ -14,15 +15,32 @@ export default function Footer() {
<Link to="/terms-of-service" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline"> <Link to="/terms-of-service" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline">
Terms of Service Terms of Service
</Link> </Link>
<span className="text-zinc-400 hidden sm:inline" aria-hidden="true"> <span className="text-zinc-400 hidden sm:inline" aria-hidden="true">
</span> </span>
<Link to="/privacy" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline"> <Link to="/privacy" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline">
Privacy Policy Privacy Policy
</Link> </Link>
<span className="text-zinc-400 hidden sm:inline" aria-hidden="true"> <span className="text-zinc-400 hidden sm:inline" aria-hidden="true">
</span> </span>
<Link
to="https://discord.gg/48cXBFKvWQ"
target="_blank"
className="text-[#5865F2] hover:text-[#454FBF] transition-colors duration-200 hover:underline inline-flex items-end gap-1"
>
<Icon icon="ic:baseline-discord" className="text-lg" />
Discord
</Link>
<span className="text-zinc-400 hidden sm:inline" aria-hidden="true">
</span>
<Link <Link
to="https://trafficlunar.net" to="https://trafficlunar.net"
target="_blank" target="_blank"
@ -32,19 +50,6 @@ export default function Footer() {
</Link> </Link>
</div> </div>
{/* Contact / feedback */}
<div className="text-center mt-2">
<p className="text-xs text-zinc-500">
Questions, bugs, or feature ideas? Contact me at{" "}
<a
href="mailto:hello@trafficlunar.net"
className="text-orange-400 hover:text-orange-500 font-medium hover:underline transition-colors duration-200"
>
hello@trafficlunar.net
</a>
</p>
</div>
{/* Copyright */} {/* Copyright */}
<div className="text-center mt-4 mb-4"> <div className="text-center mt-4 mb-4">
<p className="text-xs text-zinc-400">© {new Date().getFullYear()} TomodachiShare. All rights reserved.</p> <p className="text-xs text-zinc-400">© {new Date().getFullYear()} TomodachiShare. All rights reserved.</p>

View file

@ -1,4 +1,5 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { type FileWithPath } from "react-dropzone";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import dayjs from "dayjs"; import dayjs from "dayjs";
@ -10,7 +11,7 @@ import { useNavigate } from "react-router";
export default function ProfilePictureSettings() { export default function ProfilePictureSettings() {
const navigate = useNavigate(); const navigate = useNavigate();
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const [newPicture, setNewPicture] = useState<File | undefined>(); const [newPicture, setNewPicture] = useState<FileWithPath | undefined>();
const changeDate = dayjs().add(7, "days"); const changeDate = dayjs().add(7, "days");
@ -34,7 +35,7 @@ export default function ProfilePictureSettings() {
navigate(0); navigate(0);
}; };
const handleDrop = useCallback((acceptedFiles: File[]) => { const handleDrop = useCallback((acceptedFiles: FileWithPath[]) => {
if (!acceptedFiles[0]) return; if (!acceptedFiles[0]) return;
setNewPicture(acceptedFiles[0]); setNewPicture(acceptedFiles[0]);
}, []); }, []);

View file

@ -1,72 +1,73 @@
import { DragDropContext, Draggable, Droppable, type DropResult } from "@hello-pangea/dnd"; import { type FileWithPath } from "react-dropzone";
import { Icon } from "@iconify/react"; import { DragDropContext, Draggable, Droppable, type DropResult } from "@hello-pangea/dnd";
import { Icon } from "@iconify/react";
interface Props {
files: readonly File[]; interface Props {
setFiles: React.Dispatch<React.SetStateAction<File[]>>; files: readonly FileWithPath[];
} setFiles: React.Dispatch<React.SetStateAction<FileWithPath[]>>;
}
export default function ImageList({ files, setFiles }: Props) {
const handleDelete = (index: number) => { export default function ImageList({ files, setFiles }: Props) {
const newFiles = [...files]; const handleDelete = (index: number) => {
newFiles.splice(index, 1); const newFiles = [...files];
setFiles(newFiles); newFiles.splice(index, 1);
}; setFiles(newFiles);
};
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return; const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const items = Array.from(files);
const [reorderedItem] = items.splice(result.source.index, 1); const items = Array.from(files);
items.splice(result.destination.index, 0, reorderedItem); const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
setFiles(items);
}; setFiles(items);
};
return (
<DragDropContext onDragEnd={handleDragEnd}> return (
<Droppable droppableId="imageDroppable"> <DragDropContext onDragEnd={handleDragEnd}>
{(provided) => ( <Droppable droppableId="imageDroppable">
<div ref={provided.innerRef} {...provided.droppableProps} className="flex flex-col px-12 max-lg:px-0 max-md:px-12 max-[32rem]:px-0"> {(provided) => (
{files.map((file, index) => ( <div ref={provided.innerRef} {...provided.droppableProps} className="flex flex-col px-12 max-lg:px-0 max-md:px-12 max-[32rem]:px-0">
<Draggable key={file.name} draggableId={file.name} index={index}> {files.map((file, index) => (
{(provided) => ( <Draggable key={file.name} draggableId={file.name} index={index}>
<div {(provided) => (
ref={provided.innerRef} <div
{...provided.draggableProps} ref={provided.innerRef}
className="w-full p-4 rounded-xl bg-orange-100 border-2 border-amber-500 flex gap-2 shadow-md my-1" {...provided.draggableProps}
> className="w-full p-4 rounded-xl bg-orange-100 border-2 border-amber-500 flex gap-2 shadow-md my-1"
<img >
src={URL.createObjectURL(file)} <img
alt={file.name} src={URL.createObjectURL(file)}
width={96} alt={file.name}
height={96} width={96}
className="aspect-3/2 object-contain w-24 rounded-md bg-orange-300 border-2 border-orange-400" height={96}
/> className="aspect-3/2 object-contain w-24 rounded-md bg-orange-300 border-2 border-orange-400"
<div className="flex flex-col justify-center w-full min-w-0"> />
<span className="font-semibold overflow-hidden text-ellipsis">{file.name}</span> <div className="flex flex-col justify-center w-full min-w-0">
<button <span className="font-semibold overflow-hidden text-ellipsis">{file.name}</span>
onClick={() => handleDelete(index)} <button
className="pill button text-xs w-min px-3! py-1! bg-red-300! border-red-400! hover:bg-red-400!" onClick={() => handleDelete(index)}
> className="pill button text-xs w-min px-3! py-1! bg-red-300! border-red-400! hover:bg-red-400!"
Delete >
</button> Delete
</div> </button>
</div>
<div
{...provided.dragHandleProps} <div
className="h-full w-11 px-1 cursor-grab flex items-center justify-center rounded transition-colors hover:bg-black/10" {...provided.dragHandleProps}
> className="h-full w-11 px-1 cursor-grab flex items-center justify-center rounded transition-colors hover:bg-black/10"
<Icon icon="tabler:grip-horizontal" className="size-6 text-black/50" /> >
</div> <Icon icon="tabler:grip-horizontal" className="size-6 text-black/50" />
</div> </div>
)} </div>
</Draggable> )}
))} </Draggable>
{provided.placeholder} ))}
</div> {provided.placeholder}
)} </div>
</Droppable> )}
</DragDropContext> </Droppable>
); </DragDropContext>
} );
}

View file

@ -1,66 +1,67 @@
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import jsQR from "jsqr"; import { type FileWithPath } from "react-dropzone";
import Dropzone from "../dropzone"; import jsQR from "jsqr";
import Dropzone from "../dropzone";
interface Props {
setQrBytesRaw: React.Dispatch<React.SetStateAction<number[]>>; interface Props {
} setQrBytesRaw: React.Dispatch<React.SetStateAction<number[]>>;
}
export default function QrUpload({ setQrBytesRaw }: Props) {
const [hasImage, setHasImage] = useState(false); export default function QrUpload({ setQrBytesRaw }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(null); const [hasImage, setHasImage] = useState(false);
const canvasRef = useRef<HTMLCanvasElement>(null);
const handleDrop = useCallback(
(acceptedFiles: File[]) => { const handleDrop = useCallback(
const file = acceptedFiles[0]; (acceptedFiles: FileWithPath[]) => {
const file = acceptedFiles[0];
// Scan QR code
const reader = new FileReader(); // Scan QR code
reader.onload = async (event) => { const reader = new FileReader();
const image = new Image(); reader.onload = async (event) => {
image.onload = () => { const image = new Image();
const canvas = canvasRef.current; image.onload = () => {
if (!canvas) return; const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return; const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = image.width;
canvas.height = image.height; canvas.width = image.width;
ctx.drawImage(image, 0, 0, image.width, image.height); canvas.height = image.height;
ctx.drawImage(image, 0, 0, image.width, image.height);
const imageData = ctx.getImageData(0, 0, image.width, image.height);
const code = jsQR(imageData.data, image.width, image.height); const imageData = ctx.getImageData(0, 0, image.width, image.height);
if (!code) return; const code = jsQR(imageData.data, image.width, image.height);
if (!code) return;
setQrBytesRaw(code.binaryData!);
setHasImage(true); setQrBytesRaw(code.binaryData!);
}; setHasImage(true);
image.src = event.target!.result as string; };
}; image.src = event.target!.result as string;
reader.readAsDataURL(file); };
}, reader.readAsDataURL(file);
[setQrBytesRaw], },
); [setQrBytesRaw],
);
return (
<div className="max-w-md w-full"> return (
<Dropzone onDrop={handleDrop} options={{ maxFiles: 1 }}> <div className="max-w-md w-full">
<p className="text-center text-sm"> <Dropzone onDrop={handleDrop} options={{ maxFiles: 1 }}>
{!hasImage ? ( <p className="text-center text-sm">
<> {!hasImage ? (
Drag and drop your QR code image here <>
<br /> Drag and drop your QR code image here
or click to open <br />
</> or click to open
) : ( </>
"Uploaded!" ) : (
)} "Uploaded!"
</p> )}
</Dropzone> </p>
</Dropzone>
{/* Canvas is used to scan the QR code */}
<canvas ref={canvasRef} className="hidden" /> {/* Canvas is used to scan the QR code */}
</div> <canvas ref={canvasRef} className="hidden" />
); </div>
} );
}

View file

@ -1,4 +1,5 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { type FileWithPath } from "react-dropzone";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import Dropzone from "../dropzone"; import Dropzone from "../dropzone";
import Camera from "./camera"; import Camera from "./camera";
@ -19,7 +20,7 @@ export default function SwitchFileUpload({ text, type = "image", forceCrop, file
const [isCropOpen, setIsCropOpen] = useState(false); const [isCropOpen, setIsCropOpen] = useState(false);
const handleDrop = useCallback( const handleDrop = useCallback(
(acceptedFiles: File[]) => { (acceptedFiles: FileWithPath[]) => {
const file = acceptedFiles[0]; const file = acceptedFiles[0];
if (type === "file") { if (type === "file") {
setFile!(file); setFile!(file);

View file

@ -66,7 +66,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
> >
<div className="tooltip bg-[#FF5E5B]! border-[#FF5E5B]! before:border-b-[#FF5E5B]!">Support me (trafficlunar) on Ko-fi</div> <div className="tooltip bg-[#FF5E5B]! border-[#FF5E5B]! before:border-b-[#FF5E5B]!">Support me (trafficlunar) on Ko-fi</div>
<Icon icon="simple-icons:kofi" fontSize={20} /> <Icon icon="simple-icons:kofi" fontSize={20} />
Support TomodachiShare's costs! Support me!
</a> </a>
<main className="px-4 py-8 pt-4 max-w-7xl w-full grow flex flex-col">{children}</main> <main className="px-4 py-8 pt-4 max-w-7xl w-full grow flex flex-col">{children}</main>
<Footer /> <Footer />

View file

@ -2,6 +2,7 @@ import { useStore } from "@nanostores/react";
import { Navigate, useNavigate, useParams } from "react-router"; import { Navigate, useNavigate, useParams } from "react-router";
import { session } from "../session"; import { session } from "../session";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { type FileWithPath } from "react-dropzone";
import { nameSchema, tagsSchema } from "@tomodachi-share/shared/schemas"; import { nameSchema, tagsSchema } from "@tomodachi-share/shared/schemas";
import { type MiiGender, type MiiMakeup, type SwitchMiiInstructions, deepMerge, defaultInstructions, minifyInstructions } from "@tomodachi-share/shared"; import { type MiiGender, type MiiMakeup, type SwitchMiiInstructions, deepMerge, defaultInstructions, minifyInstructions } from "@tomodachi-share/shared";
@ -23,15 +24,15 @@ export default function EditMiiPage() {
const [mii, setMii] = useState<any>(null); const [mii, setMii] = useState<any>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<FileWithPath[]>([]);
const handleFilesChange: React.Dispatch<React.SetStateAction<File[]>> = (updater) => { const handleFilesChange: React.Dispatch<React.SetStateAction<FileWithPath[]>> = (updater) => {
hasCustomImagesChanged.current = true; hasCustomImagesChanged.current = true;
setFiles(updater); setFiles(updater);
}; };
const handleDrop = useCallback( const handleDrop = useCallback(
(acceptedFiles: File[]) => { (acceptedFiles: FileWithPath[]) => {
if (files.length >= 3) return; if (files.length >= 3) return;
hasCustomImagesChanged.current = true; hasCustomImagesChanged.current = true;

View file

@ -1,6 +1,7 @@
import { useStore } from "@nanostores/react"; import { useStore } from "@nanostores/react";
import { Navigate, useNavigate } from "react-router"; import { Navigate, useNavigate } from "react-router";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import type { FileWithPath } from "react-dropzone";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { import {
@ -34,10 +35,10 @@ import qrcode from "qrcode-generator";
export default function SubmitPage() { export default function SubmitPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const $session = useStore(session); const $session = useStore(session);
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<FileWithPath[]>([]);
const handleDrop = useCallback( const handleDrop = useCallback(
(acceptedFiles: File[]) => { (acceptedFiles: FileWithPath[]) => {
if (files.length >= 3) return; if (files.length >= 3) return;
setFiles((prev) => [...prev, ...acceptedFiles]); setFiles((prev) => [...prev, ...acceptedFiles]);
}, },
@ -337,10 +338,6 @@ export default function SubmitPage() {
{/* Makeup (switch only) — unchanged from base */} {/* Makeup (switch only) — unchanged from base */}
<div className={`w-full grid grid-cols-3 items-start ${platform === "SWITCH" ? "" : "hidden"}`}> <div className={`w-full grid grid-cols-3 items-start ${platform === "SWITCH" ? "" : "hidden"}`}>
<label className="font-semibold py-2">Face Paint</label> <label className="font-semibold py-2">Face Paint</label>
{/* Force Tailwind to load bg-gray-100 */}
<div className="bg-gray-100 hidden"></div>
<div className="col-span-2 flex flex-col gap-1.5"> <div className="col-span-2 flex flex-col gap-1.5">
{[ {[
{ value: "FULL", label: "Full", desc: "Most of the face/features are covered", color: "pink" }, { value: "FULL", label: "Full", desc: "Most of the face/features are covered", color: "pink" },

View file

@ -9,5 +9,5 @@
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"packageManager": "pnpm@11.18.0" "packageManager": "pnpm@11.8.0"
} }

2017
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -11,9 +11,3 @@ allowBuilds:
unrs-resolver: false unrs-resolver: false
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- '@types/node@26.0.0' - '@types/node@26.0.0'
- '@redis/bloom@6.2.0'
- '@redis/client@6.2.0'
- '@redis/json@6.2.0'
- '@redis/search@6.2.0'
- '@redis/time-series@6.2.0'
- redis@6.2.0

View file

@ -8,7 +8,7 @@
"fzstd": "^0.1.1", "fzstd": "^0.1.1",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"sjcl-with-all": "1.0.8", "sjcl-with-all": "1.0.8",
"zod": "^4.4.3" "zod": "^4.3.6"
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
@ -17,7 +17,7 @@
}, },
"types": "./src/index.ts", "types": "./src/index.ts",
"devDependencies": { "devDependencies": {
"@types/node": "^26.1.2", "@types/node": "^25.6.0",
"@types/sjcl": "^1.0.34" "@types/sjcl": "^1.0.34"
} }
} }