Compare commits
3 commits
7f52773bd9
...
d208565a61
| Author | SHA1 | Date | |
|---|---|---|---|
| d208565a61 | |||
| 59bc1d3ace | |||
| 84144c383c |
6
.gitignore
vendored
|
|
@ -1,7 +1,7 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
node_modules/
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
|
|
@ -14,8 +14,8 @@
|
|||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
.next/
|
||||
backend/out/
|
||||
certificates/
|
||||
|
||||
# production
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ REDIS_URL="redis://localhost:6379/0"
|
|||
|
||||
# Used for metadata, sitemaps, etc.
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
FRONTEND_URL=http://localhost:4321
|
||||
|
||||
CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX
|
||||
CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXXXX
|
||||
22
backend/next.config.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
headers: [
|
||||
{ key: "Access-Control-Allow-Origin", value: process.env.FRONTEND_URL || "http://localhost:4321" },
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST,PATCH,DELETE,OPTIONS" },
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
63
backend/package.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "@tomodachi-share/backend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postinstall": "prisma generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@2toad/profanity": "^3.3.0",
|
||||
"@auth/prisma-adapter": "2.11.1",
|
||||
"@bprogress/next": "^3.2.12",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"bit-buffer": "^0.3.0",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"dayjs": "^1.11.20",
|
||||
"downshift": "^9.3.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-type": "^22.0.1",
|
||||
"jsqr": "^1.4.0",
|
||||
"next": "16.2.3",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"qrcode-generator": "^2.0.4",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-dropzone": "^15.0.0",
|
||||
"react-image-crop": "^11.0.10",
|
||||
"redis": "^5.11.0",
|
||||
"satori": "^0.26.0",
|
||||
"seedrandom": "^3.0.5",
|
||||
"sharp": "^0.34.5",
|
||||
"sjcl-with-all": "1.0.8",
|
||||
"swr": "^2.4.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@types/sjcl": "^1.0.34",
|
||||
"eslint": "^10.2.0",
|
||||
"eslint-config-next": "16.2.3",
|
||||
"prisma": "^6.19.2",
|
||||
"schema-dts": "^2.0.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.2",
|
||||
"@tomodachi-share/shared": "workspace:*"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/types.d.ts"
|
||||
},
|
||||
"types": "./src/types.d.ts"
|
||||
}
|
||||
5082
backend/pnpm-lock.yaml
Normal file
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { idSchema } from "@/lib/schemas";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
const session = await auth();
|
||||
|
|
@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { idSchema } from "@/lib/schemas";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await auth();
|
||||
|
|
@ -5,7 +5,7 @@ import dayjs from "dayjs";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { idSchema } from "@/lib/schemas";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { PunishmentType } from "@prisma/client";
|
||||
|
||||
const punishSchema = z.object({
|
||||
|
|
@ -3,7 +3,7 @@ import { profanity } from "@2toad/profanity";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { userNameSchema } from "@/lib/schemas";
|
||||
import { userNameSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
6
backend/src/app/api/auth/signin/[provider]/route.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { type NextRequest } from "next/server";
|
||||
import { signIn } from "@/lib/auth";
|
||||
|
||||
export async function GET(req: NextRequest, { params }: { params: Promise<{ provider: string }> }) {
|
||||
return signIn((await params).provider);
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import path from "path";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { idSchema } from "@/lib/schemas";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
|
||||
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
|
||||
|
|
@ -10,11 +10,10 @@ import { profanity } from "@2toad/profanity";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { idSchema, nameSchema, switchMiiInstructionsSchema, tagsSchema } from "@/lib/schemas";
|
||||
import { idSchema, nameSchema, switchMiiInstructionsSchema, tagsSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { generateMetadataImage, validateImage } from "@/lib/images";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
import { SwitchMiiInstructions } from "@/types";
|
||||
import { minifyInstructions } from "@/lib/switch";
|
||||
import { minifyInstructions, SwitchMiiInstructions } from "@tomodachi-share/shared";
|
||||
import { settings } from "@/lib/settings";
|
||||
|
||||
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
|
||||
38
backend/src/app/api/mii/[id]/info/route.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await auth();
|
||||
const { id: slugId } = await params;
|
||||
const parsed = idSchema.safeParse(slugId);
|
||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
|
||||
const miiId = parsed.data;
|
||||
|
||||
const mii = await prisma.mii.findUnique({
|
||||
where: {
|
||||
id: miiId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
likedBy: session?.user
|
||||
? {
|
||||
where: {
|
||||
userId: Number(session.user.id),
|
||||
},
|
||||
select: { userId: true },
|
||||
}
|
||||
: false,
|
||||
_count: {
|
||||
select: { likedBy: true }, // Get total like count
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(mii);
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { idSchema } from "@/lib/schemas";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
168
backend/src/app/api/mii/list/route.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { searchSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import crypto from "crypto";
|
||||
import seedrandom from "seedrandom";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await auth();
|
||||
const parsed = searchSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams));
|
||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
|
||||
|
||||
const { q: query, sort, tags, exclude, platform, gender, makeup, allowCopying, quarantined, page = 1, limit = 24, seed, parentPage, userId } = parsed.data;
|
||||
|
||||
// My Likes page
|
||||
let miiIdsLiked: number[] | undefined = undefined;
|
||||
|
||||
if (parentPage === "likes" && session?.user?.id) {
|
||||
const likedMiis = await prisma.like.findMany({
|
||||
where: { userId: Number(session.user.id) },
|
||||
select: { miiId: true },
|
||||
});
|
||||
miiIdsLiked = likedMiis.map((like) => like.miiId);
|
||||
}
|
||||
|
||||
const where: Prisma.MiiWhereInput = {
|
||||
// In queue logic
|
||||
...(parentPage === "admin"
|
||||
? { in_queue: true } // Only show queued Miis
|
||||
: userId
|
||||
? {
|
||||
// Include queued Miis if user is on their profile
|
||||
...(Number(session?.user?.id) === userId ? {} : { in_queue: false }),
|
||||
userId,
|
||||
}
|
||||
: {
|
||||
// Don't show queued Miis on main page
|
||||
in_queue: false,
|
||||
}),
|
||||
// Only show liked miis on likes page
|
||||
...(parentPage === "likes" && miiIdsLiked && { id: { in: miiIdsLiked } }),
|
||||
// Searching
|
||||
...(query && {
|
||||
OR: [{ name: { contains: query, mode: "insensitive" } }, { tags: { has: query } }, { description: { contains: query, mode: "insensitive" } }],
|
||||
}),
|
||||
// Tag filtering
|
||||
...(tags && tags.length > 0 && { tags: { hasEvery: tags } }),
|
||||
...(exclude && exclude.length > 0 && { NOT: { tags: { hasSome: exclude } } }),
|
||||
// Platform
|
||||
...(platform && { platform: { equals: platform } }),
|
||||
// Gender
|
||||
...(gender && { gender: { equals: gender } }),
|
||||
// Allow Copying
|
||||
...(allowCopying && { allowedCopying: true }),
|
||||
// Makeup
|
||||
...(makeup && { makeup: { equals: makeup } }),
|
||||
// Quarantined
|
||||
...(!quarantined && !userId && { quarantined: false }),
|
||||
};
|
||||
|
||||
const select: Prisma.MiiSelect = {
|
||||
id: true,
|
||||
// Don't show when userId is specified
|
||||
...(!userId && {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
platform: true,
|
||||
name: true,
|
||||
imageCount: true,
|
||||
tags: true,
|
||||
createdAt: true,
|
||||
gender: true,
|
||||
makeup: true,
|
||||
allowedCopying: true,
|
||||
quarantined: true,
|
||||
in_queue: true,
|
||||
// Mii liked check
|
||||
...(session?.user?.id && {
|
||||
likedBy: {
|
||||
where: { userId: Number(session.user.id) },
|
||||
select: { userId: true },
|
||||
},
|
||||
}),
|
||||
// Like count
|
||||
_count: {
|
||||
select: { likedBy: true },
|
||||
},
|
||||
};
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
let totalCount: number;
|
||||
let filteredCount: number;
|
||||
let miis: Prisma.MiiGetPayload<{ select: typeof select }>[];
|
||||
|
||||
if (sort === "random") {
|
||||
// Get all IDs that match the where conditions
|
||||
const matchingIds = await prisma.mii.findMany({
|
||||
where,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
totalCount = matchingIds.length;
|
||||
filteredCount = Math.max(0, Math.min(limit, totalCount - skip));
|
||||
|
||||
if (matchingIds.length === 0) return;
|
||||
|
||||
// Use seed for consistent random results
|
||||
const randomSeed = seed || crypto.randomInt(0, 1_000_000_000);
|
||||
const rng = seedrandom(randomSeed.toString());
|
||||
|
||||
// Randomize all IDs using the Durstenfeld algorithm
|
||||
for (let i = matchingIds.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rng() * (i + 1));
|
||||
[matchingIds[i], matchingIds[j]] = [matchingIds[j], matchingIds[i]];
|
||||
}
|
||||
|
||||
// Convert to number[] array
|
||||
const selectedIds = matchingIds.slice(skip, skip + limit).map((i) => i.id);
|
||||
|
||||
miis = await prisma.mii.findMany({
|
||||
where: {
|
||||
id: { in: selectedIds },
|
||||
},
|
||||
select,
|
||||
});
|
||||
} else {
|
||||
// Sorting by likes, newest, or oldest
|
||||
let orderBy: Prisma.MiiOrderByWithRelationInput[];
|
||||
|
||||
if (sort === "likes") {
|
||||
orderBy = [{ likedBy: { _count: "desc" } }, { name: "asc" }];
|
||||
} else if (sort === "oldest") {
|
||||
orderBy = [{ createdAt: "asc" }, { name: "asc" }];
|
||||
} else {
|
||||
// default to newest
|
||||
orderBy = [{ createdAt: "desc" }, { name: "asc" }];
|
||||
}
|
||||
|
||||
[totalCount, filteredCount, miis] = await Promise.all([
|
||||
prisma.mii.count({ where: { ...where } }), // TODO: User id
|
||||
prisma.mii.count({ where, skip, take: limit }),
|
||||
prisma.mii.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
select,
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
const lastPage = Math.ceil(totalCount / limit);
|
||||
|
||||
return NextResponse.json({
|
||||
miis,
|
||||
totalCount,
|
||||
filteredCount,
|
||||
lastPage,
|
||||
});
|
||||
}
|
||||
25
backend/src/app/api/profile/[id]/info/route.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id: slugId } = await params;
|
||||
const parsed = idSchema.safeParse(slugId);
|
||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 400 });
|
||||
const userId = parsed.data;
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(user);
|
||||
}
|
||||
|
|
@ -11,15 +11,13 @@ import { MiiGender, MiiMakeup, MiiPlatform } from "@prisma/client";
|
|||
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { nameSchema, switchMiiInstructionsSchema, tagsSchema } from "@/lib/schemas";
|
||||
import { nameSchema, switchMiiInstructionsSchema, tagsSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
import { generateMetadataImage, validateImage } from "@/lib/images";
|
||||
import { convertQrCode } from "@/lib/qr-codes";
|
||||
import Mii from "@/lib/mii.js/mii";
|
||||
import { ThreeDsTomodachiLifeMii } from "@/lib/three-ds-tomodachi-life-mii";
|
||||
import Mii from "../../../../../shared/src/mii.js/mii";
|
||||
import { convertQrCode, minifyInstructions, ThreeDsTomodachiLifeMii } from "@tomodachi-share/shared";
|
||||
|
||||
import { SwitchMiiInstructions } from "@/types";
|
||||
import { minifyInstructions } from "@/lib/switch";
|
||||
import { SwitchMiiInstructions } from "@tomodachi-share/shared";
|
||||
import { settings } from "@/lib/settings";
|
||||
|
||||
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
|
||||
|
|
@ -5,7 +5,7 @@ import fs from "fs/promises";
|
|||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { idSchema } from "@/lib/schemas";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
import { generateMetadataImage } from "@/lib/images";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
|
@ -8,7 +8,7 @@ import dayjs from "dayjs";
|
|||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
import ReturnToIsland from "@/components/admin/return-to-island";
|
||||
// import ReturnToIsland from "@/components/admin/return-to-island";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Exiled - TomodachiShare",
|
||||
|
|
@ -109,7 +109,7 @@ export default async function ExiledPage() {
|
|||
{activePunishment.type !== "PERM_EXILE" ? (
|
||||
<>
|
||||
<p className="mb-2">Once your punishment ends, you can return by checking the box below.</p>
|
||||
<ReturnToIsland hasExpired={hasExpired} />
|
||||
{/* <ReturnToIsland hasExpired={hasExpired} /> */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
9
backend/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export default function IndexPage() {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<p>TomodachiShare API</p>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
import { idSchema } from "@/lib/schemas";
|
||||
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
|
|
@ -15,6 +15,6 @@ export default async function RandomPage() {
|
|||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!randomMii) redirect("/");
|
||||
redirect(`/mii/${randomMii.id}`);
|
||||
if (!randomMii) redirect(process.env.NEXT_PUBLIC_FRONTEND_URL || "http://localhost:4321");
|
||||
redirect(`${process.env.NEXT_PUBLIC_FRONTEND_URL}/mii/${randomMii.id}`);
|
||||
}
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Mii, ReportReason } from "@prisma/client";
|
||||
|
||||
import ReasonSelector from "./reason-selector";
|
||||
import SubmitButton from "../submit-button";
|
||||
import LikeButton from "../like-button";
|
||||
import { Mii, ReportReason } from "@prisma/client";
|
||||
|
||||
interface Props {
|
||||
mii: Mii;
|
||||
|
|
@ -32,7 +26,8 @@ export default function ReportMiiForm({ mii, likes }: Props) {
|
|||
return;
|
||||
}
|
||||
|
||||
redirect(`/`);
|
||||
// redirect(`/`);
|
||||
window.location.href = "https://tomodachishare.com";
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -45,10 +40,10 @@ export default function ReportMiiForm({ mii, likes }: Props) {
|
|||
<hr className="border-zinc-300" />
|
||||
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex">
|
||||
<Image src={`/mii/${mii.id}/image?type=mii`} alt="mii image" width={128} height={128} />
|
||||
<img src={`/mii/${mii.id}/image?type=mii`} alt="mii image" width={128} height={128} />
|
||||
<div className="p-4">
|
||||
<p className="text-xl font-bold line-clamp-1">{mii.name}</p>
|
||||
<LikeButton likes={likes} isLiked={true} disabled />
|
||||
{/* <LikeButton likes={likes} isLiked={true} disabled /> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import { Icon } from "@iconify/react";
|
||||
import { ReportReason } from "@prisma/client";
|
||||
import { useSelect } from "downshift";
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ReportReason, User } from "@prisma/client";
|
||||
|
||||
import ProfilePicture from "../profile-picture";
|
||||
import ReasonSelector from "./reason-selector";
|
||||
import SubmitButton from "../submit-button";
|
||||
import { ReportReason } from "@prisma/client";
|
||||
import { User } from "next-auth";
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
|
|
@ -30,7 +26,7 @@ export default function ReportUserForm({ user }: Props) {
|
|||
return;
|
||||
}
|
||||
|
||||
redirect(`/`);
|
||||
window.location.href = "https://tomodachishare.com";
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -43,7 +39,7 @@ export default function ReportUserForm({ user }: Props) {
|
|||
<hr className="border-zinc-300" />
|
||||
|
||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex p-4 gap-4">
|
||||
<ProfilePicture src={user.image ?? "/guest.png"} width={96} height={96} className="aspect-square rounded-full border-2 border-orange-400" />
|
||||
<image src={user.image ?? "/guest.png"} width={96} height={96} className="aspect-square rounded-full border-2 border-orange-400" />
|
||||
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
|
|
@ -9,8 +9,21 @@ import { prisma } from "@/lib/prisma";
|
|||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
adapter: PrismaAdapter(prisma),
|
||||
providers: [Discord, Github({ issuer: "https://github.com/login/oauth" }), Google],
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
trustHost: true,
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: process.env.NODE_ENV === "production" ? "__Secure-next-auth.session-token" : "next-auth.session-token",
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "none",
|
||||
path: "/",
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
strategy: "database",
|
||||
maxAge: 30 * 24 * 60 * 60,
|
||||
},
|
||||
callbacks: {
|
||||
async signIn({ user }) {
|
||||
|
|
@ -28,5 +41,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||
}
|
||||
return session;
|
||||
},
|
||||
|
||||
async redirect({ url, baseUrl }) {
|
||||
return process.env.FRONTEND_URL ?? "http://localhost:4321";
|
||||
},
|
||||
},
|
||||
});
|
||||
18
backend/src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export function deepMerge<T>(target: T, source: Partial<T>): T {
|
||||
const output = structuredClone(target);
|
||||
|
||||
if (typeof source !== "object" || source === null) return output;
|
||||
|
||||
for (const key in source) {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = (output as any)[key];
|
||||
|
||||
if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) {
|
||||
(output as any)[key] = deepMerge(targetValue, sourceValue);
|
||||
} else {
|
||||
(output as any)[key] = sourceValue;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
2
backend/src/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export type { User, Mii, Punishment, Prisma } from "@prisma/client";
|
||||
export { MiiPlatform, MiiGender, MiiMakeup, ReportReason } from "@prisma/client";
|
||||
|
|
@ -23,6 +23,20 @@
|
|||
"sjcl-with-all": ["./node_modules/@types/sjcl"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"../shared/src/constants.ts",
|
||||
"../frontend/src/lib/abbreviation.ts",
|
||||
"../shared/src/qr-codes.ts",
|
||||
"../shared/src/three-ds-tomodachi-life-mii.ts",
|
||||
"../shared/src/types.d.ts",
|
||||
"../shared/src/switch.ts",
|
||||
"../frontend/src/components/provider.tsx",
|
||||
"../shared/src/schemas.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
24
frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
4
frontend/.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
frontend/.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
frontend/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Astro Starter Kit: Minimal
|
||||
|
||||
```sh
|
||||
pnpm create astro@latest -- --template minimal
|
||||
```
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
├── src/
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `pnpm install` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `pnpm astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
30
frontend/astro.config.mjs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// @ts-check
|
||||
import { defineConfig, fontProviders } from "astro/config";
|
||||
|
||||
import react from "@astrojs/react";
|
||||
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
import icon from "astro-icon";
|
||||
|
||||
import swup from "@swup/astro";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: "static",
|
||||
integrations: [react(), icon(), swup({ theme: false })],
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
ssr: {
|
||||
noExternal: ["@tomodachi-share/shared"],
|
||||
},
|
||||
},
|
||||
fonts: [
|
||||
{
|
||||
provider: fontProviders.fontsource(),
|
||||
name: "Lexend",
|
||||
cssVariable: "--font-lexend",
|
||||
},
|
||||
],
|
||||
});
|
||||
50
frontend/package.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^5.0.3",
|
||||
"@bprogress/react": "^1.2.7",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@iconify-json/ic": "^1.2.4",
|
||||
"@iconify-json/material-icon-theme": "^1.2.59",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/stash": "^1.2.4",
|
||||
"@nanostores/react": "^1.1.0",
|
||||
"@swup/astro": "^1.8.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tomodachi-share/shared": "workspace:*",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"astro": "^6.1.7",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"dayjs": "^1.11.20",
|
||||
"downshift": "^9.3.2",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"nanostores": "^1.2.0",
|
||||
"qrcode-generator": "^2.0.4",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-dropzone": "^15.0.0",
|
||||
"react-image-crop": "^11.0.10",
|
||||
"seedrandom": "^3.0.5",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"astro-icon": "^1.1.5"
|
||||
}
|
||||
}
|
||||
4479
frontend/pnpm-lock.yaml
Normal file
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 536 B After Width: | Height: | Size: 536 B |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
1
frontend/public/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="130.734" height="105.615" viewBox="0 0 34.59 27.944"><rect width="32.208" height="25.562" x="1.191" y="1.191" rx="1.874" fill="#f8f8f8" stroke="#ff8904" stroke-width="2.381" paint-order="stroke fill markers"/><rect width="29.369" height="22.49" x="2.611" y="2.727" rx=".966" fill="#c8c8c8" paint-order="stroke fill markers"/><g fill="#fef3c6"><rect width="13.371" height="20.989" x="17.918" y="3.478" rx=".423" paint-order="stroke fill markers"/><rect width="13.371" height="20.989" x="3.301" y="3.478" rx=".423" paint-order="stroke fill markers"/></g><g fill="#ff8904"><use href="#B" paint-order="stroke fill markers"/><circle cx="9.986" cy="13.076" r="5.512" paint-order="stroke fill markers"/><use href="#B" x="14.204" y="-0.093" paint-order="stroke fill markers"/><circle cx="24.191" cy="12.983" r="5.512" paint-order="stroke fill markers"/></g><g fill="none" stroke="#c8c8c8" stroke-linejoin="round"><rect width="13.791" height="20.704" x="17.295" y="3.62" ry="1.146" rx="1.095" stroke-width="1.786" paint-order="stroke fill markers"/><rect width="13.366" height="21.167" x="3.301" y="3.389" ry="1.146" rx="1.095" stroke-width="1.323" paint-order="stroke fill markers"/></g><defs ><path id="B" d="M15.03 24.516c0-2.307-.961-4.439-2.522-5.592s-3.483-1.153-5.044 0-2.522 3.285-2.522 5.592h5.044z"/></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |