aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-04-14 22:32:20 -0700
committerPinapelz <yukais@pinapelz.com>2026-04-14 22:32:20 -0700
commit6b168927b8995d428d243052e93713a2ab86cff9 (patch)
treeac617e15fa55fffffba1906b8e37a8ae4b02baed
parent9ce23e2683c6051d15a775a439d355b83036685b (diff)
load entries from public karaoke.json
-rw-r--r--public/karaoke.json12
-rw-r--r--src/app/page.tsx147
2 files changed, 126 insertions, 33 deletions
diff --git a/public/karaoke.json b/public/karaoke.json
new file mode 100644
index 0000000..37ad9c7
--- /dev/null
+++ b/public/karaoke.json
@@ -0,0 +1,12 @@
+{
+ "J-POP": [
+ {
+ "title": "Mr.Raindrop",
+ "artist": "Amplified",
+ "thumbnail": "",
+ "has_srv": false,
+ "has_instrumental": false,
+ "code": "eyJscmMiOiJodHRwczovL3V0ZnMuaW8vZi9lMmUxOGVhNy05ODQxLTQzN2ItOWNhMy01NzIzMzU1YmQ0MWEtcmxjazQ2LmxyYyIsImZpbGUxIjoiaHR0cHM6Ly91dGZzLmlvL2YvODRmNWRmYTYtODIxZC00MDdmLWExNmQtYTY4NWIwOWMxMWQ5LTd4eDJoNC53ZWJtIiwib2Zmc2V0IjotMTU1MH0="
+ }
+ ]
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1b77a15..a278f78 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,10 +1,20 @@
"use client";
-import React, { useState } from "react";
+import { useState, useEffect } from "react";
import styled from "styled-components";
import Link from "next/link";
-import { FaPlay, FaMusic, FaBars, FaSearch, FaUserCircle } from "react-icons/fa";
+import { FaPlay, FaMusic, FaSearch, FaUserCircle } from "react-icons/fa";
import { MdLibraryMusic } from "react-icons/md";
+interface KaraokeEntry {
+ title: string;
+ artist: string;
+ thumbnail: string;
+ has_srv: boolean;
+ has_instrumental: boolean;
+ code: string;
+}
+
+type KaraokeData = Record<string, KaraokeEntry[]>;
const Root = styled.div`
min-height: 100vh;
@@ -182,9 +192,12 @@ const CardGrid = styled.div`
gap: 20px;
`;
-const Card = styled.div`
+const Card = styled(Link)`
cursor: pointer;
border-radius: 14px;
+ text-decoration: none;
+ color: inherit;
+ display: block;
transition: transform 0.15s, box-shadow 0.15s;
&:hover {
transform: translateY(-2px);
@@ -206,6 +219,12 @@ const ThumbnailWrapper = styled.div`
position: relative;
`;
+const Thumbnail = styled.img`
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+`;
+
const PlayOverlay = styled.div`
position: absolute;
inset: 0;
@@ -239,17 +258,34 @@ const PlayCircle = styled.div`
}
`;
+const BadgeRow = styled.div`
+ position: absolute;
+ bottom: 8px;
+ left: 8px;
+ display: flex;
+ gap: 4px;
+`;
+
+const Badge = styled.span<{ $color: string }>`
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.4px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ background-color: ${(p) => p.$color};
+ color: #fff;
+ text-transform: uppercase;
+`;
+
const CardMeta = styled.div`
display: flex;
gap: 12px;
margin-top: 12px;
- padding: 12px 4px;
+ padding: 0 4px 12px;
`;
-
const CardInfo = styled.div`
display: flex;
- padding: 4px;
flex-direction: column;
gap: 3px;
min-width: 0;
@@ -272,7 +308,13 @@ const CardSub = styled.span`
line-height: 1.3;
`;
-/* ── CTA Section ── */
+const EmptyState = styled.div`
+ grid-column: 1 / -1;
+ padding: 48px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909090;
+`;
const CtaSection = styled.div`
padding: 32px 24px;
@@ -312,15 +354,36 @@ const PlayerDescription = styled.p`
max-width: 480px;
`;
-
-const CHIPS = ["All", "Music", "Karaoke", "Live", "J-Pop", "Anime", "Vocaloid", "Recently added"];
-
-const STUB_ITEMS = [
- { title: "Mr.Raindrop - Amplified (Full Karaoke)", artist: "Amplified", uploaded: "2 days ago" },
-];
+function capitalize(s: string) {
+ return s.charAt(0).toUpperCase() + s.slice(1);
+}
export default function HomePage() {
+ const [data, setData] = useState<KaraokeData>({});
const [activeChip, setActiveChip] = useState("All");
+ const [search, setSearch] = useState("");
+
+ useEffect(() => {
+ fetch("/karaoke.json")
+ .then((r) => r.json())
+ .then((json: KaraokeData) => setData(json))
+ .catch(() => {});
+ }, []);
+
+ const categories = Object.keys(data);
+ const chips = ["All", ...categories.map(capitalize)];
+
+ const visibleItems: KaraokeEntry[] = activeChip === "All"
+ ? Object.values(data).flat()
+ : data[activeChip.toLowerCase()] ?? [];
+
+ const filtered = search.trim()
+ ? visibleItems.filter(
+ (item) =>
+ item.title.toLowerCase().includes(search.toLowerCase()) ||
+ item.artist.toLowerCase().includes(search.toLowerCase()),
+ )
+ : visibleItems;
return (
<Root>
@@ -336,7 +399,11 @@ export default function HomePage() {
<NavCenter>
<SearchBox>
- <SearchInput placeholder="Search songs..." />
+ <SearchInput
+ placeholder="Search songs..."
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ />
<SearchButton aria-label="Search">
<FaSearch />
</SearchButton>
@@ -352,7 +419,7 @@ export default function HomePage() {
</Navbar>
<ChipsBar>
- {CHIPS.map((chip) => (
+ {chips.map((chip) => (
<Chip
key={chip}
$active={chip === activeChip}
@@ -365,24 +432,38 @@ export default function HomePage() {
<GridContainer>
<CardGrid>
- {STUB_ITEMS.map((item) => (
- <Card key={item.title}>
- <ThumbnailWrapper>
- <FaMusic />
- <PlayOverlay>
- <PlayCircle>
- <FaPlay />
- </PlayCircle>
- </PlayOverlay>
- </ThumbnailWrapper>
- <CardMeta>
- <CardInfo>
- <CardTitle>{item.title}</CardTitle>
- <CardSub>{item.artist}</CardSub>
- </CardInfo>
- </CardMeta>
- </Card>
- ))}
+ {filtered.length === 0 ? (
+ <EmptyState>No results found.</EmptyState>
+ ) : (
+ filtered.map((item) => (
+ <Card key={item.code} href={`/player?code=${item.code}`}>
+ <ThumbnailWrapper>
+ {item.thumbnail ? (
+ <Thumbnail src={item.thumbnail} alt={item.title} />
+ ) : (
+ <FaMusic />
+ )}
+ <PlayOverlay>
+ <PlayCircle>
+ <FaPlay />
+ </PlayCircle>
+ </PlayOverlay>
+ {(item.has_srv || item.has_instrumental) && (
+ <BadgeRow>
+ {item.has_srv && <Badge $color="#7c3aed">SRV</Badge>}
+ {item.has_instrumental && <Badge $color="#0369a1">Inst.</Badge>}
+ </BadgeRow>
+ )}
+ </ThumbnailWrapper>
+ <CardMeta>
+ <CardInfo>
+ <CardTitle>{item.title}</CardTitle>
+ <CardSub>{item.artist}</CardSub>
+ </CardInfo>
+ </CardMeta>
+ </Card>
+ ))
+ )}
</CardGrid>
</GridContainer>
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage