tomodachi-share/src/app/mii/[id]/page.tsx

205 lines
5.9 KiB
TypeScript

import { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
import { Icon } from "@iconify/react";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import Carousel from "@/components/carousel";
import LikeButton from "@/components/like-button";
import ImageViewer from "@/components/image-viewer";
import DeleteMiiButton from "@/components/delete-mii";
import ScanTutorialButton from "@/components/tutorial/scan";
interface Props {
params: Promise<{ id: string }>;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params;
const mii = await prisma.mii.findUnique({
where: {
id: Number(id),
},
include: {
user: {
select: {
username: true,
},
},
_count: {
select: { likedBy: true }, // Get total like count
},
},
});
// Bots get redirected anyways
if (!mii) return {};
const miiImageUrl = `/mii/${mii.id}/image?type=mii`;
const qrCodeUrl = `/mii/${mii.id}/image?type=qr-code`;
const username = `@${mii.user.username}`;
return {
metadataBase: new URL(process.env.BASE_URL!),
title: `${mii.name} - TomodachiShare`,
description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${username} on TomodachiShare. From ${mii.islandName} Island with ${mii._count.likedBy} likes.`,
keywords: [`mii`, `tomodachi life`, `nintendo`, ...mii.tags],
creator: username,
category: "Gaming",
openGraph: {
locale: "en_US",
type: "article",
images: [miiImageUrl, qrCodeUrl],
siteName: "TomodachiShare",
publishedTime: mii.createdAt.toISOString(),
authors: username,
},
twitter: {
card: "summary_large_image",
title: `${mii.name} - TomodachiShare`,
description: `Check out '${mii.name}', a Tomodachi Life Mii created by ${username} on TomodachiShare. From ${mii.islandName} Island with ${mii._count.likedBy} likes.`,
images: [miiImageUrl, qrCodeUrl],
creator: username,
},
alternates: {
canonical: `/mii/${mii.id}`,
},
};
}
export default async function MiiPage({ params }: Props) {
const { id } = await params;
const session = await auth();
const mii = await prisma.mii.findUnique({
where: {
id: Number(id),
},
include: {
user: {
select: {
username: true,
},
},
likedBy: session?.user
? {
where: {
userId: Number(session.user.id),
},
select: { userId: true },
}
: false,
_count: {
select: { likedBy: true }, // Get total like count
},
},
});
if (!mii) redirect("/404");
const images = [
`/mii/${mii.id}/image?type=mii`,
`/mii/${mii.id}/image?type=qr-code`,
...Array.from({ length: mii.imageCount }, (_, index) => `/mii/${mii.id}/image?type=image${index}`),
];
return (
<div>
<div className="relative grid grid-cols-5 gap-2 max-sm:grid-cols-1 max-lg:grid-cols-2">
{/* Carousel */}
<div className="min-w-full flex justify-center col-span-2 max-lg:col-span-1">
<Carousel images={images} className="shadow-lg" />
</div>
{/* Information */}
<div className="flex flex-col gap-1 p-4 col-span-2 max-lg:col-span-1">
<h1 className="text-4xl font-extrabold break-words">{mii.name}</h1>
<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) => (
<Link href={{ pathname: "/", query: { tags: tag } }} key={tag}>
{tag}
</Link>
))}
</div>
<div className="mt-2">
<Link href={`/profile/${mii.userId}`} className="text-lg">
By: <span className="font-bold">@{mii.user.username}</span>
</Link>
<h4 title={`${mii.createdAt.toLocaleTimeString("en-GB", { timeZone: "UTC" })} UTC`}>
Created: {mii.createdAt.toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })}
</h4>
</div>
<div className="mt-auto">
<LikeButton
likes={mii._count.likedBy ?? 0}
miiId={mii.id}
isLiked={(mii.likedBy ?? []).length > 0}
isLoggedIn={session?.user != null}
big
/>
</div>
</div>
{/* Extra information */}
<div className="flex flex-col gap-2">
<section className="p-6 bg-orange-100 rounded-2xl shadow-lg border-2 border-orange-400 h-min">
<legend className="text-lg font-semibold mb-2">Mii Info</legend>
<ul className="text-sm *:flex *:justify-between *:items-center *:my-1">
<li>
Name:{" "}
<span className="text-right">
{mii.firstName} {mii.lastName}
</span>
</li>
<li>
From: <span className="text-right">{mii.islandName} Island</span>
</li>
<li>
Allowed Copying: <input type="checkbox" checked={mii.allowedCopying} disabled className="checkbox !cursor-auto" />
</li>
</ul>
</section>
<div className="flex gap-1 text-4xl justify-end text-orange-400">
{session && (Number(session.user.id) === mii.userId || Number(session.user.id) === Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID)) && (
<>
<Link href={`/edit/${mii.id}`} title="Edit Mii" data-tooltip="Edit" className="aspect-square">
<Icon icon="mdi:pencil" />
</Link>
<DeleteMiiButton miiId={mii.id} miiName={mii.name} likes={mii._count.likedBy ?? 0} />
</>
)}
<Link href={`/report/mii/${mii.id}`} title="Report Mii" data-tooltip="Report" className="aspect-square">
<Icon icon="material-symbols:flag-rounded" />
</Link>
<ScanTutorialButton />
</div>
</div>
</div>
{/* Images */}
<div className="overflow-x-scroll">
<div className="flex gap-2 w-max py-4">
{images.map((src, index) => (
<ImageViewer
key={index}
src={src}
alt="mii screenshot"
width={256}
height={170}
className="rounded-xl bg-zinc-300 border-2 border-zinc-300 shadow-md aspect-[3/2] h-full object-contain"
images={images}
/>
))}
</div>
</div>
</div>
);
}