Update
This commit is contained in:
40
CLAUDE.md
40
CLAUDE.md
@@ -92,3 +92,43 @@ #### Environment-Specific Features
|
|||||||
- Test routes can be enabled via `ENABLE_TEST_ROUTES=true`
|
- Test routes can be enabled via `ENABLE_TEST_ROUTES=true`
|
||||||
- Uses Laravel Horizon for queue management in production
|
- Uses Laravel Horizon for queue management in production
|
||||||
- Supports both regular and SSR deployments
|
- 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
|
||||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -22,6 +22,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@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-label": "^2.1.7",
|
||||||
"@radix-ui/react-menubar": "^1.1.15",
|
"@radix-ui/react-menubar": "^1.1.15",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
"laravel-vite-plugin": "^1.0",
|
"laravel-vite-plugin": "^1.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"motion": "^12.19.2",
|
"motion": "^12.23.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "^9.7.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": {
|
"node_modules/@radix-ui/react-id": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
@@ -5335,12 +5345,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/framer-motion": {
|
"node_modules/framer-motion": {
|
||||||
"version": "12.19.2",
|
"version": "12.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.0.tgz",
|
||||||
"integrity": "sha512-0cWMLkYr+i0emeXC4hkLF+5aYpzo32nRdQ0D/5DI460B3O7biQ3l2BpDzIGsAHYuZ0fpBP0DC8XBkVf6RPAlZw==",
|
"integrity": "sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"motion-dom": "^12.19.0",
|
"motion-dom": "^12.22.0",
|
||||||
"motion-utils": "^12.19.0",
|
"motion-utils": "^12.19.0",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
@@ -6765,12 +6775,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion": {
|
"node_modules/motion": {
|
||||||
"version": "12.19.2",
|
"version": "12.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/motion/-/motion-12.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.0.tgz",
|
||||||
"integrity": "sha512-Yb69HXE4ryhVd1xwpgWMMQAQgqEGMSGWG+NOumans2fvSCtT8gsj8JK7jhcGnc410CLT3BFPgquP67zmjbA5Jw==",
|
"integrity": "sha512-PPNwblArRH9GRC4F3KtOTiIaYd+mtp324vYq3HIL+ueseoAVqPRK5TPFTAQBcIprfVd0NWo3DLzZSiyWaYFXXQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"framer-motion": "^12.19.2",
|
"framer-motion": "^12.23.0",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -6791,9 +6801,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/motion-dom": {
|
"node_modules/motion-dom": {
|
||||||
"version": "12.19.0",
|
"version": "12.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.22.0.tgz",
|
||||||
"integrity": "sha512-m96uqq8VbwxFLU0mtmlsIVe8NGGSdpBvBSHbnnOJQxniPaabvVdGgxSamhuDwBsRhwX7xPxdICgVJlOpzn/5bw==",
|
"integrity": "sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"motion-utils": "^12.19.0"
|
"motion-utils": "^12.19.0"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@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-label": "^2.1.7",
|
||||||
"@radix-ui/react-menubar": "^1.1.15",
|
"@radix-ui/react-menubar": "^1.1.15",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
@@ -75,7 +76,7 @@
|
|||||||
"laravel-vite-plugin": "^1.0",
|
"laravel-vite-plugin": "^1.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"motion": "^12.19.2",
|
"motion": "^12.23.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-day-picker": "^9.7.0",
|
"react-day-picker": "^9.7.0",
|
||||||
|
|||||||
@@ -219,7 +219,31 @@ @theme inline {
|
|||||||
boxshadow: 0 0 0 8px var(--pulse-color);
|
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---
|
---break---
|
||||||
|
|||||||
188
resources/js/components/magicui/animated-beam.tsx
Normal file
188
resources/js/components/magicui/animated-beam.tsx
Normal file
@@ -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<HTMLElement | null>; // Container ref
|
||||||
|
fromRef: RefObject<HTMLElement | null>;
|
||||||
|
toRef: RefObject<HTMLElement | null>;
|
||||||
|
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<AnimatedBeamProps> = ({
|
||||||
|
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 (
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
width={svgDimensions.width}
|
||||||
|
height={svgDimensions.height}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute left-0 top-0 transform-gpu stroke-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d={pathD}
|
||||||
|
stroke={pathColor}
|
||||||
|
strokeWidth={pathWidth}
|
||||||
|
strokeOpacity={pathOpacity}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={pathD}
|
||||||
|
strokeWidth={pathWidth}
|
||||||
|
stroke={`url(#${id})`}
|
||||||
|
strokeOpacity="1"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<motion.linearGradient
|
||||||
|
className="transform-gpu"
|
||||||
|
id={id}
|
||||||
|
gradientUnits={"userSpaceOnUse"}
|
||||||
|
initial={{
|
||||||
|
x1: "0%",
|
||||||
|
x2: "0%",
|
||||||
|
y1: "0%",
|
||||||
|
y2: "0%",
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
x1: gradientCoordinates.x1,
|
||||||
|
x2: gradientCoordinates.x2,
|
||||||
|
y1: gradientCoordinates.y1,
|
||||||
|
y2: gradientCoordinates.y2,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
delay,
|
||||||
|
duration,
|
||||||
|
ease: [0.16, 1, 0.3, 1], // https://easings.net/#easeOutExpo
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatDelay: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<stop stopColor={gradientStartColor} stopOpacity="0"></stop>
|
||||||
|
<stop stopColor={gradientStartColor}></stop>
|
||||||
|
<stop offset="32.5%" stopColor={gradientStopColor}></stop>
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
stopColor={gradientStopColor}
|
||||||
|
stopOpacity="0"
|
||||||
|
></stop>
|
||||||
|
</motion.linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
37
resources/js/components/magicui/animated-gradient-text.tsx
Normal file
37
resources/js/components/magicui/animated-gradient-text.tsx
Normal file
@@ -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 (
|
||||||
|
<span
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--bg-size": `${speed * 300}%`,
|
||||||
|
"--color-from": colorFrom,
|
||||||
|
"--color-to": colorTo,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
`inline animate-gradient bg-gradient-to-r from-[var(--color-from)] via-[var(--color-to)] to-[var(--color-from)] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
resources/js/components/magicui/bento-grid.tsx
Normal file
109
resources/js/components/magicui/bento-grid.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid w-full auto-rows-[22rem] grid-cols-3 gap-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BentoCard = ({
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
background,
|
||||||
|
Icon,
|
||||||
|
description,
|
||||||
|
href,
|
||||||
|
cta,
|
||||||
|
...props
|
||||||
|
}: BentoCardProps) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
className={cn(
|
||||||
|
"group relative col-span-3 flex flex-col justify-between overflow-hidden rounded-xl",
|
||||||
|
// light styles
|
||||||
|
"bg-background [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]",
|
||||||
|
// dark styles
|
||||||
|
"transform-gpu dark:bg-background dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div>{background}</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="pointer-events-none z-10 flex transform-gpu flex-col gap-1 transition-all duration-300 lg:group-hover:-translate-y-10">
|
||||||
|
<Icon className="h-12 w-12 origin-left transform-gpu text-neutral-700 transition-all duration-300 ease-in-out group-hover:scale-75" />
|
||||||
|
<h3 className="text-xl font-semibold text-neutral-700 dark:text-neutral-300">
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
|
<p className="max-w-lg text-neutral-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"lg:hidden pointer-events-none flex w-full translate-y-0 transform-gpu flex-row items-center transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
asChild
|
||||||
|
size="sm"
|
||||||
|
className="pointer-events-auto p-0"
|
||||||
|
>
|
||||||
|
<a href={href}>
|
||||||
|
{cta}
|
||||||
|
<ArrowRightIcon className="ms-2 h-4 w-4 rtl:rotate-180" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"hidden lg:flex pointer-events-none absolute bottom-0 w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
asChild
|
||||||
|
size="sm"
|
||||||
|
className="pointer-events-auto p-0"
|
||||||
|
>
|
||||||
|
<a href={href}>
|
||||||
|
{cta}
|
||||||
|
<ArrowRightIcon className="ms-2 h-4 w-4 rtl:rotate-180" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { BentoCard, BentoGrid };
|
||||||
73
resources/js/components/magicui/marquee.tsx
Normal file
73
resources/js/components/magicui/marquee.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
"group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] [gap:var(--gap)]",
|
||||||
|
{
|
||||||
|
"flex-row": !vertical,
|
||||||
|
"flex-col": vertical,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Array(repeat)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn("flex shrink-0 justify-around [gap:var(--gap)]", {
|
||||||
|
"animate-marquee flex-row": !vertical,
|
||||||
|
"animate-marquee-vertical flex-col": vertical,
|
||||||
|
"group-hover:[animation-play-state:paused]": pauseOnHover,
|
||||||
|
"[animation-direction:reverse]": reverse,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
resources/js/components/magicui/word-rotate.tsx
Normal file
50
resources/js/components/magicui/word-rotate.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="overflow-hidden py-2">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.h1
|
||||||
|
key={words[index]}
|
||||||
|
className={cn(className)}
|
||||||
|
{...motionProps}
|
||||||
|
>
|
||||||
|
{words[index]}
|
||||||
|
</motion.h1>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -174,7 +174,7 @@ const Editor = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative mx-auto flex min-h-screen flex-col space-y-2 py-4" style={{ width: `${responsiveWidth}px` }}>
|
<div className="relative mx-auto flex min-h-screen flex-col space-y-2 pt-4" style={{ width: `${responsiveWidth}px` }}>
|
||||||
<EditSidebar isOpen={isEditSidebarOpen} onClose={handleEditClose} />
|
<EditSidebar isOpen={isEditSidebarOpen} onClose={handleEditClose} />
|
||||||
<EditNavSidebar isOpen={isEditNavSidebarOpen} onClose={handleEditNavClose} />
|
<EditNavSidebar isOpen={isEditNavSidebarOpen} onClose={handleEditNavClose} />
|
||||||
<TextSidebar isOpen={isTextSidebarOpen} onClose={handleTextSidebarClose} />
|
<TextSidebar isOpen={isTextSidebarOpen} onClose={handleTextSidebarClose} />
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal
|
|||||||
<Menu className="h-8 w-8" />
|
<Menu className="h-8 w-8" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<h1 className="font-display ml-0 text-lg tracking-wide md:ml-3 md:text-xl">MEMEAIGEN</h1>
|
<h1 className="font-display ml-0 text-lg tracking-wide md:ml-3 md:text-xl">
|
||||||
|
<span className="text-foreground">MEME</span>
|
||||||
|
<span className="text-muted-foreground">AI</span>
|
||||||
|
<span className="text-foreground">GEN</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import AuthUser from '@/modules/auth/auth-user';
|
import AuthUser from '@/modules/auth/auth-user';
|
||||||
import Editor from '@/modules/editor/editor.jsx';
|
import Editor from '@/modules/editor/editor.jsx';
|
||||||
import FlashMessages from '@/modules/flash/flash-messages';
|
import FlashMessages from '@/modules/flash/flash-messages';
|
||||||
|
import FAQ from './partials/FAQ.jsx';
|
||||||
|
import Features from './partials/Features.jsx';
|
||||||
|
import Hero from './partials/Hero.jsx';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-neutral-50 dark:bg-black">
|
<div className="min-h-[100vh] bg-neutral-50 pb-10 dark:bg-black">
|
||||||
<Editor />
|
<Editor />
|
||||||
<div className="dark:bg-neutral-800">MEMEAIGEN HERO</div>
|
<div className="space-y-20">
|
||||||
<div className="dark:bg-neutral-800">MEMEAIGEN features</div>
|
<Hero />
|
||||||
<div className="dark:bg-neutral-800">MEMEAIGEN FAQ</div>
|
<Features />
|
||||||
|
<FAQ />
|
||||||
|
</div>
|
||||||
<FlashMessages />
|
<FlashMessages />
|
||||||
<AuthUser />
|
<AuthUser />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
66
resources/js/pages/home/partials/FAQ.jsx
Normal file
66
resources/js/pages/home/partials/FAQ.jsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||||
|
|
||||||
|
const FAQ = () => {
|
||||||
|
const faqData = [
|
||||||
|
{
|
||||||
|
q: 'How does the platform work?',
|
||||||
|
a: 'Choose from 400+ free meme templates and backgrounds, customize with the video editor, and export as MP4. AI features coming soon!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: "What's available now?",
|
||||||
|
a: 'All 200+ meme templates and 200+ backgrounds are completely free! Create unlimited memes using our library.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Why is video export slow?',
|
||||||
|
a: 'Video processing happens entirely in your browser using advanced web technology. Export speed depends on your video content complexity and device performance. High-end devices export quickly, while older/slower devices may take longer or even crash. If your phone is too slow, try using a faster device like a desktop computer for better performance.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'What AI features are coming?',
|
||||||
|
a: "Soon you'll be able to generate custom captions and backgrounds using AI. Enter any text prompt and get tailored content!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'What can I do with the video editor?',
|
||||||
|
a: 'Play/pause your meme, drag to position text, meme templates, and backgrounds anywhere on the canvas, then export as MP4.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'What video format do you export?',
|
||||||
|
a: 'We export high-quality MP4 videos optimized for all social media platforms in 9:16 format.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'How will credits work?',
|
||||||
|
a: 'Once launched, credits will unlock AI features for generating custom captions and backgrounds. All current features remain free!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'Is there a mobile app?',
|
||||||
|
a: 'Our web app is fully responsive and works perfectly on mobile devices. Create memes anywhere with the same features!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
q: 'How often do you add new content?',
|
||||||
|
a: 'We just started building this platform and will gradually add more meme templates and backgrounds over time, so everyone can continue using it for free with fresh content!',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-muted/30">
|
||||||
|
<div className="mx-auto max-w-4xl space-y-10 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-foreground mb-4 text-3xl font-bold sm:text-4xl lg:text-5xl">Frequently Asked Questions</h2>
|
||||||
|
<p className="text-muted-foreground text-lg">Everything you need to know about creating viral memes</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-background rounded-2xl border p-6 sm:p-8">
|
||||||
|
<Accordion type="single" collapsible className="w-full" defaultValue="item-1">
|
||||||
|
{faqData.map((faq, index) => (
|
||||||
|
<AccordionItem key={index} value={`item-${index + 1}`} className="border-b last:border-b-0">
|
||||||
|
<AccordionTrigger className="py-4 text-left font-medium hover:no-underline">{faq.q}</AccordionTrigger>
|
||||||
|
<AccordionContent className="text-muted-foreground pb-4 leading-relaxed">{faq.a}</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FAQ;
|
||||||
75
resources/js/pages/home/partials/Features.jsx
Normal file
75
resources/js/pages/home/partials/Features.jsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Bot, Download, Heart, Library, Smartphone, Video } from 'lucide-react';
|
||||||
|
|
||||||
|
const Features = () => {
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: Heart,
|
||||||
|
title: 'Make video memes for free',
|
||||||
|
description: 'Access 200+ meme templates and backgrounds without paying a cent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Video,
|
||||||
|
title: 'Easy Video Editor',
|
||||||
|
description: 'Simple video editor with draggable text, background, memes and real-time preview',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
icon: Download,
|
||||||
|
title: 'Instant Export',
|
||||||
|
description: 'Download high-quality MP4 videos optimized for TikTok, Youtube Shorts, Instagram Reels, and more',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Smartphone,
|
||||||
|
title: 'Works Everywhere',
|
||||||
|
description: 'Create on desktop, tablet, or mobile with responsive design',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Library,
|
||||||
|
title: 'Library Updates',
|
||||||
|
description: 'Fresh viral memes and backgrounds updated regularly',
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Bot,
|
||||||
|
title: 'AI Caption & Backgrounds',
|
||||||
|
description: 'Smart caption and background generation coming soon',
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="">
|
||||||
|
<div className="mx-auto max-w-6xl space-y-10 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-foreground mb-4 text-3xl font-bold sm:text-4xl lg:text-5xl">Everything you need to create viral memes</h2>
|
||||||
|
<p className="text-muted-foreground mx-auto max-w-2xl text-lg">Simple, powerful tools that help creators make engaging content</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3 lg:gap-4">
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="group bg-card hover:bg-muted/50 relative rounded-2xl border p-6 transition-all duration-300 lg:p-8"
|
||||||
|
>
|
||||||
|
{feature.comingSoon && (
|
||||||
|
<div className="bg-foreground text-background absolute -top-2 -right-2 rounded-full px-2 py-1 text-xs font-medium">
|
||||||
|
Soon
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<feature.icon className="text-foreground h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-foreground mb-2 text-xl font-semibold">{feature.title}</h3>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground leading-relaxed">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Features;
|
||||||
49
resources/js/pages/home/partials/Hero.jsx
Normal file
49
resources/js/pages/home/partials/Hero.jsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const Hero = () => {
|
||||||
|
return (
|
||||||
|
<section className="relative">
|
||||||
|
{/* Subtle grid background */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
|
||||||
|
|
||||||
|
<div className="relative mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="space-y-4 text-center">
|
||||||
|
{/* Badge */}
|
||||||
|
<div className="bg-background/50 inline-flex items-center gap-2 rounded-full border px-3 py-1 text-sm backdrop-blur-sm">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||||
|
<span className="text-muted-foreground">Free meme videos • No signup required</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main heading */}
|
||||||
|
<div className="space-y-0">
|
||||||
|
<h1 className="font-display text-6xl font-black tracking-tight sm:text-7xl lg:text-8xl">
|
||||||
|
<span className="text-foreground">MEME</span>
|
||||||
|
<span className="text-muted-foreground">AI</span>
|
||||||
|
<span className="text-foreground">GEN</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h2 className="text-muted-foreground mx-auto max-w-4xl text-xl leading-relaxed font-light sm:text-2xl lg:text-3xl">
|
||||||
|
Create viral memes in seconds for free!
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="flex flex-wrap justify-center gap-8 sm:gap-12">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-foreground text-3xl font-bold sm:text-4xl">200+</div>
|
||||||
|
<div className="text-muted-foreground text-sm">Meme Templates</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-foreground text-3xl font-bold sm:text-4xl">200+</div>
|
||||||
|
<div className="text-muted-foreground text-sm">Backgrounds</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-foreground text-3xl font-bold sm:text-4xl">MP4</div>
|
||||||
|
<div className="text-muted-foreground text-sm">Export</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Hero;
|
||||||
Reference in New Issue
Block a user