diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-04-16 22:31:08 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-04-16 22:32:11 -0700 |
| commit | 284739d21c8c46ced93eba458083f379d0d134b0 (patch) | |
| tree | 2b04035205e08b25d536b79063fbc9c550e78980 | |
| parent | fcf822c966df58ae08a491aababdd6bb2bb0fea4 (diff) | |
type: lrc-type index page
| -rw-r--r-- | public/typing.json | 16 | ||||
| -rw-r--r-- | src/app/page.tsx | 2 | ||||
| -rw-r--r-- | src/app/typing/page.styles.ts | 174 | ||||
| -rw-r--r-- | src/app/typing/page.tsx | 168 |
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> + </> + ); +} |
