aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/typing/page.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/typing/page.tsx')
-rw-r--r--src/app/typing/page.tsx168
1 files changed, 168 insertions, 0 deletions
diff --git a/src/app/typing/page.tsx b/src/app/typing/page.tsx
new file mode 100644
index 0000000..3037c32
--- /dev/null
+++ b/src/app/typing/page.tsx
@@ -0,0 +1,168 @@
+"use client";
+import { useEffect, useState } from "react";
+import { FaPlay, FaMusic, FaSearch, FaUserCircle } from "react-icons/fa";
+import { MdLibraryMusic } from "react-icons/md";
+import {
+ Root,
+ Navbar,
+ Logo,
+ LogoIcon,
+ NavCtaLink,
+ NavLeft,
+ NavCenter,
+ SearchBox,
+ SearchInput,
+ SearchButton,
+ NavRight,
+ Avatar,
+ ChipsBar,
+ Chip,
+ GridContainer,
+ CardGrid,
+ Card,
+ ThumbnailWrapper,
+ Thumbnail,
+ PlayOverlay,
+ PlayCircle,
+ CardMeta,
+ CardInfo,
+ CardTitle,
+ CardSub,
+ EmptyState,
+ CtaSection,
+ SectionHeading,
+ OpenPlayerLink,
+ PlayerDescription,
+ TypingGlobalStyle,
+} from "./page.styles";
+
+interface TypingEntry {
+ title: string;
+ artist: string;
+ thumbnail: string;
+ code: string;
+}
+
+type TypingData = Record<string, TypingEntry[]>;
+
+function capitalize(s: string) {
+ return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
+export default function TypingPage() {
+ const [data, setData] = useState<TypingData>({});
+ const [activeChip, setActiveChip] = useState("all");
+ const [search, setSearch] = useState("");
+
+ useEffect(() => {
+ fetch("/typing.json")
+ .then((r) => r.json())
+ .then((json: TypingData) => setData(json))
+ .catch(() => {});
+ }, []);
+
+ const categories = Object.keys(data);
+ const chips = [
+ { key: "all", label: "All" },
+ ...categories.map((category) => ({
+ key: category,
+ label: capitalize(category),
+ })),
+ ];
+
+ const visibleItems: TypingEntry[] =
+ activeChip === "all" ? Object.values(data).flat() : data[activeChip] ?? [];
+
+ const normalizedSearch = search.trim().toLowerCase();
+ const searchableItems = normalizedSearch ? Object.values(data).flat() : visibleItems;
+
+ const filtered = normalizedSearch
+ ? searchableItems.filter(
+ (item) =>
+ item.title.toLowerCase().includes(normalizedSearch) ||
+ item.artist.toLowerCase().includes(normalizedSearch),
+ )
+ : searchableItems;
+
+ return (
+ <>
+ <TypingGlobalStyle />
+ <Root>
+ <Navbar>
+ <NavLeft>
+ <Logo href="/">
+ <LogoIcon>
+ <MdLibraryMusic />
+ </LogoIcon>
+ LRC-Type
+ </Logo>
+ </NavLeft>
+
+ <NavCenter>
+ <SearchBox>
+ <SearchInput
+ placeholder="Search typing charts..."
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ />
+ <SearchButton aria-label="Search">
+ <FaSearch />
+ </SearchButton>
+ </SearchBox>
+ </NavCenter>
+
+ <NavRight>
+ <NavCtaLink href="/">LRC-Karaoke</NavCtaLink>
+ <NavCtaLink href="/create">Create</NavCtaLink>
+ <Avatar>
+ <FaUserCircle />
+ </Avatar>
+ </NavRight>
+ </Navbar>
+
+ <ChipsBar>
+ {chips.map((chip) => (
+ <Chip
+ key={chip.key}
+ $active={chip.key === activeChip}
+ onClick={() => setActiveChip(chip.key)}
+ >
+ {chip.label}
+ </Chip>
+ ))}
+ </ChipsBar>
+
+ <GridContainer>
+ <CardGrid>
+ {filtered.length === 0 ? (
+ <EmptyState>No results found.</EmptyState>
+ ) : (
+ filtered.map((item) => (
+ <Card key={item.code} href={`/game?code=${item.code}`}>
+ <ThumbnailWrapper>
+ {item.thumbnail ? (
+ <Thumbnail src={item.thumbnail} alt={item.title} />
+ ) : (
+ <FaMusic />
+ )}
+ <PlayOverlay>
+ <PlayCircle>
+ <FaPlay />
+ </PlayCircle>
+ </PlayOverlay>
+ </ThumbnailWrapper>
+ <CardMeta>
+ <CardInfo>
+ <CardTitle>{item.title}</CardTitle>
+ <CardSub>{item.artist}</CardSub>
+ </CardInfo>
+ </CardMeta>
+ </Card>
+ ))
+ )}
+ </CardGrid>
+ </GridContainer>
+ </Root>
+ </>
+ );
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage