Update
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 (
|
||||
<>
|
||||
<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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
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