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;
|
@apply block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tooltips */
|
||||||
[data-tooltip] {
|
[data-tooltip] {
|
||||||
@apply relative z-10;
|
@apply relative z-10;
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +82,24 @@ body {
|
||||||
@apply opacity-100 scale-100;
|
@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 */
|
/* Firefox */
|
||||||
* {
|
* {
|
||||||
scrollbar-color: #ff8903 transparent;
|
scrollbar-color: #ff8903 transparent;
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,59 @@ export default async function MiiPage({ params }: Props) {
|
||||||
</ul>
|
</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 */}
|
{/* 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
|
<div
|
||||||
className={`rounded-xl flex justify-center items-center size-16 text-5xl border-2 shadow-sm ${
|
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"
|
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 (
|
return (
|
||||||
<div className="grid grid-cols-2 gap-0.5">
|
<div className="grid grid-cols-2 gap-0.5 w-fit">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleClick("MALE")}
|
onClick={() => handleClick("MALE")}
|
||||||
aria-label="Filter for Male Miis"
|
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"
|
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" />
|
<Icon icon="foundation:male" className="text-blue-400" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleClick("FEMALE")}
|
onClick={() => handleClick("FEMALE")}
|
||||||
aria-label="Filter for Female Miis"
|
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"
|
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" />
|
<Icon icon="foundation:female" className="text-pink-400" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { MiiGender, Prisma } from "@prisma/client";
|
import { MiiGender, MiiPlatform, Prisma } from "@prisma/client";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
|
@ -10,8 +10,7 @@ import { querySchema } from "@/lib/schemas";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
import GenderSelect from "./gender-select";
|
import FilterMenu from "./filter-menu";
|
||||||
import TagFilter from "./tag-filter";
|
|
||||||
import SortSelect from "./sort-select";
|
import SortSelect from "./sort-select";
|
||||||
import Carousel from "../carousel";
|
import Carousel from "../carousel";
|
||||||
import LikeButton from "../like-button";
|
import LikeButton from "../like-button";
|
||||||
|
|
@ -36,6 +35,7 @@ const searchSchema = z.object({
|
||||||
.map((tag) => tag.trim())
|
.map((tag) => tag.trim())
|
||||||
.filter((tag) => tag.length > 0)
|
.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(),
|
gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', or 'FEMALE'" }).optional(),
|
||||||
// todo: incorporate tagsSchema
|
// todo: incorporate tagsSchema
|
||||||
// Pages
|
// Pages
|
||||||
|
|
@ -60,7 +60,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
const parsed = searchSchema.safeParse(searchParams);
|
const parsed = searchSchema.safeParse(searchParams);
|
||||||
if (!parsed.success) return <h1>{parsed.error.issues[0].message}</h1>;
|
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
|
// My Likes page
|
||||||
let miiIdsLiked: number[] | undefined = undefined;
|
let miiIdsLiked: number[] | undefined = undefined;
|
||||||
|
|
@ -82,6 +82,8 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
}),
|
}),
|
||||||
// Tag filtering
|
// Tag filtering
|
||||||
...(tags && tags.length > 0 && { tags: { hasEvery: tags } }),
|
...(tags && tags.length > 0 && { tags: { hasEvery: tags } }),
|
||||||
|
// Platform
|
||||||
|
...(platform && { platform: { equals: platform } }),
|
||||||
// Gender
|
// Gender
|
||||||
...(gender && { gender: { equals: gender } }),
|
...(gender && { gender: { equals: gender } }),
|
||||||
// Profiles
|
// Profiles
|
||||||
|
|
@ -99,6 +101,7 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
platform: true,
|
||||||
name: true,
|
name: true,
|
||||||
imageCount: true,
|
imageCount: true,
|
||||||
tags: true,
|
tags: true,
|
||||||
|
|
@ -201,9 +204,8 @@ export default async function MiiList({ searchParams, userId, inLikesPage }: Pro
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<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">
|
||||||
<GenderSelect />
|
<FilterMenu />
|
||||||
<TagFilter />
|
|
||||||
<SortSelect />
|
<SortSelect />
|
||||||
</div>
|
</div>
|
||||||
</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