From 30d523e1bf825125330c160c7fcaf34ae4132bc5 Mon Sep 17 00:00:00 2001 From: star-boy Date: Thu, 5 Mar 2026 10:30:54 +0530 Subject: [PATCH 1/2] fix(create-post page): fixed some error handling --- backend/src/app.ts | 4 +- .../src/controllers/generative.controller.ts | 58 ++++++++++++++++-- backend/src/controllers/post.controller.ts | 14 ++++- backend/src/repositories/user.repository.ts | 16 ++++- backend/src/services/notification.service.ts | 4 +- .../app/(user)/dashboard/create-post/page.tsx | 2 + .../(user)/settings/notifications/page.tsx | 5 ++ frontend/src/components/LoginForm.tsx | 36 ++++++----- .../src/components/NotificationDropdown.tsx | 5 ++ .../components/create-post/CreatePostForm.tsx | 61 +++++++++++++++---- .../create-post/EditPlatformPostModal.tsx | 14 +++-- .../components/create-post/PostPreview.tsx | 9 ++- frontend/src/hooks/useCreatePost.ts | 45 +++++++++++--- frontend/src/lib/axios.ts | 3 + 14 files changed, 223 insertions(+), 53 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 2fa566e..7d09097 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -79,8 +79,8 @@ const bootstrap = async () => { expressInstance.use("/api/payments/webhook", express.raw({ type: "application/json" })); - expressInstance.use(express.json()); - expressInstance.use(express.urlencoded({ extended: true })); + expressInstance.use(express.json({ limit: "50mb" })); + expressInstance.use(express.urlencoded({ limit: "50mb", extended: true })); expressInstance.use(passport.initialize()); expressInstance.get("/health", (req, res) => { diff --git a/backend/src/controllers/generative.controller.ts b/backend/src/controllers/generative.controller.ts index 8ffb048..83bbf99 100644 --- a/backend/src/controllers/generative.controller.ts +++ b/backend/src/controllers/generative.controller.ts @@ -5,7 +5,7 @@ import logger from "../utils/logger"; import { Request, Response } from "express"; import { buildPlatformPrompt } from "../ai/prompts"; import { buildImageParts } from "../services/image.service"; -import { IncreaseCaptionGenerations } from "../repositories/user.repository"; +import { findUserByIdSafe, IncreaseCaptionGenerations } from "../repositories/user.repository"; export const generateCaptions = async (req: Request, res: Response) => { try { @@ -13,6 +13,18 @@ export const generateCaptions = async (req: Request, res: Response) => { return new ErrorResponse("Unauthorized", { status: 401 }).send(res); } + const user = await findUserByIdSafe(req.auth.id); + if (!user) { + return new ErrorResponse("User not found", { status: 404 }).send(res); + } + + if (user.usage.captionGenerations >= user.limits.maxCaptionGenerations) { + return new ErrorResponse( + "You have reached your caption generation limit. Please upgrade your plan for more.", + { status: 429 }, + ).send(res); + } + const { prompt, media } = req.body; const textPrompt = ` @@ -71,8 +83,21 @@ export const generateCaptions = async (req: Request, res: Response) => { await IncreaseCaptionGenerations(req.auth.id); return new SuccessResponse("Captions generated successfully", { data: result }).send(res); - } catch (error) { + } catch (error: any) { logger.error("Generate captions error", error); + + // Handle Gemini API Quota Exceeded (429) + if ( + error?.status === 429 || + error?.message?.includes("429") || + error?.message?.includes("quota") + ) { + return new ErrorResponse( + "AI service limit reached. Please try again after some time or upgrade your plan.", + { status: 429 }, + ).send(res); + } + return new ErrorResponse("Failed to generate captions").send(res); } }; @@ -83,6 +108,18 @@ export const generateCaptionsForSpecificPlatform = async (req: Request, res: Res return new ErrorResponse("Unauthorized", { status: 401 }).send(res); } + const user = await findUserByIdSafe(req.auth.id); + if (!user) { + return new ErrorResponse("User not found", { status: 404 }).send(res); + } + + if (user.usage.captionGenerations >= user.limits.maxCaptionGenerations) { + return new ErrorResponse( + "You have reached your caption generation limit. Please upgrade your plan for more.", + { status: 429 }, + ).send(res); + } + const { media, prompt } = req.body; const platform = req.params.platform; @@ -107,8 +144,21 @@ export const generateCaptionsForSpecificPlatform = async (req: Request, res: Res await IncreaseCaptionGenerations(req.auth.id); return new SuccessResponse("Captions are generated", { data: result }).send(res); - } catch (error) { - logger.error(error); + } catch (error: any) { + logger.error("Platform specific caption error", error); + + // Handle Gemini API Quota Exceeded (429) + if ( + error?.status === 429 || + error?.message?.includes("429") || + error?.message?.includes("quota") + ) { + return new ErrorResponse( + "AI service limit reached. Please try again after some time or upgrade your plan.", + { status: 429 }, + ).send(res); + } + return new ErrorResponse("cannot create caption").send(res); } }; diff --git a/backend/src/controllers/post.controller.ts b/backend/src/controllers/post.controller.ts index 7a60255..ed5de01 100644 --- a/backend/src/controllers/post.controller.ts +++ b/backend/src/controllers/post.controller.ts @@ -9,7 +9,7 @@ import { Types } from "mongoose"; import { z } from "zod"; import { getPresignedUploadUrl } from "../services/s3/s3.upload.service"; import { timezoneSchema, platformSpecificPostSchema } from "@hayon/schemas"; -import { IncreasePostsCreated } from "../repositories/user.repository"; +import { findUserByIdSafe, IncreasePostsCreated } from "../repositories/user.repository"; import { NotificationService } from "../services/notification.service"; const createPostSchema = z.object({ @@ -41,6 +41,18 @@ export const createPost = async (req: Request, res: Response) => { return new ErrorResponse("Unauthorized", { status: 401 }).send(res); } const userId = req.auth.id; + const user = await findUserByIdSafe(userId); + + if (!user) { + return new ErrorResponse("User not found", { status: 404 }).send(res); + } + + if (user.usage.postsCreated >= user.limits.maxPosts) { + return new ErrorResponse( + "You have reached your post creation limit. Please upgrade your plan for more.", + { status: 429 }, + ).send(res); + } // Validation const validationResult = createPostSchema.safeParse(req.body); diff --git a/backend/src/repositories/user.repository.ts b/backend/src/repositories/user.repository.ts index 52b21e8..fe99f69 100644 --- a/backend/src/repositories/user.repository.ts +++ b/backend/src/repositories/user.repository.ts @@ -98,11 +98,23 @@ export const changeUserName = async (userId: string, name: string) => { }; export const IncreaseCaptionGenerations = async (userId: string) => { - return User.findByIdAndUpdate(userId, { $inc: { "usage.captionGenerations": 1 } }, { new: true }); + const updated = await User.findByIdAndUpdate( + userId, + { $inc: { "usage.captionGenerations": 1 } }, + { new: true }, + ); + await invalidateCache(`user:profile:${userId}`); + return updated; }; export const IncreasePostsCreated = async (userId: string) => { - return User.findByIdAndUpdate(userId, { $inc: { "usage.postsCreated": 1 } }, { new: true }); + const updated = await User.findByIdAndUpdate( + userId, + { $inc: { "usage.postsCreated": 1 } }, + { new: true }, + ); + await invalidateCache(`user:profile:${userId}`); + return updated; }; export const findAllUsers = async () => { diff --git a/backend/src/services/notification.service.ts b/backend/src/services/notification.service.ts index a3b2235..e453b69 100644 --- a/backend/src/services/notification.service.ts +++ b/backend/src/services/notification.service.ts @@ -63,6 +63,8 @@ export class NotificationService { link?: string, ) { try { + // Strip HTML tags for the system pop-up + const plainTextMessage = message.replace(/<[^>]*>?/gm, ""); const user = await User.findById(recipientId).select("fcmTokens"); if (!user || !user.fcmTokens || user.fcmTokens.length === 0) { @@ -72,7 +74,7 @@ export class NotificationService { const messagePayload: any = { notification: { title, - body: message, + body: plainTextMessage, }, tokens: user.fcmTokens, }; diff --git a/frontend/src/app/(user)/dashboard/create-post/page.tsx b/frontend/src/app/(user)/dashboard/create-post/page.tsx index d5f9bae..3ae3a3e 100644 --- a/frontend/src/app/(user)/dashboard/create-post/page.tsx +++ b/frontend/src/app/(user)/dashboard/create-post/page.tsx @@ -53,6 +53,7 @@ export default function CreatePostPage() { platformPosts, updatePlatformPost, refinePlatformPostWithLLM, + platformGenerationErrors, loadDraft, draftId, } = useCreatePost(); @@ -190,6 +191,7 @@ export default function CreatePostPage() { platformPosts={platformPosts} updatePlatformPost={updatePlatformPost} refinePlatformPostWithLLM={refinePlatformPostWithLLM} + platformGenerationErrors={platformGenerationErrors} mediaFiles={mediaFiles} isGenerating={isGenerating} platformWarnings={platformWarnings} diff --git a/frontend/src/app/(user)/settings/notifications/page.tsx b/frontend/src/app/(user)/settings/notifications/page.tsx index de96cbc..91274e8 100644 --- a/frontend/src/app/(user)/settings/notifications/page.tsx +++ b/frontend/src/app/(user)/settings/notifications/page.tsx @@ -51,6 +51,11 @@ export default function NotificationsPage() { }; const highlightMessage = (message: string) => { + // If the message contains HTML tags, render it as HTML + if (message.includes("<") && message.includes(">")) { + return ; + } + const platforms = ["bluesky", "threads", "tumblr", "mastodon", "facebook", "instagram"]; const statusKeywords = ["pending", "scheduled", "posted"]; const allKeywords = [...platforms, ...statusKeywords]; diff --git a/frontend/src/components/LoginForm.tsx b/frontend/src/components/LoginForm.tsx index 3926f2a..f3bf74c 100644 --- a/frontend/src/components/LoginForm.tsx +++ b/frontend/src/components/LoginForm.tsx @@ -36,6 +36,7 @@ export default function LoginForm({ const error = searchParams.get("error"); const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [formErrors, setFormErrors] = useState({}); @@ -55,9 +56,9 @@ export default function LoginForm({ const message = errorMessages[error] || "An error occurred. Please try again."; showToast("error", "Login failed", message); - window.history.replaceState({}, "", isAdmin ? "/admin-login" : "/admin/login"); + window.history.replaceState({}, "", isAdmin ? "/admin/login" : "/login"); } - }, [error, isAdmin]); + }, [error, isAdmin, showToast]); const validateForm = (): boolean => { const result = loginSchema.safeParse({ email, password }); @@ -74,6 +75,11 @@ export default function LoginForm({ }); setFormErrors(errors); + + // Show toast for the first validation error + if (zodErrors.errors.length > 0) { + showToast("error", "Invalid Input", zodErrors.errors[0].message); + } return false; } @@ -91,30 +97,32 @@ export default function LoginForm({ setIsLoading(true); try { - console.log("Login request"); const { data } = await api.post(loginEndpoint, { email, password }); // Store access token in memory setAccessToken(data.data.accessToken); async function setupPush(userId: string) { - console.log("Setting up push"); - const permission = await Notification.requestPermission(); + try { + const permission = await Notification.requestPermission(); - if (permission !== "granted") { - console.log("Permission denied"); - return; - } + if (permission !== "granted") { + return; + } - const token = await getToken(messaging, { - vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY, - }); - console.log("FCM Token:", token); + const token = await getToken(messaging, { + vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY, + }); - await api.post("/firebase/save-token", { userId, token }); + await api.post("/firebase/save-token", { userId, token }); + } catch (pushError) { + console.error("Push setup failed", pushError); + // Don't show toast for push failure to avoid distracting from login success + } } await setupPush(data.data.user.id); + showToast("success", "Welcome!", "You have logged in successfully."); return router.push(redirectPath); } catch (error) { const axiosError = error as AxiosError<{ message?: string }>; diff --git a/frontend/src/components/NotificationDropdown.tsx b/frontend/src/components/NotificationDropdown.tsx index 8f6421b..4d83616 100644 --- a/frontend/src/components/NotificationDropdown.tsx +++ b/frontend/src/components/NotificationDropdown.tsx @@ -36,6 +36,11 @@ export const NotificationDropdown = () => { }; const highlightMessage = (message: string) => { + // If the message contains HTML tags, render it as HTML + if (message.includes("<") && message.includes(">")) { + return ; + } + const platforms = ["bluesky", "threads", "tumblr", "mastodon", "facebook", "instagram"]; const statusKeywords = ["pending", "scheduled", "posted"]; const allKeywords = [...platforms, ...statusKeywords]; diff --git a/frontend/src/components/create-post/CreatePostForm.tsx b/frontend/src/components/create-post/CreatePostForm.tsx index 4931dca..de10e61 100644 --- a/frontend/src/components/create-post/CreatePostForm.tsx +++ b/frontend/src/components/create-post/CreatePostForm.tsx @@ -6,6 +6,9 @@ import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { Platform } from "@/types/create-post"; import { api } from "@/lib/axios"; +import { useToast } from "@/context/ToastContext"; +import { GLOBAL_CONSTRAINTS } from "@hayon/schemas"; +import { AlertTriangle } from "lucide-react"; interface CreatePostFormProps { postText: string; @@ -36,6 +39,7 @@ export function CreatePostForm({ }: CreatePostFormProps) { const [selectedModel, setSelectedModel] = useState(LLM_MODELS[0]); const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false); + const { showToast } = useToast(); const [isGenerating, setIsGenerating] = useState(false); @@ -49,6 +53,21 @@ export function CreatePostForm({ } const handleGenerateCaption = async () => { + // 1. Check for oversized files + const oversizedFilesCount = mediaFiles.filter( + (f) => f.size > GLOBAL_CONSTRAINTS.maxGlobalFileSize, + ).length; + + if (oversizedFilesCount > 0) { + const limitMB = Math.floor(GLOBAL_CONSTRAINTS.maxGlobalFileSize / (1024 * 1024)); + showToast( + "error", + "Files too large", + `Some images are too large for AI processing (max ${limitMB}MB). Please remove them and try again.`, + ); + return; + } + setIsGenerating(true); try { const base64List = await Promise.all(mediaFiles.map(fileToDataUrl)); @@ -135,20 +154,36 @@ export function CreatePostForm({ {/* Media Previews (Horizontal Scroll) */} {filePreviews.length > 0 && (
- {filePreviews.map((src, idx) => ( -
- Preview - -
- ))} + Preview + + {isOversized && ( +
+ + TOO LARGE +
+ )} + + +
+ ); + })} )} diff --git a/frontend/src/components/create-post/EditPlatformPostModal.tsx b/frontend/src/components/create-post/EditPlatformPostModal.tsx index 2e450fc..0ed98d4 100644 --- a/frontend/src/components/create-post/EditPlatformPostModal.tsx +++ b/frontend/src/components/create-post/EditPlatformPostModal.tsx @@ -24,7 +24,8 @@ interface EditPlatformPostModalProps { isOpen: boolean; onClose: () => void; onUpdate: (updates: Partial) => void; - onRefine: (prompt: string) => Promise; + onRefine: (prompt: string) => Promise; + error?: string; isGenerating: boolean; } @@ -35,6 +36,7 @@ export function EditPlatformPostModal({ onClose, onUpdate, onRefine, + error, isGenerating, }: EditPlatformPostModalProps) { const [localText, setLocalText] = useState(post?.text || ""); @@ -74,9 +76,12 @@ export function EditPlatformPostModal({ const handleRefine = async () => { if (!llmPrompt.trim()) return; - await onRefine(llmPrompt); - setLlmPrompt(""); - onClose(); + const success = await onRefine(llmPrompt); + if (success) { + setLlmPrompt(""); + // Optionally stay open to let user see refined text + // onClose(); + } }; return ( @@ -152,6 +157,7 @@ export function EditPlatformPostModal({ Refine + {error &&

{error}

} {/* Media Editor */} diff --git a/frontend/src/components/create-post/PostPreview.tsx b/frontend/src/components/create-post/PostPreview.tsx index 122f0db..e2e2e4c 100644 --- a/frontend/src/components/create-post/PostPreview.tsx +++ b/frontend/src/components/create-post/PostPreview.tsx @@ -41,7 +41,8 @@ interface PostPreviewProps { connectedAccounts: SocialAccount | null; platformPosts: Record; updatePlatformPost: (id: string, updates: Partial) => void; - refinePlatformPostWithLLM: (id: string, prompt: string) => Promise; + refinePlatformPostWithLLM: (id: string, prompt: string) => Promise; + platformGenerationErrors: Record; isGenerating: boolean; platformWarnings: Record; } @@ -67,6 +68,7 @@ export function PostPreview({ platformPosts, updatePlatformPost, refinePlatformPostWithLLM, + platformGenerationErrors, isGenerating, platformWarnings, }: PostPreviewProps) { @@ -220,7 +222,10 @@ export function PostPreview({ } } onUpdate={(updates) => updatePlatformPost(editingPlatformId, updates)} - onRefine={(prompt) => refinePlatformPostWithLLM(editingPlatformId, prompt)} + onRefine={async (prompt: string) => + await refinePlatformPostWithLLM(editingPlatformId!, prompt) + } + error={platformGenerationErrors[editingPlatformId]} isGenerating={isGenerating} /> )} diff --git a/frontend/src/hooks/useCreatePost.ts b/frontend/src/hooks/useCreatePost.ts index 93d79d8..1d4b50c 100644 --- a/frontend/src/hooks/useCreatePost.ts +++ b/frontend/src/hooks/useCreatePost.ts @@ -47,6 +47,9 @@ export function useCreatePost() { const [timeZone, setTimeZone] = useState(""); const [errors, setErrors] = useState([]); const [platformWarnings, setPlatformWarnings] = useState>({}); + const [platformGenerationErrors, setPlatformGenerationErrors] = useState>( + {}, + ); // --- Configurations --- const FRONTEND_PLATFORM_CONFIG: Record< @@ -247,14 +250,16 @@ export function useCreatePost() { const oversizedFiles = newFiles.filter((f) => f.size > GLOBAL_CONSTRAINTS.maxGlobalFileSize); if (oversizedFiles.length > 0) { const limitMB = Math.floor(GLOBAL_CONSTRAINTS.maxGlobalFileSize / (1024 * 1024)); - setErrors((prev) => [...prev, `Some files are too large (max ${limitMB}MB).`]); - return; + setErrors((prev) => [ + ...prev, + `Some files are too large (max ${limitMB}MB). Please remove them before generating.`, + ]); } setMediaFiles((prev) => [...prev, ...newFiles]); const newPreviews = newFiles.map((file) => URL.createObjectURL(file)); setFilePreviews((prev) => [...prev, ...newPreviews]); - setErrors([]); // Clear errors on success + // setErrors([]); // Don't clear errors automatically if we just added some } }; @@ -394,9 +399,15 @@ export function useCreatePost() { const refinePlatformPostWithLLM = async (id: string, prompt: string) => { const currentPost = platformPosts[id]; - if (!currentPost) return; + if (!currentPost) return false; setIsGenerating(true); + setPlatformGenerationErrors((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); + try { const base64List = await Promise.all(currentPost.mediaFiles.map(fileToDataUrl)); @@ -410,21 +421,34 @@ export function useCreatePost() { media: base64List, }); - console.log(response); - const refinedText: string | undefined = - response.data?.data?.candidates[0].content.parts[0].text; + response.data?.data?.candidates?.[0]?.content?.parts?.[0]?.text; - console.log(refinedText); if (!refinedText) { throw new Error("AI refinement returned no text"); } updatePlatformPost(id, { text: refinedText }); + return true; } catch (error: any) { - if (error.response?.status !== 429) { - console.error("LLM Refinement failed", error); + console.error("LLM Refinement failed", error); + + let errorMessage = "Failed to refine caption. Please try again."; + + if (error.response?.status === 413) { + errorMessage = + "The images are too large for the AI to process. Try using fewer or smaller images."; + } else if (error.response?.status === 429) { + errorMessage = error.response.data?.message || "AI limit reached. Please try again later."; + } else if (error.response?.data?.message) { + errorMessage = error.response.data.message; } + + setPlatformGenerationErrors((prev) => ({ + ...prev, + [id]: errorMessage, + })); + return false; } finally { setIsGenerating(false); } @@ -810,6 +834,7 @@ export function useCreatePost() { platformPosts, updatePlatformPost, refinePlatformPostWithLLM, + platformGenerationErrors, loadDraft, draftId, }; diff --git a/frontend/src/lib/axios.ts b/frontend/src/lib/axios.ts index 069df55..abf9718 100644 --- a/frontend/src/lib/axios.ts +++ b/frontend/src/lib/axios.ts @@ -85,6 +85,9 @@ api.interceptors.response.use( if ( error.response?.status !== 401 || originalRequest.url?.includes("/auth/refresh") || + originalRequest.url?.includes("/auth/login") || + originalRequest.url?.includes("/auth/register") || + originalRequest.url?.includes("/auth/admin-login") || originalRequest._retry ) { return Promise.reject(error); From e089dd091ff863340ba557046832d1e58e166c16 Mon Sep 17 00:00:00 2001 From: star-boy Date: Thu, 5 Mar 2026 10:51:06 +0530 Subject: [PATCH 2/2] feat(oops! modal): added a oops modal when the limit is ecxeeded --- .../app/(user)/dashboard/create-post/page.tsx | 6 ++ .../components/create-post/CreatePostForm.tsx | 13 +++++ .../components/shared/LimitExceededModal.tsx | 57 +++++++++++++++++++ frontend/src/hooks/useCreatePost.ts | 4 ++ 4 files changed, 80 insertions(+) create mode 100644 frontend/src/components/shared/LimitExceededModal.tsx diff --git a/frontend/src/app/(user)/dashboard/create-post/page.tsx b/frontend/src/app/(user)/dashboard/create-post/page.tsx index 3ae3a3e..2d962e6 100644 --- a/frontend/src/app/(user)/dashboard/create-post/page.tsx +++ b/frontend/src/app/(user)/dashboard/create-post/page.tsx @@ -13,6 +13,7 @@ import { CreatePostForm } from "@/components/create-post/CreatePostForm"; import { PostPreview } from "@/components/create-post/PostPreview"; import { useSearchParams, useRouter } from "next/navigation"; import { SubmittingOverlay } from "@/components/create-post/SubmittingOverlay"; +import { LimitExceededModal } from "@/components/shared/LimitExceededModal"; export default function CreatePostPage() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); @@ -56,6 +57,8 @@ export default function CreatePostPage() { platformGenerationErrors, loadDraft, draftId, + isLimitModalOpen, + setIsLimitModalOpen, } = useCreatePost(); // Load draft if draftId query param is present @@ -138,6 +141,8 @@ export default function CreatePostPage() { availablePlatforms={availablePlatforms} /> + setIsLimitModalOpen(false)} /> + {/* Main Content Area */}
{viewMode === "create" ? ( @@ -154,6 +159,7 @@ export default function CreatePostPage() { platformWarnings={platformWarnings} selectedPlatforms={selectedPlatforms} availablePlatforms={availablePlatforms} + onOpenLimitModal={() => setIsLimitModalOpen(true)} /> {/* Right Column: Platform Selection */} diff --git a/frontend/src/components/create-post/CreatePostForm.tsx b/frontend/src/components/create-post/CreatePostForm.tsx index de10e61..2e45135 100644 --- a/frontend/src/components/create-post/CreatePostForm.tsx +++ b/frontend/src/components/create-post/CreatePostForm.tsx @@ -21,6 +21,7 @@ interface CreatePostFormProps { platformWarnings: Record; selectedPlatforms: string[]; availablePlatforms: Platform[]; + onOpenLimitModal: () => void; } const LLM_MODELS = [{ id: "gemini-1.5-flash", name: "gemini-2.5 flash", provider: "Gemini" }]; @@ -36,6 +37,7 @@ export function CreatePostForm({ platformWarnings, selectedPlatforms, availablePlatforms, + onOpenLimitModal, }: CreatePostFormProps) { const [selectedModel, setSelectedModel] = useState(LLM_MODELS[0]); const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false); @@ -79,6 +81,17 @@ export function CreatePostForm({ console.log(response); const generatedCaption = response.data.data.candidates[0].content.parts[0].text; setPostText(generatedCaption); + } catch (error: any) { + console.error("Caption generation failed", error); + if (error.response?.status === 429) { + onOpenLimitModal(); + } else { + showToast( + "error", + "Generation failed", + error.response?.data?.message || "Failed to generate caption. Please try again.", + ); + } } finally { setIsGenerating(false); } diff --git a/frontend/src/components/shared/LimitExceededModal.tsx b/frontend/src/components/shared/LimitExceededModal.tsx new file mode 100644 index 0000000..04536c8 --- /dev/null +++ b/frontend/src/components/shared/LimitExceededModal.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { AlertCircle, CreditCard } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { useRouter } from "next/navigation"; + +interface LimitExceededModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function LimitExceededModal({ isOpen, onClose }: LimitExceededModalProps) { + const router = useRouter(); + + const handleUpgrade = () => { + router.push("/pricing"); + onClose(); + }; + + return ( + + +
+
+ +
+ +
+

Oops! Plan Limit Reached

+

+ You've reached your generation limit for your current plan. Buy some extra credits to + keep generating amazing content! +

+
+ +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/hooks/useCreatePost.ts b/frontend/src/hooks/useCreatePost.ts index 1d4b50c..3204aa8 100644 --- a/frontend/src/hooks/useCreatePost.ts +++ b/frontend/src/hooks/useCreatePost.ts @@ -50,6 +50,7 @@ export function useCreatePost() { const [platformGenerationErrors, setPlatformGenerationErrors] = useState>( {}, ); + const [isLimitModalOpen, setIsLimitModalOpen] = useState(false); // --- Configurations --- const FRONTEND_PLATFORM_CONFIG: Record< @@ -439,6 +440,7 @@ export function useCreatePost() { errorMessage = "The images are too large for the AI to process. Try using fewer or smaller images."; } else if (error.response?.status === 429) { + setIsLimitModalOpen(true); errorMessage = error.response.data?.message || "AI limit reached. Please try again later."; } else if (error.response?.data?.message) { errorMessage = error.response.data.message; @@ -837,5 +839,7 @@ export function useCreatePost() { platformGenerationErrors, loadDraft, draftId, + isLimitModalOpen, + setIsLimitModalOpen, }; }