From c718971397f88d58cfbb75dd3f50c8e6c74c2dc4 Mon Sep 17 00:00:00 2001 From: Hemant Date: Mon, 31 Jul 2023 19:15:41 +0530 Subject: [PATCH] Added pro subscription modal when api limit exhausts --- app/(dashboard)/(routes)/code/page.tsx | 6 +- .../(routes)/conversation/page.tsx | 6 +- app/(dashboard)/(routes)/image/page.tsx | 6 +- app/(dashboard)/(routes)/music/page.tsx | 8 +- app/(dashboard)/(routes)/video/page.tsx | 8 +- app/(dashboard)/layout.tsx | 2 +- app/layout.tsx | 10 +- components/free-counter.tsx | 6 +- components/modal-provider.tsx | 19 +++ components/pro-modal.tsx | 77 +++++++++++ components/ui/badge.tsx | 37 ++++++ components/ui/dialog.tsx | 123 ++++++++++++++++++ hooks/use-pro-modal.ts | 13 ++ package.json | 4 +- yarn.lock | 21 +++ 15 files changed, 322 insertions(+), 24 deletions(-) create mode 100644 components/modal-provider.tsx create mode 100644 components/pro-modal.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 hooks/use-pro-modal.ts diff --git a/app/(dashboard)/(routes)/code/page.tsx b/app/(dashboard)/(routes)/code/page.tsx index 1929c3a..4e4e8bb 100644 --- a/app/(dashboard)/(routes)/code/page.tsx +++ b/app/(dashboard)/(routes)/code/page.tsx @@ -20,14 +20,14 @@ import { Input } from "@/components/ui/input"; import { UserAvatar } from "@/components/user-avatar"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; -// import { useProModal } from "@/hooks/use-pro-modal"; +import { useProModal } from "@/hooks/use-pro-modal"; import { NextPage } from "next"; import { formSchema } from "./constants"; const CodePage: NextPage = () => { const router = useRouter(); - // const proModal = useProModal(); + const proModal = useProModal(); const [messages, setMessages] = useState([]); const form = useForm>({ @@ -50,7 +50,7 @@ const CodePage: NextPage = () => { form.reset(); } catch (error: any) { if (error?.response?.status === 403) { - // proModal.onOpen(); + proModal.onOpen(); } else { toast.error("Something went wrong."); } diff --git a/app/(dashboard)/(routes)/conversation/page.tsx b/app/(dashboard)/(routes)/conversation/page.tsx index 7f4ba02..530b2f6 100644 --- a/app/(dashboard)/(routes)/conversation/page.tsx +++ b/app/(dashboard)/(routes)/conversation/page.tsx @@ -20,13 +20,13 @@ import { Input } from "@/components/ui/input"; import { UserAvatar } from "@/components/user-avatar"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; -// import { useProModal } from "@/hooks/use-pro-modal"; +import { useProModal } from "@/hooks/use-pro-modal"; import { formSchema } from "./constants"; const ConversationPage: NextPage = () => { const router = useRouter(); - // const proModal = useProModal(); + const proModal = useProModal(); const [messages, setMessages] = useState([]); const form = useForm>({ @@ -49,7 +49,7 @@ const ConversationPage: NextPage = () => { form.reset(); } catch (error: any) { if (error?.response?.status === 403) { - // proModal.onOpen(); + proModal.onOpen(); } else { toast.error("Something went wrong."); } diff --git a/app/(dashboard)/(routes)/image/page.tsx b/app/(dashboard)/(routes)/image/page.tsx index c91c740..5acd183 100644 --- a/app/(dashboard)/(routes)/image/page.tsx +++ b/app/(dashboard)/(routes)/image/page.tsx @@ -18,12 +18,12 @@ import { Empty } from "@/components/ui/empty"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -// import { useProModal } from "@/hooks/use-pro-modal"; +import { useProModal } from "@/hooks/use-pro-modal"; import { amountOptions, formSchema, resolutionOptions } from "./constants"; const PhotoPage = () => { - // const proModal = useProModal(); + const proModal = useProModal(); const router = useRouter(); const [photos, setPhotos] = useState([]); @@ -49,7 +49,7 @@ const PhotoPage = () => { setPhotos(urls); } catch (error: any) { if (error?.response?.status === 403) { - // proModal.onOpen(); + proModal.onOpen(); } else { toast.error("Something went wrong."); } diff --git a/app/(dashboard)/(routes)/music/page.tsx b/app/(dashboard)/(routes)/music/page.tsx index 8faf010..bdaf360 100644 --- a/app/(dashboard)/(routes)/music/page.tsx +++ b/app/(dashboard)/(routes)/music/page.tsx @@ -15,12 +15,12 @@ import { Input } from "@/components/ui/input"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Loader } from "@/components/loader"; import { Empty } from "@/components/ui/empty"; -// import { useProModal } from "@/hooks/use-pro-modal"; +import { useProModal } from "@/hooks/use-pro-modal"; import { formSchema } from "./constants"; const MusicPage = () => { - // const proModal = useProModal(); + const proModal = useProModal(); const router = useRouter(); const [music, setMusic] = useState(); @@ -42,7 +42,7 @@ const MusicPage = () => { form.reset(); } catch (error: any) { if (error?.response?.status === 403) { - // proModal.onOpen(); + proModal.onOpen(); } else { toast.error("Something went wrong."); } @@ -73,7 +73,7 @@ const MusicPage = () => { { const router = useRouter(); - // const proModal = useProModal(); + const proModal = useProModal(); const [video, setVideo] = useState(); const form = useForm>({ @@ -43,7 +43,7 @@ const VideoPage = () => { form.reset(); } catch (error: any) { if (error?.response?.status === 403) { - // proModal.onOpen(); + proModal.onOpen(); } else { toast.error("Something went wrong."); } @@ -74,7 +74,7 @@ const VideoPage = () => { { return (
-
+
diff --git a/app/layout.tsx b/app/layout.tsx index 1d110ff..b453507 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,8 +1,11 @@ import type { Metadata } from 'next' import { Inter } from 'next/font/google' -import './globals.css' import { ClerkProvider } from '@clerk/nextjs' +import './globals.css' + +import { ModalProvider } from '@/components/modal-provider' + const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { @@ -14,7 +17,10 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - {children} + + + {children} + ) diff --git a/components/free-counter.tsx b/components/free-counter.tsx index 97b11bb..84a9e35 100644 --- a/components/free-counter.tsx +++ b/components/free-counter.tsx @@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { MAX_FREE_COUNTS } from "@/constants"; -// import { useProModal } from "@/hooks/use-pro-modal"; +import { useProModal } from "@/hooks/use-pro-modal"; interface FreeCounterProps { isPro?: boolean; @@ -16,7 +16,7 @@ interface FreeCounterProps { export const FreeCounter: React.FC = ({ isPro = false, apiLimitCount = 0 }) => { const [mounted, setMounted] = useState(false); - // const proModal = useProModal(); + const proModal = useProModal(); useEffect(() => setMounted(true), []); @@ -34,7 +34,7 @@ export const FreeCounter: React.FC = ({ isPro = false, apiLimi

- diff --git a/components/modal-provider.tsx b/components/modal-provider.tsx new file mode 100644 index 0000000..bc44dd8 --- /dev/null +++ b/components/modal-provider.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { ProModal } from "@/components/pro-modal"; + +export const ModalProvider = () => { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => setIsMounted(true), []); + + if (!isMounted) return null; + + return ( + <> + + + ); +}; diff --git a/components/pro-modal.tsx b/components/pro-modal.tsx new file mode 100644 index 0000000..74a00be --- /dev/null +++ b/components/pro-modal.tsx @@ -0,0 +1,77 @@ +"use client"; + +import axios from "axios"; +import { useState } from "react"; +import { Check, Zap } from "lucide-react"; +import { toast } from "react-hot-toast"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { useProModal } from "@/hooks/use-pro-modal"; +import { tools } from "@/constants"; +import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; + +export const ProModal = () => { + const proModal = useProModal(); + const [loading, setLoading] = useState(false); + + const onSubscribe = async () => { + try { + setLoading(true); + const response = await axios.get("/api/stripe"); + + window.location.href = response.data.url; + } catch (error) { + toast.error("Something went wrong"); + } finally { + setLoading(false); + } + } + + return ( + + + + +
+ Upgrade to Genius + + pro + +
+
+ + {tools.map((tool) => ( + +
+
+ +
+
+ {tool.label} +
+
+ +
+ ))} +
+
+ + + +
+
+ ); +}; diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..41e6755 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.25 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + premium: "bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-primary-foreground border-0" + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps { } + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..8cbe0d4 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,123 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = ({ + className, + ...props +}: DialogPrimitive.DialogPortalProps) => ( + +) +DialogPortal.displayName = DialogPrimitive.Portal.displayName + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/hooks/use-pro-modal.ts b/hooks/use-pro-modal.ts new file mode 100644 index 0000000..53adf07 --- /dev/null +++ b/hooks/use-pro-modal.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand'; + +interface useProModalStore { + isOpen: boolean; + onOpen: () => void; + onClose: () => void; +} + +export const useProModal = create((set) => ({ + isOpen: false, + onOpen: () => set({ isOpen: true }), + onClose: () => set({ isOpen: false }), +})); diff --git a/package.json b/package.json index 7a0df07..b9bf0e7 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-slot": "^1.0.2", "@types/node": "20.4.5", @@ -40,7 +41,8 @@ "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.6", "typescript": "5.1.6", - "typewriter-effect": "^2.20.1" + "typewriter-effect": "^2.20.1", + "zustand": "^4.3.9" }, "devDependencies": { "prisma": "^5.0.0" diff --git a/yarn.lock b/yarn.lock index 0e0b4d7..adc85eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -517,6 +517,15 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.2" +"@radix-ui/react-progress@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.0.3.tgz#8380272fdc64f15cbf263a294dea70a7d5d9b4fa" + integrity sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-select@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-1.2.2.tgz#caa981fa0d672cf3c1b2a5240135524e69b32181" @@ -3910,6 +3919,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -4015,3 +4029,10 @@ zod@3.21.4: version "3.21.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + +zustand@^4.3.9: + version "4.3.9" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.9.tgz#a7d4332bbd75dfd25c6848180b3df1407217f2ad" + integrity sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw== + dependencies: + use-sync-external-store "1.2.0"