diff --git a/package-lock.json b/package-lock.json index ed487b2..e5d443e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "date-fns": "^4.1.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.23.0", "globals": "^15.14.0", "input-otp": "^1.4.2", "konva": "^9.3.20", diff --git a/package.json b/package.json index 8a548a2..5578739 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "date-fns": "^4.1.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.23.0", "globals": "^15.14.0", "input-otp": "^1.4.2", "konva": "^9.3.20", diff --git a/resources/css/app.css b/resources/css/app.css index 7de2155..87af0e9 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -219,31 +219,31 @@ @theme inline { boxshadow: 0 0 0 8px var(--pulse-color); } } - --animate-gradient: gradient 8s linear infinite -; - @keyframes gradient { - to { - background-position: var(--bg-size, 300%) 0; + --animate-gradient: gradient 8s linear infinite; + @keyframes gradient { + to { + background-position: var(--bg-size, 300%) 0; } } - --animate-marquee: marquee var(--duration) infinite linear; - --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; - @keyframes marquee { - from { - transform: translateX(0); + --animate-marquee: marquee var(--duration) infinite linear; + --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; + @keyframes marquee { + from { + transform: translateX(0); } - to { - transform: translateX(calc(-100% - var(--gap))); + to { + transform: translateX(calc(-100% - var(--gap))); } } - @keyframes marquee-vertical { - from { - transform: translateY(0); + @keyframes marquee-vertical { + from { + transform: translateY(0); } - to { - transform: translateY(calc(-100% - var(--gap))); + to { + transform: translateY(calc(-100% - var(--gap))); } - }} + } +} /* ---break--- @@ -256,4 +256,28 @@ @layer base { body { @apply bg-background text-foreground; } -} \ No newline at end of file +} + +/* REACTBITS */ +.shiny-text { + color: #b5b5b5a4; /* Adjust this color to change intensity/style */ + background: linear-gradient(120deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 60%); + background-size: 200% 100%; + -webkit-background-clip: text; + background-clip: text; + display: inline-block; + animation: shine 5s linear infinite; +} + +@keyframes shine { + 0% { + background-position: 100%; + } + 100% { + background-position: -100%; + } +} + +.shiny-text.disabled { + animation: none; +} diff --git a/resources/js/components/reactbits/CountUp/CountUp.jsx b/resources/js/components/reactbits/CountUp/CountUp.jsx new file mode 100644 index 0000000..8dae984 --- /dev/null +++ b/resources/js/components/reactbits/CountUp/CountUp.jsx @@ -0,0 +1,81 @@ +import { useInView, useMotionValue, useSpring } from 'framer-motion'; +import { useEffect, useRef } from 'react'; + +export default function CountUp({ + to, + from = 0, + direction = 'up', + delay = 0, + duration = 2, + className = '', + postFix = '', + startWhen = true, + separator = '', + onStart, + onEnd, +}) { + const ref = useRef(null); + const motionValue = useMotionValue(direction === 'down' ? to : from); + + const damping = 20 + 40 * (1 / duration); + const stiffness = 100 * (1 / duration); + + const springValue = useSpring(motionValue, { + damping, + stiffness, + }); + + const isInView = useInView(ref, { once: true, margin: '0px' }); + + useEffect(() => { + if (ref.current) { + ref.current.textContent = String(direction === 'down' ? to : from); + } + }, [from, to, direction]); + + useEffect(() => { + if (isInView && startWhen) { + if (typeof onStart === 'function') { + onStart(); + } + + const timeoutId = setTimeout(() => { + motionValue.set(direction === 'down' ? from : to); + }, delay * 1000); + + const durationTimeoutId = setTimeout( + () => { + if (typeof onEnd === 'function') { + onEnd(); + } + }, + delay * 1000 + duration * 1000, + ); + + return () => { + clearTimeout(timeoutId); + clearTimeout(durationTimeoutId); + }; + } + }, [isInView, startWhen, motionValue, direction, from, to, delay, onStart, onEnd, duration]); + + useEffect(() => { + const unsubscribe = springValue.on('change', (latest) => { + if (ref.current) { + const options = { + useGrouping: !!separator, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }; + + const formattedNumber = Intl.NumberFormat('en-US', options).format(latest.toFixed(0)); + + ref.current.textContent = (separator ? formattedNumber.replace(/,/g, separator) : formattedNumber) + postFix; + } + }); + + return () => unsubscribe(); + }, [springValue, separator]); + + return ; +} diff --git a/resources/js/components/reactbits/ShinyText/ShinyText.jsx b/resources/js/components/reactbits/ShinyText/ShinyText.jsx new file mode 100644 index 0000000..c6c98b5 --- /dev/null +++ b/resources/js/components/reactbits/ShinyText/ShinyText.jsx @@ -0,0 +1,29 @@ +const ShinyText = ({ text, disabled = false, speed = 5, className = '' }) => { + const animationDuration = `${speed}s`; + + return ( +
+ {text} +
+ ); +}; + +export default ShinyText; + +// tailwind.config.js +// module.exports = { +// theme: { +// extend: { +// keyframes: { +// shine: { +// '0%': { 'background-position': '100%' }, +// '100%': { 'background-position': '-100%' }, +// }, +// }, +// animation: { +// shine: 'shine 5s linear infinite', +// }, +// }, +// }, +// plugins: [], +// }; diff --git a/resources/js/components/reactbits/TiltedCard/TiltedCard.jsx b/resources/js/components/reactbits/TiltedCard/TiltedCard.jsx new file mode 100644 index 0000000..47bd3f4 --- /dev/null +++ b/resources/js/components/reactbits/TiltedCard/TiltedCard.jsx @@ -0,0 +1,132 @@ +import { motion, useMotionValue, useSpring } from 'framer-motion'; +import { useRef, useState } from 'react'; + +const springValues = { + damping: 30, + stiffness: 100, + mass: 2, +}; + +export default function TiltedCard({ + imageSrc, + altText = 'Tilted card image', + captionText = '', + containerHeight = '300px', + containerWidth = '100%', + imageHeight = '300px', + imageWidth = '300px', + scaleOnHover = 1.1, + rotateAmplitude = 14, + showMobileWarning = true, + showTooltip = true, + overlayContent = null, + displayOverlayContent = false, +}) { + const ref = useRef(null); + const x = useMotionValue(0); + const y = useMotionValue(0); + const rotateX = useSpring(useMotionValue(0), springValues); + const rotateY = useSpring(useMotionValue(0), springValues); + const scale = useSpring(1, springValues); + const opacity = useSpring(0); + const rotateFigcaption = useSpring(0, { + stiffness: 350, + damping: 30, + mass: 1, + }); + + const [lastY, setLastY] = useState(0); + + function handleMouse(e) { + if (!ref.current) return; + + const rect = ref.current.getBoundingClientRect(); + const offsetX = e.clientX - rect.left - rect.width / 2; + const offsetY = e.clientY - rect.top - rect.height / 2; + + const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude; + const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude; + + rotateX.set(rotationX); + rotateY.set(rotationY); + + x.set(e.clientX - rect.left); + y.set(e.clientY - rect.top); + + const velocityY = offsetY - lastY; + rotateFigcaption.set(-velocityY * 0.6); + setLastY(offsetY); + } + + function handleMouseEnter() { + scale.set(scaleOnHover); + opacity.set(1); + } + + function handleMouseLeave() { + opacity.set(0); + scale.set(1); + rotateX.set(0); + rotateY.set(0); + rotateFigcaption.set(0); + } + + return ( +
+ {showMobileWarning && ( +
This effect is not optimized for mobile. Check on desktop.
+ )} + + + + + {displayOverlayContent && overlayContent && ( + + {overlayContent} + + )} + + + {showTooltip && ( + + {captionText} + + )} +
+ ); +} diff --git a/resources/js/modules/editor/partials/canvas/video-preview.jsx b/resources/js/modules/editor/partials/canvas/video-preview.jsx index c0ed958..e56413c 100644 --- a/resources/js/modules/editor/partials/canvas/video-preview.jsx +++ b/resources/js/modules/editor/partials/canvas/video-preview.jsx @@ -127,6 +127,16 @@ const VideoPreview = ({ } }; + // Pre-select first text node on load + useEffect(() => { + if (activeElements.length > 0 && !selectedElementId) { + const firstTextElement = activeElements.find(element => element.type === 'text'); + if (firstTextElement) { + setSelectedElementId(firstTextElement.id); + } + } + }, [activeElements, selectedElementId, setSelectedElementId]); + // Update transformer when selection changes useEffect(() => { if (transformerRef.current) { diff --git a/resources/js/modules/editor/partials/editor-ai-sheet.jsx b/resources/js/modules/editor/partials/editor-ai-sheet.jsx index 1a403d0..f837f1e 100644 --- a/resources/js/modules/editor/partials/editor-ai-sheet.jsx +++ b/resources/js/modules/editor/partials/editor-ai-sheet.jsx @@ -1,24 +1,30 @@ 'use client'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'; -import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { useMitt } from '@/plugins/MittContext'; -import CoinIcon from '@/reusables/coin-icon'; import useMediaStore from '@/stores/MediaStore'; import useUserStore from '@/stores/UserStore'; import { usePage } from '@inertiajs/react'; -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; const EditorAISheet = () => { const [isOpen, setIsOpen] = useState(false); const [prompt, setPrompt] = useState(''); const emitter = useMitt(); - const { generateMeme, isGeneratingMeme, keywords, isLoadingAIHints, fetchAIHints, checkMemeJobStatus, updateMemeResult, setGeneratingMeme, checkActiveJob } = useMediaStore(); - + const { + generateMeme, + isGeneratingMeme, + keywords, + isLoadingAIHints, + fetchAIHints, + checkMemeJobStatus, + updateMemeResult, + setGeneratingMeme, + checkActiveJob, + } = useMediaStore(); + const pollingIntervalRef = useRef(null); const currentJobIdRef = useRef(null); @@ -50,7 +56,7 @@ const EditorAISheet = () => { const response = await checkActiveJob(); if (response?.success?.data) { const { job_id, status, result } = response.success.data; - + if (status === 'pending' || status === 'processing') { // Resume polling for active job setGeneratingMeme(true); @@ -70,7 +76,7 @@ const EditorAISheet = () => { const handleOpenChange = (open) => { setIsOpen(open); - + // If sheet is being closed while generating, stop polling if (!open && isGeneratingMeme) { stopPolling(); @@ -79,7 +85,7 @@ const EditorAISheet = () => { const startPolling = (jobId) => { currentJobIdRef.current = jobId; - + // Clear existing interval if any if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); @@ -88,10 +94,10 @@ const EditorAISheet = () => { const checkJobStatus = async () => { try { const response = await checkMemeJobStatus(jobId); - + if (response?.success?.data) { const { status, result, error } = response.success.data; - + if (status === 'completed') { // Job completed successfully if (result?.generate) { @@ -117,7 +123,7 @@ const EditorAISheet = () => { // Start polling every 5 seconds pollingIntervalRef.current = setInterval(checkJobStatus, 5000); - + // Also check immediately checkJobStatus(); }; @@ -151,7 +157,7 @@ const EditorAISheet = () => { setIsOpen(false); setPrompt(''); }, 1000); - + return () => clearTimeout(timer); } }, [isGeneratingMeme, isOpen]); @@ -172,6 +178,11 @@ const EditorAISheet = () => { onEscapeKeyDown={(e) => isGeneratingMeme && e.preventDefault()} > + AI features coming soon! + + + + {/* {isGeneratingMeme ? 'Creating...' : 'What meme would you like to create?'} @@ -188,7 +199,6 @@ const EditorAISheet = () => { className="bg-muted/30 max-h-20 min-h-12 resize-none rounded-3xl border-0 p-4 text-center text-base" /> - {/* AI Keywords */} {isLoadingAIHints &&
Loading AI hints...
} {keywords.length > 0 && !isLoadingAIHints && ( @@ -259,7 +269,7 @@ const EditorAISheet = () => { )} - + */} ); diff --git a/resources/js/modules/editor/partials/editor-header.jsx b/resources/js/modules/editor/partials/editor-header.jsx index 86dbc40..858b412 100644 --- a/resources/js/modules/editor/partials/editor-header.jsx +++ b/resources/js/modules/editor/partials/editor-header.jsx @@ -16,7 +16,7 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal return (
- @@ -28,7 +28,7 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal