aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/create
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/create')
-rw-r--r--src/app/create/page.styles.ts168
-rw-r--r--src/app/create/page.tsx207
2 files changed, 375 insertions, 0 deletions
diff --git a/src/app/create/page.styles.ts b/src/app/create/page.styles.ts
new file mode 100644
index 0000000..b54e095
--- /dev/null
+++ b/src/app/create/page.styles.ts
@@ -0,0 +1,168 @@
+import styled from "styled-components";
+
+export const Content = styled.div`
+ max-width: 600px;
+ margin: 40px auto;
+ padding: 0 24px 60px;
+`;
+
+export const Heading = styled.h1`
+ font-size: 22px;
+ font-weight: 800;
+ margin: 0 0 4px;
+`;
+
+export const Subheading = styled.p`
+ font-size: 13px;
+ color: #909090;
+ margin: 0 0 32px;
+`;
+
+export const Form = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+`;
+
+export const FieldGroup = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+`;
+
+export const Label = styled.label`
+ font-size: 12px;
+ font-weight: 600;
+ color: #606060;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+`;
+
+export const Input = styled.input`
+ height: 40px;
+ padding: 0 12px;
+ border: 1px solid #d4d4d4;
+ border-radius: 8px;
+ font-size: 14px;
+ color: #1a1a1a;
+ background-color: #fff;
+ transition: border-color 0.15s;
+ &:focus {
+ outline: none;
+ border-color: #1a1a1a;
+ }
+ &::placeholder {
+ color: #b0b0b0;
+ }
+`;
+
+export const Divider = styled.div`
+ height: 1px;
+ background-color: #e5e5e5;
+ margin: 6px 0;
+`;
+
+export const Row = styled.div`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+`;
+
+export const GenerateButton = styled.button`
+ height: 42px;
+ padding: 0 24px;
+ border-radius: 10px;
+ border: none;
+ background-color: #1a1a1a;
+ color: #fff;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.15s;
+ margin-top: 6px;
+ &:hover {
+ background-color: #333;
+ }
+`;
+
+export const ModeButton = styled.button<{ $active: boolean }>`
+ height: 42px;
+ padding: 0 24px;
+ border-radius: 10px;
+ border: none;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ background-color: ${(p) => (p.$active ? "#1a1a1a" : "#e5e5e5")};
+ color: ${(p) => (p.$active ? "#fff" : "#1a1a1a")};
+ transition: background-color 0.15s;
+`;
+
+export const OutputSection = styled.div`
+ margin-top: 32px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+`;
+
+export const OutputLabel = styled.div`
+ font-size: 12px;
+ font-weight: 600;
+ color: #606060;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 5px;
+`;
+
+export const CodeBox = styled.div`
+ position: relative;
+ background-color: #f0f0f0;
+ border: 1px solid #d4d4d4;
+ border-radius: 10px;
+ padding: 14px 48px 14px 14px;
+ font-family: "Courier New", monospace;
+ font-size: 13px;
+ color: #1a1a1a;
+ word-break: break-all;
+ line-height: 1.5;
+`;
+
+export const CopyButton = styled.button<{ $copied: boolean }>`
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ width: 30px;
+ height: 30px;
+ border-radius: 6px;
+ border: none;
+ background-color: ${(p) => (p.$copied ? "#22c55e" : "#d4d4d4")};
+ color: ${(p) => (p.$copied ? "#fff" : "#606060")};
+ font-size: 13px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color 0.15s, color 0.15s;
+ &:hover {
+ background-color: ${(p) => (p.$copied ? "#16a34a" : "#c0c0c0")};
+ color: #1a1a1a;
+ }
+`;
+
+export const OpenLink = styled.a`
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ color: #1a1a1a;
+ text-decoration: none;
+ border: 1px solid #d4d4d4;
+ border-radius: 8px;
+ padding: 8px 14px;
+ background-color: #fff;
+ transition: background-color 0.15s;
+ &:hover {
+ background-color: #f0f0f0;
+ }
+`;
diff --git a/src/app/create/page.tsx b/src/app/create/page.tsx
new file mode 100644
index 0000000..744ab95
--- /dev/null
+++ b/src/app/create/page.tsx
@@ -0,0 +1,207 @@
+"use client";
+import { useState } from "react";
+import { MdLibraryMusic } from "react-icons/md";
+import { FaCopy, FaCheck, FaExternalLinkAlt } from "react-icons/fa";
+import { Root, Navbar, Logo, LogoIcon, NavLink } from "../styles/shared";
+import {
+ Content,
+ Heading,
+ Subheading,
+ Form,
+ FieldGroup,
+ Label,
+ Input,
+ Divider,
+ Row,
+ GenerateButton,
+ OutputSection,
+ OutputLabel,
+ CodeBox,
+ CopyButton,
+ OpenLink,
+} from "./page.styles";
+
+interface TypingPayload {
+ file1?: string;
+ lrc?: string;
+ offset?: number;
+ title?: string;
+ artist?: string;
+ skip_backing?: boolean;
+}
+
+export default function CreatePage() {
+ const [lrc, setLrc] = useState("");
+ const [file1, setFile1] = useState("");
+ const [offset, setOffset] = useState("");
+
+ const [typingTitle, setTypingTitle] = useState("");
+ const [typingArtist, setTypingArtist] = useState("");
+ const [skipBacking, setSkipBacking] = useState(true);
+
+ const [code, setCode] = useState<string | null>(null);
+ const [copiedCode, setCopiedCode] = useState(false);
+ const [copiedUrl, setCopiedUrl] = useState(false);
+
+ const resetCopyStates = () => {
+ setCopiedCode(false);
+ setCopiedUrl(false);
+ };
+
+ const generate = () => {
+ const payload: TypingPayload = {};
+ if (file1.trim()) payload.file1 = file1.trim();
+ if (lrc.trim()) payload.lrc = lrc.trim();
+ if (offset.trim() !== "") payload.offset = Number(offset);
+ if (typingTitle.trim()) payload.title = typingTitle.trim();
+ if (typingArtist.trim()) payload.artist = typingArtist.trim();
+ payload.skip_backing = skipBacking;
+
+ setCode(btoa(JSON.stringify(payload)));
+ resetCopyStates();
+ };
+
+ const copy = (text: string, which: "code" | "url") => {
+ navigator.clipboard.writeText(text);
+ if (which === "code") {
+ setCopiedCode(true);
+ setTimeout(() => setCopiedCode(false), 2000);
+ } else {
+ setCopiedUrl(true);
+ setTimeout(() => setCopiedUrl(false), 2000);
+ }
+ };
+
+ const shareUrl = code ? `${window.location.origin}/game?code=${code}` : "";
+
+ return (
+ <Root>
+ <Navbar>
+ <Logo href="/typing">
+ <LogoIcon>
+ <MdLibraryMusic />
+ </LogoIcon>
+ LRC-Type
+ </Logo>
+ <NavLink href="/typing">← Back</NavLink>
+ </Navbar>
+
+ <Content>
+ <Heading>Create a Code</Heading>
+ <Subheading>
+ Generate a shareable code for your typing game session.
+ </Subheading>
+
+ <Form>
+ <FieldGroup>
+ <Label>Primary Media</Label>
+ <Input
+ type="url"
+ placeholder="https://example.com/song.mp4"
+ value={file1}
+ onChange={(e) => setFile1(e.target.value)}
+ />
+ </FieldGroup>
+
+ <FieldGroup>
+ <Label>LRC Lyrics</Label>
+ <Input
+ type="url"
+ placeholder="https://example.com/song.lrc"
+ value={lrc}
+ onChange={(e) => setLrc(e.target.value)}
+ />
+ </FieldGroup>
+
+ <FieldGroup>
+ <Label title="Offset in milliseconds. Increase this value if the main audio is ahead of the lyrics.">
+ LRC Offset (ms)
+ </Label>
+ <Input
+ type="number"
+ placeholder="0"
+ value={offset}
+ onChange={(e) => setOffset(e.target.value)}
+ step="25"
+ />
+ </FieldGroup>
+
+ <Divider />
+
+ <Row>
+ <FieldGroup>
+ <Label>Title</Label>
+ <Input
+ type="text"
+ placeholder="Song Title"
+ value={typingTitle}
+ onChange={(e) => setTypingTitle(e.target.value)}
+ />
+ </FieldGroup>
+ <FieldGroup>
+ <Label>Artist</Label>
+ <Input
+ type="text"
+ placeholder="Artist Name"
+ value={typingArtist}
+ onChange={(e) => setTypingArtist(e.target.value)}
+ />
+ </FieldGroup>
+ </Row>
+
+ <Row>
+ <FieldGroup>
+ <Label title="When enabled, lyrics inside parentheses are treated as backing lyrics and skipped.">
+ Skip Backing
+ </Label>
+ <Input
+ type="checkbox"
+ checked={skipBacking}
+ onChange={(e) => setSkipBacking(e.target.checked)}
+ style={{ width: "18px", height: "18px", marginTop: "10px" }}
+ />
+ </FieldGroup>
+ </Row>
+
+ <GenerateButton onClick={generate}>Generate Code</GenerateButton>
+ </Form>
+
+ {code && (
+ <OutputSection>
+ <div>
+ <OutputLabel>Code</OutputLabel>
+ <CodeBox>
+ {code}
+ <CopyButton
+ $copied={copiedCode}
+ onClick={() => copy(code, "code")}
+ aria-label="Copy code"
+ >
+ {copiedCode ? <FaCheck /> : <FaCopy />}
+ </CopyButton>
+ </CodeBox>
+ </div>
+
+ <div>
+ <OutputLabel>Share URL</OutputLabel>
+ <CodeBox>
+ {shareUrl}
+ <CopyButton
+ $copied={copiedUrl}
+ onClick={() => copy(shareUrl, "url")}
+ aria-label="Copy URL"
+ >
+ {copiedUrl ? <FaCheck /> : <FaCopy />}
+ </CopyButton>
+ </CodeBox>
+ </div>
+
+ <OpenLink href={shareUrl} target="_blank" rel="noopener noreferrer">
+ <FaExternalLinkAlt /> Open in Typing Game
+ </OpenLink>
+ </OutputSection>
+ )}
+ </Content>
+ </Root>
+ );
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage