aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/typing
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-06-01 21:19:05 -0700
committerPinapelz <yukais@pinapelz.com>2026-06-01 21:19:51 -0700
commit0d35e75edbc75f186e4a1ed52fbc3549ee9f5cd6 (patch)
tree90abc1a6d556fc54e4277907dc340736791a5677 /src/app/typing
init commit
Diffstat (limited to 'src/app/typing')
-rw-r--r--src/app/typing/page.styles.ts168
-rw-r--r--src/app/typing/page.tsx161
2 files changed, 329 insertions, 0 deletions
diff --git a/src/app/typing/page.styles.ts b/src/app/typing/page.styles.ts
new file mode 100644
index 0000000..34f4ebc
--- /dev/null
+++ b/src/app/typing/page.styles.ts
@@ -0,0 +1,168 @@
+import styled, { createGlobalStyle } from "styled-components";
+import {
+ Root as BaseRoot,
+ Navbar as BaseNavbar,
+ Logo as BaseLogo,
+ LogoIcon as BaseLogoIcon,
+ NavLink as BaseNavLink,
+ NavCtaLink as BaseNavCtaLink,
+} from "../styles/shared";
+import {
+ NavLeft,
+ NavCenter,
+ SearchBox as BaseSearchBox,
+ SearchInput as BaseSearchInput,
+ SearchButton as BaseSearchButton,
+ NavRight,
+ ChipsBar as BaseChipsBar,
+ Chip as BaseChip,
+ GridContainer,
+ CardGrid,
+ Card as BaseCard,
+ ThumbnailWrapper as BaseThumbnailWrapper,
+ Thumbnail,
+ PlayOverlay,
+ PlayCircle,
+ CardMeta,
+ CardInfo,
+ CardTitle as BaseCardTitle,
+ CardSub as BaseCardSub,
+ EmptyState as BaseEmptyState,
+ CtaSection as BaseCtaSection,
+ SectionHeading as BaseSectionHeading,
+ OpenPlayerLink as BaseOpenPlayerLink,
+ PlayerDescription as BasePlayerDescription,
+} from "../page.styles";
+
+export { NavLeft, NavCenter, NavRight, GridContainer, CardGrid, Thumbnail, PlayOverlay, PlayCircle, CardMeta, CardInfo };
+
+export const TypingGlobalStyle = createGlobalStyle`
+ html,
+ body {
+ background-color: #0b0b10;
+ }
+`;
+
+export const Root = styled(BaseRoot)`
+ background-color: #0b0b10;
+ color: #f5f5f5;
+`;
+
+export const Navbar = styled(BaseNavbar)`
+ background-color: rgba(11, 11, 16, 0.9);
+ border-bottom: 1px solid #1f1f2a;
+`;
+
+export const Logo = styled(BaseLogo)`
+ color: #f5f5f5;
+`;
+
+export const LogoIcon = styled(BaseLogoIcon)`
+ background-color: #f5f5f5;
+ color: #0b0b10;
+`;
+
+export const NavLink = styled(BaseNavLink)`
+ color: #b0b3bd;
+ &:hover {
+ background-color: #1a1d29;
+ color: #fff;
+ }
+`;
+
+export const NavCtaLink = styled(BaseNavCtaLink)`
+ background-color: #1a1d29;
+ border-color: #2a2f3d;
+ color: #fff;
+ &:hover {
+ background-color: #222838;
+ border-color: #3a4154;
+ }
+`;
+
+export const SearchBox = styled(BaseSearchBox)`
+ border-color: #2a2f3d;
+ background-color: #141824;
+ &:focus-within {
+ border-color: #4b5563;
+ }
+`;
+
+export const SearchInput = styled(BaseSearchInput)`
+ color: #f5f5f5;
+ &::placeholder {
+ color: #8b90a0;
+ }
+`;
+
+export const SearchButton = styled(BaseSearchButton)`
+ background-color: #1a1d29;
+ border-left-color: #2a2f3d;
+ color: #c0c4d0;
+ &:hover {
+ background-color: #222838;
+ color: #fff;
+ }
+`;
+
+
+
+export const ChipsBar = styled(BaseChipsBar)`
+ background-color: #0f111a;
+`;
+
+export const Chip = styled(BaseChip)`
+ border-color: #2a2f3d;
+ color: #b8bcc7;
+ background-color: transparent;
+ &:hover {
+ background-color: #1a1d29;
+ color: #fff;
+ }
+`;
+
+export const Card = styled(BaseCard)`
+ border: 1px solid #1f1f2a;
+ background-color: #0f111a;
+ &:hover {
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
+ border-color: #2a2f3d;
+ }
+`;
+
+export const ThumbnailWrapper = styled(BaseThumbnailWrapper)`
+ background-color: #1a1d29;
+ color: #4b5563;
+`;
+
+export const CardTitle = styled(BaseCardTitle)`
+ color: #f5f5f5;
+`;
+
+export const CardSub = styled(BaseCardSub)`
+ color: #9aa0ad;
+`;
+
+export const EmptyState = styled(BaseEmptyState)`
+ color: #9aa0ad;
+`;
+
+export const CtaSection = styled(BaseCtaSection)`
+ border-top-color: #1f1f2a;
+`;
+
+export const SectionHeading = styled(BaseSectionHeading)`
+ color: #f5f5f5;
+`;
+
+export const OpenPlayerLink = styled(BaseOpenPlayerLink)`
+ background-color: #f5f5f5;
+ color: #0b0b10;
+ &:hover {
+ background-color: #e5e7eb;
+ }
+`;
+
+export const PlayerDescription = styled(BasePlayerDescription)`
+ color: #9aa0ad;
+`; \ No newline at end of file
diff --git a/src/app/typing/page.tsx b/src/app/typing/page.tsx
new file mode 100644
index 0000000..27ad173
--- /dev/null
+++ b/src/app/typing/page.tsx
@@ -0,0 +1,161 @@
+"use client";
+import { useEffect, useState } from "react";
+import { FaPlay, FaMusic, FaSearch } from "react-icons/fa";
+import { MdLibraryMusic } from "react-icons/md";
+import {
+ Root,
+ Navbar,
+ Logo,
+ LogoIcon,
+ NavCtaLink,
+ NavLeft,
+ NavCenter,
+ SearchBox,
+ SearchInput,
+ SearchButton,
+ NavRight,
+
+ ChipsBar,
+ Chip,
+ GridContainer,
+ CardGrid,
+ Card,
+ ThumbnailWrapper,
+ Thumbnail,
+ PlayOverlay,
+ PlayCircle,
+ CardMeta,
+ CardInfo,
+ CardTitle,
+ CardSub,
+ EmptyState,
+ 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="/create">Create</NavCtaLink>
+
+ </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}`} target="_blank" rel="noopener noreferrer">
+ <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