feat: platform filter, filtering redesign, show platform on mii pages
This commit is contained in:
parent
90a6b741be
commit
43c67d75a9
6 changed files with 240 additions and 12 deletions
|
|
@ -64,6 +64,7 @@ body {
|
|||
@apply block;
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
[data-tooltip] {
|
||||
@apply relative z-10;
|
||||
}
|
||||
|
|
@ -81,7 +82,24 @@ body {
|
|||
@apply opacity-100 scale-100;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
/* Fallback Tooltips */
|
||||
[data-tooltip-span] {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
[data-tooltip-span] > .tooltip {
|
||||
@apply absolute left-1/2 top-full mt-2 px-2 py-1 bg-orange-400 border border-orange-400 rounded-md text-sm text-white whitespace-nowrap select-none pointer-events-none shadow-md opacity-0 scale-75 transition-all duration-200 ease-out origin-top -translate-x-1/2 z-[999999];
|
||||
}
|
||||
|
||||
[data-tooltip-span] > .tooltip::before {
|
||||
@apply content-[''] absolute left-1/2 -translate-x-1/2 -top-2 border-4 border-transparent border-b-orange-400;
|
||||
}
|
||||
|
||||
[data-tooltip-span]:hover > .tooltip {
|
||||
@apply opacity-100 scale-100;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-color: #ff8903 transparent;
|
||||
|
|
|
|||
|
|
@ -149,8 +149,59 @@ export default async function MiiPage({ params }: Props) {
|
|||
</ul>
|
||||
)}
|
||||
|
||||
{/* Mii Platform */}
|
||||
<div className={`flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 w-full ${mii.platform !== "THREE_DS" && "mt-2"}`}>
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
<span>Platform</span>
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
</div>
|
||||
|
||||
<div data-tooltip-span title={mii.platform} className="grid grid-cols-2 gap-2 mb-2">
|
||||
<div
|
||||
className={`tooltip !mt-1 ${
|
||||
mii.platform === "THREE_DS"
|
||||
? "!bg-sky-400 !border-sky-400 before:!border-b-sky-400"
|
||||
: "!bg-red-400 !border-red-400 before:!border-b-red-400"
|
||||
}`}
|
||||
>
|
||||
{mii.platform === "THREE_DS" ? "3DS" : "Switch"}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`rounded-xl flex justify-center items-center size-16 text-4xl border-2 shadow-sm ${
|
||||
mii.platform === "THREE_DS" ? "bg-sky-100 border-sky-400" : "bg-white border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Icon icon="cib:nintendo-3ds" className="text-sky-500" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`rounded-xl flex justify-center items-center size-16 text-4xl border-2 shadow-sm ${
|
||||
mii.platform === "SWITCH" ? "bg-red-100 border-red-400" : "bg-white border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Icon icon="cib:nintendo-switch" className="text-red-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mii Gender */}
|
||||
<div className={`grid grid-cols-2 gap-2 ${mii.platform !== "THREE_DS" && "mt-4"}`}>
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium mb-2 w-full">
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
<span>Gender</span>
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
</div>
|
||||
|
||||
<div data-tooltip-span title={mii.gender ?? "NULL"} className="grid grid-cols-2 gap-2">
|
||||
<div
|
||||
className={`tooltip !mt-1 ${
|
||||
mii.gender === "MALE"
|
||||
? "!bg-blue-400 !border-blue-400 before:!border-b-blue-400"
|
||||
: "!bg-pink-400 !border-pink-400 before:!border-b-pink-400"
|
||||
}`}
|
||||
>
|
||||
{mii.gender === "MALE" ? "Male" : "Female"}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`rounded-xl flex justify-center items-center size-16 text-5xl border-2 shadow-sm ${
|
||||
mii.gender === "MALE" ? "bg-blue-100 border-blue-400" : "bg-white border-gray-300"
|
||||
|
|
|
|||
95
src/components/mii-list/filter-menu.tsx
Normal file
95
src/components/mii-list/filter-menu.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
import { MiiGender, MiiPlatform } from "@prisma/client";
|
||||
|
||||
import TagFilter from "./tag-filter";
|
||||
import PlatformSelect from "./platform-select";
|
||||
import GenderSelect from "./gender-select";
|
||||
|
||||
export default function FilterMenu() {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const rawTags = searchParams.get("tags") || "";
|
||||
const platform = (searchParams.get("platform") as MiiPlatform) || undefined;
|
||||
const gender = (searchParams.get("gender") as MiiGender) || undefined;
|
||||
|
||||
const tags = useMemo(
|
||||
() =>
|
||||
rawTags
|
||||
? rawTags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag.length > 0)
|
||||
: [],
|
||||
[rawTags]
|
||||
);
|
||||
|
||||
const [filterCount, setFilterCount] = useState(tags.length);
|
||||
|
||||
// Filter menu button handler
|
||||
const handleClick = () => {
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
// slight delay to trigger animation
|
||||
setTimeout(() => setIsVisible(true), 10);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
setTimeout(() => {
|
||||
setIsOpen(false);
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
// Count all active filters
|
||||
useEffect(() => {
|
||||
let count = tags.length;
|
||||
if (platform) count++;
|
||||
if (gender) count++;
|
||||
|
||||
setFilterCount(count);
|
||||
}, [tags, platform, gender]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button className="pill button gap-2" onClick={handleClick}>
|
||||
<Icon icon="mdi:filter" className="text-xl" />
|
||||
Filter {filterCount !== 0 ? `(${filterCount})` : ""}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`absolute w-80 left-0 top-full mt-8 z-50 flex flex-col items-center bg-orange-50
|
||||
border-2 border-amber-500 rounded-2xl shadow-lg p-4 transition-discrete duration-200 ${
|
||||
isVisible ? "translate-y-0 opacity-100" : "-translate-y-2 opacity-0"
|
||||
}`}
|
||||
>
|
||||
{/* Arrow */}
|
||||
<div className="absolute bottom-full left-1/6 -translate-x-1/2 size-0 border-8 border-transparent border-b-amber-500"></div>
|
||||
|
||||
<TagFilter />
|
||||
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1">
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
<span>Platform</span>
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
</div>
|
||||
<PlatformSelect />
|
||||
|
||||
<div className="flex items-center gap-4 text-zinc-500 text-sm font-medium w-full mt-2 mb-1">
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
<span>Gender</span>
|
||||
<hr className="flex-grow border-zinc-300" />
|
||||
</div>
|
||||
<GenderSelect />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -29,24 +29,28 @@ export default function GenderSelect() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-0.5">
|
||||
<div className="grid grid-cols-2 gap-0.5 w-fit">
|
||||
<button
|
||||
onClick={() => handleClick("MALE")}
|
||||
aria-label="Filter for Male Miis"
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all ${
|
||||
data-tooltip-span
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
|
||||
selected === "MALE" ? "bg-blue-100 border-blue-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
<div className="tooltip !bg-blue-400 !border-blue-400 before:!border-b-blue-400">Male</div>
|
||||
<Icon icon="foundation:male" className="text-blue-400" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleClick("FEMALE")}
|
||||
aria-label="Filter for Female Miis"
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-11 text-4xl border-2 transition-all ${
|
||||
data-tooltip-span
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-5xl border-2 transition-all ${
|
||||
selected === "FEMALE" ? "bg-pink-100 border-pink-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
<div className="tooltip !bg-pink-400 !border-pink-400 before:!border-b-pink-400">Female</div>
|
||||
<Icon icon="foundation:female" className="text-pink-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { MiiGender, Prisma } from "@prisma/client";
|
||||
import { MiiGender, MiiPlatform, Prisma } from "@prisma/client";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { z } from "zod";
|
||||
|
||||
|
|
@ -10,8 +10,7 @@ import { querySchema } from "@/lib/schemas";
|
|||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
import GenderSelect from "./gender-select";
|
||||
import TagFilter from "./tag-filter";
|
||||
import FilterMenu from "./filter-menu";
|
||||
import SortSelect from "./sort-select";
|
||||
import Carousel from "../carousel";
|
||||
import LikeButton from "../like-button";
|
||||
|
|
@ -36,6 +35,7 @@ const searchSchema = z.object({
|
|||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag.length > 0)
|
||||
),
|
||||
platform: z.enum(MiiPlatform, { error: "Platform must be either 'THREE_DS', or 'SWITCH'" }).optional(),
|
||||
gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', or 'FEMALE'" }).optional(),
|
||||
// todo: incorporate tagsSchema
|
||||
// Pages
|
||||
|
|
@ -60,7 +60,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
const parsed = searchSchema.safeParse(searchParams);
|
||||
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
||||
|
||||
const { q: query, sort, tags, gender, page = 1, limit = 24, seed } = parsed.data;
|
||||
const { q: query, sort, tags, platform, gender, page = 1, limit = 24, seed } = parsed.data;
|
||||
|
||||
// My Likes page
|
||||
let miiIdsLiked: number[] | undefined = undefined;
|
||||
|
|
@ -82,6 +82,8 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
}),
|
||||
// Tag filtering
|
||||
...(tags && tags.length > 0 && { tags: { hasEvery: tags } }),
|
||||
// Platform
|
||||
...(platform && { platform: { equals: platform } }),
|
||||
// Gender
|
||||
...(gender && { gender: { equals: gender } }),
|
||||
// Profiles
|
||||
|
|
@ -99,6 +101,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
},
|
||||
},
|
||||
}),
|
||||
platform: true,
|
||||
name: true,
|
||||
imageCount: true,
|
||||
tags: true,
|
||||
|
|
@ -201,9 +204,8 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 w-full min-[56rem]:max-w-2/3 max-[56rem]:justify-center max-sm:flex-col">
|
||||
<GenderSelect />
|
||||
<TagFilter />
|
||||
<div className="relative flex items-center justify-end gap-2 w-full min-[56rem]:max-w-2/3 max-[56rem]:justify-center max-sm:flex-col">
|
||||
<FilterMenu />
|
||||
<SortSelect />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
58
src/components/mii-list/platform-select.tsx
Normal file
58
src/components/mii-list/platform-select.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { MiiPlatform } from "@prisma/client";
|
||||
|
||||
export default function PlatformSelect() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
const [selected, setSelected] = useState<MiiPlatform | null>((searchParams.get("platform") as MiiPlatform) ?? null);
|
||||
|
||||
const handleClick = (platform: MiiPlatform) => {
|
||||
const filter = selected === platform ? null : platform;
|
||||
setSelected(filter);
|
||||
|
||||
const params = new URLSearchParams(searchParams);
|
||||
if (filter) {
|
||||
params.set("platform", filter);
|
||||
} else {
|
||||
params.delete("platform");
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
router.push(`?${params.toString()}`);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-0.5 w-fit">
|
||||
<button
|
||||
onClick={() => handleClick("THREE_DS")}
|
||||
aria-label="Filter for 3DS Miis"
|
||||
data-tooltip-span
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-3xl border-2 transition-all ${
|
||||
selected === "THREE_DS" ? "bg-sky-100 border-sky-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
<div className="tooltip !bg-sky-400 !border-sky-400 before:!border-b-sky-400">3DS</div>
|
||||
<Icon icon="cib:nintendo-3ds" className="text-sky-400" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleClick("SWITCH")}
|
||||
aria-label="Filter for Switch Miis"
|
||||
data-tooltip-span
|
||||
className={`cursor-pointer rounded-xl flex justify-center items-center size-13 text-3xl border-2 transition-all ${
|
||||
selected === "SWITCH" ? "bg-red-100 border-red-400 shadow-md" : "bg-white border-gray-300 hover:border-gray-400"
|
||||
}`}
|
||||
>
|
||||
<div className="tooltip !bg-red-400 !border-red-400 before:!border-b-red-400">Switch</div>
|
||||
<Icon icon="cib:nintendo-switch" className="text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue