feat: profile pages
This commit is contained in:
parent
961ec9bfcd
commit
61088950d9
4 changed files with 194 additions and 103 deletions
133
src/app/components/mii-list.tsx
Normal file
133
src/app/components/mii-list.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default async function ProfileOverview() {
|
export default async function ProfileOverview() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li title="Your profile">
|
<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
|
<Image
|
||||||
src={session?.user?.image ?? "/missing.webp"}
|
src={session?.user?.image ?? "/missing.webp"}
|
||||||
alt="profile picture"
|
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"
|
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>
|
<span className="pr-4 overflow-hidden whitespace-nowrap text-ellipsis w-full">{session?.user?.username ?? "unknown"}</span>
|
||||||
</button>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
src/app/page.tsx
111
src/app/page.tsx
|
|
@ -1,113 +1,22 @@
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import MiiList from "./components/mii-list";
|
||||||
|
|
||||||
import LikeButton from "./components/like-button";
|
|
||||||
|
|
||||||
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
export default async function Page({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
||||||
const session = await auth();
|
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) {
|
if (session?.user && !session.user.username) {
|
||||||
redirect("/create-username");
|
redirect("/create-username");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// await prisma.mii.create({
|
||||||
<div className="w-full">
|
// data: {
|
||||||
<div className="flex justify-between items-end mb-2">
|
// userId: 1,
|
||||||
<p className="text-lg">
|
// name: "Himmel",
|
||||||
{totalMiiCount == shownMiiCount ? (
|
// pictures: ["https://placehold.co/600x400", "/missing.webp"],
|
||||||
<>
|
// tags: ["Anime", "Osaka"],
|
||||||
<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">
|
return <MiiList searchParams={searchParams} />;
|
||||||
{/* 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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
src/app/profile/[slug]/page.tsx
Normal file
48
src/app/profile/[slug]/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue