Compare commits
No commits in common. "d208565a61773c8e1c6cd136c43ac5fe34cd593d" and "df6e31ba892cbaaac16b35bfde96c4463ac3b25f" have entirely different histories.
d208565a61
...
df6e31ba89
|
|
@ -5,7 +5,6 @@ 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
|
||||
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 @@ node_modules/
|
|||
/coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
backend/out/
|
||||
/.next/
|
||||
/out/
|
||||
certificates/
|
||||
|
||||
# production
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
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,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
export default function IndexPage() {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<p>TomodachiShare API</p>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
interface Props {
|
||||
onClick: () => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
text?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function SubmitButton({ onClick, disabled = false, text = "Submit", className }: Props) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleClick = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onClick();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button type="submit" aria-label={text} onClick={handleClick} disabled={disabled} className={`pill button w-min ${className}`}>
|
||||
{text}
|
||||
{isLoading && <Icon icon="svg-spinners:180-ring-with-bg" className="ml-2" />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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
|
|
@ -1,2 +0,0 @@
|
|||
export type { User, Mii, Punishment, Prisma } from "@prisma/client";
|
||||
export { MiiPlatform, MiiGender, MiiMakeup, ReportReason } from "@prisma/client";
|
||||
24
frontend/.gitignore
vendored
|
|
@ -1,24 +0,0 @@
|
|||
# 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
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
frontend/.vscode/launch.json
vendored
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# 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).
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
// @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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,65 +0,0 @@
|
|||
// import { useSearchParams } from "next/navigation";
|
||||
// import { Suspense, useEffect, useState } from "react";
|
||||
|
||||
// import useSWR from "swr";
|
||||
// import { Icon } from "@iconify/react";
|
||||
|
||||
// interface ApiResponse {
|
||||
// message: string;
|
||||
// }
|
||||
|
||||
// const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
// function RedirectBanner() {
|
||||
// const searchParams = useSearchParams();
|
||||
// const from = searchParams.get("from");
|
||||
// if (from !== "old-domain") return null;
|
||||
|
||||
// return (
|
||||
// <div className="w-full h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center items-center gap-2 text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-start">
|
||||
// <Icon icon="humbleicons:link" className="text-2xl min-w-6" />
|
||||
// <span>We have moved URLs, welcome to tomodachishare.com!</span>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export default function AdminBanner() {
|
||||
// const { data } = useSWR<ApiResponse>("/api/admin/banner", fetcher);
|
||||
// const [shouldShow, setShouldShow] = useState(true);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!data?.message) return;
|
||||
|
||||
// // Check if the current banner text was closed by the user
|
||||
// const closedBanner = window.localStorage.getItem("closedBanner");
|
||||
// setShouldShow(data.message !== closedBanner);
|
||||
// }, [data]);
|
||||
|
||||
// const handleClose = () => {
|
||||
// if (!data) return;
|
||||
|
||||
// // Close banner and remember it
|
||||
// window.localStorage.setItem("closedBanner", data.message);
|
||||
// setShouldShow(false);
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// {data && data.message && shouldShow && (
|
||||
// <div className="relative w-full h-10 bg-orange-300 border-y-2 border-y-orange-400 mt-1 pl-2 shadow-md flex justify-center text-orange-900 text-nowrap overflow-x-auto font-semibold max-sm:justify-between">
|
||||
// <div className="flex gap-2 h-full items-center w-fit">
|
||||
// <Icon icon="humbleicons:exclamation" className="text-2xl min-w-6" />
|
||||
// <span>{data.message}</span>
|
||||
// </div>
|
||||
|
||||
// <button onClick={handleClose} className="min-sm:absolute right-2 cursor-pointer p-1.5">
|
||||
// <Icon icon="humbleicons:times" className="text-2xl min-w-6" />
|
||||
// </button>
|
||||
// </div>
|
||||
// )}
|
||||
// <Suspense>
|
||||
// <RedirectBanner />
|
||||
// </Suspense>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
// import { settings } from "@/lib/settings";
|
||||
// import { useState } from "react";
|
||||
|
||||
// export default function ControlCenter() {
|
||||
// const [canSubmit, setCanSubmit] = useState(settings.canSubmit);
|
||||
// const [isQueueEnabled, setIsQeueueEnabled] = useState(settings.queueEnabled);
|
||||
|
||||
// const onClickSet = async () => {
|
||||
// await fetch("/api/admin/can-submit", { method: "PATCH", body: JSON.stringify(canSubmit) });
|
||||
// await fetch("/api/admin/queue", { method: "PATCH", body: JSON.stringify(isQueueEnabled) });
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="bg-orange-100 rounded-xl border-2 border-orange-400 p-2 flex flex-col gap-2">
|
||||
// <div className="flex items-center gap-2">
|
||||
// <input
|
||||
// id="submit"
|
||||
// type="checkbox"
|
||||
// className="checkbox size-6!"
|
||||
// placeholder="Enter banner text"
|
||||
// checked={canSubmit}
|
||||
// onChange={(e) => setCanSubmit(e.target.checked)}
|
||||
// />
|
||||
// <label htmlFor="submit">Enable Submissions</label>
|
||||
// </div>
|
||||
// <div className="flex items-center gap-2">
|
||||
// <input
|
||||
// id="queue"
|
||||
// type="checkbox"
|
||||
// className="checkbox size-6!"
|
||||
// placeholder="Enter banner text"
|
||||
// checked={isQueueEnabled}
|
||||
// onChange={(e) => setIsQeueueEnabled(e.target.checked)}
|
||||
// />
|
||||
// <label htmlFor="queue">Enable Queue</label>
|
||||
// </div>
|
||||
|
||||
// <div className="flex gap-2 self-end">
|
||||
// <button type="submit" className="pill button" onClick={onClickSet}>
|
||||
// Set
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
// import { Prisma } from "@prisma/client";
|
||||
// import { useMemo, useRef, useState } from "react";
|
||||
// import Carousel from "../carousel";
|
||||
// import Link from "next/link";
|
||||
// import { Icon } from "@iconify/react";
|
||||
|
||||
// interface Props {
|
||||
// miis: Prisma.MiiGetPayload<{ include: { user: { select: { id: true; name: true } }; _count: { select: { likedBy: true } } } }>[];
|
||||
// }
|
||||
|
||||
// type Decision = "accept" | "reject" | null;
|
||||
|
||||
// export default function Queue({ miis }: Props) {
|
||||
// const [currentIndex, setCurrentIndex] = useState(4); // Current index in the miis array, not visible
|
||||
// const [visibleMiis, setVisibleMiis] = useState(miis.slice(0, 4));
|
||||
// const [decision, setDecision] = useState<Decision>(null);
|
||||
// const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
// const [dragOffset, setDragOffset] = useState(0);
|
||||
// const dragStart = useRef<number | null>(null);
|
||||
// const isDragging = useRef(false);
|
||||
|
||||
// const rotations = useMemo(() => {
|
||||
// const map: Record<string, number> = {};
|
||||
// miis.forEach((mii) => {
|
||||
// map[mii.id] = Math.random() * 15 - 5;
|
||||
// });
|
||||
// return map;
|
||||
// }, [miis]);
|
||||
|
||||
// const handleDecision = (decision: Decision) => {
|
||||
// if (isAnimating) return;
|
||||
// setDecision(decision);
|
||||
// setIsAnimating(true);
|
||||
// setDragOffset(decision === "accept" ? -300 : 300);
|
||||
|
||||
// setTimeout(() => {
|
||||
// setVisibleMiis((prev) => {
|
||||
// const newQueue = prev.slice(1); // Remove first Mii
|
||||
// if (miis[currentIndex]) newQueue.push(miis[currentIndex]); // Add a new Mii to the end of the list
|
||||
// return newQueue;
|
||||
// });
|
||||
// setCurrentIndex((prev) => prev + 1);
|
||||
// setDecision(null);
|
||||
// setIsAnimating(false);
|
||||
// setDragOffset(0);
|
||||
// }, 500);
|
||||
// };
|
||||
|
||||
// const onDragStart = (clientX: number) => {
|
||||
// if (isAnimating) return;
|
||||
// dragStart.current = clientX;
|
||||
// isDragging.current = true;
|
||||
// };
|
||||
|
||||
// const onDragMove = (clientX: number) => {
|
||||
// if (!isDragging.current || !dragStart.current) return;
|
||||
// setDragOffset(clientX - dragStart.current);
|
||||
// };
|
||||
|
||||
// const onDragEnd = () => {
|
||||
// if (!isDragging.current) return;
|
||||
// isDragging.current = false;
|
||||
|
||||
// if (dragOffset < -80) handleDecision("accept");
|
||||
// else if (dragOffset > 80) handleDecision("reject");
|
||||
// else setDragOffset(0);
|
||||
|
||||
// dragStart.current = null;
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="w-full flex justify-center items-center gap-8 relative h-100 mt-4 mb-8">
|
||||
// <button
|
||||
// onClick={() => handleDecision("accept")}
|
||||
// className="pointer-coarse:hidden aspect-square cursor-pointer size-12 bg-zinc-50 border-2 border-zinc-300 rounded-full flex justify-center items-center text-2xl text-zinc-500 shadow-xs"
|
||||
// >
|
||||
// <Icon icon="material-symbols:check-rounded" />
|
||||
// </button>
|
||||
|
||||
// <div className="relative w-full max-w-96 h-96 aspect-square">
|
||||
// {visibleMiis.map((mii, i) => {
|
||||
// const isTopCard = i === 0;
|
||||
|
||||
// // Calculate rotation/opacity based on drag distance
|
||||
// const dragRotation = isTopCard ? dragOffset / 10 : 0;
|
||||
// const dragOpacity = isTopCard ? 1 - Math.min(Math.abs(dragOffset) / 300, 1) : undefined;
|
||||
|
||||
// return (
|
||||
// <div
|
||||
// key={mii.id}
|
||||
// className={`absolute inset-0 flex flex-col bg-zinc-50 rounded-3xl border-2 shadow-lg p-[0.8rem] border-zinc-300 *:select-none
|
||||
// ${!isDragging.current ? "transition-all duration-500" : "transition-none"}
|
||||
// ${isTopCard ? "cursor-grab active:cursor-grabbing" : "pointer-events-none"}`}
|
||||
// style={{
|
||||
// transform: isTopCard
|
||||
// ? `translate(${dragOffset}px, ${Math.abs(dragOffset) * 0.1}px) rotate(${rotations[mii.id] + dragRotation}deg)`
|
||||
// : `translateY(${i * 10}px) rotate(${rotations[mii.id]}deg)`,
|
||||
// zIndex: (visibleMiis.length - i) * 10,
|
||||
// opacity: dragOpacity,
|
||||
// }}
|
||||
// onMouseDown={(e) => isTopCard && onDragStart(e.clientX)}
|
||||
// onMouseMove={(e) => isTopCard && onDragMove(e.clientX)}
|
||||
// onMouseUp={() => isTopCard && onDragEnd()}
|
||||
// onMouseLeave={() => isTopCard && isDragging.current && onDragEnd()}
|
||||
// onTouchStart={(e) => isTopCard && onDragStart(e.touches[0].clientX)}
|
||||
// onTouchMove={(e) => isTopCard && onDragMove(e.touches[0].clientX)}
|
||||
// onTouchEnd={() => isTopCard && onDragEnd()}
|
||||
// >
|
||||
// <Carousel
|
||||
// images={[
|
||||
// `/mii/${mii.id}/image?type=mii`,
|
||||
// ...(mii.platform === "THREE_DS" ? [`/mii/${mii.id}/image?type=qr-code`] : [`/mii/${mii.id}/image?type=features`]),
|
||||
// ...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image?type=image${index}`),
|
||||
// ]}
|
||||
// onlyButtons
|
||||
// />
|
||||
|
||||
// <div className="p-4 flex flex-col gap-1 h-full">
|
||||
// <div className="flex justify-between items-center">
|
||||
// <Link
|
||||
// href={`/mii/${mii.id}`}
|
||||
// draggable={false}
|
||||
// className="relative font-bold text-2xl line-clamp-1 w-full text-ellipsis wrap-break-word"
|
||||
// title={mii.name}
|
||||
// >
|
||||
// {mii.name}
|
||||
// </Link>
|
||||
// <div title={mii.platform === "SWITCH" ? "Switch" : "3DS"} className="-mr-3 text-[1.25rem] opacity-25">
|
||||
// {mii.platform === "SWITCH" ? (
|
||||
// <Icon icon="cib:nintendo-switch" className="text-red-400" />
|
||||
// ) : (
|
||||
// <Icon icon="cib:nintendo-3ds" className="text-sky-400" />
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// <div id="tags" className="flex flex-wrap gap-1">
|
||||
// {mii.tags.map((tag) => (
|
||||
// <Link href={{ query: { tags: tag } }} draggable={false} key={tag} className="px-2 py-1 bg-orange-300 rounded-full text-xs">
|
||||
// {tag}
|
||||
// </Link>
|
||||
// ))}
|
||||
// </div>
|
||||
|
||||
// <div className="mt-auto grid grid-cols-2 gap-4 items-center">
|
||||
// <p className="text-sm">{mii.createdAt.toLocaleString("en-GB", { timeZone: "UTC" })}</p>
|
||||
|
||||
// <Link href={`/profile/${mii.user.id}`} draggable={false} className="text-sm text-right overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
// @{mii.user?.name}
|
||||
// </Link>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// })}
|
||||
// </div>
|
||||
|
||||
// <button
|
||||
// onClick={() => handleDecision("reject")}
|
||||
// className="pointer-coarse:hidden aspect-square cursor-pointer size-12 bg-zinc-50 border-2 border-zinc-300 rounded-full flex justify-center items-center text-2xl text-zinc-500 shadow-xs"
|
||||
// >
|
||||
// <Icon icon="material-symbols:close-rounded" />
|
||||
// </button>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
// import { revalidatePath } from "next/cache";
|
||||
|
||||
// import { Icon } from "@iconify/react";
|
||||
// import { ReportStatus } from "@prisma/client";
|
||||
|
||||
// import { prisma } from "@/lib/prisma";
|
||||
// import ReportTabs from "./report-tabs";
|
||||
|
||||
// const PAGE_SIZE = 20;
|
||||
|
||||
// export default async function Reports({ searchParams }: { searchParams: { status?: string; page?: string } }) {
|
||||
// const status = searchParams.status as ReportStatus | undefined;
|
||||
// const page = Number(searchParams.page ?? 1);
|
||||
|
||||
// const [reports, total] = await Promise.all([
|
||||
// prisma.report.findMany({
|
||||
// where: status ? { status } : undefined,
|
||||
// orderBy: { createdAt: "desc" },
|
||||
// skip: (page - 1) * PAGE_SIZE,
|
||||
// take: PAGE_SIZE,
|
||||
// }),
|
||||
// prisma.report.count({
|
||||
// where: status ? { status } : undefined,
|
||||
// }),
|
||||
// ]);
|
||||
|
||||
// const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||
|
||||
// const updateStatus = async (formData: FormData) => {
|
||||
// "use server";
|
||||
// const id = Number(formData.get("id"));
|
||||
// const status = formData.get("status") as ReportStatus;
|
||||
|
||||
// await prisma.report.update({
|
||||
// where: { id },
|
||||
// data: { status },
|
||||
// });
|
||||
|
||||
// revalidatePath("/admin");
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <div className="bg-orange-100 rounded-xl border-2 border-orange-400">
|
||||
// <ReportTabs status={status} />
|
||||
|
||||
// {/* Grid */}
|
||||
// <div className="grid grid-cols-2 gap-2 p-2 max-lg:grid-cols-1">
|
||||
// {reports.map((report) => (
|
||||
// <div key={report.id} className="p-4 bg-white border border-orange-300 shadow-sm rounded-md">
|
||||
// <div className="w-full overflow-x-scroll">
|
||||
// <div className="flex gap-1 w-max">
|
||||
// <span
|
||||
// className={`text-xs font-semibold px-2 py-1 rounded-full border ${
|
||||
// report.reportType == "USER" ? "bg-red-200 text-red-800 border-red-400" : "bg-cyan-200 text-cyan-800 border-cyan-400"
|
||||
// }`}
|
||||
// >
|
||||
// {report.reportType}
|
||||
// </span>
|
||||
|
||||
// <span
|
||||
// className={`text-xs font-semibold px-2 py-1 rounded-full border ${
|
||||
// report.status == "OPEN"
|
||||
// ? "bg-orange-200 text-orange-800 border-orange-400"
|
||||
// : report.status == "RESOLVED"
|
||||
// ? "bg-green-200 text-green-800 border-green-400"
|
||||
// : "bg-zinc-200 text-zinc-800 border-zinc-400"
|
||||
// }`}
|
||||
// >
|
||||
// {report.status}
|
||||
// </span>
|
||||
|
||||
// <span className="ml-2 flex items-center gap-1 text-sm text-zinc-500">
|
||||
// <Icon icon="lucide:calendar" className="text-base" />
|
||||
// {report.createdAt.toLocaleString("en-GB", {
|
||||
// day: "2-digit",
|
||||
// month: "long",
|
||||
// year: "numeric",
|
||||
// hour: "2-digit",
|
||||
// minute: "2-digit",
|
||||
// second: "2-digit",
|
||||
// timeZone: "UTC",
|
||||
// })}{" "}
|
||||
// UTC
|
||||
// </span>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div className="grid grid-cols-4 text-xs text-zinc-600 mt-4 max-sm:grid-cols-2">
|
||||
// <div>
|
||||
// <p>Target ID</p>
|
||||
// <a href={report.reportType === "MII" ? `/mii/${report.targetId}` : `/profile/${report.targetId}`} className="text-blue-600 text-sm">
|
||||
// {report.targetId}
|
||||
// </a>
|
||||
// </div>
|
||||
|
||||
// <div>
|
||||
// <p>Creator ID</p>
|
||||
// <a href={`/profile/${report.creatorId}`} className="text-blue-600 text-sm">
|
||||
// {report.creatorId}
|
||||
// </a>
|
||||
// </div>
|
||||
|
||||
// <div>
|
||||
// <p>Reporter</p>
|
||||
// <a href={`/profile/${report.authorId}`} className="text-blue-600 text-sm">
|
||||
// {report.authorId}
|
||||
// </a>
|
||||
// </div>
|
||||
|
||||
// <div>
|
||||
// <p>Reason</p>
|
||||
// <p className="font-medium text-black text-sm">{report.reason}</p>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div className="mt-4 border border-orange-200 bg-orange-100/50 rounded-md p-2">
|
||||
// <p className="text-zinc-600 text-xs">Notes</p>
|
||||
// <p>{report.reasonNotes}</p>
|
||||
// </div>
|
||||
|
||||
// <div className="mt-4 flex gap-4">
|
||||
// <form action={updateStatus}>
|
||||
// <input type="hidden" name="id" value={report.id} />
|
||||
// <input type="hidden" name="status" value={"OPEN"} />
|
||||
|
||||
// <button
|
||||
// type="submit"
|
||||
// aria-label="Open"
|
||||
// className="cursor-pointer text-orange-400 flex items-center gap-1 p-1.5 rounded-lg transition-colors hover:bg-orange-400/15"
|
||||
// >
|
||||
// <Icon icon="mdi:alert-circle" className="text-xl" />
|
||||
// <span className="text-sm">Open</span>
|
||||
// </button>
|
||||
// </form>
|
||||
// <form action={updateStatus}>
|
||||
// <input type="hidden" name="id" value={report.id} />
|
||||
// <input type="hidden" name="status" value={"RESOLVED"} />
|
||||
|
||||
// <button
|
||||
// type="submit"
|
||||
// aria-label="Resolve"
|
||||
// className="cursor-pointer text-green-500 flex items-center gap-1 p-1.5 rounded-lg transition-colors hover:bg-green-500/15"
|
||||
// >
|
||||
// <Icon icon="mdi:check-circle" className="text-xl" />
|
||||
// <span className="text-sm">Resolve</span>
|
||||
// </button>
|
||||
// </form>
|
||||
// <form action={updateStatus}>
|
||||
// <input type="hidden" name="id" value={report.id} />
|
||||
// <input type="hidden" name="status" value={"DISMISSED"} />
|
||||
|
||||
// <button
|
||||
// type="submit"
|
||||
// aria-label="Dismiss"
|
||||
// className="cursor-pointer text-zinc-400 flex items-center gap-1 p-1.5 rounded-lg transition-colors hover:bg-zinc-400/15"
|
||||
// >
|
||||
// <Icon icon="mdi:close-circle" className="text-xl" />
|
||||
// <span className="text-sm">Dismiss</span>
|
||||
// </button>
|
||||
// </form>
|
||||
// </div>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
|
||||
// {reports.length === 0 && (
|
||||
// <div className="text-center py-12 text-gray-500">
|
||||
// <p className="text-lg font-medium">No reports to display</p>
|
||||
// <p className="text-sm">Reports will appear here when users submit them</p>
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// {/* Pagination */}
|
||||
// {totalPages > 1 && (
|
||||
// <div className="flex justify-between items-center p-3 border-t border-orange-300">
|
||||
// <span className="text-sm text-orange-700">{total} total</span>
|
||||
// <div className="flex items-center gap-3">
|
||||
// {page > 1 && (
|
||||
// <a
|
||||
// href={`/admin?${new URLSearchParams({ ...(status && { status }), page: String(page - 1) })}`}
|
||||
// className="text-sm px-3 py-1 rounded-full font-medium border bg-white text-orange-700 border-orange-300 hover:bg-orange-50 transition-colors"
|
||||
// >
|
||||
// Previous
|
||||
// </a>
|
||||
// )}
|
||||
// <span className="text-sm text-orange-700">
|
||||
// Page {page} of {totalPages}
|
||||
// </span>
|
||||
// {page < totalPages && (
|
||||
// <a
|
||||
// href={`/admin?${new URLSearchParams({ ...(status && { status }), page: String(page + 1) })}`}
|
||||
// className="text-sm px-3 py-1 rounded-full font-medium border bg-white text-orange-700 border-orange-300 hover:bg-orange-50 transition-colors"
|
||||
// >
|
||||
// Next
|
||||
// </a>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
---
|
||||
|
||||
<footer class="mt-auto">
|
||||
<div class="max-w-4xl mx-auto px-4 py-4">
|
||||
{/* Main disclaimer */}
|
||||
<div class="text-center mb-2">
|
||||
<p class="text-sm text-zinc-600 font-medium">TomodachiShare is not affiliated with Nintendo</p>
|
||||
</div>
|
||||
|
||||
{/* Links section */}
|
||||
<div class="flex flex-wrap justify-center items-center gap-x-4 text-sm max-sm:gap-x-12">
|
||||
<a href="/terms-of-service" class="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline"> Terms of Service </a>
|
||||
|
||||
<span class="text-zinc-400 hidden sm:inline" aria-hidden="true">•</span>
|
||||
|
||||
<a href="/privacy" class="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline"> Privacy Policy </a>
|
||||
|
||||
<span class="text-zinc-400 hidden sm:inline" aria-hidden="true">•</span>
|
||||
|
||||
<a
|
||||
href="https://discord.gg/48cXBFKvWQ"
|
||||
target="_blank"
|
||||
class="text-[#5865F2] hover:text-[#454FBF] transition-colors duration-200 hover:underline inline-flex items-end gap-1"
|
||||
>
|
||||
<Icon name="ic:baseline-discord" class="text-lg" />
|
||||
Discord
|
||||
</a>
|
||||
|
||||
<span class="text-zinc-400 hidden sm:inline" aria-hidden="true"> • </span>
|
||||
|
||||
<a href="https://trafficlunar.net" target="_blank" class="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline group">
|
||||
Made by <span class="text-orange-400 group-hover:text-orange-500 font-medium transition-colors duration-200">trafficlunar</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<div class="text-center mt-4 mb-4">
|
||||
<p class="text-xs text-zinc-400">© {new Date().getFullYear()} TomodachiShare. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { Icon } from "@iconify/react";
|
||||
import { useEffect } from "react";
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { session } from "../session";
|
||||
|
||||
export default function HeaderProfile() {
|
||||
const API_BASE_URL = import.meta.env.PUBLIC_API_URL;
|
||||
const $session = useStore(session);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE_URL}/api/auth/session`, { credentials: "include" })
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Failed to get session");
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
session.set(data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!$session?.user ? (
|
||||
<li>
|
||||
<a href={"/login"} className="pill button h-full">
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
) : (
|
||||
<>
|
||||
<li title="Your profile">
|
||||
<a
|
||||
href={`/profile/${$session?.user?.id}`}
|
||||
aria-label="Go to profile"
|
||||
className="pill button gap-2! p-0! h-full max-w-64"
|
||||
data-tooltip="Your Profile"
|
||||
>
|
||||
<img
|
||||
src={$session?.user?.image ?? "/guest.png"}
|
||||
alt="profile picture"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-full aspect-square object-cover h-full bg-white outline-2 outline-orange-400"
|
||||
/>
|
||||
<span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{$session?.user?.name ?? "unknown"}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li title="Logout">
|
||||
<a href={`${API_BASE_URL}/api/auth/signout`} aria-label="Log Out" className="pill button p-2! aspect-square h-full" data-tooltip="Log Out">
|
||||
<Icon icon="ic:round-logout" fontSize={24} />
|
||||
</a>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import SearchBar from "./search-bar";
|
||||
import HeaderProfile from "./header-profile";
|
||||
---
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-50 w-full p-4 grid grid-cols-3 gap-2 gap-x-4 items-center bg-amber-50 border-b-4 border-amber-500 shadow-md max-lg:grid-cols-2 max-md:grid-cols-1"
|
||||
>
|
||||
<a href={"/"} aria-label="Go to Home Page" class="font-black text-3xl text-orange-400 flex items-center gap-2 max-md:justify-center max-md:col-span-2">
|
||||
<img src="/logo.svg" width={56} height={45} alt="logo" />
|
||||
TomodachiShare
|
||||
</a>
|
||||
|
||||
<div class="flex justify-center max-lg:justify-end max-md:justify-center">
|
||||
<SearchBar client:only />
|
||||
</div>
|
||||
|
||||
<ul class="flex justify-end gap-3 items-center h-11 *:h-full max-lg:col-span-2 max-md:justify-center">
|
||||
<li title="Random Mii">
|
||||
<a
|
||||
href={`${import.meta.env.PUBLIC_API_URL}/random`}
|
||||
aria-label="Go to Random Link"
|
||||
class="pill button p-0! h-full aspect-square"
|
||||
data-tooltip="Go to a Random Mii"
|
||||
>
|
||||
<Icon name="mdi:dice-3" size={28} />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={"/submit"} class="pill button h-full"> Submit </a>
|
||||
</li>
|
||||
<HeaderProfile client:only />
|
||||
</ul>
|
||||
</header>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { Icon } from "@iconify/react";
|
||||
import DeleteMiiButton from "./delete-mii-button";
|
||||
|
||||
interface Props {
|
||||
mii: any;
|
||||
}
|
||||
|
||||
export default function AuthorButtons({ mii }: Props) {
|
||||
// const session = useSession();
|
||||
|
||||
// if (!session.data || (Number(session.data.user?.id) !== mii.userId && Number(session.data.user?.id) !== Number(import.meta.env.NEXT_PUBLIC_ADMIN_USER_ID)))
|
||||
// return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<a aria-label="Edit Mii" href={`/edit/${mii.id}`}>
|
||||
<Icon icon="mdi:pencil" />
|
||||
<span>Edit</span>
|
||||
</a>
|
||||
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy ?? 0} inMiiPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import { Suspense, useEffect, useState } from "react";
|
||||
import FilterMenu from "../mii/list/filter-menu";
|
||||
import SortSelect from "../mii/list/sort-select";
|
||||
import MiiGrid from "../mii/list/mii-grid";
|
||||
import Pagination from "../pagination";
|
||||
import Skeleton from "../mii/list/skeleton";
|
||||
|
||||
interface ApiResponse {
|
||||
totalCount: number;
|
||||
filteredCount: number;
|
||||
miis: any[];
|
||||
lastPage: number;
|
||||
}
|
||||
|
||||
export default function IndexPage() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const [data, setData] = useState<ApiResponse>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.PUBLIC_API_URL}/api/mii/list?${searchParams.toString()}`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Failed to fetch Miis");
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="sr-only">
|
||||
{searchParams.get("tags") ? `Miis tagged with '${searchParams.get("tags")}' - TomodachiShare` : "TomodachiShare - index mii list"}
|
||||
</h1>
|
||||
|
||||
<p className="text-center mb-4">We're currently going through some major code changes therefore some features won't work.</p>
|
||||
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
{!loading && data ? (
|
||||
<div className="w-full">
|
||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex justify-between items-center gap-2 mb-2 max-md:flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
{data.totalCount == data.filteredCount ? (
|
||||
<>
|
||||
<span className="text-2xl font-bold text-amber-900">{data.totalCount}</span>
|
||||
<span className="text-lg text-amber-700">{data.totalCount === 1 ? "Mii" : "Miis"}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-2xl font-bold text-amber-900">{data.filteredCount}</span>
|
||||
<span className="text-sm text-amber-700">of</span>
|
||||
<span className="text-lg font-semibold text-amber-800">{data.totalCount}</span>
|
||||
<span className="text-lg text-amber-700">Miis</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center justify-end gap-2 w-full md:max-w-2/3 max-md:justify-center">
|
||||
<FilterMenu />
|
||||
<SortSelect />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MiiGrid miis={data.miis} />
|
||||
<Pagination lastPage={data.lastPage} />
|
||||
</div>
|
||||
) : (
|
||||
<p>No Miis found :( Has the server died?</p>
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import ProfileInformation from "../profile-information";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default function ProfilePage({ id }: Props) {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.PUBLIC_API_URL}/api/profile/${id}/info`)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Failed to fetch profile");
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setUser(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setLoading(false);
|
||||
window.location.href = "/404";
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading || !user) {
|
||||
return <div className="p-6 text-center">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProfileInformation user={user} />
|
||||
{/* <Suspense fallback={<Skeleton />}>
|
||||
<MiiList searchParams={await searchParams} userId={user.id} />
|
||||
</Suspense> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,456 +0,0 @@
|
|||
// import { redirect } from "next/navigation";
|
||||
|
||||
// import { useCallback, useEffect, useRef, useState } from "react";
|
||||
// import { FileWithPath } from "react-dropzone";
|
||||
// import { Mii, MiiGender, MiiMakeup } from "@prisma/client";
|
||||
// import { useSession } from "next-auth/react";
|
||||
|
||||
// import { nameSchema, tagsSchema } from "@tomodachi-share/shared/schemas";
|
||||
// import { defaultInstructions, minifyInstructions } from "@/lib/switch";
|
||||
// import { SwitchMiiInstructions } from "@tomodachi-share/shared";
|
||||
|
||||
// import TagSelector from "../tag-selector";
|
||||
// import ImageList from "./image-list";
|
||||
// import LikeButton from "../like-button";
|
||||
// import Carousel from "../carousel";
|
||||
// import SubmitButton from "../submit-button";
|
||||
// import Dropzone from "../dropzone";
|
||||
// import MiiEditor from "./mii-editor";
|
||||
// import SwitchSubmitTutorialButton from "../tutorial/switch-submit";
|
||||
// import { Icon } from "@iconify/react";
|
||||
// import SwitchFileUpload from "./switch-file-upload";
|
||||
|
||||
// interface Props {
|
||||
// mii: Mii;
|
||||
// likes: number;
|
||||
// }
|
||||
|
||||
// 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;
|
||||
// }
|
||||
|
||||
// export default function EditForm({ mii, likes }: Props) {
|
||||
// const session = useSession();
|
||||
// const [files, setFiles] = useState<FileWithPath[]>([]);
|
||||
|
||||
// const handleFilesChange: React.Dispatch<React.SetStateAction<FileWithPath[]>> = (updater) => {
|
||||
// hasCustomImagesChanged.current = true;
|
||||
// setFiles(updater);
|
||||
// };
|
||||
|
||||
// const handleDrop = useCallback(
|
||||
// (acceptedFiles: FileWithPath[]) => {
|
||||
// if (files.length >= 3) return;
|
||||
// hasCustomImagesChanged.current = true;
|
||||
|
||||
// setFiles((prev) => [...prev, ...acceptedFiles]);
|
||||
// },
|
||||
// [files.length],
|
||||
// );
|
||||
|
||||
// const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
// const [name, setName] = useState(mii.name);
|
||||
// const [tags, setTags] = useState(mii.tags);
|
||||
// const [description, setDescription] = useState(mii.description);
|
||||
// const [gender, setGender] = useState<MiiGender>(mii.gender ?? "MALE");
|
||||
// const [makeup, setMakeup] = useState<MiiMakeup>(mii.makeup ?? "PARTIAL");
|
||||
// const [miiPortraitUri, setMiiPortraitUri] = useState<string | undefined>(`/mii/${mii.id}/image?type=mii`);
|
||||
// const [miiFeaturesUri, setMiiFeaturesUri] = useState<string | undefined>(`/mii/${mii.id}/image?type=features`);
|
||||
// const [youtubeId, setYouTubeId] = useState(mii.youtubeId ?? "");
|
||||
// const instructions = useRef<SwitchMiiInstructions>(deepMerge(defaultInstructions, (mii.instructions as object) ?? {}));
|
||||
|
||||
// const [quarantined, setQuarantined] = useState(mii.quarantined);
|
||||
// const hasCustomImagesChanged = useRef(false);
|
||||
// const hasMiiPortraitChanged = useRef(false);
|
||||
// const hasMiiFeaturesChanged = useRef(false);
|
||||
|
||||
// const handleSubmit = async () => {
|
||||
// // Validate before sending request
|
||||
// const nameValidation = nameSchema.safeParse(name);
|
||||
// if (!nameValidation.success) {
|
||||
// setError(nameValidation.error.issues[0].message);
|
||||
// return;
|
||||
// }
|
||||
// const tagsValidation = tagsSchema.safeParse(tags);
|
||||
// if (!tagsValidation.success) {
|
||||
// setError(tagsValidation.error.issues[0].message);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Send request to server
|
||||
// const formData = new FormData();
|
||||
// if (name != mii.name) formData.append("name", name);
|
||||
// if (tags != mii.tags) formData.append("tags", JSON.stringify(tags));
|
||||
// if (description && description != mii.description) formData.append("description", description);
|
||||
// if (gender != mii.gender) formData.append("gender", gender);
|
||||
// if (makeup != mii.makeup) formData.append("makeup", makeup);
|
||||
// if (miiPortraitUri) formData.append("miiPortraitUri", miiPortraitUri);
|
||||
// if (quarantined != mii.quarantined) formData.append("quarantined", JSON.stringify(quarantined));
|
||||
// if (youtubeId != mii.youtubeId) formData.append("youtubeId", youtubeId);
|
||||
// if (minifyInstructions(structuredClone(instructions.current)) !== (mii.instructions as object))
|
||||
// formData.append("instructions", JSON.stringify(instructions.current));
|
||||
|
||||
// if (hasCustomImagesChanged.current) {
|
||||
// files.forEach((file, index) => {
|
||||
// // image1, image2, etc.
|
||||
// formData.append(`image${index + 1}`, file);
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Switch pictures
|
||||
// async function getBlob(uri: string): Promise<Blob | null> {
|
||||
// const response = await fetch(uri);
|
||||
// if (!response.ok) {
|
||||
// setError("Failed to get Mii portrait/features screenshot. Did you upload one?");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// const blob = await response.blob();
|
||||
// if (!blob.type.startsWith("image/")) {
|
||||
// setError("Invalid image file found");
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// return blob;
|
||||
// }
|
||||
|
||||
// if (miiPortraitUri && hasMiiPortraitChanged.current) {
|
||||
// const blob = await getBlob(miiPortraitUri);
|
||||
// if (blob) formData.append("miiPortraitImage", blob);
|
||||
// }
|
||||
// if (miiFeaturesUri && hasMiiFeaturesChanged.current) {
|
||||
// const blob = await getBlob(miiFeaturesUri);
|
||||
// if (blob) formData.append("miiFeaturesImage", blob);
|
||||
// }
|
||||
|
||||
// const response = await fetch(`/api/mii/${mii.id}/edit`, {
|
||||
// method: "PATCH",
|
||||
// body: formData,
|
||||
// });
|
||||
// const { error } = await response.json();
|
||||
|
||||
// if (!response.ok) {
|
||||
// setError(error);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// redirect(`/mii/${mii.id}`);
|
||||
// };
|
||||
|
||||
// const handleMiiPortraitChange = (uri: string | undefined) => {
|
||||
// hasMiiPortraitChanged.current = true;
|
||||
// setMiiPortraitUri(uri);
|
||||
// };
|
||||
|
||||
// const handleMiiFeaturesChange = (uri: string | undefined) => {
|
||||
// hasMiiFeaturesChanged.current = true;
|
||||
// setMiiFeaturesUri(uri);
|
||||
// };
|
||||
|
||||
// // Load existing images - converts image URLs to File objects
|
||||
// useEffect(() => {
|
||||
// const loadExistingImages = async () => {
|
||||
// try {
|
||||
// const existing = await Promise.all(
|
||||
// Array.from({ length: mii.imageCount }, async (_, index) => {
|
||||
// const path = `/mii/${mii.id}/image?type=image${index}`;
|
||||
// const response = await fetch(path);
|
||||
// const blob = await response.blob();
|
||||
|
||||
// return Object.assign(new File([blob], `image${index}.png`, { type: "image/png" }), { path });
|
||||
// }),
|
||||
// );
|
||||
|
||||
// setFiles(existing);
|
||||
// } catch (error) {
|
||||
// console.error("Error loading existing images:", error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// loadExistingImages();
|
||||
// }, [mii.id, mii.imageCount]);
|
||||
|
||||
// return (
|
||||
// <div className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center">
|
||||
// <div className="flex justify-center">
|
||||
// <div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
|
||||
// <Carousel
|
||||
// images={[
|
||||
// miiPortraitUri ?? `/mii/${mii.id}/image?type=mii`,
|
||||
// ...(mii.platform === "THREE_DS" ? [`/mii/${mii.id}/image?type=qr-code`] : [miiFeaturesUri ?? `/mii/${mii.id}/image?type=features`]),
|
||||
// ...files.map((file) => URL.createObjectURL(file)),
|
||||
// ]}
|
||||
// />
|
||||
|
||||
// <div className="p-4 flex flex-col gap-1 h-full">
|
||||
// <h1 className="font-bold text-2xl line-clamp-1" title={name}>
|
||||
// {name || "Mii name"}
|
||||
// </h1>
|
||||
// <div id="tags" className="flex flex-wrap gap-1">
|
||||
// {tags.length == 0 && <span className="px-2 py-1 bg-orange-300 rounded-full text-xs">tag</span>}
|
||||
// {tags.map((tag) => (
|
||||
// <span key={tag} className="px-2 py-1 bg-orange-300 rounded-full text-xs">
|
||||
// {tag}
|
||||
// </span>
|
||||
// ))}
|
||||
// </div>
|
||||
|
||||
// <div className="mt-auto">
|
||||
// <LikeButton likes={likes} isLiked={false} abbreviate disabled />
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-2 max-w-2xl w-full">
|
||||
// <div>
|
||||
// <h2 className="text-2xl font-bold">Edit your Mii</h2>
|
||||
// <p className="text-sm text-zinc-500">Make changes to your existing Mii.</p>
|
||||
// </div>
|
||||
|
||||
// {/* Separator */}
|
||||
// <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium my-1">
|
||||
// <hr className="grow border-zinc-300" />
|
||||
// <span>Info</span>
|
||||
// <hr className="grow border-zinc-300" />
|
||||
// </div>
|
||||
|
||||
// <div className="w-full grid grid-cols-3 items-center">
|
||||
// <label htmlFor="name" className="font-semibold">
|
||||
// Name
|
||||
// </label>
|
||||
// <input
|
||||
// id="name"
|
||||
// type="text"
|
||||
// className="pill input w-full col-span-2"
|
||||
// minLength={2}
|
||||
// maxLength={64}
|
||||
// placeholder="Type your mii's name here..."
|
||||
// value={name}
|
||||
// onChange={(e) => setName(e.target.value)}
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// <div className="w-full grid grid-cols-3 items-center">
|
||||
// <label htmlFor="tags" className="font-semibold">
|
||||
// Tags
|
||||
// </label>
|
||||
// <TagSelector tags={tags} setTags={setTags} showTagLimit />
|
||||
// </div>
|
||||
|
||||
// <div className="w-full grid grid-cols-3 items-start">
|
||||
// <label htmlFor="reason-note" className="font-semibold py-2">
|
||||
// Description
|
||||
// </label>
|
||||
// <textarea
|
||||
// rows={5}
|
||||
// maxLength={512}
|
||||
// placeholder="(optional) Type a description..."
|
||||
// className="pill input rounded-xl! resize-none col-span-2 text-sm"
|
||||
// value={description ?? ""}
|
||||
// onChange={(e) => setDescription(e.target.value)}
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// {session.data?.user?.id == import.meta.env.NEXT_PUBLIC_ADMIN_USER_ID && (
|
||||
// <>
|
||||
// <div className="w-full grid grid-cols-3 items-center">
|
||||
// <label htmlFor="quarantined" className="font-semibold py-2">
|
||||
// Quarantined
|
||||
// </label>
|
||||
|
||||
// <div className="col-span-2 flex gap-1">
|
||||
// <input type="checkbox" id="quarantined" className="checkbox-alt" checked={quarantined} onChange={(e) => setQuarantined(e.target.checked)} />
|
||||
// </div>
|
||||
// </div>
|
||||
// </>
|
||||
// )}
|
||||
|
||||
// {/* Makeup/Images/Instructions (Switch only) */}
|
||||
// {mii.platform === "SWITCH" && (
|
||||
// <>
|
||||
// <div className="w-full grid grid-cols-3 items-start z-20">
|
||||
// <label htmlFor="gender" className="font-semibold py-2">
|
||||
// Gender
|
||||
// </label>
|
||||
// <div className="col-span-2 flex gap-1">
|
||||
// <button
|
||||
// type="button"
|
||||
// onClick={() => setGender("MALE")}
|
||||
// aria-label="Filter for Male Miis"
|
||||
// data-tooltip="Male"
|
||||
// className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-blue-400! after:border-blue-400! before:border-b-blue-400! ${
|
||||
// gender === "MALE" ? "bg-blue-100 border-blue-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
// }`}
|
||||
// >
|
||||
// <Icon icon="foundation:male" className="text-blue-400" />
|
||||
// </button>
|
||||
|
||||
// <button
|
||||
// type="button"
|
||||
// onClick={() => setGender("FEMALE")}
|
||||
// aria-label="Filter for Female Miis"
|
||||
// data-tooltip="Female"
|
||||
// className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${
|
||||
// gender === "FEMALE" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
// }`}
|
||||
// >
|
||||
// <Icon icon="foundation:female" className="text-pink-400" />
|
||||
// </button>
|
||||
|
||||
// <button
|
||||
// type="button"
|
||||
// onClick={() => setGender("NONBINARY")}
|
||||
// aria-label="Filter for Nonbinary Miis"
|
||||
// data-tooltip="Nonbinary"
|
||||
// className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${
|
||||
// gender === "NONBINARY" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
// }`}
|
||||
// >
|
||||
// <Icon icon="mdi:gender-non-binary" className="text-purple-400" />
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// <div className="w-full grid grid-cols-3 items-start">
|
||||
// <label htmlFor="makeup" className="font-semibold py-2">
|
||||
// Face Paint
|
||||
// </label>
|
||||
|
||||
// <div className="col-span-2 flex gap-1">
|
||||
// {/* Full Makeup */}
|
||||
// <button
|
||||
// type="button"
|
||||
// onClick={() => setMakeup("FULL")}
|
||||
// aria-label="Full Face Paint"
|
||||
// data-tooltip="Full Face Paint"
|
||||
// className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-pink-400! after:border-pink-400! before:border-b-pink-400! ${
|
||||
// makeup === "FULL" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
// }`}
|
||||
// >
|
||||
// <Icon icon="mdi:palette" className="text-pink-400" />
|
||||
// </button>
|
||||
|
||||
// {/* Partial Makeup */}
|
||||
// <button
|
||||
// type="button"
|
||||
// onClick={() => setMakeup("PARTIAL")}
|
||||
// aria-label="Partial Face Paint"
|
||||
// data-tooltip="Partial Face Paint"
|
||||
// className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-purple-400! after:border-purple-400! before:border-b-purple-400! ${
|
||||
// makeup === "PARTIAL" ? "bg-purple-100 border-purple-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
// }`}
|
||||
// >
|
||||
// <Icon icon="mdi:lipstick" className="text-purple-400" />
|
||||
// </button>
|
||||
|
||||
// {/* No Makeup */}
|
||||
// <button
|
||||
// type="button"
|
||||
// onClick={() => setMakeup("NONE")}
|
||||
// aria-label="No Face Paint"
|
||||
// data-tooltip="No Face Paint"
|
||||
// className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all after:bg-gray-400! after:border-gray-400! before:border-b-gray-400! ${
|
||||
// makeup === "NONE" ? "bg-gray-200 border-gray-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
// }`}
|
||||
// >
|
||||
// <Icon icon="codex:cross" className="text-gray-400" />
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// {/* (Switch Only) Mii Portrait */}
|
||||
// <div>
|
||||
// {/* Separator */}
|
||||
// <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8 mb-2">
|
||||
// <hr className="grow border-zinc-300" />
|
||||
// <span>Mii Portrait</span>
|
||||
// <hr className="grow border-zinc-300" />
|
||||
// </div>
|
||||
|
||||
// <div className="flex flex-col items-center gap-2">
|
||||
// <SwitchFileUpload text="a screenshot of your Mii here" image={miiPortraitUri} setImage={handleMiiPortraitChange} forceCrop />
|
||||
// <SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={handleMiiFeaturesChange} />
|
||||
// <SwitchSubmitTutorialButton />
|
||||
// </div>
|
||||
|
||||
// <p className="text-xs text-zinc-400 text-center mt-2">You must upload a screenshot of the features, check tutorial on how.</p>
|
||||
// </div>
|
||||
|
||||
// <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8">
|
||||
// <hr className="grow border-zinc-300" />
|
||||
// <span>Instructions</span>
|
||||
// <hr className="grow border-zinc-300" />
|
||||
// </div>
|
||||
|
||||
// {/* YouTube */}
|
||||
// <div className="w-full grid grid-cols-3 items-center">
|
||||
// <label htmlFor="youtube" className="font-semibold">
|
||||
// YouTube Video
|
||||
// </label>
|
||||
// <input
|
||||
// id="youtube"
|
||||
// type="text"
|
||||
// className="pill input w-full col-span-2"
|
||||
// minLength={2}
|
||||
// maxLength={64}
|
||||
// placeholder="Paste a URL or video ID..."
|
||||
// value={youtubeId}
|
||||
// onChange={(e) => {
|
||||
// const val = e.target.value;
|
||||
// const match = val.match(/(?:youtube\.com\/(?:watch\?v=|shorts\/|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
|
||||
// setYouTubeId(match ? match[1] : val);
|
||||
// }}
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// <MiiEditor instructions={instructions} />
|
||||
// <SwitchSubmitTutorialButton />
|
||||
// </>
|
||||
// )}
|
||||
|
||||
// {/* Separator */}
|
||||
// <div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mt-8">
|
||||
// <hr className="grow border-zinc-300" />
|
||||
// <span>Custom images</span>
|
||||
// <hr className="grow border-zinc-300" />
|
||||
// </div>
|
||||
|
||||
// <div className="max-w-md w-full self-center">
|
||||
// <Dropzone onDrop={handleDrop}>
|
||||
// <p className="text-center text-sm">
|
||||
// Drag and drop your images here
|
||||
// <br />
|
||||
// or click to open
|
||||
// </p>
|
||||
// </Dropzone>
|
||||
// </div>
|
||||
|
||||
// <ImageList files={files} setFiles={handleFilesChange} />
|
||||
|
||||
// <hr className="border-zinc-300 my-2" />
|
||||
// <div className="flex justify-between items-center">
|
||||
// {error && <span className="text-red-400 font-bold">Error: {error}</span>}
|
||||
|
||||
// <SubmitButton onClick={handleSubmit} text="Edit" className="ml-auto" />
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
---
|
||||
import "./styles/global.css";
|
||||
import "react-image-crop/dist/ReactCrop.css";
|
||||
|
||||
import Header from "./components/header.astro";
|
||||
import Footer from "./components/footer.astro";
|
||||
// import AdminBanner from "./components/admin/banner";
|
||||
import Providers from "./components/provider";
|
||||
import { Font } from "astro:assets";
|
||||
// import SessionWrapper from "./components/SessionWrapper";
|
||||
|
||||
const baseUrl = import.meta.env.PUBLIC_BASE_URL;
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: "TomodachiShare",
|
||||
url: "https://tomodachishare.com",
|
||||
description: "Discover and share Mii residents for your Tomodachi Life island!",
|
||||
inLanguage: "en",
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "TomodachiShare",
|
||||
url: "https://tomodachishare.com",
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: "https://tomodachishare.com/logo.png",
|
||||
},
|
||||
sameAs: ["https://trafficlunar.net", "https://twitter.com/trafficlunr", "https://bsky.app/profile/trafficlunar.net"],
|
||||
},
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: "https://tomodachishare.com/?q={search_term_string}",
|
||||
"query-input": "required name=search_term_string",
|
||||
},
|
||||
};
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<Font cssVariable="--font-lexend" />
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<!-- SEO -->
|
||||
<title>TomodachiShare - home for Tomodachi Life Miis!</title>
|
||||
<meta name="description" content="Discover and share Mii residents for your Tomodachi Life island!" />
|
||||
<meta name="keywords" content="mii, tomodachi life, nintendo, tomodachishare, tomodachi-share, mii creator, mii collection" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:site_name" content="TomodachiShare" />
|
||||
<meta property="og:title" content="TomodachiShare" />
|
||||
<meta property="og:description" content="Discover and share Mii residents for your Tomodachi Life island!" />
|
||||
<meta property="og:image" content="/preview.png" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={baseUrl} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="TomodachiShare - Discover and Share Your Mii Residents" />
|
||||
<meta name="twitter:description" content="Discover and share Mii residents for your Tomodachi Life island!" />
|
||||
<meta name="twitter:image" content="/preview.png" />
|
||||
<meta name="twitter:creator" content="@trafficlunr" />
|
||||
|
||||
<!-- JSON-LD -->
|
||||
<script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd).replace(/</g, "\\u003c")} />
|
||||
|
||||
<!-- Analytics -->
|
||||
{
|
||||
import.meta.env.PROD && (
|
||||
<script is:inline defer src="https://analytics.trafficlunar.net/script.js" data-website-id="bc530384-9b7d-471a-b2e3-f9859da50c24" />
|
||||
)
|
||||
}
|
||||
</head>
|
||||
|
||||
<body class="font-[Lexend] antialiased flex flex-col items-center min-h-screen">
|
||||
<Providers client:load>
|
||||
<!-- <SessionWrapper client:load> -->
|
||||
<Header />
|
||||
<!-- <AdminBanner client:load /> -->
|
||||
|
||||
<main class="px-4 py-8 max-w-7xl w-full grow flex flex-col">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<!-- </SessionWrapper> -->
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import Layout from "../layout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="grow flex items-center justify-center">
|
||||
<div class="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-8 max-w-xs w-full text-center flex flex-col">
|
||||
<h2 class="text-7xl font-black">404</h2>
|
||||
<p>Page not found - you swam off the island!</p>
|
||||
<a href="/" class="pill button gap-2 mt-8 w-fit self-center">
|
||||
<Icon name="ic:round-home" size={24} />
|
||||
Travel Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
---
|
||||
import Layout from "../layout.astro";
|
||||
import IndexPage from "../components/pages/index";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<!-- <Suspense fallback={<Skeleton />}> -->
|
||||
<!-- <MiiList searchParams={Astro.url.searchParams} /> -->
|
||||
<!-- </Suspense> -->
|
||||
<IndexPage client:only />
|
||||
</Layout>
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
---
|
||||
import Layout from "../layout.astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
const API_BASE_URL = import.meta.env.PUBLIC_API_URL;
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="grow flex items-center justify-center">
|
||||
<div class="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg px-10 py-12 max-w-md text-center">
|
||||
<h1 class="text-3xl font-bold mb-4">Welcome to TomodachiShare!</h1>
|
||||
|
||||
<div class="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-8">
|
||||
<hr class="grow border-zinc-300" />
|
||||
<span>Choose your login method</span>
|
||||
<hr class="grow border-zinc-300" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<a
|
||||
href={`${API_BASE_URL}/api/auth/signin/discord`}
|
||||
aria-label="Login with Discord"
|
||||
class="pill button gap-2 px-3! bg-indigo-400! border-indigo-500! hover:bg-indigo-500!"
|
||||
>
|
||||
<Icon name="ic:baseline-discord" size={32} />
|
||||
Login with Discord
|
||||
</a>
|
||||
<a
|
||||
href={`${API_BASE_URL}/api/auth/signin/github`}
|
||||
aria-label="Login with GitHub"
|
||||
class="pill button gap-2 px-3! bg-zinc-700! border-zinc-800! hover:bg-zinc-800! text-white"
|
||||
>
|
||||
<Icon name="mdi:github" size={32} />
|
||||
Login with GitHub
|
||||
</a>
|
||||
<a
|
||||
href={`${API_BASE_URL}/api/auth/signin/google`}
|
||||
aria-label="Login with Google"
|
||||
class="pill button gap-2 px-3! bg-white! border-gray-300! hover:bg-gray-100! text-black! flex items-center"
|
||||
>
|
||||
<Icon name="material-icon-theme:google" size={32} />
|
||||
Login with Google
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 text-xs text-zinc-400">
|
||||
By signing up, you agree to the{" "}
|
||||
<a href="/terms-of-service" class="underline hover:text-zinc-600">Terms of Service</a>{" "}
|
||||
and{" "}
|
||||
<a href="/privacy" class="underline hover:text-zinc-600">Privacy Policy</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
import MiiPage from "../../components/pages/mii";
|
||||
import Layout from "../../layout.astro";
|
||||
|
||||
const { id } = Astro.params;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return Array.from({ length: 30000 }, (_, i) => ({
|
||||
params: { id: String(i + 1) },
|
||||
}));
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<MiiPage client:load id={id} />
|
||||
</Layout>
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
---
|
||||
import Layout from "../layout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="bg-amber-50 border-2 border-amber-500 rounded-2xl p-6">
|
||||
<h1 class="text-2xl font-bold">Privacy Policy</h1>
|
||||
<h2 class="font-light">
|
||||
<strong class="font-medium">Effective Date:</strong> 13 April 2026
|
||||
</h2>
|
||||
|
||||
<hr class="border-black/20 mt-1 mb-4" />
|
||||
|
||||
<p>By using this website, you confirm that you understand and agree to this Privacy Policy.</p>
|
||||
<p class="mt-1">
|
||||
If you have any questions or concerns, please contact me at:{" "}
|
||||
<a href="mailto:hello@trafficlunar.net" class="text-blue-700"> hello@trafficlunar.net </a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<ul class="list-decimal ml-5 marker:text-xl marker:font-semibold">
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Information We Collect</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">The following types of information are stored when you use this website:</p>
|
||||
<ul class="list-disc list-inside">
|
||||
<li>
|
||||
<strong>Account Information:</strong> When you sign up or log in using Discord or Github, your name, e-mail, and profile picture are collected. Your
|
||||
authentication tokens may also be temporarily stored to maintain your login session.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Miis:</strong> We store any Miis you submit, including associated images (such as a picture of your Mii, QR codes, and custom images).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Interaction Data:</strong> The Miis you like.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Use of Cookies</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">Cookies are necessary for user sessions and authentication. We do not use cookies for tracking or advertising purposes.</p>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Analytics</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
We use{" "}
|
||||
<a href="https://umami.is/" class="text-blue-700"> Umami </a>{" "}
|
||||
to collect anonymous data about how users interact with the site. Umami is fully GDPR-compliant, and no personally identifiable information is collected
|
||||
through this service.
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Data Sharing</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
We do not sell your personal data to third parties. Your data may be sent anonymously to self-hosted third-party services or trusted third-party
|
||||
tools (such as analytics) but these services are used solely to keep the site functional.
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Your Rights</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">As a user, you have the right to:</p>
|
||||
<ul class="list-disc list-inside indent-4">
|
||||
<li>Access the personal data we hold about you.</li>
|
||||
<li>Request corrections to any inaccurate or incomplete information.</li>
|
||||
<li>Request the deletion of your personal data.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Data Deletion</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
Your data, including your Miis, will be retained for as long as you have an account on the site. You may request that your data be deleted at any
|
||||
time by going to your profile page, clicking the settings icon, and clicking the 'Delete Account' button. Upon clicking, your data will be
|
||||
promptly removed from our servers.
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Changes to this Privacy Policy</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
This Privacy Policy may be updated from time to time. We encourage you to review this policy periodically to stay informed about your privacy.
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Layout>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
import ProfilePage from "../../components/pages/profile";
|
||||
import Layout from "../../layout.astro";
|
||||
|
||||
const { id } = Astro.params;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return Array.from({ length: 50000 }, (_, i) => ({
|
||||
params: { id: String(i + 1) },
|
||||
}));
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<ProfilePage client:only id={id} />
|
||||
</Layout>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
import ProfileSettings from "../../components/profile-settings";
|
||||
import Layout from "../../layout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<!-- <ProfileInformation client:only page="settings" /> -->
|
||||
<ProfileSettings client:only currentDescription={null} />
|
||||
</Layout>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
import SubmitForm from "../components/submit-form";
|
||||
import Layout from "../layout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<SubmitForm client:load />
|
||||
</Layout>
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
---
|
||||
import Layout from "../layout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="bg-amber-50 border-2 border-amber-500 rounded-2xl p-6">
|
||||
<h1 class="text-2xl font-bold">Terms of Service</h1>
|
||||
<h2 class="font-light">
|
||||
<strong class="font-medium">Effective Date:</strong> March 26, 2026
|
||||
</h2>
|
||||
|
||||
<hr class="border-black/20 mt-1 mb-4" />
|
||||
|
||||
<p>
|
||||
By registering for, or using this service, you confirm that you understand and agree to the terms below. If you do not agree to these terms, you should
|
||||
not use the service.
|
||||
</p>
|
||||
<p class="mt-1">
|
||||
If you have any questions or concerns, please contact me at:{" "}
|
||||
<a href="mailto:hello@trafficlunar.net" class="text-blue-700"> hello@trafficlunar.net </a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<ul class="list-decimal ml-5 marker:text-xl marker:font-semibold">
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Usage Policy</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">As a user of this site, you must abide by these guidelines:</p>
|
||||
<ul class="list-disc list-inside indent-4">
|
||||
<li>Nothing that would interfere with or gain unauthorized access to the website or its systems.</li>
|
||||
<li>Nothing that is against the law in the United Kingdom.</li>
|
||||
<li>No NSFW, violent, gory, or inappropriate Miis or images.</li>
|
||||
<li>No spam.</li>
|
||||
<li>No impersonation of others.</li>
|
||||
<li>No malware, malicious links, or phishing content.</li>
|
||||
<li>No harassment, hate speech, threats, or bullying towards others.</li>
|
||||
<li>Miis must be high quality: for example, not following all instructions on the submit form correctly.</li>
|
||||
<li>Avoid using inappropriate language. Profanity may be automatically censored.</li>
|
||||
<li>No use of automated scripts, bots, or scrapers to access or interact with the site.</li>
|
||||
</ul>
|
||||
<p class="mt-2">
|
||||
If you find anybody or a Mii breaking these rules, please report it by going to their page and clicking the "Report" button.
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Termination</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
We reserve the right to suspend or terminate your access to the site at any time if you violate these Terms of Service or engage in any activities
|
||||
that disrupt the functionality of the site.
|
||||
</p>
|
||||
<p>
|
||||
To request deletion of your account and personal data, please refer to the{" "}
|
||||
<a href="/privacy" class="text-blue-700"> Privacy Policy </a>{" "}
|
||||
(see "Data Deletion") or email me at{" "}
|
||||
<a href="mailto:hello@trafficlunar.net" class="text-blue-700"> hello@trafficlunar.net </a>
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Eligibility</h3>
|
||||
<section>
|
||||
<p class="mb-2">By using this service, you confirm that you are at least 13 years old or have the consent of a parent or guardian.</p>
|
||||
</section>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Liability</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
This service is provided "as is" and without any warranties. We are not responsible for any user-generated content or the actions of users
|
||||
on the site. You use the site at your own risk.
|
||||
</p>
|
||||
<p>
|
||||
We do not guarantee continuous or secure access to the service and are not liable for any damages resulting from interruptions, loss of data, or
|
||||
unauthorized access.
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">DMCA & Copyright</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
If you believe that content uploaded to this site infringes on your copyright, you may submit a DMCA takedown request by emailing{" "}
|
||||
<a href="mailto:hello@trafficlunar.net" class="text-blue-700"> hello@trafficlunar.net </a>{" "}
|
||||
or by reporting the Mii on its page.
|
||||
</p>
|
||||
<p class="mb-2">Please include:</p>
|
||||
<ul class="list-disc list-inside indent-4">
|
||||
<li>Your name and contact information</li>
|
||||
<li>A description of the copyrighted work</li>
|
||||
<li>A link to the allegedly infringing material</li>
|
||||
<li>A statement that you have a good faith belief that the use is not authorized</li>
|
||||
<li>
|
||||
A statement that the information in the notice is accurate and, under penalty of perjury, that you are authorized to act on behalf of the
|
||||
copyright owner
|
||||
</li>
|
||||
<li>Your electronic or physical signature</li>
|
||||
</ul>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Nintendo Disclaimer</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
This site is not affiliated with, endorsed by, or associated with Nintendo in any way. "Mii" and all related character designs are
|
||||
trademarks of Nintendo Co., Ltd.
|
||||
</p>
|
||||
<p>
|
||||
All Mii-related content is shared by users under the assumption that it does not violate any third-party rights. If you believe your rights have
|
||||
been infringed, please see the DMCA section above.
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl font-semibold mt-6 mb-2">Changes to this Terms of Service</h3>
|
||||
|
||||
<section>
|
||||
<p class="mb-2">
|
||||
This Terms of Service may be updated from time to time. We encourage you to review the terms periodically to stay informed about the use of the
|
||||
site. We may notify users via a site banner or other means if changes are made to the Terms of Service.
|
||||
</p>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Layout>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { atom } from "nanostores";
|
||||
|
||||
interface SessionData {
|
||||
user?: {
|
||||
id: string;
|
||||
image: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const session = atom<SessionData | null>(null);
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
10
next.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
67
package.json
|
|
@ -1,13 +1,58 @@
|
|||
{
|
||||
"name": "tomodachi-share",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.30.3"
|
||||
"name": "tomodachi-share",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6935
pnpm-lock.yaml
|
|
@ -1,4 +0,0 @@
|
|||
packages:
|
||||
- "backend"
|
||||
- "frontend"
|
||||
- "shared"
|
||||
|
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 |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 645 B After Width: | Height: | Size: 645 B |
|
Before Width: | Height: | Size: 873 KiB After Width: | Height: | Size: 873 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |