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 { 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
111
src/app/page.tsx
111
src/app/page.tsx
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
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