import React from "react"; import { useNavigate } from "react-router-dom"; import styled from "styled-components"; import { appName } from "../constants"; import { createGroup, getStoredGroupMembership, joinGroupByToken, saveGroupMembership, } from "../helpers/group"; const getColor = (variant?: "pink" | "purple" | "cyan") => { switch (variant) { case "purple": return "var(--cl-magenta-7)"; case "cyan": return "var(--cl-cyan-6)"; default: return "var(--cl-magenta-8)"; } }; const Container = styled.div` display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 70vh; padding: 24px; gap: 18px; `; const Hero = styled.div` display: flex; flex-direction: column; align-items: center; gap: 10px; text-align: center; `; const HeroImage = styled.div` width: clamp(180px, 28vw, 260px); height: clamp(180px, 28vw, 260px); border: 2px solid var(--cl-gray-3); background-color: var(--cl-gray-2); display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 6px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06); img { width: 100%; height: 100%; object-fit: cover; display: block; } `; const Title = styled.h1` font-family: "Roboto Mono", monospace; font-size: clamp(1.75rem, 4vw, 2.5rem); font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; margin: 0; text-align: center; color: var(--cl-gray-9); `; const Subtitle = styled.p` font-family: "Roboto Mono", monospace; font-size: 0.9rem; color: var(--cl-gray-7); margin: 0; text-align: center; letter-spacing: 0.05em; `; const ModeGroups = styled.div` display: flex; flex-direction: column; gap: 22px; width: 100%; max-width: 600px; `; const ButtonGroup = styled.div` display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; @media (max-width: 480px) { flex-direction: column; align-items: stretch; } `; const GroupLabel = styled.span` display: block; font-family: "Roboto Mono", monospace; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--cl-gray-6); text-align: center; margin-bottom: 10px; `; const ModeButton = styled.button<{ variant?: "pink" | "purple" | "cyan" }>` font-family: "Roboto Mono", monospace; font-size: 1rem; font-weight: 600; padding: 16px 28px; min-width: 180px; border: 2px solid ${({ variant }) => getColor(variant)}; background: transparent; color: ${({ variant }) => getColor(variant)}; cursor: pointer; transition: background-color 0.12s ease, color 0.12s ease; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; &:hover { background: ${({ variant }) => getColor(variant)}; color: var(--cl-white, #fff); } &:focus-visible { outline: 2px solid ${({ variant }) => getColor(variant)}; outline-offset: 2px; } @media (max-width: 480px) { width: 100%; min-width: unset; } `; const ModeDescription = styled.span` font-size: 0.72rem; font-weight: 400; color: var(--cl-gray-6); margin-top: 6px; `; const GroupJoinCard = styled.div` width: 100%; max-width: 440px; border: 1px solid var(--cl-gray-4); border-radius: 8px; padding: 14px; display: flex; flex-direction: column; gap: 10px; `; const ExpandButton = styled.button` width: 100%; max-width: 440px; border: 1px solid var(--cl-gray-4); border-radius: 8px; background: transparent; color: var(--cl-white); padding: 12px 14px; font-family: "Roboto Mono", monospace; font-size: 0.88rem; font-weight: 700; text-align: left; cursor: pointer; &:hover { border-color: var(--cl-cyan-6); } `; const GroupJoinTitle = styled.h3` margin: 0; font-family: "Roboto Mono", monospace; font-size: 0.9rem; font-weight: 700; color: var(--cl-white); `; const GroupJoinRow = styled.div` display: flex; gap: 10px; flex-wrap: wrap; `; const GroupInput = styled.input` flex: 1; min-width: 180px; background: transparent; border: 1px solid var(--cl-gray-4); color: var(--cl-white); padding: 10px; font-family: "Roboto Mono", monospace; font-size: 0.8rem; `; const JoinButton = styled.button` border: 1px solid var(--cl-cyan-6); color: var(--cl-cyan-6); background: transparent; padding: 10px 14px; font-family: "Roboto Mono", monospace; font-size: 0.8rem; font-weight: 600; cursor: pointer; &:hover { background: var(--cl-cyan-6); color: var(--cl-white, #fff); } `; const GroupStatus = styled.p` margin: 0; font-family: "Roboto Mono", monospace; font-size: 0.76rem; color: var(--cl-gray-7); `; const Footer = styled.div` margin-top: 8px; display: flex; align-items: center; gap: 8px; font-family: "Roboto Mono", monospace; font-size: 0.8rem; `; const GitHubLink = styled.a` display: inline-flex; align-items: center; gap: 6px; color: var(--cl-gray-7); text-decoration: none; padding: 6px 12px; border: 1px solid var(--cl-gray-3); transition: color 0.12s ease, border-color 0.12s ease; &:hover { color: var(--cl-magenta-7); border-color: var(--cl-magenta-7); } `; const GitHubIcon = styled.svg` width: 16px; height: 16px; fill: currentColor; `; export function LandingPage() { const navigate = useNavigate(); const initialMembership = React.useMemo(() => getStoredGroupMembership(), []); const [currentMembership, setCurrentMembership] = React.useState(initialMembership); const [groupName, setGroupName] = React.useState(""); const [joinToken, setJoinToken] = React.useState( initialMembership?.joinToken ?? "" ); const [username, setUsername] = React.useState( initialMembership?.username ?? localStorage.getItem("groupUsername") ?? "" ); const [joinMessage, setJoinMessage] = React.useState(""); const [createMessage, setCreateMessage] = React.useState(""); const [isJoining, setIsJoining] = React.useState(false); const [isCreating, setIsCreating] = React.useState(false); const [isCreateOpen, setIsCreateOpen] = React.useState(false); const [isJoinOpen, setIsJoinOpen] = React.useState(false); const createNewGroup = React.useCallback(async () => { if (!groupName.trim() || !username.trim()) { setCreateMessage("Enter group name and username."); return; } setIsCreating(true); try { const membership = await createGroup(groupName, username); saveGroupMembership(membership); setCurrentMembership(membership); localStorage.setItem("groupUsername", membership.username); setJoinToken(membership.joinToken ?? ""); setCreateMessage( `Created ${membership.groupName}. Join token: ${membership.joinToken}` ); } catch (error) { setCreateMessage( error instanceof Error ? error.message : "Unable to create group." ); } finally { setIsCreating(false); } }, [groupName, username]); const joinGroup = React.useCallback(async () => { if (!joinToken.trim() || !username.trim()) { setJoinMessage("Enter both token and username."); return; } setIsJoining(true); try { const membership = await joinGroupByToken(joinToken, username); saveGroupMembership(membership); setCurrentMembership(membership); localStorage.setItem("groupUsername", membership.username); setJoinMessage(`Joined ${membership.groupName} as ${membership.username}`); } catch (error) { setJoinMessage( error instanceof Error ? error.message : "Unable to join group." ); } finally { setIsJoining(false); } }, [joinToken, username]); return ( {appName} NMIXX Haewon Stress PNG a kpop music guessing game
Song Guessing navigate("/daily")}> Daily One song per day navigate("/unlimited")}> Unlimited Endless songs, no limits
Music Video Guessing navigate("/mv")}> Daily MV Guess the MV from frames
setIsCreateOpen((open) => !open)}> {isCreateOpen ? "▾" : "▸"} Create Group {isCreateOpen && ( Create Group setGroupName(event.target.value)} maxLength={64} /> setUsername(event.target.value)} maxLength={32} /> {isCreating ? "Creating..." : "Create"} {createMessage && {createMessage}} )} setIsJoinOpen((open) => !open)}> {isJoinOpen ? "▾" : "▸"} Join Group {isJoinOpen && ( Join a Group setJoinToken(event.target.value.toUpperCase())} maxLength={12} /> setUsername(event.target.value)} maxLength={32} /> {isJoining ? "Joining..." : "Join"} {currentMembership && ( Current group: {currentMembership.groupName} ({currentMembership.username}) )} {joinMessage && {joinMessage}} )}
); }