Update
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -51,6 +51,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.0",
|
||||
"globals": "^15.14.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"konva": "^9.3.20",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"framer-motion": "^12.23.0",
|
||||
"globals": "^15.14.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"konva": "^9.3.20",
|
||||
|
||||
@@ -219,31 +219,31 @@ @theme inline {
|
||||
boxshadow: 0 0 0 8px var(--pulse-color);
|
||||
}
|
||||
}
|
||||
--animate-gradient: gradient 8s linear infinite
|
||||
;
|
||||
@keyframes gradient {
|
||||
to {
|
||||
background-position: var(--bg-size, 300%) 0;
|
||||
--animate-gradient: gradient 8s linear infinite;
|
||||
@keyframes gradient {
|
||||
to {
|
||||
background-position: var(--bg-size, 300%) 0;
|
||||
}
|
||||
}
|
||||
--animate-marquee: marquee var(--duration) infinite linear;
|
||||
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
--animate-marquee: marquee var(--duration) infinite linear;
|
||||
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(calc(-100% - var(--gap)));
|
||||
to {
|
||||
transform: translateY(calc(-100% - var(--gap)));
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
---break---
|
||||
@@ -256,4 +256,28 @@ @layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* REACTBITS */
|
||||
.shiny-text {
|
||||
color: #b5b5b5a4; /* Adjust this color to change intensity/style */
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 60%);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
display: inline-block;
|
||||
animation: shine 5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes shine {
|
||||
0% {
|
||||
background-position: 100%;
|
||||
}
|
||||
100% {
|
||||
background-position: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
.shiny-text.disabled {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
81
resources/js/components/reactbits/CountUp/CountUp.jsx
Normal file
81
resources/js/components/reactbits/CountUp/CountUp.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useInView, useMotionValue, useSpring } from 'framer-motion';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function CountUp({
|
||||
to,
|
||||
from = 0,
|
||||
direction = 'up',
|
||||
delay = 0,
|
||||
duration = 2,
|
||||
className = '',
|
||||
postFix = '',
|
||||
startWhen = true,
|
||||
separator = '',
|
||||
onStart,
|
||||
onEnd,
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const motionValue = useMotionValue(direction === 'down' ? to : from);
|
||||
|
||||
const damping = 20 + 40 * (1 / duration);
|
||||
const stiffness = 100 * (1 / duration);
|
||||
|
||||
const springValue = useSpring(motionValue, {
|
||||
damping,
|
||||
stiffness,
|
||||
});
|
||||
|
||||
const isInView = useInView(ref, { once: true, margin: '0px' });
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.textContent = String(direction === 'down' ? to : from);
|
||||
}
|
||||
}, [from, to, direction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInView && startWhen) {
|
||||
if (typeof onStart === 'function') {
|
||||
onStart();
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
motionValue.set(direction === 'down' ? from : to);
|
||||
}, delay * 1000);
|
||||
|
||||
const durationTimeoutId = setTimeout(
|
||||
() => {
|
||||
if (typeof onEnd === 'function') {
|
||||
onEnd();
|
||||
}
|
||||
},
|
||||
delay * 1000 + duration * 1000,
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
clearTimeout(durationTimeoutId);
|
||||
};
|
||||
}
|
||||
}, [isInView, startWhen, motionValue, direction, from, to, delay, onStart, onEnd, duration]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = springValue.on('change', (latest) => {
|
||||
if (ref.current) {
|
||||
const options = {
|
||||
useGrouping: !!separator,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
};
|
||||
|
||||
const formattedNumber = Intl.NumberFormat('en-US', options).format(latest.toFixed(0));
|
||||
|
||||
ref.current.textContent = (separator ? formattedNumber.replace(/,/g, separator) : formattedNumber) + postFix;
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [springValue, separator]);
|
||||
|
||||
return <span className={`${className}`} ref={ref} />;
|
||||
}
|
||||
29
resources/js/components/reactbits/ShinyText/ShinyText.jsx
Normal file
29
resources/js/components/reactbits/ShinyText/ShinyText.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
const ShinyText = ({ text, disabled = false, speed = 5, className = '' }) => {
|
||||
const animationDuration = `${speed}s`;
|
||||
|
||||
return (
|
||||
<div className={`shiny-text ${disabled ? 'disabled' : ''} ${className}`} style={{ animationDuration }}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShinyText;
|
||||
|
||||
// tailwind.config.js
|
||||
// module.exports = {
|
||||
// theme: {
|
||||
// extend: {
|
||||
// keyframes: {
|
||||
// shine: {
|
||||
// '0%': { 'background-position': '100%' },
|
||||
// '100%': { 'background-position': '-100%' },
|
||||
// },
|
||||
// },
|
||||
// animation: {
|
||||
// shine: 'shine 5s linear infinite',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// plugins: [],
|
||||
// };
|
||||
132
resources/js/components/reactbits/TiltedCard/TiltedCard.jsx
Normal file
132
resources/js/components/reactbits/TiltedCard/TiltedCard.jsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { motion, useMotionValue, useSpring } from 'framer-motion';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
const springValues = {
|
||||
damping: 30,
|
||||
stiffness: 100,
|
||||
mass: 2,
|
||||
};
|
||||
|
||||
export default function TiltedCard({
|
||||
imageSrc,
|
||||
altText = 'Tilted card image',
|
||||
captionText = '',
|
||||
containerHeight = '300px',
|
||||
containerWidth = '100%',
|
||||
imageHeight = '300px',
|
||||
imageWidth = '300px',
|
||||
scaleOnHover = 1.1,
|
||||
rotateAmplitude = 14,
|
||||
showMobileWarning = true,
|
||||
showTooltip = true,
|
||||
overlayContent = null,
|
||||
displayOverlayContent = false,
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const x = useMotionValue(0);
|
||||
const y = useMotionValue(0);
|
||||
const rotateX = useSpring(useMotionValue(0), springValues);
|
||||
const rotateY = useSpring(useMotionValue(0), springValues);
|
||||
const scale = useSpring(1, springValues);
|
||||
const opacity = useSpring(0);
|
||||
const rotateFigcaption = useSpring(0, {
|
||||
stiffness: 350,
|
||||
damping: 30,
|
||||
mass: 1,
|
||||
});
|
||||
|
||||
const [lastY, setLastY] = useState(0);
|
||||
|
||||
function handleMouse(e) {
|
||||
if (!ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const offsetX = e.clientX - rect.left - rect.width / 2;
|
||||
const offsetY = e.clientY - rect.top - rect.height / 2;
|
||||
|
||||
const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude;
|
||||
const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude;
|
||||
|
||||
rotateX.set(rotationX);
|
||||
rotateY.set(rotationY);
|
||||
|
||||
x.set(e.clientX - rect.left);
|
||||
y.set(e.clientY - rect.top);
|
||||
|
||||
const velocityY = offsetY - lastY;
|
||||
rotateFigcaption.set(-velocityY * 0.6);
|
||||
setLastY(offsetY);
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
scale.set(scaleOnHover);
|
||||
opacity.set(1);
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
opacity.set(0);
|
||||
scale.set(1);
|
||||
rotateX.set(0);
|
||||
rotateY.set(0);
|
||||
rotateFigcaption.set(0);
|
||||
}
|
||||
|
||||
return (
|
||||
<figure
|
||||
ref={ref}
|
||||
className="relative flex h-full w-full flex-col items-center justify-center [perspective:800px]"
|
||||
style={{
|
||||
height: containerHeight,
|
||||
width: containerWidth,
|
||||
}}
|
||||
onMouseMove={handleMouse}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{showMobileWarning && (
|
||||
<div className="absolute top-4 block text-center text-sm sm:hidden">This effect is not optimized for mobile. Check on desktop.</div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="relative [transform-style:preserve-3d]"
|
||||
style={{
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
rotateX,
|
||||
rotateY,
|
||||
scale,
|
||||
}}
|
||||
>
|
||||
<motion.img
|
||||
src={imageSrc}
|
||||
alt={altText}
|
||||
className="absolute top-0 left-0 [transform:translateZ(0)] rounded-[15px] object-cover will-change-transform"
|
||||
style={{
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
}}
|
||||
/>
|
||||
|
||||
{displayOverlayContent && overlayContent && (
|
||||
<motion.div className="absolute top-0 left-0 z-[2] [transform:translateZ(30px)] will-change-transform">
|
||||
{overlayContent}
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{showTooltip && (
|
||||
<motion.figcaption
|
||||
className="pointer-events-none absolute top-0 left-0 z-[3] hidden rounded-[4px] bg-white px-[10px] py-[4px] text-[10px] text-[#2d2d2d] opacity-0 sm:block"
|
||||
style={{
|
||||
x,
|
||||
y,
|
||||
opacity,
|
||||
rotate: rotateFigcaption,
|
||||
}}
|
||||
>
|
||||
{captionText}
|
||||
</motion.figcaption>
|
||||
)}
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
@@ -127,6 +127,16 @@ const VideoPreview = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Pre-select first text node on load
|
||||
useEffect(() => {
|
||||
if (activeElements.length > 0 && !selectedElementId) {
|
||||
const firstTextElement = activeElements.find(element => element.type === 'text');
|
||||
if (firstTextElement) {
|
||||
setSelectedElementId(firstTextElement.id);
|
||||
}
|
||||
}
|
||||
}, [activeElements, selectedElementId, setSelectedElementId]);
|
||||
|
||||
// Update transformer when selection changes
|
||||
useEffect(() => {
|
||||
if (transformerRef.current) {
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import CoinIcon from '@/reusables/coin-icon';
|
||||
import useMediaStore from '@/stores/MediaStore';
|
||||
import useUserStore from '@/stores/UserStore';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const EditorAISheet = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const emitter = useMitt();
|
||||
const { generateMeme, isGeneratingMeme, keywords, isLoadingAIHints, fetchAIHints, checkMemeJobStatus, updateMemeResult, setGeneratingMeme, checkActiveJob } = useMediaStore();
|
||||
|
||||
const {
|
||||
generateMeme,
|
||||
isGeneratingMeme,
|
||||
keywords,
|
||||
isLoadingAIHints,
|
||||
fetchAIHints,
|
||||
checkMemeJobStatus,
|
||||
updateMemeResult,
|
||||
setGeneratingMeme,
|
||||
checkActiveJob,
|
||||
} = useMediaStore();
|
||||
|
||||
const pollingIntervalRef = useRef(null);
|
||||
const currentJobIdRef = useRef(null);
|
||||
|
||||
@@ -50,7 +56,7 @@ const EditorAISheet = () => {
|
||||
const response = await checkActiveJob();
|
||||
if (response?.success?.data) {
|
||||
const { job_id, status, result } = response.success.data;
|
||||
|
||||
|
||||
if (status === 'pending' || status === 'processing') {
|
||||
// Resume polling for active job
|
||||
setGeneratingMeme(true);
|
||||
@@ -70,7 +76,7 @@ const EditorAISheet = () => {
|
||||
|
||||
const handleOpenChange = (open) => {
|
||||
setIsOpen(open);
|
||||
|
||||
|
||||
// If sheet is being closed while generating, stop polling
|
||||
if (!open && isGeneratingMeme) {
|
||||
stopPolling();
|
||||
@@ -79,7 +85,7 @@ const EditorAISheet = () => {
|
||||
|
||||
const startPolling = (jobId) => {
|
||||
currentJobIdRef.current = jobId;
|
||||
|
||||
|
||||
// Clear existing interval if any
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
@@ -88,10 +94,10 @@ const EditorAISheet = () => {
|
||||
const checkJobStatus = async () => {
|
||||
try {
|
||||
const response = await checkMemeJobStatus(jobId);
|
||||
|
||||
|
||||
if (response?.success?.data) {
|
||||
const { status, result, error } = response.success.data;
|
||||
|
||||
|
||||
if (status === 'completed') {
|
||||
// Job completed successfully
|
||||
if (result?.generate) {
|
||||
@@ -117,7 +123,7 @@ const EditorAISheet = () => {
|
||||
|
||||
// Start polling every 5 seconds
|
||||
pollingIntervalRef.current = setInterval(checkJobStatus, 5000);
|
||||
|
||||
|
||||
// Also check immediately
|
||||
checkJobStatus();
|
||||
};
|
||||
@@ -151,7 +157,7 @@ const EditorAISheet = () => {
|
||||
setIsOpen(false);
|
||||
setPrompt('');
|
||||
}, 1000);
|
||||
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isGeneratingMeme, isOpen]);
|
||||
@@ -172,6 +178,11 @@ const EditorAISheet = () => {
|
||||
onEscapeKeyDown={(e) => isGeneratingMeme && e.preventDefault()}
|
||||
>
|
||||
<SheetHeader className="mb-2 px-5">
|
||||
<SheetTitle className="text-center text-xl font-semibold text-balance">AI features coming soon!</SheetTitle>
|
||||
<SheetDescription className="hidden"></SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{/* <SheetHeader className="mb-2 px-5">
|
||||
<SheetTitle className="text-center text-xl font-semibold text-balance">
|
||||
{isGeneratingMeme ? 'Creating...' : 'What meme would you like to create?'}
|
||||
</SheetTitle>
|
||||
@@ -188,7 +199,6 @@ const EditorAISheet = () => {
|
||||
className="bg-muted/30 max-h-20 min-h-12 resize-none rounded-3xl border-0 p-4 text-center text-base"
|
||||
/>
|
||||
|
||||
{/* AI Keywords */}
|
||||
{isLoadingAIHints && <div className="text-muted-foreground text-center text-sm">Loading AI hints...</div>}
|
||||
|
||||
{keywords.length > 0 && !isLoadingAIHints && (
|
||||
@@ -259,7 +269,7 @@ const EditorAISheet = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal
|
||||
|
||||
return (
|
||||
<div className={cn('flex w-full items-center justify-between rounded-xl bg-white p-2 shadow-sm dark:bg-neutral-800', className)}>
|
||||
<Button onClick={onNavClick} variant="outline" size="icon" className="rounded">
|
||||
<Button onClick={onNavClick} variant="outline" size="icon" className="invisible rounded">
|
||||
<Menu className="h-8 w-8" />
|
||||
</Button>
|
||||
|
||||
@@ -28,7 +28,7 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="inline-flex gap-1 rounded"
|
||||
className="invisible inline-flex gap-1 rounded"
|
||||
onClick={() => {
|
||||
openUpgradeSheet();
|
||||
}}
|
||||
|
||||
@@ -11,7 +11,7 @@ const Home = () => {
|
||||
<div className="to-muted/10 w-full bg-gradient-to-b from-transparent dark:from-transparent dark:to-neutral-900">
|
||||
<Editor />
|
||||
</div>
|
||||
<div className="space-y-20">
|
||||
<div className="space-y-16">
|
||||
<Hero />
|
||||
<Features />
|
||||
<FAQ />
|
||||
|
||||
@@ -3,36 +3,28 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/
|
||||
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: 'How can I create a meme video?',
|
||||
a: 'Use the video editor on top to start making your meme! Edit your caption, background and meme. Once satisfied, press the Export button to download your video!',
|
||||
},
|
||||
{
|
||||
q: "What's available now?",
|
||||
a: 'All 200+ meme templates and 200+ backgrounds are completely free! Create unlimited memes using our library.',
|
||||
a: 'At the moment, All 200+ meme templates and 200+ backgrounds are completely free! We will be adding more memes and backgrounds to the library soon!',
|
||||
},
|
||||
{
|
||||
q: 'Why is video export slow?',
|
||||
q: 'Why is video export slow for me?',
|
||||
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!',
|
||||
a: "Our web app is fully responsive and works perfectly on mobile devices. Do you want a mobile app? We'll see what can be done.",
|
||||
},
|
||||
{
|
||||
q: 'How often do you add new content?',
|
||||
@@ -41,7 +33,7 @@ const FAQ = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="bg-muted/30">
|
||||
<section className="">
|
||||
<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>
|
||||
|
||||
@@ -5,18 +5,18 @@ const Features = () => {
|
||||
{
|
||||
icon: Heart,
|
||||
title: 'Make video memes for free',
|
||||
description: 'Access 200+ meme and background libraries without paying a cent',
|
||||
description: 'Access 200+ meme and background libraries without paying a cent!',
|
||||
},
|
||||
{
|
||||
icon: Video,
|
||||
title: 'Web-powered Video Editor',
|
||||
description: 'Easy video editor with editable text, background, memes, built into the web. No additional software required!',
|
||||
description: 'Easy video editor with editable text, background, memes, built into the web. No additional software required.',
|
||||
},
|
||||
|
||||
{
|
||||
icon: Download,
|
||||
title: 'Export in minutes',
|
||||
description: 'Download high-quality 720p MP4 videos optimized for TikTok, Youtube Shorts, Instagram Reels, and more',
|
||||
description: 'Download high-quality 720p MP4 videos optimized for TikTok, Youtube Shorts, Instagram Reels, and more.',
|
||||
},
|
||||
{
|
||||
icon: Smartphone,
|
||||
@@ -32,7 +32,7 @@ const Features = () => {
|
||||
{
|
||||
icon: Bot,
|
||||
title: 'AI Caption & Backgrounds',
|
||||
description: 'Smart caption and background generation coming soon',
|
||||
description: 'Smart caption and background generation coming soon.',
|
||||
comingSoon: true,
|
||||
},
|
||||
];
|
||||
@@ -40,11 +40,6 @@ const Features = () => {
|
||||
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
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import CountUp from '@/components/reactbits/CountUp/CountUp';
|
||||
import ShinyText from '@/components/reactbits/ShinyText/ShinyText';
|
||||
|
||||
const Hero = () => {
|
||||
return (
|
||||
<section className="from-muted/50 relative bg-gradient-to-b to-transparent dark:from-neutral-900 dark:to-transparent">
|
||||
@@ -17,19 +20,40 @@ const Hero = () => {
|
||||
<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 className="">
|
||||
<ShinyText
|
||||
text="Create viral memes in seconds for free"
|
||||
disabled={false}
|
||||
speed={3}
|
||||
className="text-muted-foreground mx-auto max-w-4xl text-xl leading-relaxed font-light sm:text-2xl lg:text-3xl"
|
||||
/>
|
||||
</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>
|
||||
<CountUp
|
||||
from={0}
|
||||
to={200}
|
||||
separator=","
|
||||
direction="up"
|
||||
duration={1}
|
||||
className="text-foreground text-3xl font-bold sm:text-4xl"
|
||||
postFix="+"
|
||||
/>{' '}
|
||||
<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>
|
||||
<CountUp
|
||||
from={0}
|
||||
to={200}
|
||||
separator=","
|
||||
direction="up"
|
||||
duration={1}
|
||||
className="text-foreground text-3xl font-bold sm:text-4xl"
|
||||
postFix="+"
|
||||
/>
|
||||
<div className="text-muted-foreground text-sm">Backgrounds</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
|
||||
Reference in New Issue
Block a user