This commit is contained in:
ct
2025-07-04 16:08:46 +08:00
parent b3ffc261a3
commit bd361b1ea6
15 changed files with 753 additions and 22 deletions

View File

@@ -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;
}
}
}

View 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>
);
};

View 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>
);
}

View 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 };

View 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>
);
}

View 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>
);
}

View File

@@ -174,7 +174,7 @@ const Editor = () => {
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} />
<EditNavSidebar isOpen={isEditNavSidebarOpen} onClose={handleEditNavClose} />
<TextSidebar isOpen={isTextSidebarOpen} onClose={handleTextSidebarClose} />

View File

@@ -20,7 +20,11 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal
<Menu className="h-8 w-8" />
</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
variant="outline"

View File

@@ -1,14 +1,19 @@
import AuthUser from '@/modules/auth/auth-user';
import Editor from '@/modules/editor/editor.jsx';
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 = () => {
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 />
<div className="dark:bg-neutral-800">MEMEAIGEN HERO</div>
<div className="dark:bg-neutral-800">MEMEAIGEN features</div>
<div className="dark:bg-neutral-800">MEMEAIGEN FAQ</div>
<div className="space-y-20">
<Hero />
<Features />
<FAQ />
</div>
<FlashMessages />
<AuthUser />
</div>

View 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;

View 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;

View 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;