diff --git a/CLAUDE.md b/CLAUDE.md index 64f0d4f..15ca558 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,4 +91,44 @@ #### Special Configurations #### Environment-Specific Features - Test routes can be enabled via `ENABLE_TEST_ROUTES=true` - Uses Laravel Horizon for queue management in production -- Supports both regular and SSR deployments \ No newline at end of file +- Supports both regular and SSR deployments + +## Platform Features Overview + +### Core AI Features +- **Text-to-Meme Generation**: Users input text prompts, AI generates appropriate memes using vector similarity matching +- **Multi-AI Integration**: OpenAI, Cloudflare AI, and Runware AI for different generation tasks +- **Smart Keyword Matching**: PostgreSQL pgvector for semantic search and content matching +- **AI Hint System**: Provides keyword suggestions organized by action/emotion/misc categories +- **AI Caption & Background Generation**: Automatic witty captions and custom background creation + +### Video/Media Editing +- **Canvas-Based Editor**: 9:16 aspect ratio video canvas (720x1280px) with responsive scaling +- **Drag-and-Drop Interface**: Interactive positioning of elements with timeline controls +- **Template Library**: 30+ meme video templates and 30+ AI-generated background media +- **Export System**: Multiple formats (MOV, WebM, GIF, WebP) with watermark-free premium exports +- **FFmpeg Integration**: Client-side processing via FFmpeg.wasm + +### User Management & Authentication +- **Google OAuth + Email/Password**: Dual authentication methods via Laravel Sanctum +- **UUID-based Public IDs**: Secure user identification using Hashids +- **User Dashboard**: Personalized interface with usage statistics and generation history + +### Credit System & Monetization +- **Credit-Based Economy**: 2 credits per meme generation (1 caption + 1 background) +- **Stripe Integration**: Laravel Cashier for subscriptions and one-time purchases +- **Usage Tracking**: Real-time credit balance, transaction history, and consumption analytics +- **Subscription Management**: Multiple plan types with billing portal access + +### Technical Features +- **Queue Processing**: Laravel Horizon with background job processing and real-time status updates +- **Vector Search**: Semantic content matching using PostgreSQL pgvector extension +- **Responsive Design**: Mobile-first interface with progressive enhancement +- **Generation History**: Users can view last 20 generations with regeneration capability +- **Security**: CSRF protection, rate limiting, input validation, secure file handling + +### Admin/Management +- **Content Management**: Categorized meme library with hierarchical categories (kalnoy/nestedset) +- **Background Generation Tool**: Admin interface for creating new background media +- **Usage Analytics**: Platform-wide statistics and user consumption patterns +- **Bulk Operations**: Mass content management and moderation tools \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5f8ab3a..ed487b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.15", "@radix-ui/react-navigation-menu": "^1.2.13", @@ -56,7 +57,7 @@ "laravel-vite-plugin": "^1.0", "lucide-react": "^0.475.0", "mitt": "^3.0.1", - "motion": "^12.19.2", + "motion": "^12.23.0", "next-themes": "^0.4.6", "react": "^19.0.0", "react-day-picker": "^9.7.0", @@ -1742,6 +1743,15 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -5335,12 +5345,12 @@ } }, "node_modules/framer-motion": { - "version": "12.19.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.19.2.tgz", - "integrity": "sha512-0cWMLkYr+i0emeXC4hkLF+5aYpzo32nRdQ0D/5DI460B3O7biQ3l2BpDzIGsAHYuZ0fpBP0DC8XBkVf6RPAlZw==", + "version": "12.23.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.0.tgz", + "integrity": "sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==", "license": "MIT", "dependencies": { - "motion-dom": "^12.19.0", + "motion-dom": "^12.22.0", "motion-utils": "^12.19.0", "tslib": "^2.4.0" }, @@ -6765,12 +6775,12 @@ } }, "node_modules/motion": { - "version": "12.19.2", - "resolved": "https://registry.npmjs.org/motion/-/motion-12.19.2.tgz", - "integrity": "sha512-Yb69HXE4ryhVd1xwpgWMMQAQgqEGMSGWG+NOumans2fvSCtT8gsj8JK7jhcGnc410CLT3BFPgquP67zmjbA5Jw==", + "version": "12.23.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.0.tgz", + "integrity": "sha512-PPNwblArRH9GRC4F3KtOTiIaYd+mtp324vYq3HIL+ueseoAVqPRK5TPFTAQBcIprfVd0NWo3DLzZSiyWaYFXXQ==", "license": "MIT", "dependencies": { - "framer-motion": "^12.19.2", + "framer-motion": "^12.23.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -6791,9 +6801,9 @@ } }, "node_modules/motion-dom": { - "version": "12.19.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.19.0.tgz", - "integrity": "sha512-m96uqq8VbwxFLU0mtmlsIVe8NGGSdpBvBSHbnnOJQxniPaabvVdGgxSamhuDwBsRhwX7xPxdICgVJlOpzn/5bw==", + "version": "12.22.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.22.0.tgz", + "integrity": "sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==", "license": "MIT", "dependencies": { "motion-utils": "^12.19.0" diff --git a/package.json b/package.json index de23995..8a548a2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.15", "@radix-ui/react-navigation-menu": "^1.2.13", @@ -75,7 +76,7 @@ "laravel-vite-plugin": "^1.0", "lucide-react": "^0.475.0", "mitt": "^3.0.1", - "motion": "^12.19.2", + "motion": "^12.23.0", "next-themes": "^0.4.6", "react": "^19.0.0", "react-day-picker": "^9.7.0", diff --git a/resources/css/app.css b/resources/css/app.css index 6ab1367..7de2155 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -219,7 +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-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))); + } + } + @keyframes marquee-vertical { + from { + transform: translateY(0); + } + to { + transform: translateY(calc(-100% - var(--gap))); + } + }} /* ---break--- @@ -232,4 +256,4 @@ @layer base { body { @apply bg-background text-foreground; } -} +} \ No newline at end of file diff --git a/resources/js/components/magicui/animated-beam.tsx b/resources/js/components/magicui/animated-beam.tsx new file mode 100644 index 0000000..4dd04a7 --- /dev/null +++ b/resources/js/components/magicui/animated-beam.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { motion } from "motion/react"; +import { RefObject, useEffect, useId, useState } from "react"; + +import { cn } from "@/lib/utils"; + +export interface AnimatedBeamProps { + className?: string; + containerRef: RefObject; // Container ref + fromRef: RefObject; + toRef: RefObject; + curvature?: number; + reverse?: boolean; + pathColor?: string; + pathWidth?: number; + pathOpacity?: number; + gradientStartColor?: string; + gradientStopColor?: string; + delay?: number; + duration?: number; + startXOffset?: number; + startYOffset?: number; + endXOffset?: number; + endYOffset?: number; +} + +export const AnimatedBeam: React.FC = ({ + className, + containerRef, + fromRef, + toRef, + curvature = 0, + reverse = false, // Include the reverse prop + duration = Math.random() * 3 + 4, + delay = 0, + pathColor = "gray", + pathWidth = 2, + pathOpacity = 0.2, + gradientStartColor = "#ffaa40", + gradientStopColor = "#9c40ff", + startXOffset = 0, + startYOffset = 0, + endXOffset = 0, + endYOffset = 0, +}) => { + const id = useId(); + const [pathD, setPathD] = useState(""); + const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 }); + + // Calculate the gradient coordinates based on the reverse prop + const gradientCoordinates = reverse + ? { + x1: ["90%", "-10%"], + x2: ["100%", "0%"], + y1: ["0%", "0%"], + y2: ["0%", "0%"], + } + : { + x1: ["10%", "110%"], + x2: ["0%", "100%"], + y1: ["0%", "0%"], + y2: ["0%", "0%"], + }; + + useEffect(() => { + const updatePath = () => { + if (containerRef.current && fromRef.current && toRef.current) { + const containerRect = containerRef.current.getBoundingClientRect(); + const rectA = fromRef.current.getBoundingClientRect(); + const rectB = toRef.current.getBoundingClientRect(); + + const svgWidth = containerRect.width; + const svgHeight = containerRect.height; + setSvgDimensions({ width: svgWidth, height: svgHeight }); + + const startX = + rectA.left - containerRect.left + rectA.width / 2 + startXOffset; + const startY = + rectA.top - containerRect.top + rectA.height / 2 + startYOffset; + const endX = + rectB.left - containerRect.left + rectB.width / 2 + endXOffset; + const endY = + rectB.top - containerRect.top + rectB.height / 2 + endYOffset; + + const controlY = startY - curvature; + const d = `M ${startX},${startY} Q ${ + (startX + endX) / 2 + },${controlY} ${endX},${endY}`; + setPathD(d); + } + }; + + // Initialize ResizeObserver + const resizeObserver = new ResizeObserver((entries) => { + // For all entries, recalculate the path + for (let entry of entries) { + updatePath(); + } + }); + + // Observe the container element + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + // Call the updatePath initially to set the initial path + updatePath(); + + // Clean up the observer on component unmount + return () => { + resizeObserver.disconnect(); + }; + }, [ + containerRef, + fromRef, + toRef, + curvature, + startXOffset, + startYOffset, + endXOffset, + endYOffset, + ]); + + return ( + + + + + + + + + + + + + ); +}; diff --git a/resources/js/components/magicui/animated-gradient-text.tsx b/resources/js/components/magicui/animated-gradient-text.tsx new file mode 100644 index 0000000..22a7569 --- /dev/null +++ b/resources/js/components/magicui/animated-gradient-text.tsx @@ -0,0 +1,37 @@ +import { cn } from "@/lib/utils"; +import { ComponentPropsWithoutRef } from "react"; + +export interface AnimatedGradientTextProps + extends ComponentPropsWithoutRef<"div"> { + speed?: number; + colorFrom?: string; + colorTo?: string; +} + +export function AnimatedGradientText({ + children, + className, + speed = 1, + colorFrom = "#ffaa40", + colorTo = "#9c40ff", + ...props +}: AnimatedGradientTextProps) { + return ( + + {children} + + ); +} diff --git a/resources/js/components/magicui/bento-grid.tsx b/resources/js/components/magicui/bento-grid.tsx new file mode 100644 index 0000000..cdccc8e --- /dev/null +++ b/resources/js/components/magicui/bento-grid.tsx @@ -0,0 +1,109 @@ +import { ArrowRightIcon } from "@radix-ui/react-icons"; +import { ComponentPropsWithoutRef, ReactNode } from "react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface BentoGridProps extends ComponentPropsWithoutRef<"div"> { + children: ReactNode; + className?: string; +} + +interface BentoCardProps extends ComponentPropsWithoutRef<"div"> { + name: string; + className: string; + background: ReactNode; + Icon: React.ElementType; + description: string; + href: string; + cta: string; +} + +const BentoGrid = ({ children, className, ...props }: BentoGridProps) => { + return ( +
+ {children} +
+ ); +}; + +const BentoCard = ({ + name, + className, + background, + Icon, + description, + href, + cta, + ...props +}: BentoCardProps) => ( +
+
{background}
+
+
+ +

+ {name} +

+

{description}

+
+ + +
+ + + +
+
+); + +export { BentoCard, BentoGrid }; diff --git a/resources/js/components/magicui/marquee.tsx b/resources/js/components/magicui/marquee.tsx new file mode 100644 index 0000000..fa9c129 --- /dev/null +++ b/resources/js/components/magicui/marquee.tsx @@ -0,0 +1,73 @@ +import { cn } from "@/lib/utils"; +import { ComponentPropsWithoutRef } from "react"; + +interface MarqueeProps extends ComponentPropsWithoutRef<"div"> { + /** + * Optional CSS class name to apply custom styles + */ + className?: string; + /** + * Whether to reverse the animation direction + * @default false + */ + reverse?: boolean; + /** + * Whether to pause the animation on hover + * @default false + */ + pauseOnHover?: boolean; + /** + * Content to be displayed in the marquee + */ + children: React.ReactNode; + /** + * Whether to animate vertically instead of horizontally + * @default false + */ + vertical?: boolean; + /** + * Number of times to repeat the content + * @default 4 + */ + repeat?: number; +} + +export function Marquee({ + className, + reverse = false, + pauseOnHover = false, + children, + vertical = false, + repeat = 4, + ...props +}: MarqueeProps) { + return ( +
+ {Array(repeat) + .fill(0) + .map((_, i) => ( +
+ {children} +
+ ))} +
+ ); +} diff --git a/resources/js/components/magicui/word-rotate.tsx b/resources/js/components/magicui/word-rotate.tsx new file mode 100644 index 0000000..17be594 --- /dev/null +++ b/resources/js/components/magicui/word-rotate.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { AnimatePresence, motion, MotionProps } from "motion/react"; +import { useEffect, useState } from "react"; + +import { cn } from "@/lib/utils"; + +interface WordRotateProps { + words: string[]; + duration?: number; + motionProps?: MotionProps; + className?: string; +} + +export function WordRotate({ + words, + duration = 2500, + motionProps = { + initial: { opacity: 0, y: -50 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: 50 }, + transition: { duration: 0.25, ease: "easeOut" }, + }, + className, +}: WordRotateProps) { + const [index, setIndex] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setIndex((prevIndex) => (prevIndex + 1) % words.length); + }, duration); + + // Clean up interval on unmount + return () => clearInterval(interval); + }, [words, duration]); + + return ( +
+ + + {words[index]} + + +
+ ); +} diff --git a/resources/js/modules/editor/editor.jsx b/resources/js/modules/editor/editor.jsx index 3126959..4531c35 100644 --- a/resources/js/modules/editor/editor.jsx +++ b/resources/js/modules/editor/editor.jsx @@ -174,7 +174,7 @@ const Editor = () => { return ( <> -
+
diff --git a/resources/js/modules/editor/partials/editor-header.jsx b/resources/js/modules/editor/partials/editor-header.jsx index 59d7dff..86dbc40 100644 --- a/resources/js/modules/editor/partials/editor-header.jsx +++ b/resources/js/modules/editor/partials/editor-header.jsx @@ -20,7 +20,11 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal -

MEMEAIGEN

+

+ MEME + AI + GEN +