From 271f9e6b3b87aa3798097a6295f34f2c2e3acd32 Mon Sep 17 00:00:00 2001 From: wheval Date: Fri, 27 Mar 2026 21:50:02 +0100 Subject: [PATCH] feat: add ShareStreakCard modal component and wire to streak page --- frontend/app/streak/page.tsx | 82 +------- frontend/components/ShareStreakCard.tsx | 245 ++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 80 deletions(-) create mode 100644 frontend/components/ShareStreakCard.tsx diff --git a/frontend/app/streak/page.tsx b/frontend/app/streak/page.tsx index a1bb8de8..520ff052 100644 --- a/frontend/app/streak/page.tsx +++ b/frontend/app/streak/page.tsx @@ -4,6 +4,7 @@ import React, { useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import ShareStreakCard from "@/components/ShareStreakCard"; export interface StreakData { [date: string]: { @@ -265,86 +266,7 @@ const StreakSummaryCard: React.FC = ({ ); }; -interface ShareStreakModalProps { - streakCount: number; - onClose: () => void; -} - -const ShareStreakModal: React.FC = ({ streakCount, onClose }) => { - const shareOptions = [ - { label: "Contacts", icon: "👤" }, - { label: "Telegram", icon: "✈️" }, - { label: "Twitter", icon: "𝕏" }, - { label: "Whatsapp", icon: "💬" }, - { label: "E-mail", icon: "✉️", highlight: true }, - { label: "More", icon: "⋯" }, - ]; - - return ( -
- {/* Backdrop */} -
- - {/* Share card preview */} -
-
-
-

I'm on a

-
-
- {streakCount} -
-
-

day streak!

-

mind block

-
-
- - - - - -
-
-
- {/* Bottom sheet */} -
-
- -

Share Your Streak

-
-
- -
- {shareOptions.map((opt) => ( - - ))} -
-
-
- ); -}; interface StreakNavbarProps { streakCount: number; @@ -478,7 +400,7 @@ export default function StreakPage() { {/* Share Modal */} {showShare && ( - setShowShare(false)} /> diff --git a/frontend/components/ShareStreakCard.tsx b/frontend/components/ShareStreakCard.tsx new file mode 100644 index 00000000..29d066b0 --- /dev/null +++ b/frontend/components/ShareStreakCard.tsx @@ -0,0 +1,245 @@ +"use client"; + +import React, { useCallback, useEffect, useId, useRef, useState } from "react"; +import Image from "next/image"; + +interface ShareStreakCardProps { + streakCount: number; + username?: string; + onClose?: () => void; +} + +interface ShareOption { + label: string; + icon: React.ReactNode; +} + +export default function ShareStreakCard({ + streakCount, + username, + onClose, +}: ShareStreakCardProps) { + const EXIT_ANIMATION_MS = 220; + const modalRef = useRef(null); + const closeButtonRef = useRef(null); + const titleId = useId(); + const descriptionId = useId(); + const [isClosing, setIsClosing] = useState(false); + const [isEntering, setIsEntering] = useState(true); + const shareOptions: ShareOption[] = [ + { + label: "Contacts", + icon: ( + + + + + ), + }, + { + label: "Telegram", + icon: ( + + + + + ), + }, + { + label: "Twitter", + icon: ( + + + + + ), + }, + { + label: "Whatsapp", + icon: ( + + + + + ), + }, + { + label: "E-mail", + icon: ( + + + + ), + }, + { + label: "More", + icon: ( + + + + + + + + + + + ), + }, + ]; + + const handleClose = useCallback(() => { + if (isClosing) return; + setIsClosing(true); + window.setTimeout(() => { + onClose?.(); + }, EXIT_ANIMATION_MS); + }, [isClosing, onClose]); + + useEffect(() => { + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + const enterFrame = window.requestAnimationFrame(() => { + setIsEntering(false); + }); + + const timer = setTimeout(() => { + closeButtonRef.current?.focus(); + }, 50); + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + handleClose(); + } + }; + + document.addEventListener("keydown", handleEscape); + + return () => { + window.cancelAnimationFrame(enterFrame); + clearTimeout(timer); + document.removeEventListener("keydown", handleEscape); + document.body.style.overflow = previousOverflow; + }; + }, [handleClose]); + + const handleTabKey = useCallback((event: React.KeyboardEvent) => { + if (event.key !== "Tab" || !modalRef.current) return; + + const focusable = modalRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + + if (focusable.length === 0) return; + + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + return; + } + + if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + }, []); + + const shareText = username + ? `${username} is on a ${streakCount} day streak!` + : `I'm on a ${streakCount} day streak!`; + + return ( +
+
+
+
event.stopPropagation()} + > +
+
+
+

+ I'm on a + + {streakCount} + + day streak! +

+

+ {shareText} +

+
+ Streak flame +
+

mind block

+
+
+
+ +
event.stopPropagation()} + > +
+
+ +

Share Your Streak

+
+
+
+ {shareOptions.map((option) => ( + + ))} +
+
+
+
+
+ ); +}