mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-08-12 12:29:44 +00:00
chore: update packages
Also: - Remove Discord link and replace with email contact - Fix none face paint type in submit form being transparent - Update dockerfile
This commit is contained in:
parent
c2c1136c58
commit
211774e309
17 changed files with 1273 additions and 1148 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
FROM node:23-alpine AS base
|
FROM node:26-alpine AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
RUN corepack enable && corepack prepare pnpm@11.1.1 --activate
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
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 but I don't have time to fix it
|
# I know all the paths are messed up
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
experimental: {
|
||||||
|
useTypeScriptCli: true,
|
||||||
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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.8.0",
|
"packageManager": "pnpm@11.18.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.2",
|
"@auth/prisma-adapter": "2.11.3",
|
||||||
"@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.3.6",
|
"downshift": "^9.4.0",
|
||||||
"file-type": "^22.0.1",
|
"file-type": "^22.0.1",
|
||||||
"next": "16.2.9",
|
"next": "16.2.12",
|
||||||
"next-auth": "5.0.0-beta.30",
|
"next-auth": "5.0.0-beta.30",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.8",
|
||||||
"redis": "^6.0.0",
|
"redis": "^6.2.0",
|
||||||
"satori": "^0.26.0",
|
"satori": "^0.29.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.5",
|
"@eslint/eslintrc": "^3.3.6",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@tailwindcss/postcss": "^4.3.1",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/node": "^26.0.0",
|
"@types/node": "^26.1.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.4",
|
||||||
"@types/sjcl": "^1.0.34",
|
"@types/sjcl": "^1.0.34",
|
||||||
"eslint": "^10.5.0",
|
"eslint": "^10.8.0",
|
||||||
"eslint-config-next": "16.2.9",
|
"eslint-config-next": "16.2.12",
|
||||||
"prisma": "^6.19.3",
|
"prisma": "^6.19.3",
|
||||||
"schema-dts": "^2.0.0",
|
"schema-dts": "^2.0.0",
|
||||||
"tailwindcss": "^4.3.1",
|
"tailwindcss": "^4.3.3",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^7.0.2"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/types.d.ts"
|
".": "./src/types.d.ts"
|
||||||
|
|
|
||||||
|
|
@ -11,43 +11,43 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bprogress/react": "^1.2.7",
|
"@bprogress/react": "^1.2.7",
|
||||||
"@fontsource-variable/lexend": "^5.2.11",
|
"@fontsource-variable/lexend": "^5.3.0",
|
||||||
"@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.0",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
"@tomodachi-share/shared": "workspace:*",
|
"@tomodachi-share/shared": "workspace:*",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.4",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.21",
|
||||||
"downshift": "^9.3.2",
|
"downshift": "^9.4.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"nanostores": "^1.3.0",
|
"nanostores": "^1.4.2",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.8",
|
||||||
"react-dropzone": "^15.0.0",
|
"react-dropzone": "^19.1.1",
|
||||||
"react-image-crop": "^11.0.10",
|
"react-image-crop": "^11.1.2",
|
||||||
"react-router": "^7.15.0",
|
"react-router": "^8.3.0",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.3",
|
||||||
"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": "^9.39.4",
|
"@eslint/js": "^10.0.1",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^24.12.4",
|
"@types/node": "^26.1.2",
|
||||||
"@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.1",
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^10.8.0",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.3",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.8.0",
|
||||||
"typescript": "~6.0.3",
|
"typescript": "~7.0.2",
|
||||||
"typescript-eslint": "^8.59.3",
|
"typescript-eslint": "^8.65.0",
|
||||||
"vite": "^8.0.12"
|
"vite": "^8.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { type ReactNode, useState } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
import { type DropzoneOptions, type FileWithPath, useDropzone } from "react-dropzone";
|
import { type DropzoneOptions, 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: FileWithPath[]) => void;
|
onDrop: (acceptedFiles: File[]) => 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: FileWithPath[]) => {
|
const handleDrop = (acceptedFiles: File[]) => {
|
||||||
setIsDraggingOver(false);
|
setIsDraggingOver(false);
|
||||||
onDrop(acceptedFiles);
|
onDrop(acceptedFiles);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { Icon } from "@iconify/react";
|
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
|
|
@ -15,32 +14,15 @@ 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"
|
||||||
|
|
@ -50,6 +32,19 @@ 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>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
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";
|
||||||
|
|
@ -11,7 +10,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<FileWithPath | undefined>();
|
const [newPicture, setNewPicture] = useState<File | undefined>();
|
||||||
|
|
||||||
const changeDate = dayjs().add(7, "days");
|
const changeDate = dayjs().add(7, "days");
|
||||||
|
|
||||||
|
|
@ -35,7 +34,7 @@ export default function ProfilePictureSettings() {
|
||||||
navigate(0);
|
navigate(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = useCallback((acceptedFiles: FileWithPath[]) => {
|
const handleDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
if (!acceptedFiles[0]) return;
|
if (!acceptedFiles[0]) return;
|
||||||
setNewPicture(acceptedFiles[0]);
|
setNewPicture(acceptedFiles[0]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,72 @@
|
||||||
import { type FileWithPath } from "react-dropzone";
|
import { DragDropContext, Draggable, Droppable, type DropResult } from "@hello-pangea/dnd";
|
||||||
import { DragDropContext, Draggable, Droppable, type DropResult } from "@hello-pangea/dnd";
|
import { Icon } from "@iconify/react";
|
||||||
import { Icon } from "@iconify/react";
|
|
||||||
|
interface Props {
|
||||||
interface Props {
|
files: readonly File[];
|
||||||
files: readonly FileWithPath[];
|
setFiles: React.Dispatch<React.SetStateAction<File[]>>;
|
||||||
setFiles: React.Dispatch<React.SetStateAction<FileWithPath[]>>;
|
}
|
||||||
}
|
|
||||||
|
export default function ImageList({ files, setFiles }: Props) {
|
||||||
export default function ImageList({ files, setFiles }: Props) {
|
const handleDelete = (index: number) => {
|
||||||
const handleDelete = (index: number) => {
|
const newFiles = [...files];
|
||||||
const newFiles = [...files];
|
newFiles.splice(index, 1);
|
||||||
newFiles.splice(index, 1);
|
setFiles(newFiles);
|
||||||
setFiles(newFiles);
|
};
|
||||||
};
|
|
||||||
|
const handleDragEnd = (result: DropResult) => {
|
||||||
const handleDragEnd = (result: DropResult) => {
|
if (!result.destination) return;
|
||||||
if (!result.destination) return;
|
|
||||||
|
const items = Array.from(files);
|
||||||
const items = Array.from(files);
|
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
items.splice(result.destination.index, 0, reorderedItem);
|
||||||
items.splice(result.destination.index, 0, reorderedItem);
|
|
||||||
|
setFiles(items);
|
||||||
setFiles(items);
|
};
|
||||||
};
|
|
||||||
|
return (
|
||||||
return (
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
<DragDropContext onDragEnd={handleDragEnd}>
|
<Droppable droppableId="imageDroppable">
|
||||||
<Droppable droppableId="imageDroppable">
|
{(provided) => (
|
||||||
{(provided) => (
|
<div ref={provided.innerRef} {...provided.droppableProps} className="flex flex-col px-12 max-lg:px-0 max-md:px-12 max-[32rem]:px-0">
|
||||||
<div ref={provided.innerRef} {...provided.droppableProps} className="flex flex-col px-12 max-lg:px-0 max-md:px-12 max-[32rem]:px-0">
|
{files.map((file, index) => (
|
||||||
{files.map((file, index) => (
|
<Draggable key={file.name} draggableId={file.name} index={index}>
|
||||||
<Draggable key={file.name} draggableId={file.name} index={index}>
|
{(provided) => (
|
||||||
{(provided) => (
|
<div
|
||||||
<div
|
ref={provided.innerRef}
|
||||||
ref={provided.innerRef}
|
{...provided.draggableProps}
|
||||||
{...provided.draggableProps}
|
className="w-full p-4 rounded-xl bg-orange-100 border-2 border-amber-500 flex gap-2 shadow-md my-1"
|
||||||
className="w-full p-4 rounded-xl bg-orange-100 border-2 border-amber-500 flex gap-2 shadow-md my-1"
|
>
|
||||||
>
|
<img
|
||||||
<img
|
src={URL.createObjectURL(file)}
|
||||||
src={URL.createObjectURL(file)}
|
alt={file.name}
|
||||||
alt={file.name}
|
width={96}
|
||||||
width={96}
|
height={96}
|
||||||
height={96}
|
className="aspect-3/2 object-contain w-24 rounded-md bg-orange-300 border-2 border-orange-400"
|
||||||
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">
|
||||||
<div className="flex flex-col justify-center w-full min-w-0">
|
<span className="font-semibold overflow-hidden text-ellipsis">{file.name}</span>
|
||||||
<span className="font-semibold overflow-hidden text-ellipsis">{file.name}</span>
|
<button
|
||||||
<button
|
onClick={() => handleDelete(index)}
|
||||||
onClick={() => handleDelete(index)}
|
className="pill button text-xs w-min px-3! py-1! bg-red-300! border-red-400! hover:bg-red-400!"
|
||||||
className="pill button text-xs w-min px-3! py-1! bg-red-300! border-red-400! hover:bg-red-400!"
|
>
|
||||||
>
|
Delete
|
||||||
Delete
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div
|
||||||
<div
|
{...provided.dragHandleProps}
|
||||||
{...provided.dragHandleProps}
|
className="h-full w-11 px-1 cursor-grab flex items-center justify-center rounded transition-colors hover:bg-black/10"
|
||||||
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" />
|
||||||
<Icon icon="tabler:grip-horizontal" className="size-6 text-black/50" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Draggable>
|
||||||
</Draggable>
|
))}
|
||||||
))}
|
{provided.placeholder}
|
||||||
{provided.placeholder}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Droppable>
|
||||||
</Droppable>
|
</DragDropContext>
|
||||||
</DragDropContext>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,66 @@
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { type FileWithPath } from "react-dropzone";
|
import jsQR from "jsqr";
|
||||||
import jsQR from "jsqr";
|
import Dropzone from "../dropzone";
|
||||||
import Dropzone from "../dropzone";
|
|
||||||
|
interface Props {
|
||||||
interface Props {
|
setQrBytesRaw: React.Dispatch<React.SetStateAction<number[]>>;
|
||||||
setQrBytesRaw: React.Dispatch<React.SetStateAction<number[]>>;
|
}
|
||||||
}
|
|
||||||
|
export default function QrUpload({ setQrBytesRaw }: Props) {
|
||||||
export default function QrUpload({ setQrBytesRaw }: Props) {
|
const [hasImage, setHasImage] = useState(false);
|
||||||
const [hasImage, setHasImage] = useState(false);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
const handleDrop = useCallback(
|
||||||
const handleDrop = useCallback(
|
(acceptedFiles: File[]) => {
|
||||||
(acceptedFiles: FileWithPath[]) => {
|
const file = acceptedFiles[0];
|
||||||
const file = acceptedFiles[0];
|
|
||||||
|
// Scan QR code
|
||||||
// Scan QR code
|
const reader = new FileReader();
|
||||||
const reader = new FileReader();
|
reader.onload = async (event) => {
|
||||||
reader.onload = async (event) => {
|
const image = new Image();
|
||||||
const image = new Image();
|
image.onload = () => {
|
||||||
image.onload = () => {
|
const canvas = canvasRef.current;
|
||||||
const canvas = canvasRef.current;
|
if (!canvas) return;
|
||||||
if (!canvas) return;
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
const ctx = canvas.getContext("2d");
|
if (!ctx) return;
|
||||||
if (!ctx) return;
|
|
||||||
|
canvas.width = image.width;
|
||||||
canvas.width = image.width;
|
canvas.height = image.height;
|
||||||
canvas.height = image.height;
|
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
|
||||||
|
const imageData = ctx.getImageData(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 code = jsQR(imageData.data, image.width, image.height);
|
if (!code) return;
|
||||||
if (!code) return;
|
|
||||||
|
setQrBytesRaw(code.binaryData!);
|
||||||
setQrBytesRaw(code.binaryData!);
|
setHasImage(true);
|
||||||
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 (
|
||||||
return (
|
<div className="max-w-md w-full">
|
||||||
<div className="max-w-md w-full">
|
<Dropzone onDrop={handleDrop} options={{ maxFiles: 1 }}>
|
||||||
<Dropzone onDrop={handleDrop} options={{ maxFiles: 1 }}>
|
<p className="text-center text-sm">
|
||||||
<p className="text-center text-sm">
|
{!hasImage ? (
|
||||||
{!hasImage ? (
|
<>
|
||||||
<>
|
Drag and drop your QR code image here
|
||||||
Drag and drop your QR code image here
|
<br />
|
||||||
<br />
|
or click to open
|
||||||
or click to open
|
</>
|
||||||
</>
|
) : (
|
||||||
) : (
|
"Uploaded!"
|
||||||
"Uploaded!"
|
)}
|
||||||
)}
|
</p>
|
||||||
</p>
|
</Dropzone>
|
||||||
</Dropzone>
|
|
||||||
|
{/* Canvas is used to scan the QR code */}
|
||||||
{/* Canvas is used to scan the QR code */}
|
<canvas ref={canvasRef} className="hidden" />
|
||||||
<canvas ref={canvasRef} className="hidden" />
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
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";
|
||||||
|
|
@ -20,7 +19,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: FileWithPath[]) => {
|
(acceptedFiles: File[]) => {
|
||||||
const file = acceptedFiles[0];
|
const file = acceptedFiles[0];
|
||||||
if (type === "file") {
|
if (type === "file") {
|
||||||
setFile!(file);
|
setFile!(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 me!
|
Support TomodachiShare's costs!
|
||||||
</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 />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ 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";
|
||||||
|
|
@ -24,15 +23,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<FileWithPath[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
|
||||||
const handleFilesChange: React.Dispatch<React.SetStateAction<FileWithPath[]>> = (updater) => {
|
const handleFilesChange: React.Dispatch<React.SetStateAction<File[]>> = (updater) => {
|
||||||
hasCustomImagesChanged.current = true;
|
hasCustomImagesChanged.current = true;
|
||||||
setFiles(updater);
|
setFiles(updater);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(acceptedFiles: FileWithPath[]) => {
|
(acceptedFiles: File[]) => {
|
||||||
if (files.length >= 3) return;
|
if (files.length >= 3) return;
|
||||||
hasCustomImagesChanged.current = true;
|
hasCustomImagesChanged.current = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
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 {
|
||||||
|
|
@ -35,10 +34,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<FileWithPath[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(acceptedFiles: FileWithPath[]) => {
|
(acceptedFiles: File[]) => {
|
||||||
if (files.length >= 3) return;
|
if (files.length >= 3) return;
|
||||||
setFiles((prev) => [...prev, ...acceptedFiles]);
|
setFiles((prev) => [...prev, ...acceptedFiles]);
|
||||||
},
|
},
|
||||||
|
|
@ -338,6 +337,10 @@ 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" },
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,5 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@11.8.0"
|
"packageManager": "pnpm@11.18.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1981
pnpm-lock.yaml
generated
1981
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -11,3 +11,9 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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.3.6"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
},
|
},
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^26.1.2",
|
||||||
"@types/sjcl": "^1.0.34"
|
"@types/sjcl": "^1.0.34"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue