mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 21:27:46 +00:00
Compare commits
10 commits
4059ada5a7
...
05c6212c7e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05c6212c7e | ||
| 7bd84ea454 | |||
| 18a49a74e8 | |||
|
|
6211adc548 | ||
| 61cbdad812 | |||
| 7f87a42b11 | |||
|
|
d5b488a5c9 | ||
| 8c12a835a6 | |||
|
|
2bbc6d7c05 | ||
| 90a65102c1 |
15 changed files with 182 additions and 33 deletions
|
|
@ -6,7 +6,6 @@ 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
|
||||||
NEXT_PUBLIC_FRONTEND_URL=http://localhost:5173
|
NEXT_PUBLIC_FRONTEND_URL=http://localhost:5173
|
||||||
NEXT_PUBLIC_STATIC_URL=
|
|
||||||
|
|
||||||
CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX
|
CLOUDFLARE_ZONE_ID=XXXXXXXXXXXXXXXX
|
||||||
CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXXXX
|
CLOUDFLARE_API_TOKEN=XXXXXXXXXXXXXXXX
|
||||||
|
|
|
||||||
|
|
@ -246,8 +246,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
files: [
|
files: [
|
||||||
`${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}`,
|
`${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}`,
|
||||||
`${process.env.NEXT_PUBLIC_STATIC_URL}/mii/${miiId}/mii.png`,
|
`${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}/image?type=mii`,
|
||||||
`${process.env.NEXT_PUBLIC_STATIC_URL}/mii/${miiId}/features.png`,
|
`${process.env.NEXT_PUBLIC_BASE_URL}/mii/${miiId}/image?type=features`,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
|
|
||||||
|
|
@ -34,5 +34,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!mii) return NextResponse.json({ error: "Mii not found" }, { status: 404 });
|
||||||
|
|
||||||
return NextResponse.json(mii);
|
return NextResponse.json(mii);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!user) return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||||
|
|
||||||
return NextResponse.json(user);
|
return NextResponse.json(user);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
115
backend/src/app/mii/[id]/image/route.ts
Normal file
115
backend/src/app/mii/[id]/image/route.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
import { generateMetadataImage } from "@/lib/images";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
const searchParamsSchema = z.object({
|
||||||
|
type: z
|
||||||
|
.enum(["mii", "qr-code", "features", "image0", "image1", "image2", "metadata"], {
|
||||||
|
message: "Image type must be either 'mii', 'qr-code', 'features', 'image[number from 0 to 2]' or 'metadata'",
|
||||||
|
})
|
||||||
|
.default("mii"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const rateLimit = new RateLimit(request, 200, "/mii/image");
|
||||||
|
const check = await rateLimit.handle();
|
||||||
|
if (check) return check;
|
||||||
|
|
||||||
|
const { id: slugId } = await params;
|
||||||
|
const parsed = idSchema.safeParse(slugId);
|
||||||
|
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
||||||
|
const miiId = parsed.data;
|
||||||
|
|
||||||
|
const searchParamsParsed = searchParamsSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams));
|
||||||
|
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400);
|
||||||
|
const { type: imageType } = searchParamsParsed.data;
|
||||||
|
|
||||||
|
const filePath = path.join(process.cwd(), "uploads", "mii", miiId.toString(), `${imageType}.png`);
|
||||||
|
|
||||||
|
let buffer: Buffer | undefined;
|
||||||
|
// Only find Mii if image type is 'metadata'
|
||||||
|
let mii: Prisma.MiiGetPayload<{
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
name: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}> | null = null;
|
||||||
|
|
||||||
|
if (imageType === "metadata") {
|
||||||
|
mii = await prisma.mii.findUnique({
|
||||||
|
where: {
|
||||||
|
id: miiId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mii) {
|
||||||
|
return rateLimit.sendResponse({ error: "Mii not found" }, 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to read file
|
||||||
|
buffer = await fs.readFile(filePath);
|
||||||
|
} catch {
|
||||||
|
// If the readFile() fails, that probably means it doesn't exist
|
||||||
|
if (imageType === "metadata" && mii) {
|
||||||
|
// Metadata images were added after 1274 Miis were submitted, so we generate it on-the-fly
|
||||||
|
console.log(`Metadata image not found for mii ID ${miiId}, generating metadata image...`);
|
||||||
|
const { buffer: metadataBuffer, error, status } = await generateMetadataImage(mii, mii.user.name!);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return rateLimit.sendResponse({ error }, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = metadataBuffer;
|
||||||
|
} else {
|
||||||
|
return rateLimit.sendResponse({ error: "Image not found" }, 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buffer) return rateLimit.sendResponse({ error: "Image not found" }, 404);
|
||||||
|
|
||||||
|
// Set the file name for the metadata image in the response for SEO
|
||||||
|
if (mii && imageType === "metadata") {
|
||||||
|
const slugify = (str: string) =>
|
||||||
|
str
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-") // replace non-alphanumeric with hyphens
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
|
||||||
|
const name = slugify(mii.name);
|
||||||
|
const tags = mii.tags.map(slugify).join("-");
|
||||||
|
|
||||||
|
const filename = `${name}-mii-${tags}.png`;
|
||||||
|
|
||||||
|
return rateLimit.sendResponse(buffer, 200, {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Content-Disposition": `inline; filename="${filename}"`,
|
||||||
|
"Cache-Control": "public, max-age=31536000",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rateLimit.sendResponse(buffer, 200, {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"X-Robots-Tag": "noindex, noimageindex, nofollow",
|
||||||
|
"Cache-Control": "public, max-age=60, stale-while-revalidate=30",
|
||||||
|
});
|
||||||
|
}
|
||||||
27
backend/src/app/profile/[id]/picture/route.ts
Normal file
27
backend/src/app/profile/[id]/picture/route.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
import { idSchema } from "@tomodachi-share/shared/schemas";
|
||||||
|
import { RateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const rateLimit = new RateLimit(request, 16, "/profile/picture");
|
||||||
|
const check = await rateLimit.handle();
|
||||||
|
if (check) return check;
|
||||||
|
|
||||||
|
const { id: slugId } = await params;
|
||||||
|
const parsed = idSchema.safeParse(slugId);
|
||||||
|
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
||||||
|
const userId = parsed.data;
|
||||||
|
|
||||||
|
const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.png`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await fs.readFile(filePath);
|
||||||
|
return new NextResponse(new Uint8Array(buffer)); // convert to Uint8Array due to weird types issue
|
||||||
|
} catch {
|
||||||
|
return rateLimit.sendResponse({ error: "Image not found" }, 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,8 @@ export const revalidate = 43200; // update every 12 hours
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
|
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
|
||||||
const staticUrl = process.env.NEXT_PUBLIC_STATIC_URL;
|
if (!baseUrl) {
|
||||||
if (!baseUrl || !staticUrl) {
|
console.error("NEXT_PUBLIC_BASE_URL environment variable missing");
|
||||||
console.error("NEXT_PUBLIC_BASE_URL or NEXT_PUBLIC_STATIC_URL environment variable missing");
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +34,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
lastModified: mii.createdAt,
|
lastModified: mii.createdAt,
|
||||||
changeFrequency: "weekly",
|
changeFrequency: "weekly",
|
||||||
priority: 0.7,
|
priority: 0.7,
|
||||||
images: [`${staticUrl}/mii/${mii.id}/metadata.png`],
|
images: [`${baseUrl}/mii/${mii.id}/image?type=metadata`],
|
||||||
}) as SitemapRoute,
|
}) as SitemapRoute,
|
||||||
),
|
),
|
||||||
...users.map(
|
...users.map(
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
},
|
},
|
||||||
|
|
||||||
async redirect({ url, baseUrl }) {
|
async redirect({ url, baseUrl }) {
|
||||||
|
if (url.startsWith(baseUrl)) return url;
|
||||||
return process.env.NEXT_PUBLIC_FRONTEND_URL ?? "http://localhost:4321";
|
return process.env.NEXT_PUBLIC_FRONTEND_URL ?? "http://localhost:4321";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes, inMiiPage }: Pr
|
||||||
<p className="text-sm text-zinc-500">Are you sure? This will delete your Mii permanently. This action cannot be undone.</p>
|
<p className="text-sm text-zinc-500">Are you sure? This will delete your Mii permanently. This action cannot be undone.</p>
|
||||||
|
|
||||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 mt-4 flex overflow-hidden">
|
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 mt-4 flex overflow-hidden">
|
||||||
<img src={`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/mii.png`} alt="mii image" width={128} height={128} />
|
<img src={`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=mii`} alt="mii image" width={128} height={128} />
|
||||||
<div className="p-4 min-w-0">
|
<div className="p-4 min-w-0">
|
||||||
<p className="text-xl font-bold line-clamp-3 wrap-anywhere" title={miiName}>
|
<p className="text-xl font-bold line-clamp-3 wrap-anywhere" title={miiName}>
|
||||||
{miiName}
|
{miiName}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export default function MiiList({ parentPage, userId }: Props) {
|
||||||
{parentPage !== "admin" ? (
|
{parentPage !== "admin" ? (
|
||||||
<Link to={`/mii/${mii.id}`} className="overflow-hidden rounded-xl bg-zinc-300 shrink-0">
|
<Link to={`/mii/${mii.id}`} className="overflow-hidden rounded-xl bg-zinc-300 shrink-0">
|
||||||
<img
|
<img
|
||||||
src={`${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/mii.png`}
|
src={`${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`}
|
||||||
width={240}
|
width={240}
|
||||||
height={160}
|
height={160}
|
||||||
alt="mii image"
|
alt="mii image"
|
||||||
|
|
@ -136,11 +136,11 @@ export default function MiiList({ parentPage, userId }: Props) {
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 gap-1 rounded-xl bg-zinc-200">
|
<div className="grid grid-cols-2 gap-1 rounded-xl bg-zinc-200">
|
||||||
{[
|
{[
|
||||||
`${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/mii.png`,
|
`${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=mii`,
|
||||||
mii.platform === "THREE_DS"
|
mii.platform === "THREE_DS"
|
||||||
? `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/qr-code.png`
|
? `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=qr-code`
|
||||||
: `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/features.png`,
|
: `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=features`,
|
||||||
...Array.from({ length: mii.imageCount }, (_, i) => `${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/image${i}.png`),
|
...Array.from({ length: mii.imageCount }, (_, i) => `${import.meta.env.VITE_API_URL}/mii/${mii.id}/image?type=image${i}`),
|
||||||
].map((src, i) => (
|
].map((src, i) => (
|
||||||
<img key={i} src={src} alt="mii image" className="w-full bg-zinc-200" />
|
<img key={i} src={src} alt="mii image" className="w-full bg-zinc-200" />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export default function ShareMiiButton({ miiId }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyImage = async () => {
|
const handleCopyImage = async () => {
|
||||||
const response = await fetch(`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/metadata.png`);
|
const response = await fetch(`${import.meta.env.VITE_BASE_URL}/mii/${miiId}/image?type=metadata`);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
|
|
||||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||||||
|
|
@ -118,7 +118,7 @@ export default function ShareMiiButton({ miiId }: Props) {
|
||||||
|
|
||||||
<div className="flex justify-center items-center p-4 w-full bg-orange-100 border border-orange-400 rounded-lg">
|
<div className="flex justify-center items-center p-4 w-full bg-orange-100 border border-orange-400 rounded-lg">
|
||||||
<img
|
<img
|
||||||
src={`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/metadata.png`}
|
src={`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=metadata`}
|
||||||
alt="mii 'metadata' image"
|
alt="mii 'metadata' image"
|
||||||
width={248}
|
width={248}
|
||||||
height={248}
|
height={248}
|
||||||
|
|
@ -130,7 +130,7 @@ export default function ShareMiiButton({ miiId }: Props) {
|
||||||
<div className="flex gap-2 w-full">
|
<div className="flex gap-2 w-full">
|
||||||
{/* Save button */}
|
{/* Save button */}
|
||||||
<Link
|
<Link
|
||||||
to={`${import.meta.env.VITE_STATIC_URL}/mii/${miiId}/metadata.png`}
|
to={`${import.meta.env.VITE_API_URL}/mii/${miiId}/image?type=metadata`}
|
||||||
className="pill button p-0! aspect-square size-11 cursor-pointer text-xl"
|
className="pill button p-0! aspect-square size-11 cursor-pointer text-xl"
|
||||||
aria-label="Save Image"
|
aria-label="Save Image"
|
||||||
data-tooltip="Save Image"
|
data-tooltip="Save Image"
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ export default function EditMiiPage() {
|
||||||
try {
|
try {
|
||||||
const existing = await Promise.all(
|
const existing = await Promise.all(
|
||||||
Array.from({ length: mii.imageCount }, async (_, index) => {
|
Array.from({ length: mii.imageCount }, async (_, index) => {
|
||||||
const path = `${STATIC_URL}/mii/${mii.id}/image${index}.png`;
|
const path = `${API_URL}/mii/${mii.id}/image?type=image${index}`;
|
||||||
const response = await fetch(path);
|
const response = await fetch(path);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
|
@ -167,7 +167,6 @@ export default function EditMiiPage() {
|
||||||
}, [mii, mii?.id, mii?.imageCount]);
|
}, [mii, mii?.id, mii?.imageCount]);
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
const STATIC_URL = import.meta.env.VITE_STATIC_URL;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${API_URL}/api/mii/${id}/info`)
|
fetch(`${API_URL}/api/mii/${id}/info`)
|
||||||
|
|
@ -182,8 +181,8 @@ export default function EditMiiPage() {
|
||||||
setDescription(data.description);
|
setDescription(data.description);
|
||||||
setGender(data.gender ?? "MALE");
|
setGender(data.gender ?? "MALE");
|
||||||
setMakeup(data.makeup ?? "PARTIAL");
|
setMakeup(data.makeup ?? "PARTIAL");
|
||||||
setMiiPortraitUri(`${STATIC_URL}/mii/${data.id}/mii.png`);
|
setMiiPortraitUri(`${API_URL}/mii/${data.id}/image?type=mii`);
|
||||||
setMiiFeaturesUri(`${STATIC_URL}/mii/${data.id}/features.png`);
|
setMiiFeaturesUri(`${API_URL}/mii/${data.id}/image?type=features`);
|
||||||
setYouTubeId(data.youtubeId ?? "");
|
setYouTubeId(data.youtubeId ?? "");
|
||||||
setQuarantined(data.quarantined);
|
setQuarantined(data.quarantined);
|
||||||
instructions.current = deepMerge(defaultInstructions, (data.instructions as object) ?? {});
|
instructions.current = deepMerge(defaultInstructions, (data.instructions as object) ?? {});
|
||||||
|
|
@ -209,8 +208,10 @@ export default function EditMiiPage() {
|
||||||
<div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
|
<div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
|
||||||
<Carousel
|
<Carousel
|
||||||
images={[
|
images={[
|
||||||
miiPortraitUri ?? `${STATIC_URL}/mii/${mii.id}/mii.png`,
|
miiPortraitUri ?? `${API_URL}/mii/${mii.id}/image?type=mii`,
|
||||||
...(mii.platform === "THREE_DS" ? [`${STATIC_URL}/mii/${mii.id}/qr-code.png`] : [miiFeaturesUri ?? `${STATIC_URL}/mii/${mii.id}/features.png`]),
|
...(mii.platform === "THREE_DS"
|
||||||
|
? [`${API_URL}/mii/${mii.id}/image?type=qr-code`]
|
||||||
|
: [miiFeaturesUri ?? `${API_URL}/mii/${mii.id}/image?type=features`]),
|
||||||
...files.map((file) => URL.createObjectURL(file)),
|
...files.map((file) => URL.createObjectURL(file)),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ export default function MiiPage() {
|
||||||
const [isLiked, setIsLiked] = useState(false);
|
const [isLiked, setIsLiked] = useState(false);
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
const STATIC_URL = import.meta.env.VITE_STATIC_URL;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${API_URL}/api/mii/${id}/info`)
|
fetch(`${API_URL}/api/mii/${id}/info`)
|
||||||
|
|
@ -32,6 +31,8 @@ export default function MiiPage() {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (!data) throw new Error("Mii not found");
|
||||||
|
|
||||||
setMii(data);
|
setMii(data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
|
@ -51,7 +52,7 @@ export default function MiiPage() {
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
if (loading || !mii) return <div className="p-6 text-center">Loading...</div>;
|
if (loading || !mii) return <div className="p-6 text-center">Loading...</div>;
|
||||||
const images = [...Array.from({ length: mii.imageCount ?? 0 }, (_, index) => `${STATIC_URL}/mii/${mii.id}/image${index}.png`)];
|
const images = [...Array.from({ length: mii.imageCount ?? 0 }, (_, index) => `${API_URL}/mii/${mii.id}/image?type=image${index}`)];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
|
|
@ -77,7 +78,7 @@ export default function MiiPage() {
|
||||||
{/* Mii Image */}
|
{/* Mii Image */}
|
||||||
<div className="bg-linear-to-b from-amber-100 to-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center">
|
<div className="bg-linear-to-b from-amber-100 to-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center">
|
||||||
<ImageViewer
|
<ImageViewer
|
||||||
src={`${STATIC_URL}/mii/${mii.id}/mii.png`}
|
src={`${API_URL}/mii/${mii.id}/image?type=mii`}
|
||||||
alt="mii headshot"
|
alt="mii headshot"
|
||||||
width={250}
|
width={250}
|
||||||
height={250}
|
height={250}
|
||||||
|
|
@ -88,7 +89,7 @@ export default function MiiPage() {
|
||||||
{mii.platform === "THREE_DS" ? (
|
{mii.platform === "THREE_DS" ? (
|
||||||
<div className="bg-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center p-2">
|
<div className="bg-amber-200 overflow-hidden rounded-xl w-full mb-4 flex justify-center p-2">
|
||||||
<ImageViewer
|
<ImageViewer
|
||||||
src={`${STATIC_URL}/mii/${mii.id}/qr-code.png`}
|
src={`${API_URL}/mii/${mii.id}/image?type=qr-code`}
|
||||||
alt="mii qr code"
|
alt="mii qr code"
|
||||||
width={128}
|
width={128}
|
||||||
height={128}
|
height={128}
|
||||||
|
|
@ -97,7 +98,7 @@ export default function MiiPage() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ImageViewer
|
<ImageViewer
|
||||||
src={`${STATIC_URL}/mii/${mii.id}/features.png`}
|
src={`${API_URL}/mii/${mii.id}/image?type=features`}
|
||||||
alt="mii features"
|
alt="mii features"
|
||||||
width={300}
|
width={300}
|
||||||
height={300}
|
height={300}
|
||||||
|
|
@ -270,7 +271,7 @@ export default function MiiPage() {
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
<div id="tags" className="flex flex-wrap gap-1 mt-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
|
||||||
{mii.tags.map((tag: string) => (
|
{mii.tags.map((tag: string) => (
|
||||||
<Link to={`/tags=${tag}`} key={tag}>
|
<Link to={`/?tags=${encodeURIComponent(tag)}`} key={tag}>
|
||||||
{tag}
|
{tag}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ export default function ProfileLayout() {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (!data) throw new Error("Profile not found");
|
||||||
|
|
||||||
setUser(data);
|
setUser(data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
|
|
@ -42,11 +44,11 @@ export default function ProfileLayout() {
|
||||||
return <div className="p-6 text-center">Loading...</div>;
|
return <div className="p-6 text-center">Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUser = user ?? $session?.user;
|
const sessionUserId = $session?.user?.id ? Number($session.user.id) : null;
|
||||||
const page = location.pathname;
|
const page = location.pathname;
|
||||||
const isAdmin = currentUser?.id === Number(import.meta.env.VITE_ADMIN_USER_ID);
|
const isAdmin = sessionUserId === Number(import.meta.env.VITE_ADMIN_USER_ID);
|
||||||
const isContributor = import.meta.env.VITE_CONTRIBUTORS_USER_IDS?.split(",").includes(user?.id);
|
const isContributor = import.meta.env.VITE_CONTRIBUTORS_USER_IDS?.split(",").includes(String(user?.id));
|
||||||
const isOwnProfile = currentUser?.id === user?.id;
|
const isOwnProfile = sessionUserId === user?.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export default function ReportMiiPage() {
|
||||||
<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">
|
||||||
<img src={`${import.meta.env.VITE_STATIC_URL}/mii/${mii.id}/mii.png`} alt="mii image" width={128} height={128} />
|
<img src={`${import.meta.env.VITE_API_URL}/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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue