diff --git a/web/package-lock.json b/web/package-lock.json index 15bd003f3..de42a1ac7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -37,6 +37,7 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", "embla-carousel-react": "^8.2.0", + "framer-motion": "^11.5.4", "hls.js": "^1.5.14", "idb-keyval": "^6.2.1", "immer": "^10.1.1", @@ -4717,6 +4718,31 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.5.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.5.4.tgz", + "integrity": "sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", diff --git a/web/package.json b/web/package.json index 148d74919..bb15ea4b8 100644 --- a/web/package.json +++ b/web/package.json @@ -43,6 +43,7 @@ "copy-to-clipboard": "^3.3.3", "date-fns": "^3.6.0", "embla-carousel-react": "^8.2.0", + "framer-motion": "^11.5.4", "hls.js": "^1.5.14", "idb-keyval": "^6.2.1", "immer": "^10.1.1", diff --git a/web/src/components/mobile/MobilePage.tsx b/web/src/components/mobile/MobilePage.tsx new file mode 100644 index 000000000..8dbcc2f61 --- /dev/null +++ b/web/src/components/mobile/MobilePage.tsx @@ -0,0 +1,121 @@ +import { cn } from "@/lib/utils"; +import { isPWA } from "@/utils/isPWA"; +import { ReactNode, useEffect, useState } from "react"; +import { Button } from "../ui/button"; +import { IoMdArrowRoundBack } from "react-icons/io"; +import { motion, AnimatePresence } from "framer-motion"; + +type MobilePageProps = { + children: ReactNode; + open: boolean; + onOpenChange: (open: boolean) => void; +}; + +export function MobilePage({ children, open, onOpenChange }: MobilePageProps) { + const [isVisible, setIsVisible] = useState(open); + + useEffect(() => { + if (open) { + setIsVisible(true); + } + }, [open]); + + const handleAnimationComplete = () => { + if (!open) { + setIsVisible(false); + onOpenChange(false); + } + }; + + return ( + + {isVisible && ( + + {children} + + )} + + ); +} + +type MobileComponentProps = { + children: ReactNode; + className?: string; +}; + +export function MobilePageContent({ + children, + className, + ...props +}: MobileComponentProps) { + return ( +
+ {children} +
+ ); +} + +export function MobilePageDescription({ + children, + className, + ...props +}: MobileComponentProps) { + return ( +

+ {children} +

+ ); +} + +interface MobilePageHeaderProps extends React.HTMLAttributes { + onClose: () => void; +} + +export function MobilePageHeader({ + children, + className, + onClose, + ...props +}: MobilePageHeaderProps) { + return ( +
+ +
{children}
+
+ ); +} + +export function MobilePageTitle({ + children, + className, + ...props +}: MobileComponentProps) { + return ( +

+ {children} +

+ ); +}