Update
This commit is contained in:
78
resources/js/modules/auth/AuthDialog.jsx
Normal file
78
resources/js/modules/auth/AuthDialog.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const AuthDialog = ({ onOpenChange }) => {
|
||||
const emitter = useMitt();
|
||||
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
window.location.href = route('auth.google.redirect');
|
||||
};
|
||||
|
||||
// Listen for text element selection (but don't auto-open sidebar)
|
||||
useEffect(() => {
|
||||
const handleOpenAuth = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
emitter.on('401', handleOpenAuth);
|
||||
|
||||
return () => {
|
||||
emitter.off('401', handleOpenAuth);
|
||||
};
|
||||
}, [emitter]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">{isLogin ? 'Welcome back' : 'Create account'}</DialogTitle>
|
||||
<DialogDescription className="text-center">
|
||||
{isLogin ? 'Sign in to your account to continue' : 'Sign up to get started'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-6">
|
||||
<Button variant="outline" className="bg-background hover:bg-accent h-12 w-full" type="button" onClick={() => handleGoogleLogin()}>
|
||||
<svg className="mr-3 h-5 w-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
{isLogin ? 'Sign in with Google' : 'Sign up with Google'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground text-center text-xs">By continuing, you agree to our Terms of Service and Privacy Policy</div>
|
||||
|
||||
<div className="mt-4 text-center text-sm">
|
||||
{isLogin ? "Don't have an account?" : 'Already have an account?'}{' '}
|
||||
<Button variant="link" className="px-0 text-sm font-semibold" onClick={() => setIsLogin(!isLogin)}>
|
||||
{isLogin ? 'Sign up' : 'Sign in'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthDialog;
|
||||
@@ -8,6 +8,7 @@ import useVideoEditorStore from '@/stores/VideoEditorStore';
|
||||
// Import fonts first - this loads all Fontsource packages
|
||||
import '@/modules/editor/fonts';
|
||||
|
||||
import UpgradeSheet from '../upgrade/upgrade-sheet';
|
||||
import EditNavSidebar from './partials/edit-nav-sidebar';
|
||||
import EditSidebar from './partials/edit-sidebar';
|
||||
import EditorCanvas from './partials/editor-canvas';
|
||||
@@ -216,6 +217,7 @@ const Editor = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<UpgradeSheet />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { usePage } from '@inertiajs/react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import useLocalSettingsStore from '@/stores/localSettingsStore';
|
||||
import { SettingsIcon } from 'lucide-react';
|
||||
|
||||
export default function EditNavSidebar({ isOpen, onClose }) {
|
||||
const { auth } = usePage().props;
|
||||
|
||||
const { getSetting, setSetting } = useLocalSettingsStore();
|
||||
|
||||
return (
|
||||
@@ -17,36 +18,22 @@ export default function EditNavSidebar({ isOpen, onClose }) {
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="link">
|
||||
<SettingsIcon className="h-6 w-6" /> Settings
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogDescription>Change your settings here.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="grid px-2">
|
||||
{/* {!auth.user && <Button variant="outline">Join Now</Button>}
|
||||
{!auth.user && <Button variant="link">Login</Button>} */}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="genAlphaSlang"
|
||||
checked={getSetting('genAlphaSlang')}
|
||||
onCheckedChange={() => setSetting('genAlphaSlang', !getSetting('genAlphaSlang'))}
|
||||
/>
|
||||
<label
|
||||
htmlFor="genAlphaSlang"
|
||||
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Use gen alpha slang
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DialogFooter></DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="grid px-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.href = route('home');
|
||||
}}
|
||||
variant="link"
|
||||
>
|
||||
Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
import CoinIcon from '@/reusables/coin-icon';
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import CartIcon from '@/reusables/cart-icon';
|
||||
import useLocalSettingsStore from '@/stores/localSettingsStore';
|
||||
import { Menu } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = false }) => {
|
||||
const { getSetting } = useLocalSettingsStore();
|
||||
|
||||
const [openCoinDialog, setOpenCoinDialog] = useState(false);
|
||||
const emitter = useMitt();
|
||||
|
||||
const openUpgradeSheet = () => {
|
||||
emitter.emit('openUpgradeSheet');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex w-full items-center justify-between rounded-xl bg-white p-2 shadow-sm dark:bg-neutral-700', className)}>
|
||||
@@ -19,33 +22,16 @@ const EditorHeader = ({ className = '', onNavClick = () => {}, isNavActive = fal
|
||||
|
||||
<h1 className="font-display ml-0 text-lg tracking-wide md:ml-3 md:text-xl">MEMEAIGEN</h1>
|
||||
|
||||
<Button variant="outline" className="inline-flex gap-1 rounded" onClick={() => setOpenCoinDialog(true)}>
|
||||
<span className="text-sm font-semibold">0</span>
|
||||
<CoinIcon className="h-8 w-8" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="inline-flex gap-1 rounded"
|
||||
onClick={() => {
|
||||
openUpgradeSheet();
|
||||
}}
|
||||
>
|
||||
{/* <span className="text-sm font-semibold">0</span> */}
|
||||
<CartIcon className="h-8 w-8" />
|
||||
</Button>
|
||||
|
||||
<Dialog open={openCoinDialog} onOpenChange={(open) => setOpenCoinDialog(open)}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{getSetting('genAlphaSlang') ? 'Bruh' : 'Feature coming soon'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{getSetting('genAlphaSlang')
|
||||
? "No cap, soon you'll be able to get AI cooking memes that absolutely slay! But lowkey fam, we gotta focus on making these core features bussin' first."
|
||||
: "Soon you'll be able to prompt AI to generate memes! Let us focus on nailing the core features first."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<div className="flex justify-between gap-1">
|
||||
{/* <span class="text-muted-foreground text-xs italic">
|
||||
Note: You can turn {getSetting('genAlphaSlang') ? 'off' : 'on'} gen alpha slang in Settings.
|
||||
</span> */}
|
||||
<Button variant="outline" onClick={() => setOpenCoinDialog(false)}>
|
||||
{getSetting('genAlphaSlang') ? 'Bet' : 'Okay'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
103
resources/js/modules/upgrade/partials/upgrade-plan-carousel.tsx
Normal file
103
resources/js/modules/upgrade/partials/upgrade-plan-carousel.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import { Carousel, CarouselContent, CarouselItem, type CarouselApi } from '@/components/ui/carousel';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
import { CheckCircle, Handshake, Lock, Zap } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const upgradePlanData = [
|
||||
{
|
||||
icon: Zap,
|
||||
title: 'Remove watermarks',
|
||||
description: 'Export up to 50 watermark-free videos, perfect for posting to your creator channel (8¢ per video)',
|
||||
},
|
||||
{
|
||||
icon: CheckCircle,
|
||||
title: 'Personal license included',
|
||||
description: 'Full rights to use videos for personal social media, creator and non-commercial projects',
|
||||
},
|
||||
{
|
||||
icon: Lock,
|
||||
title: 'Lock in your pricing',
|
||||
description: 'Subscribe now and keep this price forever - even when we raise prices for new users',
|
||||
},
|
||||
{
|
||||
icon: Handshake,
|
||||
title: 'Support our development',
|
||||
description: 'Help us to improve and grow so you get the best features & experience',
|
||||
},
|
||||
];
|
||||
|
||||
const UpgradePlanCarousel = () => {
|
||||
const [api, setApi] = useState<CarouselApi>();
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCount(api.scrollSnapList().length);
|
||||
setCurrent(api.selectedScrollSnap() + 1);
|
||||
|
||||
api.on('select', () => {
|
||||
setCurrent(api.selectedScrollSnap() + 1);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
const scrollTo = (index: number) => {
|
||||
api?.scrollTo(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-2xl space-y-4">
|
||||
<Carousel
|
||||
plugins={[
|
||||
Autoplay({
|
||||
delay: 5500,
|
||||
}),
|
||||
]}
|
||||
setApi={setApi}
|
||||
className="w-full"
|
||||
>
|
||||
<CarouselContent>
|
||||
{upgradePlanData.map((item, index) => {
|
||||
const IconComponent = item.icon;
|
||||
return (
|
||||
<CarouselItem key={index}>
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<div className="space-y-2 text-center">
|
||||
<div className="mx-auto mb-0 flex h-20 w-20 items-center justify-center rounded-full">
|
||||
<IconComponent className="h-10 w-10" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">{item.title}</h3>
|
||||
<p className="max-w-sm text-sm">{item.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
);
|
||||
})}
|
||||
</CarouselContent>
|
||||
</Carousel>
|
||||
|
||||
{/* Centered Dot Navigation */}
|
||||
<div className="flex justify-center space-x-2">
|
||||
{Array.from({ length: count }, (_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => scrollTo(index)}
|
||||
className={cn(
|
||||
'h-3 w-3 rounded-full transition-all duration-200 hover:scale-110',
|
||||
current === index + 1 ? 'bg-primary scale-110' : 'bg-muted-foreground/30 hover:bg-muted-foreground/50',
|
||||
)}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpgradePlanCarousel;
|
||||
198
resources/js/modules/upgrade/upgrade-sheet.jsx
Normal file
198
resources/js/modules/upgrade/upgrade-sheet.jsx
Normal file
@@ -0,0 +1,198 @@
|
||||
// resources/js/Pages/User/Partials/upgrade-sheet.jsx
|
||||
import { SparklesText } from '@/components/magicui/sparkles-text';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Spinner } from '@/components/ui/spinner.js';
|
||||
import { useMitt } from '@/plugins/MittContext';
|
||||
import CartIcon from '@/reusables/cart-icon.jsx';
|
||||
import CoinIcon from '@/reusables/coin-icon.jsx';
|
||||
import usePricingStore from '@/stores/PricingStore.js';
|
||||
import { useEffect, useState } from 'react';
|
||||
import UpgradePlanCarousel from './partials/upgrade-plan-carousel.tsx';
|
||||
|
||||
const UpgradeSheet = () => {
|
||||
const { subscription, one_times, isFetchingPricing, fetchPricing, isCheckingOut, checkoutSubscribe } = usePricingStore();
|
||||
|
||||
// State to control sheet visibility
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Get mitt emitter
|
||||
const emitter = useMitt();
|
||||
|
||||
useEffect(() => {
|
||||
fetchPricing();
|
||||
}, []);
|
||||
|
||||
// FAQ data array
|
||||
const faqData = [
|
||||
{
|
||||
q: "What's included in the $4/m Personal Creator Plan?",
|
||||
a: 'This $4/m plan includes 50 non-watermark videos for export (8¢ per video), which is sufficient for every creator to post almost twice a day. <br><br>This plan also includes a Personal license, which allows you to use the app for personal social media, creator and non-commercial projects.<br><br> If you are a creator looking to monetize your channel, this plan is the perfect choice for you.',
|
||||
},
|
||||
{
|
||||
q: 'Why are your plans extremely affordable for creators?',
|
||||
a: "We're glad you think this way! As creators ourselves, we know the journey of creating and monetizing your channel is not easy.<br><br> We believe that creators deserve access to high quality videos, and we're committed to making it accessible to every creator.<br><br>If one day you've made it to the top, don't forget us! We'd love to hear your growth stories.",
|
||||
},
|
||||
{
|
||||
q: 'Can I use the Personal Creator Plan for my business & commercial projects?',
|
||||
a: "A hard NO.<br><br> The Personal Creator Plan is designed for personal social media, creator and non-commercial projects.<br><br> If you are a creator looking to monetize your channel, this plan is the right choice for you. The Personal Creator Plan is designed to empower creators, helping them to monetize their channels and grow their audience. <br><br>However, if you are a business, we recommend you to use the Business Creator Plan (coming soon), which contains a Business License that you can use for commercial projects.<br><br>Contact us if you have an urgent need for this plan, and we'll see what can be done to help you.",
|
||||
},
|
||||
{
|
||||
q: 'How do I lock in my subscription price?',
|
||||
a: "Simple! Just subscribe to the plan before the next price change. We can't guarantee that you'll be able to purchase the same plan at the current price that you've locked in. But we'll do our best to make sure you get the best value for your money.",
|
||||
},
|
||||
{
|
||||
q: 'Is the $4/month pricing permanent?',
|
||||
a: 'Yes, the $4/month launch pricing is a special offer for our first 1000 users. After 1000 users, the plan will be priced at a higher rate. Lock in your launch pricing by subscribing now!',
|
||||
},
|
||||
{
|
||||
q: 'What can I do with credits?',
|
||||
a: 'You can use credits to access AI features such as AI caption generation & AI background generation.',
|
||||
},
|
||||
// {
|
||||
// q: 'What is the difference of purchasing credits from a subscription vs a one-time credit pack?',
|
||||
// a: 'When you purchase credits from a subscription, (e.g. 500 Credits/m) you get monthly allowance of 500 credits. Unused credits will not be carry forwarded to the next month.<br><br>When you purchase credits from a credit pack, (e.g. 500 Credit Pack) the credits will not expire and you can use them anytime.',
|
||||
// },
|
||||
{
|
||||
q: 'Can I cancel my subscription anytime?',
|
||||
a: "Yes! You can cancel your subscription at any time. Your plan will remain active until the end of your current billing period, and you'll retain access to all premium features during that time. Just know that you might not be able to purchase the same plan at the current price that you've locked in.",
|
||||
},
|
||||
{
|
||||
q: 'Do you offer refunds?',
|
||||
a: 'Yes, we offer refunds for subscription plans. Credits are non-refundable once purchased as they do not expire.',
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for open modal event
|
||||
const openModalListener = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
// Register listener for opening the modal
|
||||
emitter.on('openUpgradeSheet', openModalListener);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
emitter.off('openUpgradeSheet', openModalListener);
|
||||
};
|
||||
}, [emitter]);
|
||||
|
||||
// Handle sheet state changes
|
||||
const handleOpenChange = (open) => {
|
||||
setIsOpen(open);
|
||||
|
||||
// If sheet is closing, emit the close event
|
||||
if (!open) {
|
||||
emitter.emit('closeUpgradeSheet');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribe = (subscription) => {
|
||||
checkoutSubscribe(subscription.stripe_monthly_price_id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<SheetContent side="bottom" className="max-h-screen overflow-y-scroll pb-1">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center justify-center gap-2 sm:text-center">
|
||||
<CartIcon className={'h-4 w-4'} /> Store
|
||||
</SheetTitle>
|
||||
<SheetDescription className="hidden"> </SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="mx-auto w-full max-w-[600px] space-y-3 px-4">
|
||||
{subscription ? (
|
||||
<div id="plan-purchase" className="mx-auto space-y-6 rounded-lg border p-4 text-center sm:p-7">
|
||||
<SparklesText className="text-xl font-bold sm:text-2xl">
|
||||
Upgrade to {subscription?.name} Plan<br></br> at only {subscription?.symbol}
|
||||
{subscription?.amount}
|
||||
{subscription?.primary_interval === 'month' ? '/m' : '/y'}*
|
||||
</SparklesText>
|
||||
<UpgradePlanCarousel />
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
disabled={isCheckingOut}
|
||||
onClick={() => {
|
||||
handleSubscribe(subscription);
|
||||
}}
|
||||
size="lg"
|
||||
className="mx-auto w-[220px] text-sm sm:text-base"
|
||||
>
|
||||
{isCheckingOut ? (
|
||||
<Spinner className="text-muted h-4 w-4" />
|
||||
) : (
|
||||
<span>
|
||||
Subscribe at ({subscription?.symbol}
|
||||
{subscription?.amount}
|
||||
{subscription?.primary_interval === 'month' ? '/m' : '/y'})*
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-muted-foreground text-xs">* Launch pricing limited to first 1000 users</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
isFetchingPricing && (
|
||||
<div className="mx-auto w-full">
|
||||
<Spinner className="h-6 w-6" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<div id="credit-purchase" className="space-y-6 rounded-lg border p-4 text-center sm:p-7">
|
||||
<div className="text-xl font-extrabold sm:text-2xl">Buy Credit Packs</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Unlock AI meme captions and backgrounds with credits. Perfect for overcoming creator's block and discovering new concepts.
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-col gap-3 rounded-lg border p-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div className="grid">
|
||||
<div className="text inline-flex items-center font-semibold">
|
||||
<CoinIcon className="inline h-4 w-4 flex-shrink-0" />
|
||||
<span className="ml-1">500 Credit Pack</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs font-semibold break-words">
|
||||
Approx. 250 AI captions & 250 AI backgrounds
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-muted-foreground text-sm">$4.00</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-full flex-shrink-0 sm:w-auto">Buy</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="faq" className="space-y-4 rounded-lg border p-4 sm:p-7">
|
||||
<div className="text-center text-xl font-extrabold sm:text-2xl">Frequently Asked Questions</div>
|
||||
<Accordion type="single" collapsible className="w-full" defaultValue="item-1">
|
||||
{faqData.map((faq, index) => (
|
||||
<AccordionItem key={index} value={`item-${index + 1}`}>
|
||||
<AccordionTrigger className="text-left text-sm break-words sm:text-base">{faq.q}</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<div className="overflow-wrap-anywhere text-sm break-words" dangerouslySetInnerHTML={{ __html: faq.a }} />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SheetFooter>
|
||||
<SheetClose asChild className="hidden">
|
||||
<Button variant="outline">Close</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpgradeSheet;
|
||||
Reference in New Issue
Block a user