aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-04-16 22:31:08 -0700
committerPinapelz <yukais@pinapelz.com>2026-04-16 22:32:11 -0700
commit284739d21c8c46ced93eba458083f379d0d134b0 (patch)
tree2b04035205e08b25d536b79063fbc9c550e78980
parentfcf822c966df58ae08a491aababdd6bb2bb0fea4 (diff)
type: lrc-type index page
-rw-r--r--public/typing.json16
-rw-r--r--src/app/page.tsx2
-rw-r--r--src/app/typing/page.styles.ts174
-rw-r--r--src/app/typing/page.tsx168
4 files changed, 359 insertions, 1 deletions
diff --git a/public/typing.json b/public/typing.json
new file mode 100644
index 0000000..4a42174
--- /dev/null
+++ b/public/typing.json
@@ -0,0 +1,16 @@
+{
+ "K-POP": [
+ {
+ "title": "CRAZY (English)",
+ "artist": "LE SSERAFIM",
+ "thumbnail": "https://file.garden/aeFyzu6P_R_1-oSm/LRC-TYPE/CRAZY%20(ENGLISH)%20LE%20SSERAFIM/iI5hnXYo5as-HD.jpg",
+ "code": "eyJmaWxlMSI6Imh0dHBzOi8vZmlsZS5nYXJkZW4vYWVGeXp1NlBfUl8xLW9TbS9MUkMtVFlQRS9DUkFaWSUyMChFTkdMSVNIKSUyMExFJTIwU1NFUkFGSU0vTEUlMjBTU0VSQUZJTSUyMCglRUIlQTUlQjQlRUMlODQlQjglRUIlOUQlQkMlRUQlOTUlOEMpJTIwJ0NSQVpZJTIwKEVuZ2xpc2glMjB2ZXIuKSclMjBPRkZJQ0lBTCUyME1WJTIwJTVCaUk1aG5YWW81YXMlNUQud2VibSIsImxyYyI6Imh0dHBzOi8vZmlsZS5nYXJkZW4vYWVGeXp1NlBfUl8xLW9TbS9MUkMtVFlQRS9DUkFaWSUyMChFTkdMSVNIKSUyMExFJTIwU1NFUkFGSU0vY3JhenkyLmxyYyIsIm9mZnNldCI6LTEzMDAsInRpdGxlIjoiQ1JBWlkgKEVOR0xJU0gpIiwiYXJ0aXN0IjoiTEUgU1NFUkFGSU0iLCJza2lwX2JhY2tpbmciOnRydWV9"
+ },
+ {
+ "title": "1-800-hot-n-fun",
+ "artist": "LE SSERAFIM",
+ "thumbnail": "https://file.garden/aeFyzu6P_R_1-oSm/LRC-TYPE/LE%20SSERAFIM%201800-hotnfun/rGD5U8u1Dk0-HD.jpg",
+ "code": "eyJmaWxlMSI6Imh0dHBzOi8vZmlsZS5nYXJkZW4vYWVGeXp1NlBfUl8xLW9TbS9MUkMtVFlQRS9MRSUyMFNTRVJBRklNJTIwMTgwMC1ob3RuZnVuLzEtODAwLWhvdC1uLWZ1biUyMCU1QnJHRDVVOHUxRGswJTVELndlYm0iLCJscmMiOiJodHRwczovL2ZpbGUuZ2FyZGVuL2FlRnl6dTZQX1JfMS1vU20vTFJDLVRZUEUvTEUlMjBTU0VSQUZJTSUyMDE4MDAtaG90bmZ1bi8xODAwaG90bmZ1bi5scmMiLCJvZmZzZXQiOjAsInRpdGxlIjoiMS04MDAtaG90LW4tZnVuIiwiYXJ0aXN0IjoiTEUgU1NFUkFGSU0iLCJza2lwX2JhY2tpbmciOnRydWV9"
+ }
+ ]
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 0670509..1471a31 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -110,7 +110,7 @@ export default function HomePage() {
</NavCenter>
<NavRight>
- <NavCtaLink href="/game">LRC-Type</NavCtaLink>
+ <NavCtaLink href="/typing">LRC-Type</NavCtaLink>
<NavCtaLink href="/create">Create</NavCtaLink>
<Avatar>
<FaUserCircle />
diff --git a/src/app/typing/page.styles.ts b/src/app/typing/page.styles.ts
new file mode 100644
index 0000000..521fe7a
--- /dev/null
+++ b/src/app/typing/page.styles.ts
@@ -0,0 +1,174 @@
+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,
+ Avatar as BaseAvatar,
+ 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 Avatar = styled(BaseAvatar)`
+ color: #9aa0ad;
+ &:hover {
+ 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..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