aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/MVPlayer
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/MVPlayer')
-rw-r--r--src/components/MVPlayer/index.styled.ts73
-rw-r--r--src/components/MVPlayer/index.tsx85
2 files changed, 158 insertions, 0 deletions
diff --git a/src/components/MVPlayer/index.styled.ts b/src/components/MVPlayer/index.styled.ts
new file mode 100644
index 0000000..ca8a06e
--- /dev/null
+++ b/src/components/MVPlayer/index.styled.ts
@@ -0,0 +1,73 @@
+import styled from "styled-components";
+
+export const Frame = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ margin: 16px 0;
+`;
+
+export const Images = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+`;
+
+export const ImageSlot = styled.div<{ $revealed: boolean }>`
+ position: relative;
+ width: 200px;
+ height: 113px;
+ background-color: var(--cl-gray-2);
+ border: 1px solid ${({ theme }) => theme.border};
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ }
+`;
+
+export const Placeholder = styled.p`
+ margin: 0;
+ font-family: "Roboto Mono", monospace;
+ font-size: 0.7rem;
+ color: var(--cl-gray-5);
+ text-align: center;
+ padding: 0 8px;
+`;
+
+export const Hint = styled.p`
+ margin: 0;
+ font-family: "Roboto Mono", monospace;
+ font-size: 0.75rem;
+ color: var(--cl-gray-6);
+`;
+
+export const Overlay = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.85);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ cursor: zoom-out;
+`;
+
+export const LightboxImage = styled.img`
+ max-width: 90vw;
+ max-height: 90vh;
+ object-fit: contain;
+ cursor: default;
+`;
diff --git a/src/components/MVPlayer/index.tsx b/src/components/MVPlayer/index.tsx
new file mode 100644
index 0000000..a1141ab
--- /dev/null
+++ b/src/components/MVPlayer/index.tsx
@@ -0,0 +1,85 @@
+import React from "react";
+import * as Styled from "./index.styled";
+
+interface Props {
+ currentTry: number;
+ date: string;
+}
+
+const MV_CDN_URL = import.meta.env.VITE_CDN_URL || "";
+
+function getRevealedCount(currentTry: number): number {
+ if (currentTry >= 6) return 3;
+ if (currentTry >= 3) return 2;
+ return 1;
+}
+
+export function MVPlayer({ currentTry, date }: Props) {
+ const revealed = getRevealedCount(currentTry);
+ const [errored, setErrored] = React.useState<Set<number>>(new Set());
+
+ const [activeImage, setActiveImage] = React.useState<string | null>(null);
+
+ const markError = React.useCallback((index: number) => {
+ setErrored((prev) => {
+ const next = new Set(prev);
+ next.add(index);
+ return next;
+ });
+ }, []);
+
+ React.useEffect(() => {
+ setErrored(new Set());
+ setActiveImage(null);
+ }, [date]);
+
+ return (
+ <Styled.Frame>
+ <Styled.Images>
+ {Array.from({ length: 3 }, (_, i) => {
+ const index = i + 1;
+ const isRevealed = index <= revealed;
+ const src = `${MV_CDN_URL}/k-heardle-mvs/${date}/${index}.jpg`;
+ const hasError = errored.has(index);
+
+ return (
+ <Styled.ImageSlot key={index} $revealed={isRevealed}>
+ {isRevealed && !hasError ? (
+ <img
+ src={src}
+ alt={`MV frame ${index}`}
+ onError={() => markError(index)}
+ onClick={() => setActiveImage(src)}
+ style={{ cursor: "zoom-in" }}
+ />
+ ) : isRevealed && hasError ? (
+ <Styled.Placeholder>
+ Frame {index} not available yet.
+ </Styled.Placeholder>
+ ) : (
+ <Styled.Placeholder>?</Styled.Placeholder>
+ )}
+ </Styled.ImageSlot>
+ );
+ })}
+ </Styled.Images>
+
+ <Styled.Hint>
+ {revealed < 3
+ ? `More frames unlock as you guess. (${revealed}/3 revealed)`
+ : "All frames revealed."}
+ </Styled.Hint>
+
+ {/* Lightbox */}
+ {activeImage && (
+ <Styled.Overlay onClick={() => setActiveImage(null)}>
+ <Styled.LightboxImage
+ src={activeImage}
+ alt="Full size MV frame"
+ onClick={(e) => e.stopPropagation()}
+ />
+ </Styled.Overlay>
+ )}
+ </Styled.Frame>
+ );
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage