feat: astro test
6
.gitignore
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
node_modules/
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.yarn/*
|
.yarn/*
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
.next/
|
||||||
/out/
|
backend/out/
|
||||||
certificates/
|
certificates/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ REDIS_URL="redis://localhost:6379/0"
|
||||||
|
|
||||||
# Used for metadata, sitemaps, etc.
|
# Used for metadata, sitemaps, etc.
|
||||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||||
|
FRONTEND_URL=http://localhost:4321
|
||||||
|
|
||||||
CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX
|
CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX
|
||||||
CLOUDFLARE_API_TOKEN=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 { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { idSchema } from "@/lib/schemas";
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
|
|
||||||
export async function PATCH(request: NextRequest) {
|
export async function PATCH(request: NextRequest) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { idSchema } from "@/lib/schemas";
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
@ -5,7 +5,7 @@ import dayjs from "dayjs";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { idSchema } from "@/lib/schemas";
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
import { PunishmentType } from "@prisma/client";
|
import { PunishmentType } from "@prisma/client";
|
||||||
|
|
||||||
const punishSchema = z.object({
|
const punishSchema = z.object({
|
||||||
|
|
@ -3,7 +3,7 @@ import { profanity } from "@2toad/profanity";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { userNameSchema } from "@/lib/schemas";
|
import { userNameSchema } from "@tomodachi-share/shared/schemas";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export async function PATCH(request: NextRequest) {
|
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 { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { idSchema } from "@/lib/schemas";
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
|
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
|
||||||
|
|
@ -10,11 +10,10 @@ import { profanity } from "@2toad/profanity";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
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 { generateMetadataImage, validateImage } from "@/lib/images";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
import { SwitchMiiInstructions } from "@/types";
|
import { minifyInstructions, SwitchMiiInstructions } from "@tomodachi-share/shared";
|
||||||
import { minifyInstructions } from "@/lib/switch";
|
|
||||||
import { settings } from "@/lib/settings";
|
import { settings } from "@/lib/settings";
|
||||||
|
|
||||||
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
|
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 { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { idSchema } from "@/lib/schemas";
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
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 { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
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 { RateLimit } from "@/lib/rate-limit";
|
||||||
import { generateMetadataImage, validateImage } from "@/lib/images";
|
import { generateMetadataImage, validateImage } from "@/lib/images";
|
||||||
import { convertQrCode } from "@/lib/qr-codes";
|
import Mii from "../../../../../shared/src/mii.js/mii";
|
||||||
import Mii from "@/lib/mii.js/mii";
|
import { convertQrCode, minifyInstructions, ThreeDsTomodachiLifeMii } from "@tomodachi-share/shared";
|
||||||
import { ThreeDsTomodachiLifeMii } from "@/lib/three-ds-tomodachi-life-mii";
|
|
||||||
|
|
||||||
import { SwitchMiiInstructions } from "@/types";
|
import { SwitchMiiInstructions } from "@tomodachi-share/shared";
|
||||||
import { minifyInstructions } from "@/lib/switch";
|
|
||||||
import { settings } from "@/lib/settings";
|
import { settings } from "@/lib/settings";
|
||||||
|
|
||||||
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
|
const uploadsDirectory = path.join(process.cwd(), "uploads", "mii");
|
||||||
|
|
@ -5,7 +5,7 @@ import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { idSchema } from "@/lib/schemas";
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
import { generateMetadataImage } from "@/lib/images";
|
import { generateMetadataImage } from "@/lib/images";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
@ -8,7 +8,7 @@ import dayjs from "dayjs";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "Exiled - TomodachiShare",
|
title: "Exiled - TomodachiShare",
|
||||||
|
|
@ -109,7 +109,7 @@ export default async function ExiledPage() {
|
||||||
{activePunishment.type !== "PERM_EXILE" ? (
|
{activePunishment.type !== "PERM_EXILE" ? (
|
||||||
<>
|
<>
|
||||||
<p className="mb-2">Once your punishment ends, you can return by checking the box below.</p>
|
<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 fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { idSchema } from "@/lib/schemas";
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
import { RateLimit } from "@/lib/rate-limit";
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
|
@ -15,6 +15,6 @@ export default async function RandomPage() {
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!randomMii) redirect("/");
|
if (!randomMii) redirect(process.env.NEXT_PUBLIC_FRONTEND_URL || "http://localhost:4321");
|
||||||
redirect(`/mii/${randomMii.id}`);
|
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 { useState } from "react";
|
||||||
import { Mii, ReportReason } from "@prisma/client";
|
import { type Mii, ReportReason } from "@tomodachi-share/backend";
|
||||||
|
|
||||||
import ReasonSelector from "./reason-selector";
|
import ReasonSelector from "./reason-selector";
|
||||||
import SubmitButton from "../submit-button";
|
import SubmitButton from "../submit-button";
|
||||||
import LikeButton from "../like-button";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mii: Mii;
|
mii: Mii;
|
||||||
|
|
@ -32,7 +26,8 @@ export default function ReportMiiForm({ mii, likes }: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(`/`);
|
// redirect(`/`);
|
||||||
|
window.location.href = "https://tomodachishare.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -45,10 +40,10 @@ export default function ReportMiiForm({ mii, likes }: Props) {
|
||||||
<hr className="border-zinc-300" />
|
<hr className="border-zinc-300" />
|
||||||
|
|
||||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex">
|
<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">
|
<div className="p-4">
|
||||||
<p className="text-xl font-bold line-clamp-1">{mii.name}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { ReportReason } from "@prisma/client";
|
import { ReportReason } from "@prisma/client";
|
||||||
import { useSelect } from "downshift";
|
import { useSelect } from "downshift";
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ReportReason, User } from "@prisma/client";
|
|
||||||
|
|
||||||
import ProfilePicture from "../profile-picture";
|
|
||||||
import ReasonSelector from "./reason-selector";
|
import ReasonSelector from "./reason-selector";
|
||||||
import SubmitButton from "../submit-button";
|
import SubmitButton from "../submit-button";
|
||||||
|
import { ReportReason } from "@prisma/client";
|
||||||
|
import { User } from "next-auth";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
|
|
@ -30,7 +26,7 @@ export default function ReportUserForm({ user }: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(`/`);
|
window.location.href = "https://tomodachishare.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -43,7 +39,7 @@ export default function ReportUserForm({ user }: Props) {
|
||||||
<hr className="border-zinc-300" />
|
<hr className="border-zinc-300" />
|
||||||
|
|
||||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex p-4 gap-4">
|
<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>
|
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
|
||||||
|
|
@ -9,8 +9,21 @@ import { prisma } from "@/lib/prisma";
|
||||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
providers: [Discord, Github({ issuer: "https://github.com/login/oauth" }), Google],
|
providers: [Discord, Github({ issuer: "https://github.com/login/oauth" }), Google],
|
||||||
pages: {
|
trustHost: true,
|
||||||
signIn: "/login",
|
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: {
|
callbacks: {
|
||||||
async signIn({ user }) {
|
async signIn({ user }) {
|
||||||
|
|
@ -28,5 +41,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
}
|
}
|
||||||
return session;
|
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"]
|
"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"]
|
"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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
51
frontend/package.json
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"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/backend": "workspace:*",
|
||||||
|
"@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 |