feat: profile pages

This commit is contained in:
trafficlunar 2025-03-30 18:41:59 +01:00
parent 961ec9bfcd
commit 61088950d9
4 changed files with 194 additions and 103 deletions

View file

@ -0,0 +1,133 @@
import { Prisma } from "@prisma/client";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import LikeButton from "./like-button";
import Link from "next/link";
interface Props {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
userId?: number;
}
export default async function MiiList({ searchParams, userId }: Props) {
const session = await auth();
const resolvedSearchParams = await searchParams;
// sort search param
const orderBy: { createdAt?: Prisma.SortOrder; likes?: Prisma.SortOrder } = {};
if (resolvedSearchParams.sort === "newest") {
orderBy.createdAt = "desc";
} else if (resolvedSearchParams.sort === "likes") {
orderBy.likes = "desc";
} else {
orderBy.createdAt = "desc"; // Default to newest if no valid sort is provided
}
// tag search param
const rawTags = resolvedSearchParams.tags;
const tagFilter =
typeof rawTags === "string"
? rawTags
.split(",")
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0)
: [];
const whereTags = tagFilter.length > 0 ? { tags: { hasSome: tagFilter } } : undefined;
// If the mii list is on a user's profile, don't query for the username
const include =
userId == null
? {
user: {
select: {
id: true,
username: true,
},
},
}
: {};
const totalMiiCount = await prisma.mii.count({ where: { userId } });
const shownMiiCount = await prisma.mii.count({
where: {
...whereTags,
userId,
},
});
const miis = await prisma.mii.findMany({
where: {
...whereTags,
userId,
},
orderBy,
include,
});
return (
<div className="w-full">
<div className="flex justify-between items-end mb-2">
<p className="text-lg">
{totalMiiCount == shownMiiCount ? (
<>
<span className="font-extrabold">{totalMiiCount}</span> Miis
</>
) : (
<>
<span className="font-extrabold">{shownMiiCount}</span> of <span className="font-extrabold">{totalMiiCount}</span> Miis
</>
)}
</p>
<div className="flex gap-2">
{/* todo: replace with react-select */}
<div className="pill gap-2">
<label htmlFor="sort">Filter:</label>
<span>todo</span>
</div>
<div className="pill gap-2">
<label htmlFor="sort">Sort:</label>
<select name="sort">
<option value="likes">Likes</option>
<option value="newest">Newest</option>
</select>
</div>
</div>
</div>
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-sm:grid-cols-2 max-[25rem]:grid-cols-1">
{miis.map((mii) => (
<div
key={mii.id}
className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600"
>
<img src="https://placehold.co/600x400" alt="mii" className="rounded-xl" />
<div className="p-4 flex flex-col gap-1 h-full">
<h3 className="font-bold text-2xl overflow-hidden text-ellipsis line-clamp-2" title={mii.name}>
{mii.name}
</h3>
<div id="tags" className="flex gap-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
{mii.tags.map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
<div className="mt-auto grid grid-cols-2 items-center">
<LikeButton likes={mii.likes} isLoggedIn={session?.user != null} />
{userId == null && (
<Link href={`/profile/${mii.user.id}`} className="text-sm text-right text-ellipsis">
@{mii.user?.username}
</Link>
)}
</div>
</div>
</div>
))}
</div>
</div>
);
}

View file

@ -1,12 +1,13 @@
import Image from "next/image";
import { auth } from "@/lib/auth";
import Link from "next/link";
export default async function ProfileOverview() {
const session = await auth();
return (
<li title="Your profile">
<button className="pill button !gap-2 !p-0 h-full max-w-64">
<Link href={`/profile/${session?.user.id}`} className="pill button !gap-2 !p-0 h-full max-w-64">
<Image
src={session?.user?.image ?? "/missing.webp"}
alt="profile picture"
@ -15,7 +16,7 @@ export default async function ProfileOverview() {
className="rounded-full aspect-square object-cover h-full outline-2 outline-orange-400"
/>
<span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{session?.user?.username ?? "unknown"}</span>
</button>
</Link>
</li>
);
}

View file

@ -1,113 +1,22 @@
import { redirect } from "next/navigation";
import { Prisma } from "@prisma/client";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import LikeButton from "./components/like-button";
import MiiList from "./components/mii-list";
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
const session = await auth();
const resolvedSearchParams = await searchParams;
// sort search param
const orderBy: { createdAt?: Prisma.SortOrder; likes?: Prisma.SortOrder } = {};
if (resolvedSearchParams.sort === "newest") {
orderBy.createdAt = "desc";
} else if (resolvedSearchParams.sort === "likes") {
orderBy.likes = "desc";
} else {
orderBy.createdAt = "desc"; // Default to newest if no valid sort is provided
}
// tag search param
const rawTags = resolvedSearchParams.tags;
const tagFilter =
typeof rawTags === "string"
? rawTags
.split(",")
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0)
: [];
const where = tagFilter.length > 0 ? { tags: { hasSome: tagFilter } } : undefined;
const totalMiiCount = await prisma.mii.count();
const shownMiiCount = await prisma.mii.count({ where });
const miis = await prisma.mii.findMany({
where: where,
orderBy,
include: {
user: {
select: {
username: true,
},
},
},
});
if (session?.user && !session.user.username) {
redirect("/create-username");
}
return (
<div className="w-full">
<div className="flex justify-between items-end mb-2">
<p className="text-lg">
{totalMiiCount == shownMiiCount ? (
<>
<span className="font-extrabold">{totalMiiCount}</span> Miis
</>
) : (
<>
<span className="font-extrabold">{shownMiiCount}</span> of <span className="font-extrabold">{totalMiiCount}</span> Miis
</>
)}
</p>
// await prisma.mii.create({
// data: {
// userId: 1,
// name: "Himmel",
// pictures: ["https://placehold.co/600x400", "/missing.webp"],
// tags: ["Anime", "Osaka"],
// },
// });
<div className="flex gap-2">
{/* todo: replace with react-select */}
<div className="pill gap-2">
<label htmlFor="sort">Filter:</label>
<span>todo</span>
</div>
<div className="pill gap-2">
<label htmlFor="sort">Sort:</label>
<select name="sort">
<option value="likes">Likes</option>
<option value="newest">Newest</option>
</select>
</div>
</div>
</div>
<div className="grid grid-cols-4 gap-4 max-lg:grid-cols-3 max-sm:grid-cols-2 max-[25rem]:grid-cols-1">
{miis.map((mii) => (
<div
key={mii.id}
className="flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3 transition hover:scale-105 hover:bg-cyan-100 hover:border-cyan-600"
>
<img src="https://placehold.co/600x400" alt="mii" className="rounded-xl" />
<div className="p-4 flex flex-col gap-1 h-full">
<h3 className="font-bold text-2xl overflow-hidden text-ellipsis line-clamp-2" title={mii.name}>
{mii.name}
</h3>
<div id="tags" className="flex gap-1 *:px-2 *:py-1 *:bg-orange-300 *:rounded-full *:text-xs">
{mii.tags.map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
<div className="mt-auto grid grid-cols-2 items-center">
<LikeButton likes={mii.likes} isLoggedIn={session?.user != null} />
<span className="text-sm text-right text-ellipsis">@{mii.user?.username}</span>
</div>
</div>
</div>
))}
</div>
</div>
);
return <MiiList searchParams={searchParams} />;
}

View file

@ -0,0 +1,48 @@
import Image from "next/image";
import { prisma } from "@/lib/prisma";
import MiiList from "@/app/components/mii-list";
interface Props {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function ProfilePage({ params, searchParams }: Props) {
const { slug } = await params;
const user = await prisma.user.findFirst({
where: {
id: Number(slug),
},
});
const likedMiis = await prisma.like.count({ where: { userId: Number(slug) } });
return (
<div>
<div className="flex gap-4">
<Image
src={user?.image ?? "/missing.webp"}
alt="profile picture"
width={128}
height={128}
className="rounded-full border-2 border-amber-500 shadow"
/>
<div className="flex flex-col">
<h1 className="text-4xl font-extrabold">{user?.name}</h1>
<h2 className="text-lg font-semibold">@{user?.username}</h2>
<h4 className="mt-auto">
Liked <span className="font-bold">{likedMiis}</span> Miis
</h4>
<h4 className="text-sm" title={`${user?.createdAt.toLocaleTimeString("en-GB", { timeZone: "UTC" })} UTC`}>
Created: {user?.createdAt.toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })}
</h4>
</div>
</div>
<MiiList searchParams={searchParams} userId={user?.id} />
</div>
);
}